From 20e60b3a3b122b1989aa6e38ba6efdc16ccac811 Mon Sep 17 00:00:00 2001 From: jdu Date: Tue, 6 May 2014 16:49:06 +0200 Subject: [PATCH 001/826] Fixes #328 --- src/tsd/HttpJsonSerializer.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 3d98b0086a..e82d4cf6f0 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -309,12 +309,18 @@ public Tree parseTreeV1() { } else { tree.setEnabled(false); } - } else if (entry.getKey().toLowerCase().equals("strictMatch")) { + } else if (entry.getKey().toLowerCase().equals("strictmatch")) { if (entry.getValue().toLowerCase().equals("true")) { tree.setStrictMatch(true); } else { tree.setStrictMatch(false); } + } else if (entry.getKey().toLowerCase().equals("storefailures")) { + if (entry.getValue().toLowerCase().equals("true")) { + tree.setStoreFailures(true); + } else { + tree.setStoreFailures(false); + } } } return tree; From 7b43e7f2b4f00c697b43f44321ea707bf3cc2e00 Mon Sep 17 00:00:00 2001 From: clarsen Date: Thu, 5 Jun 2014 15:12:23 -0400 Subject: [PATCH 002/826] Fix #342 where compacted millisecond columns were throwing errors during an fsck when they shouldn't have been. Also add some unit tests to validate the fix. --- src/tools/Fsck.java | 3 +- test/tools/TestFsck.java | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index f9a7319408..3f9c6e3bda 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -205,7 +205,8 @@ final class DP { LOG.debug("Found an object from a future version of OpenTSDB\n\t" + kv); continue; - } else if (qual.length >= 4 && !Internal.inMilliseconds(qual[0])) { + } else if (qual.length == 4 && !Internal.inMilliseconds(qual[0]) + || qual.length > 4) { // compacted row if (value[value.length - 1] > Const.MS_MIXED_COMPACT) { errors++; diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index c32824b1e2..e8834392d5 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -83,6 +83,11 @@ public void before() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); storage.setFamily("t".getBytes(MockBase.ASCII())); + PowerMockito.mockStatic(System.class); + when(System.nanoTime()) + .thenReturn(1357300800000000L) + .thenReturn(1357300900000000L); + // replace the "real" field objects with mocks Field met = tsdb.getClass().getDeclaredField("metrics"); met.setAccessible(true); @@ -256,6 +261,57 @@ public void NoErrorsCompacted() throws Exception { assertEquals(0, errors); } + @Test + public void NoErrorsCompactedMS() throws Exception { + final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { (byte) 0xF0, 0x00, 0x04, 0x07 }; + final byte[] val3 = Bytes.fromLong(6L); + + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + int errors = (Integer)fsck.invoke(null, tsdb, client, + "tsdb".getBytes(MockBase.ASCII()), false, new String[] { + "1356998400", "1357002000", "sum", "sys.cpu.user" }); + assertEquals(0, errors); + assertEquals(1, storage.numColumns(ROW)); + } + + @Test + public void NoErrorsCompactedMix() throws Exception { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final byte[] val12 = MockBase.concatByteArrays(val1, val2, new byte[] { 0 }); + storage.addColumn(ROW, qual12, val12); + int errors = (Integer)fsck.invoke(null, tsdb, client, + "tsdb".getBytes(MockBase.ASCII()), false, new String[] { + "1356998400", "1357002000", "sum", "sys.cpu.user" }); + assertEquals(0, errors); + assertEquals(1, storage.numColumns(ROW)); + } + + @Test + public void NoErrorsCompactedMixReverse() throws Exception { + final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final byte[] val12 = MockBase.concatByteArrays(val1, val2, new byte[] { 0 }); + storage.addColumn(ROW, qual12, val12); + int errors = (Integer)fsck.invoke(null, tsdb, client, + "tsdb".getBytes(MockBase.ASCII()), false, new String[] { + "1356998400", "1357002000", "sum", "sys.cpu.user" }); + assertEquals(0, errors); + assertEquals(1, storage.numColumns(ROW)); + } + @Test public void lastCompactedByteNotZero() throws Exception { final byte[] qual1 = { 0x00, 0x07 }; @@ -562,6 +618,26 @@ public void compactedWSameTS() throws Exception { assertEquals(2, storage.numColumns(ROW)); } + @Test + public void compactedMSWSameTS() throws Exception { + final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { (byte) 0xF0, 0x00, 0x04, 0x07 }; + final byte[] val3 = Bytes.fromLong(6L); + + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + storage.addColumn(ROW, qual3, val3); + int errors = (Integer)fsck.invoke(null, tsdb, client, + "tsdb".getBytes(MockBase.ASCII()), false, new String[] { + "1356998400", "1357002000", "sum", "sys.cpu.user" }); + assertEquals(1, errors); + assertEquals(2, storage.numColumns(ROW)); + } + @Test public void compactedWSameTSFix() throws Exception { final byte[] qual1 = { 0x0, 0x07 }; @@ -582,4 +658,23 @@ public void compactedWSameTSFix() throws Exception { assertEquals(1, storage.numColumns(ROW)); } + @Test + public void compactedMSWSameTSFix() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x0, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + storage.addColumn(ROW, qual3, val3); + int errors = (Integer)fsck.invoke(null, tsdb, client, + "tsdb".getBytes(MockBase.ASCII()), true, new String[] { + "1356998400", "1357002000", "sum", "sys.cpu.user" }); + assertEquals(1, errors); + assertEquals(1, storage.numColumns(ROW)); + } } From 8d7e006fef35e0a68e99585db99009f3d00b2396 Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 20 Jun 2014 21:11:11 -0400 Subject: [PATCH 003/826] Fix Scan raw unit test where the annotation column resolves to 3 bytes instead of 5 when normalized from milliseconds to seconds. --- test/tools/TestDumpSeries.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tools/TestDumpSeries.java b/test/tools/TestDumpSeries.java index 9ae4d65e0c..d6366378fa 100644 --- a/test/tools/TestDumpSeries.java +++ b/test/tools/TestDumpSeries.java @@ -186,7 +186,7 @@ public void dumpRaw() throws Exception { "[0, 0, 1, 80, -30, 53, 16, 0, 0, 1, 0, 0, 1] sys.cpu.user 1357002000", log_lines[8].substring(0, 68)); assertEquals( - " [1, 0, 0, 0, 0]\t[123, 34, 116, 115, 117, 105, 100, " + " [1, 0, 0]\t[123, 34, 116, 115, 117, 105, 100, " + "34, 58, 34, 48, 48, 48, 48, 48, 49, 48, 48, 48, 48, 48, 49, 48, 48, " + "48, 48, 48, 49, 34, 44, 34, 115, 116, 97, 114, 116, 84, 105, 109, " + "101, 34, 58, 49, 51, 53, 55, 48, 48, 50, 48, 48, 48, 48, 48, 48, " @@ -199,7 +199,7 @@ public void dumpRaw() throws Exception { + "\"000001000001000001\",\"startTime\":1357002000000,\"endTime\":0," + "\"description\":\"Annotation on milliseconds\",\"notes\":\"\"," + "\"custom\":null}\t1357002016000", - log_lines[9].substring(0, 780)); + log_lines[9].substring(0, 774)); assertEquals( " [-16, 0, 0, 0]\t[42]\t0\tl\t1357002000000", log_lines[10].substring(0, 39)); From d9081d0988b6e0b594f09a0f1c0c33f0fc628279 Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 20 Jun 2014 17:08:26 -0400 Subject: [PATCH 004/826] Add the Pair class since Map.Entry doesn't deserialize in Jackson automatically or easily and we don't want to include a library like Apache Commons for a single class. --- Makefile.am | 2 + src/utils/Pair.java | 119 ++++++++++++++++++++ test/utils/TestPair.java | 234 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 src/utils/Pair.java create mode 100644 test/utils/TestPair.java diff --git a/Makefile.am b/Makefile.am index 7ccac4bc11..9fd43bb447 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,6 +114,7 @@ tsdb_SRC := \ src/utils/DateTime.java \ src/utils/JSON.java \ src/utils/JSONException.java \ + src/utils/Pair.java \ src/utils/PluginLoader.java tsdb_DEPS = \ @@ -180,6 +181,7 @@ test_SRC := \ test/utils/TestConfig.java \ test/utils/TestDateTime.java \ test/utils/TestJSON.java \ + test/utils/TestPair.java \ test/utils/TestPluginLoader.java test_plugin_SRC := \ diff --git a/src/utils/Pair.java b/src/utils/Pair.java new file mode 100644 index 0000000000..dc71fb4684 --- /dev/null +++ b/src/utils/Pair.java @@ -0,0 +1,119 @@ +// 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.utils; + +/** + * Simple key/value pair class where either of the values may be null. + * Pairs are particularly useful in lists where you may have duplicate keys, + * values or both. This class also deserializes easily through Jackson. + * + * Other implementations of pairs exist: + * - {@code org.apache.commons.lang3.tuple.Pair} is one an example but we don't + * want to include a whole dependency for a single class. + * - {@code java.util.Map.Entry} is an interface implemented by + * {@code java.util.AbstractMap.SimpleEntry} and that works great throughout the + * code but Jackson chokes on deserializing and would require a complicated, + * custom deserializer class. + * + * Thus we have this class that can be deserialized easily when nested in + * another class like a list with: + * {@code final TypeReference>> TR = + * new TypeReference>>() \{\};} + * + * @param Object type for the key + * @param Object type for the value + */ +public class Pair { + + /** The key or left hand value */ + protected K key; + + /** The value or right hand value */ + protected V value; + + /** + * Default ctor that leaves the key and value objects as null + */ + public Pair() { + } + + /** + * Ctor that stores references to the objects + * @param key The key or left hand value to store + * @param value The value or right hand value to store + */ + public Pair(final K key, final V value) { + this.key = key; + this.value = value; + } + + /** + * Calculates the hash by ORing the key and value hash codes + * @return a hash code for this pair + */ + @Override + public int hashCode() { + return (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()); + } + + /** @return a descriptive string in the format "key=K, value=V" */ + @Override + public String toString() { + return new StringBuilder().append("key=") + .append(key).append(", value=").append(value).toString(); + } + + /** + * Compares the two pairs for equality. If the incoming object reference is + * the same, the result is true. Then {@code .equals} is called on both + * objects (if they are not null) + * @return true if the objects refer to the same address or both objects are + * equal + */ + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + if (object instanceof Pair) { + final Pair other_pair = (Pair)object; + return + (key == null ? other_pair.getKey() == null : + key.equals(other_pair.key)) + && (value == null ? other_pair.getValue() == null : + value.equals(other_pair.value)); + } + return false; + } + + /** @return The stored key/left value, may be null */ + public K getKey() { + return key; + } + + /** @return The stored value/right value, may be null */ + public V getValue() { + return value; + } + + /** @param key The key/left value to store, may be null */ + public void setKey(final K key) { + this.key = key; + } + + /** @param value The value/right value to store, may be null */ + public void setValue(final V value) { + this.value = value; + } +} diff --git a/test/utils/TestPair.java b/test/utils/TestPair.java new file mode 100644 index 0000000000..c1df3aecb7 --- /dev/null +++ b/test/utils/TestPair.java @@ -0,0 +1,234 @@ +// 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.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import com.fasterxml.jackson.core.type.TypeReference; + +public class TestPair { + + @Test + public void defaultCtor() { + final Pair pair = new Pair(); + assertNotNull(pair); + assertNull(pair.getKey()); + assertNull(pair.getValue()); + } + + @Test + public void ctorWithArgs() { + final Pair pair = new Pair("host", "web01"); + assertNotNull(pair); + assertEquals("host", pair.getKey()); + assertEquals("web01", pair.getValue()); + } + + @Test + public void ctorWithNullKey() { + final Pair pair = new Pair(null, "web01"); + assertNotNull(pair); + assertNull(pair.getKey()); + assertEquals("web01", pair.getValue()); + } + + @Test + public void ctorWithNullValue() { + final Pair pair = new Pair("host", null); + assertNotNull(pair); + assertEquals("host", pair.getKey()); + assertNull(pair.getValue()); + } + + @Test + public void ctorWithNulls() { + final Pair pair = new Pair(null, null); + assertNotNull(pair); + assertNull(pair.getKey()); + assertNull(pair.getValue()); + } + + @Test + public void hashcodeTest() { + final Pair pair = new Pair("host", "web01"); + assertEquals(109885949, pair.hashCode()); + } + + @Test + public void hashcodeTestNullKey() { + final Pair pair = new Pair(null, "web01"); + assertEquals(113003605, pair.hashCode()); + } + + @Test + public void hashcodeTestNullValue() { + final Pair pair = new Pair("host", null); + assertEquals(3208616, pair.hashCode()); + } + + @Test + public void hashcodeTestNulls() { + final Pair pair = new Pair(); + assertEquals(0, pair.hashCode()); + } + + @Test + public void equalsTest() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = new Pair("host", "web01"); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestSameReference() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = pair; + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffKey() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = new Pair("diff", "web01"); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffVal() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = new Pair("host", "diff"); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffNullKey() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = new Pair(null, "web01"); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffNullVal() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = new Pair("host", null); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestNullKeys() { + final Pair pair = new Pair(null, "web01"); + final Pair pair2 = new Pair(null, "web01"); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestNullValues() { + final Pair pair = new Pair("host", null); + final Pair pair2 = new Pair("host", null); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestNulls() { + final Pair pair = new Pair(); + final Pair pair2 = new Pair(); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffTypes() { + final Pair pair = new Pair("host", "web01"); + final Pair pair2 = new Pair(1, 42); + assertFalse(pair.equals(pair2)); + } + + @Test + public void toStringTest() { + final Pair pair = new Pair("host", "web01"); + assertEquals("key=host, value=web01", pair.toString()); + } + + @Test + public void toStringTestNulls() { + final Pair pair = new Pair(); + assertEquals("key=null, value=null", pair.toString()); + } + + @Test + public void toStringTestNumbers() { + final Pair pair = new Pair(1, 42L); + assertEquals("key=1, value=42", pair.toString()); + } + + @Test + public void serdes() { + final Pair ser = new Pair("host", "web01"); + final String json = JSON.serializeToString(ser); + assertEquals("{\"key\":\"host\",\"value\":\"web01\"}", json); + + @SuppressWarnings("unchecked") + final Pair des = JSON.parseToObject(json, Pair.class); + assertEquals("host", des.getKey()); + assertEquals("web01", des.getValue()); + } + + @Test + public void serdesNulls() { + final Pair ser = new Pair(); + final String json = JSON.serializeToString(ser); + assertEquals("{\"key\":null,\"value\":null}", json); + + @SuppressWarnings("unchecked") + final Pair des = JSON.parseToObject(json, Pair.class); + assertNull(des.getKey()); + assertNull(des.getValue()); + } + + @Test + public void serdesList() { + final List> ser = + new ArrayList>(2); + ser.add(new Pair("host", "web01")); + ser.add(new Pair(null, "keyisnull")); + final String json = JSON.serializeToString(ser); + assertEquals("[{\"key\":\"host\",\"value\":\"web01\"}," + + "{\"key\":null,\"value\":\"keyisnull\"}]", json); + + final TypeReference>> TR = + new TypeReference>>() {}; + + final List> des = JSON.parseToObject(json, TR); + assertEquals(2, des.size()); + assertEquals("host", des.get(0).getKey()); + assertEquals("web01", des.get(0).getValue()); + assertNull(des.get(1).getKey()); + assertEquals("keyisnull", des.get(1).getValue()); + } + + @Test + public void rawObjects() { + final Pair pair = new Pair("host", "web01"); + assertNotNull(pair); + assertEquals("host", pair.getKey()); + assertEquals("web01", pair.getValue()); + } +} From 6cb726e198cc9bdbcff01265df5d58435b3f65f9 Mon Sep 17 00:00:00 2001 From: clarsen Date: Tue, 17 Jun 2014 11:39:08 -0400 Subject: [PATCH 005/826] Add ByteArrayPair utility class for storing two byte arrays in a list with a comparator that sorts on the bytes. Also allows storing nulls for the key or value of the pair. This will be needed for some situations where we may query for a tagk without a tagv or a tagv without a tagk. --- Makefile.am | 2 + src/utils/ByteArrayPair.java | 81 ++++++++++++ test/utils/TestByteArrayPair.java | 212 ++++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 src/utils/ByteArrayPair.java create mode 100644 test/utils/TestByteArrayPair.java diff --git a/Makefile.am b/Makefile.am index 9fd43bb447..4b7c7e85af 100644 --- a/Makefile.am +++ b/Makefile.am @@ -110,6 +110,7 @@ tsdb_SRC := \ src/uid/NoSuchUniqueName.java \ src/uid/UniqueId.java \ src/uid/UniqueIdInterface.java \ + src/utils/ByteArrayPair.java \ src/utils/Config.java \ src/utils/DateTime.java \ src/utils/JSON.java \ @@ -178,6 +179,7 @@ test_SRC := \ test/tsd/TestUniqueIdRpc.java \ test/uid/TestNoSuchUniqueId.java \ test/uid/TestUniqueId.java \ + test/utils/TestByteArrayPair.java \ test/utils/TestConfig.java \ test/utils/TestDateTime.java \ test/utils/TestJSON.java \ diff --git a/src/utils/ByteArrayPair.java b/src/utils/ByteArrayPair.java new file mode 100644 index 0000000000..1a1a6146d3 --- /dev/null +++ b/src/utils/ByteArrayPair.java @@ -0,0 +1,81 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.utils; + +import java.util.Arrays; + +import org.hbase.async.Bytes; + +/** + * Simple helper class to store a pair of byte arrays for use in situations + * where a map or Map.Entry doesn't make sense. Extends the Pair class and + * overrides the equals method using {@code Bytes.memcmp()} to determine if both + * arrays have the same amount of data in the same order. + * Sorting is performed on the key first, then on the value. + */ +public class ByteArrayPair extends Pair + implements Comparable { + + /** + * Default constructor initializes the object + * @param key The key to store, may be null + * @param value The value to store, may be null + * @throws IllegalArgumentException If both values are null + */ + public ByteArrayPair(final byte[] key, final byte[] value) { + this.key = key; + this.value = value; + } + + /** + * Sorts on the key first then on the value. Nulls are allowed and are ordered + * first. + * @param a The value to compare against. + */ + public int compareTo(ByteArrayPair a) { + final int key_compare = Bytes.memcmpMaybeNull(this.key, a.key); + if (key_compare == 0) { + return Bytes.memcmpMaybeNull(this.value, a.value); + } + return key_compare; + } + + /** @return a descriptive string in the format "key=K, value=V" */ + @Override + public String toString() { + return new StringBuilder().append("key=") + .append(Arrays.toString(key)).append(", value=") + .append(Arrays.toString(value)).toString(); + } + + /** + * Compares the two byte arrays for equality using {@code Bytes.memcmp()} + * @return true if the objects refer to the same address or both objects are + * have the same bytes in the same order + */ + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + if (object instanceof ByteArrayPair) { + final ByteArrayPair other_pair = (ByteArrayPair)object; + return + (key == null ? other_pair.getKey() == null : + Bytes.memcmp(key, other_pair.key) == 0) + && (value == null ? other_pair.getValue() == null : + Bytes.memcmp(value, other_pair.value) == 0); + } + return false; + } +} diff --git a/test/utils/TestByteArrayPair.java b/test/utils/TestByteArrayPair.java new file mode 100644 index 0000000000..e34823efeb --- /dev/null +++ b/test/utils/TestByteArrayPair.java @@ -0,0 +1,212 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +public class TestByteArrayPair { + final byte[] key = new byte[] { 1 }; + final byte[] val = new byte[] { 2 }; + final byte[] val2 = new byte[] { 3 }; + final byte[] set1 = new byte[] { 1, 2, 3 }; + final byte[] set2 = new byte[] { 1, 2, 4 }; + + @Test + public void defaultCtor() { + final ByteArrayPair pair = new ByteArrayPair(key, val); + assertNotNull(pair); + assertArrayEquals(key, pair.getKey()); + assertArrayEquals(val, pair.getValue()); + } + + @Test + public void defaultCtorNullKey() { + final ByteArrayPair pair = new ByteArrayPair(null, val); + assertNotNull(pair); + assertNull(pair.getKey()); + assertArrayEquals(val, pair.getValue()); + } + + @Test + public void defaultCtorNullValue() { + final ByteArrayPair pair = new ByteArrayPair(key, null); + assertNotNull(pair); + assertArrayEquals(key, pair.getKey()); + assertNull(pair.getValue()); + } + + @Test + public void defaultCtorBothNull() { + final ByteArrayPair pair = new ByteArrayPair(null, null); + assertNotNull(pair); + assertNull(pair.getKey()); + assertNull(pair.getValue()); + } + + @Test + public void toStringTest() { + final ByteArrayPair pair = new ByteArrayPair(key, val); + assertEquals("key=[1], value=[2]", pair.toString()); + } + + @Test + public void toStringTestNullKey() { + final ByteArrayPair pair = new ByteArrayPair(null, val); + assertEquals("key=null, value=[2]", pair.toString()); + } + + @Test + public void toStringTestNullVal() { + final ByteArrayPair pair = new ByteArrayPair(key, null); + assertEquals("key=[1], value=null", pair.toString()); + } + + @Test + public void toStringTestNulls() { + final ByteArrayPair pair = new ByteArrayPair(null, null); + assertEquals("key=null, value=null", pair.toString()); + } + + @Test + public void equalsTest() { + final ByteArrayPair pair = new ByteArrayPair(key, val); + final ByteArrayPair pair2 = new ByteArrayPair(key, val); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestSets() { + final ByteArrayPair pair = new ByteArrayPair(set1, val); + final ByteArrayPair pair2 = new ByteArrayPair(set1, val); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestSameReference() { + final ByteArrayPair pair = new ByteArrayPair(key, val); + final ByteArrayPair pair2 = pair; + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffKey() { + final ByteArrayPair pair = new ByteArrayPair(key, val); + final ByteArrayPair pair2 = new ByteArrayPair(val, val); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffVal() { + final ByteArrayPair pair = new ByteArrayPair(key, val); + final ByteArrayPair pair2 = new ByteArrayPair(key, key); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffKeySets() { + final ByteArrayPair pair = new ByteArrayPair(set1, val); + final ByteArrayPair pair2 = new ByteArrayPair(set2, key); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestDiffValSets() { + final ByteArrayPair pair = new ByteArrayPair(key, set1); + final ByteArrayPair pair2 = new ByteArrayPair(key, set2); + assertFalse(pair.equals(pair2)); + } + + @Test + public void equalsTestNullKeys() { + final ByteArrayPair pair = new ByteArrayPair(null, val); + final ByteArrayPair pair2 = new ByteArrayPair(null, val); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestNullValues() { + final ByteArrayPair pair = new ByteArrayPair(key, null); + final ByteArrayPair pair2 = new ByteArrayPair(key, null); + assertTrue(pair.equals(pair2)); + } + + @Test + public void equalsTestNulls() { + final ByteArrayPair pair = new ByteArrayPair(null, null); + final ByteArrayPair pair2 = new ByteArrayPair(null, null); + assertTrue(pair.equals(pair2)); + } + + @Test + public void sortTest() { + List pairs = new ArrayList(2); + pairs.add(new ByteArrayPair(val, key)); + pairs.add(new ByteArrayPair(key, val)); + Collections.sort(pairs); + assertArrayEquals(key, pairs.get(0).getKey()); + assertArrayEquals(val, pairs.get(0).getValue()); + assertArrayEquals(val, pairs.get(1).getKey()); + assertArrayEquals(key, pairs.get(1).getValue()); + } + + @Test + public void sortTestSets() { + List pairs = new ArrayList(2); + pairs.add(new ByteArrayPair(set2, val)); + pairs.add(new ByteArrayPair(set1, val)); + Collections.sort(pairs); + assertArrayEquals(set1, pairs.get(0).getKey()); + assertArrayEquals(val, pairs.get(0).getValue()); + assertArrayEquals(set2, pairs.get(1).getKey()); + assertArrayEquals(val, pairs.get(1).getValue()); + } + + @Test + public void sortTestWithNullKey() { + List pairs = new ArrayList(2); + pairs.add(new ByteArrayPair(val, key)); + pairs.add(new ByteArrayPair(null, val)); + Collections.sort(pairs); + assertNull(pairs.get(0).getKey()); + assertArrayEquals(val, pairs.get(0).getValue()); + assertArrayEquals(val, pairs.get(1).getKey()); + assertArrayEquals(key, pairs.get(1).getValue()); + } + + @Test + public void sortTestonValue() { + List pairs = new ArrayList(3); + pairs.add(new ByteArrayPair(val, key)); + pairs.add(new ByteArrayPair(key, val2)); + pairs.add(new ByteArrayPair(key, val)); + + Collections.sort(pairs); + assertArrayEquals(key, pairs.get(0).getKey()); + assertArrayEquals(val, pairs.get(0).getValue()); + assertArrayEquals(key, pairs.get(1).getKey()); + assertArrayEquals(val2, pairs.get(1).getValue()); + assertArrayEquals(val, pairs.get(2).getKey()); + assertArrayEquals(key, pairs.get(2).getValue()); + } +} From a73a51110f0a1033a9230e6f19e869f747e703cf Mon Sep 17 00:00:00 2001 From: clarsen Date: Tue, 17 Jun 2014 21:12:13 -0400 Subject: [PATCH 006/826] Make RowKey.metricNameAsync() public so it can be used elsewhere --- src/core/RowKey.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/RowKey.java b/src/core/RowKey.java index 29fccb7072..ee733daa23 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -48,7 +48,8 @@ static String metricName(final TSDB tsdb, final byte[] row) { * @return The name of the metric. * @since 1.2 */ - static Deferred metricNameAsync(final TSDB tsdb, final byte[] row) { + public static Deferred metricNameAsync(final TSDB tsdb, + final byte[] row) { final byte[] id = Arrays.copyOfRange(row, 0, tsdb.metrics.width()); return tsdb.metrics.getNameAsync(id); } From 15cb0baf38023123f8450c77d7822dd770d98329 Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 20 Jun 2014 17:10:29 -0400 Subject: [PATCH 007/826] Extend SearchQuery for lookups with a metric and a tags field. --- src/search/SearchQuery.java | 115 +++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 3 deletions(-) diff --git a/src/search/SearchQuery.java b/src/search/SearchQuery.java index 321849c197..6cc78fa74d 100644 --- a/src/search/SearchQuery.java +++ b/src/search/SearchQuery.java @@ -15,6 +15,8 @@ import java.util.Collections; import java.util.List; +import net.opentsdb.utils.Pair; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -25,6 +27,9 @@ * Class used for passing and executing simple queries against with the search * plugin. This may not be able to take advantage of all of the search engine's * features but is intended to satisfy most common search requests. + * With 2.1 it now allows for time series lookup queries, using the meta or full + * data tables to determine what time series exist for a given metric, tag name, + * tag value or combination thereof. * @since 2.0 */ @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) @@ -41,7 +46,8 @@ public enum SearchType { TSMETA_SUMMARY, TSUIDS, UIDMETA, - ANNOTATION + ANNOTATION, + LOOKUP } /** The type of search to execute */ @@ -49,9 +55,18 @@ public enum SearchType { /** The actual query to execute */ private String query; + + /** The metric to iterate over, may be null */ + private String metric; + + /** Optional tags to match on, may be null */ + private List> tags; + + /** Whether or not to use the tsdb-meta table for lookups. Defaults to true */ + private boolean use_meta; /** Limit the number of responses so we don't overload the TSD or client */ - private int limit = 25; + private int limit; /** Used for paging through a result set */ private int start_index; @@ -67,6 +82,68 @@ public enum SearchType { /** Results from the search engine. Object depends on the query type */ private List results; + /** + * Default ctor. Only sets use_meta to true. Other fields are left null. + */ + public SearchQuery() { + use_meta = true; + limit = 25; + } + + /** + * Overload to set the metric on creation + * @param metric The metric to filter on + */ + public SearchQuery(final String metric) { + this.metric = metric; + use_meta = true; + limit = 25; + } + + /** + * Overload to set just the tags + * @param tags List of tagk/tagv pairs, either of which may be null + */ + public SearchQuery(final List> tags) { + this.tags = tags; + use_meta = true; + limit = 25; + } + + /** + * Overload to set both metric and tags + * @param metric The metric to filter on + * @param tags List of tagk/tagv pairs, either of which may be null + */ + public SearchQuery(final String metric, + final List> tags) { + this.metric = metric; + this.tags = tags; + use_meta = true; + limit = 25; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("type=").append(type).append(", query=") + .append(query).append(", metric=").append(metric) + .append(", tags=["); + if (tags != null) { + for(int i = 0; i < tags.size(); i++) { + if (i > 0) { + buf.append(", "); + } + buf.append("{").append(tags.get(i).getKey()).append("=") + .append(tags.get(i).getValue()).append("}"); + } + } + buf.append("], use_meta=").append(use_meta) + .append(", limit=").append(limit).append(", start_index=") + .append(start_index); + return buf.toString(); + } + /** * Converts the human readable string to the proper enum * @param type The string to parse @@ -89,11 +166,13 @@ public static SearchType parseSearchType(final String type) { return SearchType.UIDMETA; } else if (type.toLowerCase().equals("annotation")) { return SearchType.ANNOTATION; + } else if (type.toLowerCase().equals("lookup")) { + return SearchType.LOOKUP; } else { throw new IllegalArgumentException("Unknown type: " + type); } } - + // GETTERS AND SETTERS -------------------------- /** @return The type of query executed */ @@ -106,6 +185,21 @@ public String getQuery() { return query; } + /** @return Name of a metric to use for filtering */ + public String getMetric() { + return metric; + } + + /** @return List of tagk/tagv pairs, either of which may be null */ + public List> getTags() { + return tags; + } + + /** @return Whether or not the lookup should be done on the main data table */ + public boolean useMeta() { + return use_meta; + } + /** @return A limit on the number of results returned per query */ public int getLimit() { return limit; @@ -144,6 +238,21 @@ public void setQuery(String query) { this.query = query; } + /** @param metric A metric to use for lookup filtering */ + public void setMetric(String metric) { + this.metric = metric; + } + + /** @param tags A list of tagk, tagv pairs, either of which may be null */ + public void setTags(List> tags) { + this.tags = tags; + } + + /** @param use_meta Whether or not to use the data or meta table for lookups */ + public void setUseMeta(boolean use_meta) { + this.use_meta = use_meta; + } + /** @param limit A limit to the number of results to return */ public void setLimit(int limit) { this.limit = limit; From 1f03d66a2de5c930ccbb291da9a6edd9ce39c797 Mon Sep 17 00:00:00 2001 From: clarsen Date: Tue, 17 Jun 2014 22:52:00 -0400 Subject: [PATCH 008/826] Add overloads to parse() and parseWithMetric() in the Tags class for parsing a time series lookup query where a metric, tag, and or tagv may be missing. These overloads allow for null values in the results. Add unit test for the new overloads Add unit tests for the normal Tags.parseWithMetric() method. --- src/core/Tags.java | 74 +++++++++ test/core/TestTags.java | 322 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 394 insertions(+), 2 deletions(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index eede3e4774..0c1f4e8068 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -29,6 +29,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.utils.Pair; /** Helper functions to deal with tags. */ public final class Tags { @@ -94,6 +95,34 @@ public static void parse(final HashMap tags, tags.put(kv[0], kv[1]); } + /** + * Parses a tag into a list of key/value pairs, allowing nulls for either + * value. + * @param tags The list into which the parsed tag should be stored + * @param tag A string of the form "tag=value" or "=value" or "tag=" + * @throws IllegalArgumentException if the tag is malformed. + * @since 2.1 + */ + public static void parse(final List> tags, + final String tag) { + if (tag == null || tag.isEmpty() || tag.length() < 2) { + throw new IllegalArgumentException("Missing tag pair"); + } + if (tag.charAt(0) == '=') { + tags.add(new Pair(null, tag.substring(1))); + return; + } else if (tag.charAt(tag.length() - 1) == '=') { + tags.add(new Pair(tag.substring(0, tag.length() - 1), null)); + return; + } + + final String[] kv = splitString(tag, '='); + if (kv.length != 2 || kv[0].length() <= 0 || kv[1].length() <= 0) { + throw new IllegalArgumentException("invalid tag: " + tag); + } + tags.add(new Pair(kv[0], kv[1])); + } + /** * Parses the metric and tags out of the given string. * @param metric A string of the form "metric" or "metric{tag=value,...}". @@ -128,6 +157,51 @@ public static String parseWithMetric(final String metric, return metric.substring(0, curly); } + /** + * Parses an optional metric and tags out of the given string, any of + * which may be null. Requires at least one metric, tagk or tagv. + * @param metric A string of the form "metric" or "metric{tag=value,...}" + * or even "{tag=value,...}" where the metric may be missing. + * @param tags The list to populate with parsed tag pairs + * @return The name of the metric if it exists, null otherwise + * @throws IllegalArgumentException if the metric is malformed. + * @since 2.1 + */ + public static String parseWithMetric(final String metric, + final List> tags) { + final int curly = metric.indexOf('{'); + if (curly < 0) { + if (metric.isEmpty()) { + throw new IllegalArgumentException("Metric string was empty"); + } + return metric; + } + final int len = metric.length(); + if (metric.charAt(len - 1) != '}') { // "foo{" + throw new IllegalArgumentException("Missing '}' at the end of: " + metric); + } else if (curly == len - 2) { // "foo{}" + if (metric.charAt(0) == '{') { + throw new IllegalArgumentException("Missing metric and tags: " + metric); + } + return metric.substring(0, len - 2); + } + // substring the tags out of "foo{a=b,...,x=y}" and parse them. + for (final String tag : splitString(metric.substring(curly + 1, len - 1), + ',')) { + try { + parse(tags, tag); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("When parsing tag '" + tag + + "': " + e.getMessage()); + } + } + // Return the "foo" part of "foo{a=b,...,x=y}" + if (metric.charAt(0) == '{') { + return null; + } + return metric.substring(0, curly); + } + /** * Parses an integer value as a long from the given character sequence. *

diff --git a/test/core/TestTags.java b/test/core/TestTags.java index 39ed62e82b..ef49a16b5a 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -21,6 +21,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Pair; import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; @@ -37,6 +38,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -50,11 +53,326 @@ public final class TestTags { private TSDB tsdb; private Config config; private HBaseClient client; - private MockBase storage; + private MockBase storage = null; private UniqueId metrics = mock(UniqueId.class); private UniqueId tag_names = mock(UniqueId.class); private UniqueId tag_values = mock(UniqueId.class); + @Test + public void parseWithMetricWTag() { + final HashMap tags = new HashMap(1); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01}", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(1, tags.size()); + assertEquals("web01", tags.get("host")); + } + + @Test + public void parseWithMetricWTags() { + final HashMap tags = new HashMap(2); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01,dc=lga}", + tags); + assertEquals("sys.cpu.user", metric); + assertEquals(2, tags.size()); + assertEquals("web01", tags.get("host")); + assertEquals("lga", tags.get("dc")); + } + + @Test + public void parseWithMetricMetricOnly() { + final HashMap tags = new HashMap(0); + final String metric = Tags.parseWithMetric("sys.cpu.user", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(0, tags.size()); + } + + @Test + public void parseWithMetricMetricEmptyCurlies() { + final HashMap tags = new HashMap(0); + final String metric = Tags.parseWithMetric("sys.cpu.user{}", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(0, tags.size()); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullMetric() { + final HashMap tags = new HashMap(1); + Tags.parseWithMetric("{host=}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullTagv() { + final HashMap tags = new HashMap(1); + Tags.parseWithMetric("sys.cpu.user{host=}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullTagk() { + final HashMap tags = new HashMap(1); + Tags.parseWithMetric("sys.cpu.user{=web01}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullTagv2() { + final HashMap tags = new HashMap(2); + Tags.parseWithMetric("sys.cpu.user{host=web01,dc=}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullTagk2() { + final HashMap tags = new HashMap(2); + Tags.parseWithMetric("sys.cpu.user{host=web01,=lga}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullTagv3() { + final HashMap tags = new HashMap(3); + Tags.parseWithMetric("sys.cpu.user{host=web01,dc=,=root}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricNullTagk3() { + final HashMap tags = new HashMap(3); + Tags.parseWithMetric("sys.cpu.user{host=web01,=lga,owner=}", tags); + } + + @Test (expected = NullPointerException.class) + public void parseWithMetricNull() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric(null, tags); + } + + @Test + public void parseWithMetricEmpty() { + final HashMap tags = new HashMap(0); + assertTrue(Tags.parseWithMetric("", tags).isEmpty()); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricMissingClosingCurly() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric("sys.cpu.user{host=web01", tags); + } + + // Maybe this one should throw an exception. Usually this will be used before + // a UID lookup so it will toss an exception then. + @Test + public void parseWithMetricMissingOpeningCurly() { + final HashMap tags = new HashMap(0); + assertEquals("sys.cpu.user host=web01}", + Tags.parseWithMetric("sys.cpu.user host=web01}", tags)); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricMissingEquals() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric("sys.cpu.user{hostweb01}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricMissingComma() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric("sys.cpu.user{host=web01 dc=lga}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricTrailingComma() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric("sys.cpu.user{host=web01,}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricForwardComma() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric("sys.cpu.user{,host=web01}", tags); + } + + @Test + public void parseWithMetricListMetricOnly() { + final List> tags = + new ArrayList>(0); + final String metric = Tags.parseWithMetric("sys.cpu.user", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(0, tags.size()); + } + + @Test + public void parseWithMetricListMetricEmptyCurlies() { + final List> tags = + new ArrayList>(0); + final String metric = Tags.parseWithMetric("sys.cpu.user{}", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(0, tags.size()); + } + + @Test + public void parseWithMetricListNullMetric() { + final List> tags = + new ArrayList>(1); + final String metric = Tags.parseWithMetric("{host=}", tags); + assertNull(metric); + assertEquals(1, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertNull(tags.get(0).getValue()); + } + + @Test + public void parseWithMetricListNullTagv() { + final List> tags = + new ArrayList>(1); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=}", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(1, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertNull(tags.get(0).getValue()); + } + + @Test + public void parseWithMetricListNullTagk() { + final List> tags = new ArrayList>(1); + final String metric = Tags.parseWithMetric("sys.cpu.user{=web01}", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(1, tags.size()); + assertNull(tags.get(0).getKey()); + assertEquals("web01", tags.get(0).getValue()); + } + + @Test + public void parseWithMetricListWTag() { + final List> tags = + new ArrayList>(1); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01}", tags); + assertEquals("sys.cpu.user", metric); + assertEquals(1, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertEquals("web01", tags.get(0).getValue()); + } + + @Test + public void parseWithMetricListNullTagv2() { + final List> tags = + new ArrayList>(2); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01,dc=}", + tags); + assertEquals("sys.cpu.user", metric); + assertEquals(2, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertEquals("web01", tags.get(0).getValue()); + assertEquals("dc", tags.get(1).getKey()); + assertNull(tags.get(1).getValue()); + } + + @Test + public void parseWithMetricListNullTagk2() { + final List> tags = + new ArrayList>(2); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01,=lga}", + tags); + assertEquals("sys.cpu.user", metric); + assertEquals(2, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertEquals("web01", tags.get(0).getValue()); + assertNull(tags.get(1).getKey()); + assertEquals("lga", tags.get(1).getValue()); + } + + @Test + public void parseWithMetricListNullTagv3() { + final List> tags = + new ArrayList>(3); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01,dc=,=root}", + tags); + assertEquals("sys.cpu.user", metric); + assertEquals(3, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertEquals("web01", tags.get(0).getValue()); + assertEquals("dc", tags.get(1).getKey()); + assertNull(tags.get(1).getValue()); + assertNull(tags.get(2).getKey()); + assertEquals("root", tags.get(2).getValue()); + } + + @Test + public void parseWithMetricListNullTagk3() { + final List> tags = + new ArrayList>(3); + final String metric = Tags.parseWithMetric("sys.cpu.user{host=web01,=lga,owner=}", + tags); + assertEquals("sys.cpu.user", metric); + assertEquals(3, tags.size()); + assertEquals("host", tags.get(0).getKey()); + assertEquals("web01", tags.get(0).getValue()); + assertNull(tags.get(1).getKey()); + assertEquals("lga", tags.get(1).getValue()); + assertEquals("owner", tags.get(2).getKey()); + assertNull(tags.get(2).getValue()); + } + + @Test (expected = NullPointerException.class) + public void parseWithMetricListNull() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric(null, tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricListEmpty() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric("", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricListMissingClosingCurly() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric("sys.cpu.user{host=web01", tags); + } + + // Maybe this one should throw an exception. Usually this will be used before + // a UID lookup so it will toss an exception then. + @Test + public void parseWithMetricListMissingOpeningCurly() { + final List> tags = + new ArrayList>(0); + assertEquals("sys.cpu.user host=web01}", + Tags.parseWithMetric("sys.cpu.user host=web01}", tags)); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricListMissingEquals() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric("sys.cpu.user{hostweb01}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricListMissingComma() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric("sys.cpu.user{host=web01 dc=lga}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricListTrailingComma() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric("sys.cpu.user{host=web01,}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricListForwardComma() { + final List> tags = + new ArrayList>(0); + Tags.parseWithMetric("sys.cpu.user{,host=web01}", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricOnlyEquals() { + final HashMap tags = new HashMap(0); + Tags.parseWithMetric("{=}", tags); + } + @Test public void parseSuccessful() { final HashMap tags = new HashMap(2); @@ -66,7 +384,7 @@ public void parseSuccessful() { assertEquals("bar", tags.get("foo")); assertEquals("baz", tags.get("qux")); } - + @Test(expected=IllegalArgumentException.class) public void parseNoEqualSign() { Tags.parse(new HashMap(1), "foo"); From 92640080db1ba506aaeb8e58252db55714fd1441 Mon Sep 17 00:00:00 2001 From: clarsen Date: Tue, 17 Jun 2014 21:14:41 -0400 Subject: [PATCH 009/826] Add the TimeSeriesLookup.java for looking up time series related to a metric, tag name and/or tagk. Includes unit tests. --- Makefile.am | 2 + src/search/TimeSeriesLookup.java | 350 ++++++++++++++++++ test/search/TestTimeSeriesLookup.java | 504 ++++++++++++++++++++++++++ 3 files changed, 856 insertions(+) create mode 100644 src/search/TimeSeriesLookup.java create mode 100644 test/search/TestTimeSeriesLookup.java diff --git a/Makefile.am b/Makefile.am index 4b7c7e85af..852bcb9f82 100644 --- a/Makefile.am +++ b/Makefile.am @@ -63,6 +63,7 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ + src/search/TimeSeriesLookup.java \ src/stats/Histogram.java \ src/stats/StatsCollector.java \ src/tools/ArgP.java \ @@ -152,6 +153,7 @@ test_SRC := \ test/meta/TestUIDMeta.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ + test/search/TestTimeSeriesLookup.java \ test/stats/TestHistogram.java \ test/storage/MockBase.java \ test/tools/TestDumpSeries.java \ diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java new file mode 100644 index 0000000000..eb71489254 --- /dev/null +++ b/src/search/TimeSeriesLookup.java @@ -0,0 +1,350 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.search; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import net.opentsdb.core.Const; +import net.opentsdb.core.RowKey; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.ByteArrayPair; +import net.opentsdb.utils.Pair; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Lookup series related to a metric, tagk, tagv or any combination thereof. + * This class doesn't handle wild-card searching yet. + * + * When dealing with tags, we can lookup on tagks, tagvs or pairs. Thus: + * tagk, null <- lookup all series with a tagk + * tagk, tagv <- lookup all series with a tag pair + * null, tagv <- lookup all series with a tag value somewhere + * + * The user can supply multiple tags in a query so the logic is a little goofy + * but here it is: + * - Different tagks are AND'd, e.g. given "host=web01 dc=lga" we will lookup + * series that contain both of those tag pairs. Also when given "host= dc=" + * then we lookup series with both tag keys regardless of their values. + * - Tagks without a tagv will override tag pairs. E.g. "host=web01 host=" will + * return all series with the "host" tagk. + * - Tagvs without a tagk are OR'd. Given "=lga =phx" the lookup will fetch + * anything with either "lga" or "phx" as the value for a pair. When combined + * with a tagk, e.g. "host=web01 =lga" then it will return any series with the + * tag pair AND any tag with the "lga" value. + * + * To avoid running performance degrading regexes in HBase regions, we'll double + * filter when necessary. If tagks are present, those are used in the rowkey + * filter and a secondary filter is applied in the TSD with remaining tagvs. + * E.g. the query "host=web01 =lga" will issue a rowkey filter with "host=web01" + * then within the TSD scanner, we'll filter out only the rows that contain an + * "lga" tag value. We don't know where in a row key the tagv may fall, so we + * would have to first match on the pair, then backtrack to find the value and + * make sure the pair is skipped. Thus its easier on the region server to execute + * a simpler rowkey regex, pass all the results to the TSD, then let us filter on + * tag values only when necessary. (if a query only has tag values, then this is + * moot and we can pass them in a rowkey filter since they're OR'd). + * + * @since 2.1 + */ +public class TimeSeriesLookup { + private static final Logger LOG = + LoggerFactory.getLogger(TimeSeriesLookup.class); + + /** Charset used to convert Strings to byte arrays and back. */ + private static final Charset CHARSET = Charset.forName("ISO-8859-1"); + + /** The query with metrics and/or tags to use */ + private final SearchQuery query; + + /** Whether or not to dump the output to standard out for CLI commands */ + private boolean to_stdout; + + /** The TSD to use for lookups */ + private final TSDB tsdb; + + /** + * Default ctor + * @param tsdb The TSD to which we belong + * @param metric A metric to match on, may be null + * @param tags One or more tags to match on, may be null + */ + public TimeSeriesLookup(final TSDB tsdb, final SearchQuery query) { + this.tsdb = tsdb; + this.query = query; + } + + /** + * Lookup time series associated with the given metric, tagk, tagv or tag + * pairs. Either the meta table or the data table will be scanned. If no + * metric is given, a full table scan must be performed and this call may take + * a long time to complete. + * When dumping to stdout, if an ID can't be looked up, it will be logged and + * skipped. + * @return A list of TSUIDs matching the given lookup query. + * @throws NoSuchUniqueName if any of the given names fail to resolve to a + * UID. + */ + public List lookup() { + LOG.info(query.toString()); + final StringBuilder tagv_filter = new StringBuilder(); + final Scanner scanner = getScanner(tagv_filter); + final List tsuids = new ArrayList(); + final Pattern tagv_regex = tagv_filter.length() > 1 ? + Pattern.compile(tagv_filter.toString()) : null; + // we don't really know what size the UIDs will resolve to so just grab + // a decent amount. + final StringBuffer buf = to_stdout ? new StringBuffer(2048) : null; + final long start = System.currentTimeMillis(); + + ArrayList> rows; + byte[] last_tsuid = null; // used to avoid dupes when scanning the data table + + try { + // synchronous to avoid stack overflows when scanning across the main data + // table. + while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { + for (final ArrayList row : rows) { + final byte[] tsuid = query.useMeta() ? row.get(0).key() : + UniqueId.getTSUIDFromKey(row.get(0).key(), TSDB.metrics_width(), + Const.TIMESTAMP_BYTES); + + // TODO - there MUST be a better way than creating a ton of temp + // string objects. + if (tagv_regex != null && + !tagv_regex.matcher(new String(tsuid, CHARSET)).find()) { + continue; + } + + if (to_stdout) { + if (last_tsuid != null && Bytes.memcmp(last_tsuid, tsuid) == 0) { + continue; + } + last_tsuid = tsuid; + + try { + buf.append(UniqueId.uidToString(tsuid)).append(" "); + buf.append(RowKey.metricNameAsync(tsdb, tsuid) + .joinUninterruptibly()); + buf.append(" "); + + final List tag_ids = UniqueId.getTagPairsFromTSUID(tsuid); + final Map resolved_tags = + Tags.resolveIdsAsync(tsdb, tag_ids).joinUninterruptibly(); + for (final Map.Entry tag_pair : + resolved_tags.entrySet()) { + buf.append(tag_pair.getKey()).append("=") + .append(tag_pair.getValue()).append(" "); + } + System.out.println(buf.toString()); + } catch (NoSuchUniqueId nsui) { + LOG.error("Unable to resolve UID in TSUID (" + + UniqueId.uidToString(tsuid) + ") " + nsui.getMessage()); + } + buf.setLength(0); // reset the buffer so we can re-use it + } else { + tsuids.add(tsuid); + } + } + } + } catch (Exception e) { + throw new RuntimeException("Shouldn't be here", e); + } finally { + scanner.close(); + } + + LOG.debug("Lookup query matched " + tsuids.size() + " time series in " + + (System.currentTimeMillis() - start) + " ms"); + return tsuids; + } + + /** + * Configures the scanner for iterating over the meta or data tables. If the + * metric has been set, then we scan a small slice of the table where the + * metric lies, otherwise we have to scan the whole table. If tags are + * given then we setup a row key regex + * @return A configured scanner + */ + private Scanner getScanner(final StringBuilder tagv_filter) { + final Scanner scanner = tsdb.getClient().newScanner( + query.useMeta() ? tsdb.metaTable() : tsdb.dataTable()); + + // if a metric is given, we need to resolve it's UID and set the start key + // to the UID and the stop key to the next row by incrementing the UID. + if (query.getMetric() != null && !query.getMetric().isEmpty()) { + final byte[] metric_uid = tsdb.getUID(UniqueIdType.METRIC, + query.getMetric()); + LOG.debug("Found UID (" + UniqueId.uidToString(metric_uid) + + ") for metric (" + query.getMetric() + ")"); + scanner.setStartKey(metric_uid); + long uid = UniqueId.uidToLong(metric_uid, TSDB.metrics_width()); + uid++; // TODO - see what happens when this rolls over + scanner.setStopKey(UniqueId.longToUID(uid, TSDB.metrics_width())); + } else { + LOG.debug("Performing full table scan, no metric provided"); + } + + if (query.getTags() != null && !query.getTags().isEmpty()) { + final List pairs = + new ArrayList(query.getTags().size()); + for (Pair tag : query.getTags()) { + final byte[] tagk = tag.getKey() != null ? + tsdb.getUID(UniqueIdType.TAGK, tag.getKey()) : null; + final byte[] tagv = tag.getValue() != null ? + tsdb.getUID(UniqueIdType.TAGV, tag.getValue()) : null; + pairs.add(new ByteArrayPair(tagk, tagv)); + } + // remember, tagks are sorted in the row key so we need to supply a sorted + // regex or matching will fail. + Collections.sort(pairs); + + final short name_width = TSDB.tagk_width(); + final short value_width = TSDB.tagv_width(); + final short tagsize = (short) (name_width + value_width); + + int index = 0; + final StringBuilder buf = new StringBuilder( + 22 // "^.{N}" + "(?:.{M})*" + "$" + wiggle + + ((13 + tagsize) // "(?:.{M})*\\Q" + tagsize bytes + "\\E" + * (pairs.size()))); + buf.append("(?s)^.{").append(TSDB.metrics_width()) + .append("}"); + if (!query.useMeta()) { + buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + } + buf.append("(?:.{").append(tagsize).append("})*"); + + // at the top of the list will be the null=tagv pairs. We want to compile + // a separate regex for them. + for (; index < pairs.size(); index++) { + if (pairs.get(index).getKey() != null) { + break; + } + + if (index > 0) { + buf.append("|"); + } + buf.append("(?:.{").append(name_width).append("})"); + buf.append("\\Q"); + addId(buf, pairs.get(index).getValue()); + buf.append("\\E"); + } + buf.append("(?:.{").append(tagsize).append("})*") + .append("$"); + + if (index > 0 && index < pairs.size()) { + // we had one or more tagvs to lookup AND we have tagk or tag pairs to + // filter on, so we dump the previous regex into the tagv_filter and + // continue on with a row key + tagv_filter.append(buf.toString()); + LOG.debug("Setting tagv filter: " + buf.toString()); + } else if (index >= pairs.size()) { + // in this case we don't have any tagks to deal with so we can just + // pass the previously compiled regex to the rowkey filter of the + // scanner + scanner.setKeyRegexp(buf.toString(), CHARSET); + LOG.debug("Setting scanner row key filter with tagvs only: " + + buf.toString()); + } + + // catch any left over tagk/tag pairs + if (index < pairs.size()){ + buf.setLength(0); + buf.append("(?s)^.{").append(TSDB.metrics_width()) + .append("}"); + if (!query.useMeta()) { + buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + } + + ByteArrayPair last_pair = null; + for (; index < pairs.size(); index++) { + if (last_pair != null && last_pair.getValue() == null && + Bytes.memcmp(last_pair.getKey(), pairs.get(index).getKey()) == 0) { + // tagk=null is a wildcard so we don't need to bother adding + // tagk=tagv pairs with the same tagk. + LOG.debug("Skipping pair due to wildcard: " + pairs.get(index)); + } else if (last_pair != null && + Bytes.memcmp(last_pair.getKey(), pairs.get(index).getKey()) == 0) { + // in this case we're ORing e.g. "host=web01|host=web02" + buf.append("|\\Q"); + addId(buf, pairs.get(index).getKey()); + addId(buf, pairs.get(index).getValue()); + buf.append("\\E"); + } else { + if (last_pair != null) { + buf.append(")"); + } + // moving on to the next tagk set + buf.append("(?:.{6})*"); // catch tag pairs in between + buf.append("(?:"); + if (pairs.get(index).getKey() != null && + pairs.get(index).getValue() != null) { + buf.append("\\Q"); + addId(buf, pairs.get(index).getKey()); + addId(buf, pairs.get(index).getValue()); + buf.append("\\E"); + } else { + buf.append("\\Q"); + addId(buf, pairs.get(index).getKey()); + buf.append("\\E"); + buf.append("(?:.{").append(value_width).append("})+"); + } + } + last_pair = pairs.get(index); + } + buf.append(")(?:.{").append(tagsize).append("})*").append("$"); + + scanner.setKeyRegexp(buf.toString(), CHARSET); + LOG.debug("Setting scanner row key filter: " + buf.toString()); + } + } + return scanner; + } + + /** + * Appends the given ID to the given buffer, escaping where appropriate + * @param buf The string buffer to append to + * @param id The ID to append + */ + private static void addId(final StringBuilder buf, final byte[] id) { + boolean backslash = false; + for (final byte b : id) { + buf.append((char) (b & 0xFF)); + if (b == 'E' && backslash) { // If we saw a `\' and now we have a `E'. + // So we just terminated the quoted section because we just added \E + // to `buf'. So let's put a litteral \E now and start quoting again. + buf.append("\\\\E\\Q"); + } else { + backslash = b == '\\'; + } + } + } + + /** @param to_stdout Whether or not to dump to standard out as we scan */ + public void setToStdout(final boolean to_stdout) { + this.to_stdout = to_stdout; + } +} diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java new file mode 100644 index 0000000000..1d26ea70ca --- /dev/null +++ b/test/search/TestTimeSeriesLookup.java @@ -0,0 +1,504 @@ +// 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.search; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.meta.TSMeta; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Pair; + +import org.hbase.async.HBaseClient; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, + KeyValue.class, Scanner.class, TimeSeriesLookup.class}) +public class TestTimeSeriesLookup { + private Config config; + private TSDB tsdb = null; + private HBaseClient client = mock(HBaseClient.class); + private UniqueId metrics = mock(UniqueId.class); + private UniqueId tag_names = mock(UniqueId.class); + private UniqueId tag_values = mock(UniqueId.class); + private MockBase storage = null; + + // tsuids + private static List test_tsuids = new ArrayList(7); + static { + test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }); + test_tsuids.add(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1 }); + test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 1, 0, 0, 4, 0, 0, 5}); + test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 2, 0, 0, 4, 0, 0, 5}); + test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 1, + 0, 0, 9, 0, 0, 3}); + test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 10, + 0, 0, 9, 0, 0, 3}); + } + + @Before + public void before() throws Exception { + PowerMockito.whenNew(HBaseClient.class) + .withArguments(anyString(), anyString()).thenReturn(client); + config = new Config(false); + tsdb = new TSDB(config); + + // replace the "real" field objects with mocks + Field met = tsdb.getClass().getDeclaredField("metrics"); + met.setAccessible(true); + met.set(tsdb, metrics); + + Field tagk = tsdb.getClass().getDeclaredField("tag_names"); + tagk.setAccessible(true); + tagk.set(tsdb, tag_names); + + Field tagv = tsdb.getClass().getDeclaredField("tag_values"); + tagv.setAccessible(true); + tagv.set(tsdb, tag_values); + + // mock UniqueId + when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getId("sys.cpu.system")) + .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getId("sys.cpu.idle")).thenReturn(new byte[] { 0, 0, 3 }); + when(metrics.getId("no.values")).thenReturn(new byte[] { 0, 0, 11 }); + + when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getId("dc")) + .thenThrow(new NoSuchUniqueName("dc", "metric")); + when(tag_names.getId("owner")).thenReturn(new byte[] { 0, 0, 4 }); + + when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getId("web03")) + .thenThrow(new NoSuchUniqueName("web03", "metric")); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + } + + @Test + public void metricOnlyMeta() throws Exception { + generateMeta(); + final SearchQuery query = new SearchQuery("sys.cpu.user"); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(2, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(1), tsuids.get(1)); + } + + @Test + public void metricOnlyData() throws Exception { + generateData(); + final SearchQuery query = new SearchQuery("sys.cpu.user"); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(2, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(1), tsuids.get(1)); + } + + @Test + public void metricOnly2Meta() throws Exception { + generateMeta(); + final SearchQuery query = new SearchQuery("sys.cpu.nice"); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(2), tsuids.get(0)); + } + + @Test + public void metricOnly2Data() throws Exception { + generateData(); + final SearchQuery query = new SearchQuery("sys.cpu.nice"); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(2), tsuids.get(0)); + } + + @Test (expected = NoSuchUniqueName.class) + public void noSuchMetricMeta() throws Exception { + final SearchQuery query = new SearchQuery("sys.cpu.system"); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + lookup.lookup(); + } + + @Test + public void metricOnlyNoValuesMeta() throws Exception { + generateMeta(); + final SearchQuery query = new SearchQuery("no.values"); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(0, tsuids.size()); + } + + @Test + public void metricOnlyNoValuesData() throws Exception { + generateData(); + final SearchQuery query = new SearchQuery("no.values"); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + query.setUseMeta(false); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(0, tsuids.size()); + } + + @Test + public void tagkOnlyMeta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", null)); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(5, tsuids.size()); + for (int i = 0; i < 5; i++) { + assertArrayEquals(test_tsuids.get(i), tsuids.get(i)); + } + } + + @Test + public void tagkOnlyData() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", null)); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(5, tsuids.size()); + for (int i = 0; i < 5; i++) { + assertArrayEquals(test_tsuids.get(i), tsuids.get(i)); + } + } + + @Test + public void tagkOnly2Meta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("owner", null)); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(2, tsuids.size()); + assertArrayEquals(test_tsuids.get(3), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(4), tsuids.get(1)); + } + + @Test + public void tagkOnly2Data() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("owner", null)); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(2, tsuids.size()); + assertArrayEquals(test_tsuids.get(3), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(4), tsuids.get(1)); + } + + @Test (expected = NoSuchUniqueName.class) + public void noSuchTagkMeta() throws Exception { + final List> tags = + new ArrayList>(1); + tags.add(new Pair("dc", null)); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + lookup.lookup(); + } + + @Test + public void tagvOnlyMeta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web01")); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(4, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(2), tsuids.get(1)); + assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); + assertArrayEquals(test_tsuids.get(5), tsuids.get(3)); + } + + @Test + public void tagvOnlyData() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web01")); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(4, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(2), tsuids.get(1)); + assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); + assertArrayEquals(test_tsuids.get(5), tsuids.get(3)); + } + + @Test + public void tagvOnly2Meta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web02")); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(2, tsuids.size()); + assertArrayEquals(test_tsuids.get(1), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(4), tsuids.get(1)); + } + + @Test + public void tagvOnly2Data() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web02")); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(2, tsuids.size()); + assertArrayEquals(test_tsuids.get(1), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(4), tsuids.get(1)); + } + + @Test (expected = NoSuchUniqueName.class) + public void noSuchTagvMeta() throws Exception { + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web03")); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + lookup.lookup(); + } + + @Test + public void metricAndTagkMeta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", null)); + final SearchQuery query = new SearchQuery("sys.cpu.nice", + tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(2), tsuids.get(0)); + } + + @Test + public void metricAndTagkData() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", null)); + final SearchQuery query = new SearchQuery("sys.cpu.nice", + tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(2), tsuids.get(0)); + } + + @Test + public void metricAndTagvMeta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web02")); + final SearchQuery query = new SearchQuery("sys.cpu.idle", + tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(4), tsuids.get(0)); + } + + @Test + public void metricAndTagvData() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(null, "web02")); + final SearchQuery query = new SearchQuery("sys.cpu.idle", + tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(4), tsuids.get(0)); + } + + @Test + public void metricAndTagPairMeta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "web01")); + final SearchQuery query = new SearchQuery("sys.cpu.idle", + tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(3), tsuids.get(0)); + } + + @Test + public void metricAndTagPairData() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "web01")); + final SearchQuery query = new SearchQuery("sys.cpu.idle", + tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + query.setUseMeta(false); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(3), tsuids.get(0)); + } + + @Test + public void tagPairOnlyMeta() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "web01")); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(3, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(2), tsuids.get(1)); + assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); + } + + @Test + public void tagPairOnlyData() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "web01")); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(3, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(2), tsuids.get(1)); + assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); + } + + // TODO test the dump to stdout + + /** + * Stores some data in the mock tsdb-meta table for unit testing + */ + private void generateMeta() { + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + + final byte[] val = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }; + for (final byte[] tsuid : test_tsuids) { + storage.addColumn(tsuid, TSMeta.COUNTER_QUALIFIER(), val); + } + } + + /** + * Stores some data in the mock tsdb data table for unit testing + */ + private void generateData() { + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + + final byte[] qual = new byte[] { 0, 0 }; + final byte[] val = new byte[] { 1 }; + for (final byte[] tsuid : test_tsuids) { + byte[] row_key = new byte[tsuid.length + Const.TIMESTAMP_BYTES]; + System.arraycopy(tsuid, 0, row_key, 0, TSDB.metrics_width()); + System.arraycopy(tsuid, TSDB.metrics_width(), row_key, + TSDB.metrics_width() + Const.TIMESTAMP_BYTES, + tsuid.length - TSDB.metrics_width()); + storage.addColumn(row_key, qual, val); + } + } +} From 1635f2c9fa95aebcd9c150ed1a12c89650878a8a Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 20 Jun 2014 17:11:07 -0400 Subject: [PATCH 010/826] Implement /api/search/lookup in the SearchRpc class --- src/tsd/SearchRpc.java | 100 ++++++++++++++++++++++++- test/tsd/TestSearchRpc.java | 145 +++++++++++++++++++++++++++++++++--- 2 files changed, 231 insertions(+), 14 deletions(-) diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 72d2fcc591..3b40425bff 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -12,15 +12,30 @@ // see . package net.opentsdb.tsd; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; import net.opentsdb.search.SearchQuery; +import net.opentsdb.search.TimeSeriesLookup; import net.opentsdb.search.SearchQuery.SearchType; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Pair; /** * Handles very basic search calls by passing the user's query to the configured * search plugin and pushing the response back through the serializers. + * Also allows for time series lookups given a metric, tag name, tag value or + * combination thereof using the tsdb-meta table. * @since 2.0 */ final class SearchRpc implements HttpRpc { @@ -53,11 +68,16 @@ public void execute(TSDB tsdb, HttpQuery query) { if (query.hasContent()) { search_query = query.serializer().parseSearchQueryV1(); } else { - search_query = parseQueryString(query); + search_query = parseQueryString(query, type); } search_query.setType(type); + if (type == SearchType.LOOKUP) { + processLookup(tsdb, query, search_query); + return; + } + try { final SearchQuery results = tsdb.executeSearch(search_query).joinUninterruptibly(); @@ -72,11 +92,27 @@ public void execute(TSDB tsdb, HttpQuery query) { /** * Parses required search values from the query string * @param query The HTTP query to work with + * @param type The type of search query requested * @return A parsed SearchQuery object */ - private final SearchQuery parseQueryString(HttpQuery query) { + private final SearchQuery parseQueryString(final HttpQuery query, + final SearchType type) { final SearchQuery search_query = new SearchQuery(); + if (type == SearchType.LOOKUP) { + final String query_string = query.getRequiredQueryStringParam("m"); + search_query.setTags(new ArrayList>()); + + try { + search_query.setMetric(Tags.parseWithMetric(query_string, + search_query.getTags())); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Unable to parse query", e); + } + return search_query; + } + + // process a regular search query search_query.setQuery(query.getRequiredQueryStringParam("query")); if (query.hasQueryStringParam("limit")) { @@ -101,4 +137,64 @@ private final SearchQuery parseQueryString(HttpQuery query) { return search_query; } + + /** + * Processes a lookup query against the tsdb-meta table, returning (and + * resolving) the TSUIDs of any series that matched the query. + * @param tsdb The TSDB to which we belong + * @param query The HTTP query to work with + * @param search_query A search query configured with at least a metric + * or a list of tag pairs. If neither are set, the method will throw an error. + * @throws BadRequestException if the metric and tags are null or empty or + * a UID fails to resolve. + * @since 2.1 + */ + private void processLookup(final TSDB tsdb, final HttpQuery query, + final SearchQuery search_query) { + if (search_query.getMetric() == null && + (search_query.getTags() == null || search_query.getTags().size() < 1)) { + throw new BadRequestException( + "Missing metric and tags. Please supply at least one value."); + } + final long start = System.currentTimeMillis(); + try { + final List tsuids = + new TimeSeriesLookup(tsdb, search_query).lookup(); + + search_query.setTotalResults(tsuids.size()); + // TODO maybe track in nanoseconds so we can get a floating point. But most + // lookups will probably take a fair amount of time. + search_query.setTime(System.currentTimeMillis() - start); + + final List results = new ArrayList(tsuids.size()); + + Map series; + List tag_ids; + + // TODO - honor limit and pagination + for (final byte[] tsuid : tsuids) { + series = new HashMap((tsuid.length / 2) + 1); + try { + series.put("tsuid", UniqueId.uidToString(tsuid)); + series.put("metric", RowKey.metricNameAsync(tsdb, tsuid) + .joinUninterruptibly()); + tag_ids = UniqueId.getTagPairsFromTSUID(tsuid); + series.put("tags", Tags.resolveIdsAsync(tsdb, tag_ids) + .joinUninterruptibly()); + } catch (NoSuchUniqueId nsui) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to resolve one or more UIDs", nsui); + } catch (Exception e) { + throw new RuntimeException("Shouldn't be here", e); + } + results.add(series); + } + + search_query.setResults(results); + query.sendReply(query.serializer().formatSearchResultsV1(search_query)); + } catch (NoSuchUniqueName nsun) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to resolve one or more names", nsun); + } + } } diff --git a/test/tsd/TestSearchRpc.java b/test/tsd/TestSearchRpc.java index 0d6321d570..5232fdf512 100644 --- a/test/tsd/TestSearchRpc.java +++ b/test/tsd/TestSearchRpc.java @@ -13,24 +13,36 @@ package net.opentsdb.tsd; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyChar; +import static org.mockito.Matchers.anyList; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.lang.reflect.Field; import java.nio.charset.Charset; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; +import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; import net.opentsdb.search.SearchQuery; +import net.opentsdb.search.TimeSeriesLookup; +import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; +import net.opentsdb.utils.JSON; +import net.opentsdb.utils.Pair; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpMethod; @@ -42,18 +54,27 @@ 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 com.stumbleupon.async.Deferred; @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, HttpQuery.class}) +@PrepareForTest({TSDB.class, Config.class, HttpQuery.class, UniqueId.class, + RowKey.class, Tags.class, TimeSeriesLookup.class, SearchRpc.class}) public final class TestSearchRpc { private TSDB tsdb = null; private SearchRpc rpc = new SearchRpc(); private SearchQuery search_query = null; + private TimeSeriesLookup mock_lookup = null; private static final Charset UTF = Charset.forName("UTF-8"); + private static List test_tsuids = new ArrayList(3); + static { + test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }); + test_tsuids.add(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1 }); + } @Before public void before() throws Exception { @@ -67,7 +88,7 @@ public void constructor() { @Test public void searchTSMeta() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsmeta?query=*"); rpc.execute(tsdb, query); @@ -79,7 +100,7 @@ public void searchTSMeta() throws Exception { @Test public void searchTSMeta_Summary() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsmeta_summary?query=*"); rpc.execute(tsdb, query); @@ -91,7 +112,7 @@ public void searchTSMeta_Summary() throws Exception { @Test public void searchTSUIDs() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsuids?query=*"); rpc.execute(tsdb, query); @@ -103,7 +124,7 @@ public void searchTSUIDs() throws Exception { @Test public void searchUIDMeta() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/uidmeta?query=*"); rpc.execute(tsdb, query); @@ -115,7 +136,7 @@ public void searchUIDMeta() throws Exception { @Test public void searchAnnotation() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/annotation?query=*"); rpc.execute(tsdb, query); @@ -127,7 +148,7 @@ public void searchAnnotation() throws Exception { @Test public void searchEmptyResultSet() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/annotation?query=EMTPY"); rpc.execute(tsdb, query); @@ -139,7 +160,7 @@ public void searchEmptyResultSet() throws Exception { @Test public void searchQSParseLimit() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsmeta?query=*&limit=42"); rpc.execute(tsdb, query); @@ -149,7 +170,7 @@ public void searchQSParseLimit() throws Exception { @Test public void searchQSParseStartIndex() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsmeta?query=*&start_index=4"); rpc.execute(tsdb, query); @@ -159,7 +180,7 @@ public void searchQSParseStartIndex() throws Exception { @Test public void searchPOST() throws Exception { - setupAnswerQuery(); + setupAnswerSearchQuery(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/search/tsmeta", "{\"query\":\"*\",\"limit\":42,\"startIndex\":2}"); rpc.execute(tsdb, query); @@ -222,12 +243,57 @@ public void searchInvalidStartIndex() throws Exception { rpc.execute(tsdb, query); } + @Test + public void searchLookup() throws Exception { + setupAnswerLookupQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/search/lookup?m={host=}"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String result = query.response().getContent().toString(UTF); + assertTrue(result.contains("\"host\":\"web01\"")); + assertTrue(result.contains("\"totalResults\":3")); + } + + @Test + public void searchLookupPOST() throws Exception { + setupAnswerLookupQuery(); + SearchQuery q = new SearchQuery(); + q.setTags(new ArrayList>(2)); + q.getTags().add(new Pair("host", "web01")); + q.getTags().add(new Pair("dc", "phx")); + + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/search/lookup", "{\"tags\":[{\"key\":\"host\",\"value\":\"web01\"}]}"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String result = query.response().getContent().toString(UTF); + assertTrue(result.contains("\"host\":\"web01\"")); + assertTrue(result.contains("\"totalResults\":3")); + } + + @Test (expected = BadRequestException.class) + public void searchLookupMissingQuery() throws Exception { + setupAnswerLookupQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/search/lookup"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void searchLookupBadQuery() throws Exception { + setupAnswerLookupQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/search/lookup?m={"); + rpc.execute(tsdb, query); + } + /** * Configures an Answer to respond with when the tests call * tsdb.executeSearch(), responding to the type of query requested with valid * responses for parsing tests. */ - private void setupAnswerQuery() { + private void setupAnswerSearchQuery() { when(tsdb.executeSearch((SearchQuery)any())).thenAnswer( new Answer>() { @@ -275,7 +341,8 @@ public Deferred answer(InvocationOnMock invocation) tags_field.set(meta, tags); results.add(meta); break; - + + case LOOKUP: case TSMETA_SUMMARY: final HashMap ts = new HashMap(1); ts.put("metric", "sys.cpu.0"); @@ -284,6 +351,7 @@ public Deferred answer(InvocationOnMock invocation) tag_map.put("host", "web01"); tag_map.put("owner", "ops"); ts.put("tags", tag_map); + ts.put("tsuid", "000001000001000001"); results.add(ts); break; @@ -312,6 +380,7 @@ public Deferred answer(InvocationOnMock invocation) note.setTSUID("000001000001000001"); results.add(note); break; + } search_query.setResults(results); @@ -323,4 +392,56 @@ public Deferred answer(InvocationOnMock invocation) }); } + + @SuppressWarnings("unchecked") + private void setupAnswerLookupQuery() throws Exception { + PowerMockito.mockStatic(RowKey.class); + when(RowKey.metricNameAsync(tsdb, test_tsuids.get(0))) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(RowKey.metricNameAsync(tsdb, test_tsuids.get(1))) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(RowKey.metricNameAsync(tsdb, test_tsuids.get(2))) + .thenReturn(Deferred.fromResult("sys.cpu.nice")); + + PowerMockito.mockStatic(UniqueId.class); + final List pair_a = new ArrayList(2); + pair_a.add(new byte[] { 0, 0, 1 }); + pair_a.add(new byte[] { 0, 0, 1 }); + + final List pair_b = new ArrayList(2); + pair_b.add(new byte[] { 0, 0, 1 }); + pair_b.add(new byte[] { 0, 0, 2 }); + + when(UniqueId.getTagPairsFromTSUID(test_tsuids.get(0))) + .thenReturn(pair_a); + when(UniqueId.getTagPairsFromTSUID(test_tsuids.get(1))) + .thenReturn(pair_b); + when(UniqueId.getTagPairsFromTSUID(test_tsuids.get(2))) + .thenReturn(pair_a); + when(UniqueId.uidToString((byte[])any())).thenCallRealMethod(); + + PowerMockito.mockStatic(Tags.class); + final HashMap tags_a = new HashMap(1); + tags_a.put("host", "web01"); + + final HashMap tags_b = new HashMap(1); + tags_b.put("host", "web02"); + + when(Tags.resolveIdsAsync(tsdb, pair_a)) + .thenReturn(Deferred.fromResult(tags_a)); + when(Tags.resolveIdsAsync(tsdb, pair_b)) + .thenReturn(Deferred.fromResult(tags_b)); + + when(Tags.parseWithMetric(anyString(), anyList())).thenCallRealMethod(); + when(Tags.splitString(anyString(), anyChar())).thenCallRealMethod(); + PowerMockito.doCallRealMethod().when(Tags.class, "parse", + anyList(), anyString()); + + mock_lookup = mock(TimeSeriesLookup.class); + PowerMockito.whenNew(TimeSeriesLookup.class) + .withArguments((TSDB)any(), (SearchQuery)any()) + .thenReturn(mock_lookup); + + when(mock_lookup.lookup()).thenReturn(test_tsuids); + } } From c425203e8c73a4cbf059485e14af8011023eecde Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 20 Jun 2014 17:11:20 -0400 Subject: [PATCH 011/826] Add the Search CLI tool --- Makefile.am | 1 + src/tools/Search.java | 160 ++++++++++++++++++++++++++++++++++++++++++ tsdb.in | 3 + 3 files changed, 164 insertions(+) create mode 100644 src/tools/Search.java diff --git a/Makefile.am b/Makefile.am index 852bcb9f82..8b17442563 100644 --- a/Makefile.am +++ b/Makefile.am @@ -73,6 +73,7 @@ tsdb_SRC := \ src/tools/Fsck.java \ src/tools/MetaPurge.java \ src/tools/MetaSync.java \ + src/tools/Search.java \ src/tools/TSDMain.java \ src/tools/TextImporter.java \ src/tools/TreeSync.java \ diff --git a/src/tools/Search.java b/src/tools/Search.java new file mode 100644 index 0000000000..54db3a3cd7 --- /dev/null +++ b/src/tools/Search.java @@ -0,0 +1,160 @@ +// 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.tools; + +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.search.SearchQuery; +import net.opentsdb.search.TimeSeriesLookup; +import net.opentsdb.search.SearchQuery.SearchType; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Pair; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles searching from the command line. Enables lookups of time series + * information given a metric, tagk, tagv or combination thereof + */ +final class Search { + private static final Logger LOG = LoggerFactory.getLogger(Search.class); + + /** Prints usage. */ + static void usage(final ArgP argp, final String errmsg) { + System.err.println(errmsg); + System.err.println("Usage: search args\n" + + "Sub commands:\n" + + " lookup : Retreives a list of time series with the given\n" + + " metric, tagk, tagv or any combination thereof.\n"); + if (argp != null) { + System.err.print(argp.usage()); + } + } + + /** + * Entry point to run the search utility + * @param args Command line arguments + * @throws Exception If something goes wrong + */ + public static void main(String[] args) throws Exception { + ArgP argp = new ArgP(); + CliOptions.addCommon(argp); + argp.addOption("--use-data-table", + "Scan against the raw data table instead of the meta data table."); + args = CliOptions.parse(argp, args); + if (args == null) { + usage(argp, "Invalid usage"); + System.exit(2); + } else if (args.length < 1) { + usage(argp, "Not enough arguments"); + System.exit(2); + } + + final boolean use_data_table = argp.has("--use-data-table"); + + Config config = CliOptions.getConfig(argp); + final TSDB tsdb = new TSDB(config); + tsdb.checkNecessaryTablesExist().joinUninterruptibly(); + + int rc; + try { + rc = runCommand(tsdb, use_data_table, args); + } finally { + try { + tsdb.getClient().shutdown().joinUninterruptibly(); + LOG.info("Gracefully shutdown the TSD"); + } catch (Exception e) { + LOG.error("Unexpected exception while shutting down", e); + rc = 42; + } + } + System.exit(rc); + } + + /** + * Determines the command requested of the user can calls the appropriate + * method. + * @param tsdb The TSDB to use for communication + * @param use_data_table Whether or not lookups should be done on the full + * data table + * @param args Arguments to parse + * @return An exit code + */ + private static int runCommand(final TSDB tsdb, + final boolean use_data_table, + final String[] args) throws Exception { + final int nargs = args.length; + if (args[0].equals("lookup")) { + if (nargs < 2) { // need a query + usage(null, "Not enough arguments"); + return 2; + } + return lookup(tsdb, use_data_table, args); + } else { + usage(null, "Unknown sub command: " + args[0]); + return 2; + } + } + + /** + * Performs a time series lookup given a query like "metric tagk=tagv" where + * a list of all time series containing the given metric and tag pair will be + * dumped to standard out. Tag pairs can be given with empty tagk or tagvs to + * and the metric is option. E.g. a query of "=web01" will return all time + * series with a tag value of "web01". + * By default the lookup is performed against the tsdb-meta table. If the + * "--use_data_table" flag is supplied, the main data table will be scanned. + * @param tsdb The TSDB to use for communication + * @param use_data_table Whether or not lookups should be done on the full + * data table + * @param args Arguments to parse + * @return An exit code + */ + private static int lookup(final TSDB tsdb, + final boolean use_data_table, + final String[] args) throws Exception { + if (!use_data_table) { + tsdb.getClient().ensureTableExists( + tsdb.getConfig().getString( + "tsd.storage.hbase.meta_table")).joinUninterruptibly(); + } + + final SearchQuery query = new SearchQuery(); + query.setType(SearchType.LOOKUP); + + int index = 1; + if (!args[index].contains("=")) { + query.setMetric(args[index++]); + } + + final List> tags = + new ArrayList>(args.length - index); + for (; index < args.length; index++) { + Tags.parse(tags, args[index]); + } + query.setTags(tags); + if (use_data_table) { + query.setUseMeta(false); + LOG.warn("NOTE: Scanning the full data table may take a long time"); + } + + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + lookup.setToStdout(true); + lookup.lookup(); + return 0; + } +} diff --git a/tsdb.in b/tsdb.in index ebf6e3d6a6..f5dcae5ca9 100644 --- a/tsdb.in +++ b/tsdb.in @@ -87,6 +87,9 @@ case $1 in (scan) MAINCLASS=DumpSeries ;; + (search) + MAINCLASS=Search + ;; (uid) MAINCLASS=UidManager ;; From 499e8596be72ab5ee5bb26b3c9aad15c48e24370 Mon Sep 17 00:00:00 2001 From: clarsen Date: Mon, 23 Jun 2014 11:19:07 -0400 Subject: [PATCH 012/826] Add support for lookups with an asterisk as a wildcard. Nulls are still allowed but when writing the docs, it looks much better to write "m=*{host=*}" than "m={host=}" --- src/search/SearchQuery.java | 2 + src/search/TimeSeriesLookup.java | 7 +-- test/search/TestTimeSeriesLookup.java | 74 +++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/search/SearchQuery.java b/src/search/SearchQuery.java index 6cc78fa74d..19cac480b9 100644 --- a/src/search/SearchQuery.java +++ b/src/search/SearchQuery.java @@ -86,6 +86,7 @@ public enum SearchType { * Default ctor. Only sets use_meta to true. Other fields are left null. */ public SearchQuery() { + metric = "*"; use_meta = true; limit = 25; } @@ -106,6 +107,7 @@ public SearchQuery(final String metric) { */ public SearchQuery(final List> tags) { this.tags = tags; + metric = "*"; use_meta = true; limit = 25; } diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index eb71489254..a95e3d791a 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -194,7 +194,8 @@ private Scanner getScanner(final StringBuilder tagv_filter) { // if a metric is given, we need to resolve it's UID and set the start key // to the UID and the stop key to the next row by incrementing the UID. - if (query.getMetric() != null && !query.getMetric().isEmpty()) { + if (query.getMetric() != null && !query.getMetric().isEmpty() && + !query.getMetric().equals("*")) { final byte[] metric_uid = tsdb.getUID(UniqueIdType.METRIC, query.getMetric()); LOG.debug("Found UID (" + UniqueId.uidToString(metric_uid) + @@ -211,9 +212,9 @@ private Scanner getScanner(final StringBuilder tagv_filter) { final List pairs = new ArrayList(query.getTags().size()); for (Pair tag : query.getTags()) { - final byte[] tagk = tag.getKey() != null ? + final byte[] tagk = tag.getKey() != null && !tag.getKey().equals("*")? tsdb.getUID(UniqueIdType.TAGK, tag.getKey()) : null; - final byte[] tagv = tag.getValue() != null ? + final byte[] tagv = tag.getValue() != null && !tag.getValue().equals("*")? tsdb.getUID(UniqueIdType.TAGV, tag.getValue()) : null; pairs.add(new ByteArrayPair(tagk, tagv)); } diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index 1d26ea70ca..e302f09cfb 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -127,6 +127,17 @@ public void metricOnlyMeta() throws Exception { assertArrayEquals(test_tsuids.get(1), tsuids.get(1)); } + // returns everything + @Test + public void metricOnlyMetaStar() throws Exception { + generateMeta(); + final SearchQuery query = new SearchQuery("*"); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(7, tsuids.size()); + } + @Test public void metricOnlyData() throws Exception { generateData(); @@ -207,6 +218,22 @@ public void tagkOnlyMeta() throws Exception { } } + @Test + public void tagkOnlyMetaStar() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "*")); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(5, tsuids.size()); + for (int i = 0; i < 5; i++) { + assertArrayEquals(test_tsuids.get(i), tsuids.get(i)); + } + } + @Test public void tagkOnlyData() throws Exception { generateData(); @@ -282,6 +309,23 @@ public void tagvOnlyMeta() throws Exception { assertArrayEquals(test_tsuids.get(5), tsuids.get(3)); } + @Test + public void tagvOnlyMetaStar() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("*", "web01")); + final SearchQuery query = new SearchQuery(tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(4, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + assertArrayEquals(test_tsuids.get(2), tsuids.get(1)); + assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); + assertArrayEquals(test_tsuids.get(5), tsuids.get(3)); + } + @Test public void tagvOnlyData() throws Exception { generateData(); @@ -356,6 +400,21 @@ public void metricAndTagkMeta() throws Exception { assertArrayEquals(test_tsuids.get(2), tsuids.get(0)); } + @Test + public void metricAndTagkMetaStar() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "*")); + final SearchQuery query = new SearchQuery("sys.cpu.nice", + tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(2), tsuids.get(0)); + } + @Test public void metricAndTagkData() throws Exception { generateData(); @@ -387,6 +446,21 @@ public void metricAndTagvMeta() throws Exception { assertArrayEquals(test_tsuids.get(4), tsuids.get(0)); } + @Test + public void metricAndTagvMetaStar() throws Exception { + generateMeta(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("*", "web02")); + final SearchQuery query = new SearchQuery("sys.cpu.idle", + tags); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(4), tsuids.get(0)); + } + @Test public void metricAndTagvData() throws Exception { generateData(); From c4dd986f7ad5f4579aebc5e3bff236ee55f7d364 Mon Sep 17 00:00:00 2001 From: clarsen Date: Thu, 26 Jun 2014 15:39:45 -0400 Subject: [PATCH 013/826] Add auto_tagk and auto_tagv fields to the Config class along with defaults (to true for both) in preparation for #331. Create Config.loadStaticVariables() to load the static variables whenever a config value changes. Now overrideConfig() will reload the statics. Change setAutoMetric() so that it will update the hash map with the given value in case users access the config from the GUI and have toggled the flag via CLI. --- src/utils/Config.java | 67 +++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index e57f156bf2..9edefdac31 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -64,6 +64,12 @@ public class Config { /** tsd.core.auto_create_metrics */ private boolean auto_metric = false; + /** tsd.core.auto_create_tagk */ + private boolean auto_tagk = true; + + /** tsd.core.auto_create_tagv */ + private boolean auto_tagv = true; + /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; @@ -149,9 +155,21 @@ public boolean auto_metric() { return this.auto_metric; } + /** @return the auto_tagk value */ + public boolean auto_tagk() { + return auto_tagk; + } + + /** @return the auto_tagv value */ + public boolean auto_tagv() { + return auto_tagv; + } + /** @param auto_metric whether or not to auto create metrics */ public void setAutoMetric(boolean auto_metric) { this.auto_metric = auto_metric; + properties.put("tsd.core.auto_create_metrics", + Boolean.toString(auto_metric)); } /** @return the enable_compaction value */ @@ -205,16 +223,18 @@ public boolean enable_tree_processing() { } /** - * Allows for modifying properties after loading + * Allows for modifying properties after creation or loading. * * WARNING: This should only be used on initialization and is meant for - * command line overrides + * command line overrides. Also note that it will reset all static config + * variables when called. * * @param property The name of the property to override * @param value The value to store */ public void overrideConfig(final String property, final String value) { this.properties.put(property, value); + loadStaticVariables(); } /** @@ -397,6 +417,8 @@ protected void setDefaults() { default_map.put("tsd.network.keep_alive", "true"); default_map.put("tsd.network.reuse_address", "true"); default_map.put("tsd.core.auto_create_metrics", "false"); + default_map.put("tsd.core.auto_create_tagks", "true"); + default_map.put("tsd.core.auto_create_tagvs", "true"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); @@ -430,21 +452,7 @@ protected void setDefaults() { properties.put(entry.getKey(), entry.getValue()); } - // set statics - auto_metric = this.getBoolean("tsd.core.auto_create_metrics"); - enable_compactions = this.getBoolean("tsd.storage.enable_compaction"); - enable_chunked_requests = this.getBoolean("tsd.http.request.enable_chunked"); - enable_realtime_ts = this.getBoolean("tsd.core.meta.enable_realtime_ts"); - enable_realtime_uid = this.getBoolean("tsd.core.meta.enable_realtime_uid"); - enable_tsuid_incrementing = - this.getBoolean("tsd.core.meta.enable_tsuid_incrementing"); - enable_tsuid_tracking = - this.getBoolean("tsd.core.meta.enable_tsuid_tracking"); - if (this.hasProperty("tsd.http.request.max_chunk")) { - max_chunked_requests = this.getInt("tsd.http.request.max_chunk"); - } - enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); - fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); + loadStaticVariables(); } /** @@ -525,7 +533,30 @@ protected void loadConfig(final String file) throws FileNotFoundException, } /** - * Calld from {@link #loadConfig} to copy the properties into the hash map + * Loads the static class variables for values that are called often. This + * should be called any time the configuration changes. + */ + protected void loadStaticVariables() { + auto_metric = this.getBoolean("tsd.core.auto_create_metrics"); + 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"); + enable_chunked_requests = this.getBoolean("tsd.http.request.enable_chunked"); + enable_realtime_ts = this.getBoolean("tsd.core.meta.enable_realtime_ts"); + enable_realtime_uid = this.getBoolean("tsd.core.meta.enable_realtime_uid"); + enable_tsuid_incrementing = + this.getBoolean("tsd.core.meta.enable_tsuid_incrementing"); + enable_tsuid_tracking = + this.getBoolean("tsd.core.meta.enable_tsuid_tracking"); + if (this.hasProperty("tsd.http.request.max_chunk")) { + max_chunked_requests = this.getInt("tsd.http.request.max_chunk"); + } + enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); + fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); + } + + /** + * Called from {@link #loadConfig} to copy the properties into the hash map * Tsuna points out that the Properties class is much slower than a hash * map so if we'll be looking up config values more than once, a hash map * is the way to go From 5b205e82a0ed3a69de2d4b9631386f586fe00106 Mon Sep 17 00:00:00 2001 From: clarsen Date: Thu, 26 Jun 2014 15:40:51 -0400 Subject: [PATCH 014/826] Modify Tags.resolveAllInternal() to use the new config flags to allow or block UID assignment for tagks and tagvs. This fixes #331. Also add unit tests for the UID creation. --- src/core/Tags.java | 4 +- test/core/TestTags.java | 105 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index 0c1f4e8068..5c96bf7809 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -455,10 +455,10 @@ static ArrayList resolveAllInternal(final TSDB tsdb, throws NoSuchUniqueName { final ArrayList tag_ids = new ArrayList(tags.size()); for (final Map.Entry entry : tags.entrySet()) { - final byte[] tag_id = (create + final byte[] tag_id = (create && tsdb.getConfig().auto_tagk() ? tsdb.tag_names.getOrCreateId(entry.getKey()) : tsdb.tag_names.getId(entry.getKey())); - final byte[] value_id = (create + final byte[] value_id = (create && tsdb.getConfig().auto_tagv() ? tsdb.tag_values.getOrCreateId(entry.getValue()) : tsdb.tag_values.getId(entry.getValue())); final byte[] thistag = new byte[tag_id.length + value_id.length]; diff --git a/test/core/TestTags.java b/test/core/TestTags.java index ef49a16b5a..f1ae95b992 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -16,9 +16,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; import net.opentsdb.utils.Pair; @@ -36,6 +38,7 @@ import com.stumbleupon.async.Deferred; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -543,6 +546,92 @@ public void resolveIdsAsyncWrongLength() throws Exception { ids.add(new byte[] { 0, 0, 1, 0, 0, 0, 2 }); Tags.resolveIdsAsync(tsdb, ids).joinUninterruptibly(); } + + @Test + public void resolveOrCreateAllCreate() throws Exception { + setupStorage(); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("host", "web01"); + final List uids = Tags.resolveOrCreateAll(tsdb, tags); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1, 0, 0, 1}, uids.get(0)); + } + + @Test + public void resolveOrCreateTagkAllowed() throws Exception { + setupStorage(); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("doesnotexist", "web01"); + final List uids = Tags.resolveOrCreateAll(tsdb, tags); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 3, 0, 0, 1}, uids.get(0)); + } + + @Test + public void resolveOrCreateTagkNotAllowedGood() throws Exception { + setupStorage(); + config.overrideConfig("tsd.core.auto_create_tagks", "false"); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("pop", "web01"); + final List uids = Tags.resolveOrCreateAll(tsdb, tags); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 2, 0, 0, 1}, uids.get(0)); + } + + @Test (expected = NoSuchUniqueName.class) + public void resolveOrCreateTagkNotAllowedBlocked() throws Exception { + setupStorage(); + config.overrideConfig("tsd.core.auto_create_tagks", "false"); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("nonesuch", "web01"); + Tags.resolveOrCreateAll(tsdb, tags); + } + + @Test + public void resolveOrCreateTagvAllowed() throws Exception { + setupStorage(); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("host", "nohost"); + final List uids = Tags.resolveOrCreateAll(tsdb, tags); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1, 0, 0, 3}, uids.get(0)); + } + + @Test + public void resolveOrCreateTagvNotAllowedGood() throws Exception { + setupStorage(); + config.overrideConfig("tsd.core.auto_create_tagvs", "false"); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("host", "web02"); + final List uids = Tags.resolveOrCreateAll(tsdb, tags); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1, 0, 0, 2}, uids.get(0)); + } + + @Test (expected = NoSuchUniqueName.class) + public void resolveOrCreateTagvNotAllowedBlocked() throws Exception { + setupStorage(); + config.overrideConfig("tsd.core.auto_create_tagvs", "false"); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("host", "invalidhost"); + Tags.resolveOrCreateAll(tsdb, tags); + } + + // PRIVATE helpers to setup unit tests private void setupStorage() throws Exception { config = new Config(false); @@ -583,4 +672,20 @@ private void setupResolveIds() throws Exception { when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) .thenThrow(new NoSuchUniqueId("tagv", new byte[] { 0, 0, 2 })); } + + private void setupResolveAll() throws Exception { + when(tag_names.getOrCreateId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getOrCreateId("doesnotexist")) + .thenReturn(new byte[] { 0, 0, 3 }); + when(tag_names.getId("pop")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_names.getId("nonesuch")) + .thenThrow(new NoSuchUniqueName("tagv", "nonesuch")); + + when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getOrCreateId("nohost")) + .thenReturn(new byte[] { 0, 0, 3 }); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getId("invalidhost")) + .thenThrow(new NoSuchUniqueName("tagk", "invalidhost")); + } } From e72acfd78651999898d8abf578a445adbd983c23 Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 27 Jun 2014 15:07:39 -0400 Subject: [PATCH 015/826] Add column timestamp support in MockBase. It will now store values with a monotonically increasing timestamp or use a timestamp supplied by the caller. It will return the latest column value in the same way HBase does by default. Future updates should include support for returning multiple values. But this is good enough for testing issues like duplicate data points. --- test/storage/MockBase.java | 323 ++++++++++++++++++++++++++++--------- 1 file changed, 248 insertions(+), 75 deletions(-) diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index ae85270cdd..48d44fc410 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -22,8 +22,11 @@ import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -48,16 +51,20 @@ /** * Mock HBase implementation useful in testing calls to and from storage with - * actual pretend data. The underlying data store is the ByteMap from Asyncbase - * so it stores and orders byte arrays similar to HBase. + * actual pretend data. The underlying data store is an incredibly ugly nesting + * of ByteMaps from AsyncHbase so it stores and orders byte arrays similar to + * HBase. A MockBase instance represents a SINGLE table in HBase but it provides + * support for column families and timestamped entries. *

* It's not a perfect mock but is useful for the majority of unit tests. Gets, * puts, cas, deletes and scans are currently supported. See notes for each * inner class below about what does and doesn't work. *

- * Note: At this time, the implementation does not support multiple - * column families since almost all unit tests for OpenTSDB only work with one - * CF at a time. There is also only one table and we don't have any timestamps. + * Regarding timestamps, whenever you execute an RPC request, the + * {@code current_timestamp} will be incremented by one millisecond. By default + * the timestamp starts at 1/1/2014 00:00:00 but you can set it to any value + * at any time. If a PutRequest comes in with a specific time, that time will + * be stored and the timestamp will not be incremented. *

* Warning: To use this class, you need to prepare the classes for testing * with the @PrepareForTest annotation. The classes you need to prepare are: @@ -75,11 +82,16 @@ public final class MockBase { private static final Charset ASCII = Charset.forName("ISO-8859-1"); private TSDB tsdb; - private Bytes.ByteMap>> storage = - new Bytes.ByteMap>>(); + + // KEY Column Family Qualifier Timestamp Value + private Bytes.ByteMap>>> + storage = new Bytes.ByteMap>>>(); private HashSet scanners = new HashSet(2); private byte[] default_family; + /** Incremented every time a new value is stored (without a timestamp) */ + private long current_timestamp = 1388534400000L; + /** * Setups up mock intercepts for all of the calls. Depending on the given * flags, some mocks may not be enabled, allowing local unit tests to setup @@ -183,6 +195,16 @@ public void setFamily(final byte[] family) { this.default_family = family; } + /** @param timestamp The timestamp to use for further storage increments */ + public void setCurrentTimestamp(final long timestamp) { + this.current_timestamp = timestamp; + } + + /** @return the incrementing timestamp */ + public long getCurrentTimestamp() { + return current_timestamp; + } + /** * Add a column to the hash table using the default column family. * The proper row will be created if it doesn't exist. If the column already @@ -193,7 +215,7 @@ public void setFamily(final byte[] family) { */ public void addColumn(final byte[] key, final byte[] qualifier, final byte[] value) { - addColumn(key, default_family, qualifier, value); + addColumn(key, default_family, qualifier, value, current_timestamp++); } /** @@ -207,18 +229,45 @@ public void addColumn(final byte[] key, final byte[] qualifier, */ public void addColumn(final byte[] key, final byte[] family, final byte[] qualifier, final byte[] value) { - Bytes.ByteMap> row = storage.get(key); + addColumn(key, family, qualifier, value, current_timestamp++); + } + + /** + * Add a column to the hash table + * The proper row will be created if it doesn't exist. If the column already + * exists, the original value will be overwritten with the new data + * @param key The row key + * @param family The column family to store the value in + * @param qualifier The qualifier + * @param value The value to store + * @param timestamp The timestamp to store + */ + public void addColumn(final byte[] key, final byte[] family, + final byte[] qualifier, final byte[] value, final long timestamp) { + // AsyncHBase will throw an NPE if the user tries to write a NULL value + // so we better do the same. An empty value is ok though, i.e. new byte[] {} + if (value == null) { + throw new NullPointerException(); + } + + Bytes.ByteMap>> row = storage.get(key); if (row == null) { - row = new Bytes.ByteMap>(); + row = new Bytes.ByteMap>>(); storage.put(key, row); } - Bytes.ByteMap cf = row.get(family); + Bytes.ByteMap> cf = row.get(family); if (cf == null) { - cf = new Bytes.ByteMap(); + cf = new Bytes.ByteMap>(); row.put(family, cf); } - cf.put(qualifier, value); + TreeMap column = cf.get(qualifier); + if (column == null) { + // remember, most recent at the top! + column = new TreeMap(Collections.reverseOrder()); + cf.put(qualifier, column); + } + column.put(timestamp, value); } /** @return TTotal number of rows in the hash table */ @@ -232,7 +281,8 @@ public int numRows() { * @return -1 if the row did not exist, otherwise the number of column families. */ public int numColumnFamilies(final byte[] key) { - final Bytes.ByteMap> row = storage.get(key); + final Bytes.ByteMap>> row = + storage.get(key); if (row == null) { return -1; } @@ -245,12 +295,13 @@ public int numColumnFamilies(final byte[] key) { * @return -1 if the row did not exist, otherwise the number of columns. */ public long numColumns(final byte[] key) { - final Bytes.ByteMap> row = storage.get(key); + final Bytes.ByteMap>> row = + storage.get(key); if (row == null) { return -1; } long size = 0; - for (Map.Entry> entry : row) { + for (Map.Entry>> entry : row) { size += entry.getValue().size(); } return size; @@ -263,11 +314,12 @@ public long numColumns(final byte[] key) { * @return -1 if the row did not exist, otherwise the number of columns. */ public int numColumnsInFamily(final byte[] key, final byte[] family) { - final Bytes.ByteMap> row = storage.get(key); + final Bytes.ByteMap>> row = + storage.get(key); if (row == null) { return -1; } - final Bytes.ByteMap cf = row.get(family); + final Bytes.ByteMap> cf = row.get(family); if (cf == null) { return -1; } @@ -275,7 +327,7 @@ public int numColumnsInFamily(final byte[] key, final byte[] family) { } /** - * Retrieve the contents of a single column with the default family + * Retrieve the most recent contents of a single column with the default family * @param key The row key of the column * @param qualifier The column qualifier * @return The byte array of data or null if not found @@ -285,7 +337,7 @@ public byte[] getColumn(final byte[] key, final byte[] qualifier) { } /** - * Retrieve the contents of a single column + * Retrieve the most recent contents of a single column * @param key The row key of the column * @param family The column family * @param qualifier The column qualifier @@ -293,30 +345,83 @@ public byte[] getColumn(final byte[] key, final byte[] qualifier) { */ public byte[] getColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final Bytes.ByteMap> row = storage.get(key); + final Bytes.ByteMap>> row = + storage.get(key); if (row == null) { return null; } - final Bytes.ByteMap cf = row.get(family); + final Bytes.ByteMap> cf = row.get(family); if (cf == null) { return null; } - return cf.get(qualifier); + final TreeMap column = cf.get(qualifier); + if (column == null) { + return null; + } + return column.firstEntry().getValue(); } /** - * Returns all of the columns for a given column family + * Retrieve the full map of timestamps and values of a single column with + * the default family + * @param key The row key of the column + * @param qualifier The column qualifier + * @return The byte array of data or null if not found + */ + public TreeMap getFullColumn(final byte[] key, + final byte[] qualifier) { + return getFullColumn(key, default_family, qualifier); + } + + /** + * Retrieve the full map of timestamps and values of a single column + * @param key The row key of the column + * @param family The column family + * @param qualifier The column qualifier + * @return The tree map of timestamps and values or null if not found + */ + public TreeMap getFullColumn(final byte[] key, + final byte[] family, final byte[] qualifier) { + final Bytes.ByteMap>> row = + storage.get(key); + if (row == null) { + return null; + } + final Bytes.ByteMap> cf = row.get(family); + if (cf == null) { + return null; + } + final TreeMap column = cf.get(qualifier); + if (column == null) { + return null; + } + return column; + } + + /** + * Returns the most recent value from all columns for a given column family * @param key The row key * @param family The column family ID - * @return A hash of columns if the CF was found, null if no such CF + * @return A map of columns if the CF was found, null if no such CF */ public Bytes.ByteMap getColumnFamily(final byte[] key, final byte[] family) { - final Bytes.ByteMap> row = storage.get(key); + final Bytes.ByteMap>> row = + storage.get(key); if (row == null) { return null; } - return row.get(family); + final Bytes.ByteMap> cf = row.get(family); + if (cf == null) { + return null; + } + // convert to a byte map + final Bytes.ByteMap columns = new Bytes.ByteMap(); + for (Map.Entry> entry : cf.entrySet()) { + // the map should never be null + columns.put(entry.getKey(), entry.getValue().firstEntry().getValue()); + } + return columns; } /** @@ -347,7 +452,7 @@ public void flushRow(final byte[] key) { * @param family The family to remove */ public void flushFamily(final byte[] family) { - for (Map.Entry>> row : + for (Map.Entry>>> row : storage.entrySet()) { row.getValue().remove(family); } @@ -361,11 +466,12 @@ public void flushFamily(final byte[] family) { */ public void flushColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final Bytes.ByteMap> row = storage.get(key); + final Bytes.ByteMap>> row = + storage.get(key); if (row == null) { return; } - final Bytes.ByteMap cf = row.get(family); + final Bytes.ByteMap> cf = row.get(family); if (cf == null) { return; } @@ -390,25 +496,27 @@ public void dumpToSystemOut(final boolean ascii) { return; } - for (Map.Entry>> row : + for (Map.Entry>>> row : storage.entrySet()) { System.out.println("[Row] " + (ascii ? new String(row.getKey(), ASCII) : bytesToString(row.getKey()))); - for (Map.Entry> cf : + for (Map.Entry>> cf : row.getValue().entrySet()) { final String family = ascii ? new String(cf.getKey(), ASCII) : bytesToString(cf.getKey()); System.out.println(" [CF] " + family); - for (Map.Entry column : cf.getValue().entrySet()) { + for (Map.Entry> column : cf.getValue().entrySet()) { System.out.println(" [Qual] " + (ascii ? "\"" + new String(column.getKey(), ASCII) + "\"" : bytesToString(column.getKey()))); - System.out.println(" [Value] " + (ascii ? - new String(column.getValue(), ASCII) - : bytesToString(column.getValue()))); + for (Map.Entry cell : column.getValue().entrySet()) { + System.out.println(" [TS] " + cell.getKey() + " [Value] " + + (ascii ? new String(cell.getValue(), ASCII) + : bytesToString(cell.getValue()))); + } } } } @@ -462,7 +570,8 @@ public static byte[] concatByteArrays(final byte[]... arrays) { /** * Gets one or more columns from a row. If the row does not exist, a null is - * returned. If no qualifiers are given, the entire row is returned. + * returned. If no qualifiers are given, the entire row is returned. + * NOTE: all timestamp, value pairs are returned. */ private class MockGet implements Answer>> { @Override @@ -471,7 +580,8 @@ public Deferred> answer(InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final GetRequest get = (GetRequest)args[0]; - final Bytes.ByteMap> row = storage.get(get.key()); + final Bytes.ByteMap>> row = + storage.get(get.key()); if (row == null) { return Deferred.fromResult((ArrayList)null); @@ -493,7 +603,8 @@ public Deferred> answer(InvocationOnMock invocation) } final ArrayList kvs = new ArrayList(row.size()); - for (Map.Entry> cf : row.entrySet()) { + for (Map.Entry>> cf : + row.entrySet()) { // column family filter if (family != null && family.length > 0 && @@ -501,15 +612,19 @@ public Deferred> answer(InvocationOnMock invocation) continue; } - for (Map.Entry entry : cf.getValue().entrySet()) { + for (Map.Entry> column : + cf.getValue().entrySet()) { // qualifier filter - if (!qualifiers.isEmpty() && !qualifiers.containsKey(entry.getKey())) { + if (!qualifiers.isEmpty() && !qualifiers.containsKey(column.getKey())) { continue; } + // TODO - if we want to support multiple values, iterate over the + // tree map. Otherwise Get returns just the latest value. KeyValue kv = mock(KeyValue.class); - when(kv.value()).thenReturn(entry.getValue()); - when(kv.qualifier()).thenReturn(entry.getKey()); + when(kv.timestamp()).thenReturn(column.getValue().firstKey()); + when(kv.value()).thenReturn(column.getValue().firstEntry().getValue()); + when(kv.qualifier()).thenReturn(column.getKey()); when(kv.key()).thenReturn(get.key()); kvs.add(kv); } @@ -529,20 +644,28 @@ public Deferred answer(final InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final PutRequest put = (PutRequest)args[0]; - Bytes.ByteMap> row = storage.get(put.key()); + Bytes.ByteMap>> row = + storage.get(put.key()); if (row == null) { - row = new Bytes.ByteMap>(); + row = new Bytes.ByteMap>>(); storage.put(put.key(), row); } - Bytes.ByteMap cf = row.get(put.family()); + Bytes.ByteMap> cf = row.get(put.family()); if (cf == null) { - cf = new Bytes.ByteMap(); + cf = new Bytes.ByteMap>(); row.put(put.family(), cf); } for (int i = 0; i < put.qualifiers().length; i++) { - cf.put(put.qualifiers()[i], put.values()[i]); + TreeMap column = cf.get(put.qualifiers()[i]); + if (column == null) { + column = new TreeMap(Collections.reverseOrder()); + cf.put(put.qualifiers()[i], column); + } + + column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : + current_timestamp++, put.values()[i]); } return Deferred.fromResult(true); @@ -567,29 +690,38 @@ public Deferred answer(final InvocationOnMock invocation) final PutRequest put = (PutRequest)args[0]; final byte[] expected = (byte[])args[1]; - Bytes.ByteMap> row = storage.get(put.key()); + Bytes.ByteMap>> row = + storage.get(put.key()); if (row == null) { if (expected != null && expected.length > 0) { return Deferred.fromResult(false); } - row = new Bytes.ByteMap>(); + row = new Bytes.ByteMap>>(); storage.put(put.key(), row); } - Bytes.ByteMap cf = row.get(put.family()); + Bytes.ByteMap> cf = row.get(put.family()); if (cf == null) { if (expected != null && expected.length > 0) { return Deferred.fromResult(false); } - cf = new Bytes.ByteMap(); + cf = new Bytes.ByteMap>(); row.put(put.family(), cf); } // CAS can only operate on one cell, so if the put request has more than // one, we ignore any but the first - final byte[] stored = cf.get(put.qualifiers()[0]); + TreeMap column = cf.get(put.qualifiers()[0]); + if (column == null && (expected != null && expected.length > 0)) { + return Deferred.fromResult(false); + } + // if a timestamp was specified, maybe we're CASing against a specific + // cell. Otherwise we deal with the latest value + final byte[] stored = column == null ? null : + put.timestamp() != Long.MAX_VALUE ? column.get(put.timestamp()) : + column.firstEntry().getValue(); if (stored == null && (expected != null && expected.length > 0)) { return Deferred.fromResult(false); } @@ -602,7 +734,12 @@ public Deferred answer(final InvocationOnMock invocation) } // passed CAS! - cf.put(put.qualifiers()[0], put.value()); + if (column == null) { + column = new TreeMap(Collections.reverseOrder()); + cf.put(put.qualifiers()[0], column); + } + column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : + current_timestamp++, put.value()); return Deferred.fromResult(true); } @@ -620,7 +757,8 @@ public Deferred answer(InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final DeleteRequest delete = (DeleteRequest)args[0]; - Bytes.ByteMap> row = storage.get(delete.key()); + Bytes.ByteMap>> row = + storage.get(delete.key()); if (row == null) { return Deferred.fromResult(null); } @@ -658,8 +796,9 @@ public Deferred answer(InvocationOnMock invocation) return Deferred.fromResult(new Object()); } - ArrayList cf_removals = new ArrayList(row.entrySet().size()); - for (Map.Entry> cf : row.entrySet()) { + List cf_removals = new ArrayList(row.entrySet().size()); + for (Map.Entry>> cf : + row.entrySet()) { // column family filter if (family != null && family.length > 0 && @@ -668,7 +807,35 @@ public Deferred answer(InvocationOnMock invocation) } for (byte[] qualifier : qualifiers.keySet()) { - cf.getValue().remove(qualifier); + final TreeMap column = cf.getValue().get(qualifier); + if (column == null) { + continue; + } + + // with this flag we delete a single timestamp + if (delete.deleteAtTimestampOnly()) { + if (column != null) { + column.remove(delete.timestamp()); + if (column.isEmpty()) { + cf.getValue().remove(qualifier); + } + } + } else { + // otherwise we delete everything less than or equal to the + // delete timestamp + List column_removals = new ArrayList(column.size()); + for (Map.Entry cell : column.entrySet()) { + if (cell.getKey() <= delete.timestamp()) { + column_removals.add(cell.getKey()); + } + } + for (Long ts : column_removals) { + column.remove(ts); + } + if (column.isEmpty()) { + cf.getValue().remove(qualifier); + } + } } if (cf.getValue().isEmpty()) { @@ -814,7 +981,7 @@ public Deferred>> answer( // return all matches ArrayList> results = new ArrayList>(); - for (Map.Entry>> row : + for (Map.Entry>>> row : storage.entrySet()) { // if it's before the start row, after the end row or doesn't @@ -835,7 +1002,7 @@ public Deferred>> answer( // loop on the column families final ArrayList kvs = new ArrayList(row.getValue().size()); - for (Map.Entry> cf : + for (Map.Entry>> cf : row.getValue().entrySet()) { // column family filter @@ -844,22 +1011,24 @@ public Deferred>> answer( continue; } - for (Map.Entry entry : cf.getValue().entrySet()) { + for (Map.Entry> column : + cf.getValue().entrySet()) { // if the qualifier isn't in the set, continue if (scnr_qualifiers != null && - !scnr_qualifiers.contains(bytesToString(entry.getKey()))) { + !scnr_qualifiers.contains(bytesToString(column.getKey()))) { continue; } KeyValue kv = mock(KeyValue.class); when(kv.key()).thenReturn(row.getKey()); - when(kv.value()).thenReturn(entry.getValue()); - when(kv.qualifier()).thenReturn(entry.getKey()); + when(kv.value()).thenReturn(column.getValue().firstEntry().getValue()); + when(kv.qualifier()).thenReturn(column.getKey()); + when(kv.timestamp()).thenReturn(column.getValue().firstKey()); when(kv.family()).thenReturn(cf.getKey()); when(kv.toString()).thenReturn("[k '" + bytesToString(row.getKey()) + - "' q '" + bytesToString(entry.getKey()) + "' v '" + - bytesToString(entry.getValue()) + "']"); + "' q '" + bytesToString(column.getKey()) + "' v '" + + bytesToString(column.getValue().firstEntry().getValue()) + "']"); kvs.add(kv); } @@ -878,7 +1047,7 @@ public Deferred>> answer( } /** - * Creates or increments (possibly decremnts) a Long in the hash table at the + * Creates or increments (possibly decrements) a Long in the hash table at the * given location. */ private class MockAtomicIncrement implements @@ -889,26 +1058,30 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final AtomicIncrementRequest air = (AtomicIncrementRequest)args[0]; final long amount = air.getAmount(); - Bytes.ByteMap> row = storage.get(air.key()); + Bytes.ByteMap>> row = + storage.get(air.key()); if (row == null) { - row = new Bytes.ByteMap>(); + row = new Bytes.ByteMap>>(); storage.put(air.key(), row); } - Bytes.ByteMap cf = row.get(air.family()); + Bytes.ByteMap> cf = row.get(air.family()); if (cf == null) { - cf = new Bytes.ByteMap(); + cf = new Bytes.ByteMap>(); row.put(air.family(), cf); } - if (!cf.containsKey(air.qualifier())) { - cf.put(air.qualifier(), Bytes.fromLong(amount)); + TreeMap column = cf.get(air.qualifier()); + if (column == null) { + column = new TreeMap(Collections.reverseOrder()); + cf.put(air.qualifier(), column); + column.put(current_timestamp++, Bytes.fromLong(amount)); return Deferred.fromResult(amount); } - long incremented_value = Bytes.getLong(cf.get(air.qualifier())); + long incremented_value = Bytes.getLong(column.firstEntry().getValue()); incremented_value += amount; - cf.put(air.qualifier(), Bytes.fromLong(incremented_value)); + column.put(column.firstKey(), Bytes.fromLong(incremented_value)); return Deferred.fromResult(incremented_value); } From 1848ea468b45d802505a19b9d733852079554407 Mon Sep 17 00:00:00 2001 From: Andreas Falk Date: Wed, 2 Jul 2014 21:27:18 +0200 Subject: [PATCH 016/826] Compare strings using equals --- src/tree/Branch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree/Branch.java b/src/tree/Branch.java index 0ed34a43c9..db817916e9 100644 --- a/src/tree/Branch.java +++ b/src/tree/Branch.java @@ -171,7 +171,7 @@ public boolean equals(Object obj) { } final Branch branch = (Branch)obj; - return display_name == branch.display_name; + return display_name.equals(branch.display_name); } /** From 940e9df45c8914f4cc8f2e069e606562d108c93e Mon Sep 17 00:00:00 2001 From: Andreas Falk Date: Wed, 2 Jul 2014 21:44:59 +0200 Subject: [PATCH 017/826] Add a simple travis configuration --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..475bf0c4cb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: java +before_script: ./build.sh pom.xml && mvn dependency:go-offline +script: mvn test +jdk: + - oraclejdk7 + - openjdk6 +notifications: + email: false From 7681c2a23b12387f69451dd7755a789de842ea3a Mon Sep 17 00:00:00 2001 From: clarsen Date: Mon, 7 Jul 2014 15:36:57 -0400 Subject: [PATCH 018/826] Fix line endings in the log for fsck when handling duplicate timestamps. Add some fsck unit tests for stand-alone data points that are too short. --- src/tools/Fsck.java | 2 +- test/tools/TestFsck.java | 86 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 31a371b3c5..c1b0905c49 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -653,7 +653,7 @@ private void fsckDataPoints(final Map> datapoints) } } index++; - if (index < datapoints.size()) { + if (index < time_map.getValue().size()) { buf.append("\n"); } last_dp = dp; diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 14a2349154..dda615a323 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -660,7 +660,7 @@ public void valueTooLongMS() throws Exception { final byte[] qual1 = { 0x00, 0x07 }; final byte[] val1 = Bytes.fromLong(4L); - final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x0B }; + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 5 }; storage.addColumn(ROW, qual1, val1); storage.addColumn(ROW, qual2, val2); @@ -680,7 +680,7 @@ public void valueTooLongMSFix() throws Exception { final byte[] qual1 = { 0x00, 0x00 }; final byte[] val1 = { 4 }; - final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x0B }; + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 5 }; storage.addColumn(ROW, qual1, val1); storage.addColumn(ROW, qual2, val2); @@ -695,6 +695,88 @@ public void valueTooLongMSFix() throws Exception { assertNull(storage.getColumn(ROW, qual2)); } + @Test + public void valueTooShort() throws Exception { + when(options.fix()).thenReturn(true); + + final byte[] qual1 = { 0x00, 0x00 }; + final byte[] val1 = { 4 } ; + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = new byte[] { 0, 0, 0, 5 }; + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(1, fsck.bad_values.get()); + } + + @Test + public void valueTooShortFix() throws Exception { + when(options.fix()).thenReturn(true); + when(options.deleteBadValues()).thenReturn(true); + + final byte[] qual1 = { 0x00, 0x00 }; + final byte[] val1 = { 4 } ; + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = new byte[] { 0, 0, 0, 5 }; + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(1, fsck.bad_values.get()); + assertArrayEquals(val1, storage.getColumn(ROW, qual1)); + assertNull(storage.getColumn(ROW, qual2)); + } + + @Test + public void valueTooShortMS() throws Exception { + when(options.fix()).thenReturn(true); + + final byte[] qual1 = { 0x00, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x03 }; + final byte[] val2 = new byte[] { 0, 0, 5 }; + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(1, fsck.bad_values.get()); + } + + @Test + public void valueTooShortMSFix() throws Exception { + when(options.fix()).thenReturn(true); + when(options.deleteBadValues()).thenReturn(true); + + final byte[] qual1 = { 0x00, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x03 }; + final byte[] val2 = new byte[] { 0, 0, 5 }; + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(1, fsck.bad_values.get()); + assertArrayEquals(val1, storage.getColumn(ROW, qual1)); + assertNull(storage.getColumn(ROW, qual2)); + } + @Test public void float8byteVal4byteQual() throws Exception { final byte[] qual1 = { 0x00, 0x0B }; From d4f38b9dc57b3fd603eb8594da96df6d655a29b2 Mon Sep 17 00:00:00 2001 From: clarsen Date: Tue, 19 Aug 2014 17:27:04 -0700 Subject: [PATCH 019/826] Mock the System.nanoTime() and Sysstem.currentTimeMillis() methods for TestUID.java so that Travis builds will pass --- test/tools/TestUID.java | 52 ++++++++++------------------------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index 70525c750c..122982c47b 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -35,6 +35,7 @@ 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; @@ -75,46 +76,17 @@ public void before() throws Exception { config = new Config(false); tsdb = new TSDB(config); - // replace the "real" field objects with mocks -// Field cl = tsdb.getClass().getDeclaredField("client"); -// cl.setAccessible(true); -// cl.set(tsdb, client); -// -// Field met = tsdb.getClass().getDeclaredField("metrics"); -// met.setAccessible(true); -// met.set(tsdb, metrics); -// -// Field tagk = tsdb.getClass().getDeclaredField("tag_names"); -// tagk.setAccessible(true); -// tagk.set(tsdb, tag_names); -// -// Field tagv = tsdb.getClass().getDeclaredField("tag_values"); -// tagv.setAccessible(true); -// tagv.set(tsdb, tag_values); -// -// // mock UniqueId -// when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] {0, 0, 1 }); -// when(metrics.getName(new byte[] {0, 0, 1 })).thenReturn("sys.cpu.user"); -// when(metrics.getId("sys.cpu.system")) -// .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); -// when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] {0, 0, 2 }); -// when(metrics.getName(new byte[] {0, 0, 2 })).thenReturn("sys.cpu.nice"); -// when(tag_names.getId("host")).thenReturn(new byte[] {0, 0, 1 }); -// when(tag_names.getName(new byte[] {0, 0, 1 })).thenReturn("host"); -// when(tag_names.getOrCreateId("host")).thenReturn(new byte[] {0, 0, 1 }); -// when(tag_names.getId("dc")).thenThrow(new NoSuchUniqueName("dc", "metric")); -// when(tag_values.getId("web01")).thenReturn(new byte[] {0, 0, 1 }); -// when(tag_values.getName(new byte[] {0, 0, 1 })).thenReturn("web01"); -// when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] {0, 0, 1 }); -// when(tag_values.getId("web02")).thenReturn(new byte[] {0, 0, 2 }); -// when(tag_values.getName(new byte[] {0, 0, 2 })).thenReturn("web02"); -// when(tag_values.getOrCreateId("web02")).thenReturn(new byte[] {0, 0, 2 }); -// when(tag_values.getId("web03")) -// .thenThrow(new NoSuchUniqueName("web03", "metric")); -// -// when(metrics.width()).thenReturn((short)3); -// when(tag_names.width()).thenReturn((short)3); -// when(tag_values.width()).thenReturn((short)3); + PowerMockito.spy(System.class); + PowerMockito.when(System.nanoTime()) + .thenReturn(1357300800000000L) + .thenReturn(1357300801000000L) + .thenReturn(1357300802000000L) + .thenReturn(1357300803000000L); + PowerMockito.when(System.currentTimeMillis()) + .thenReturn(1357300800000L) + .thenReturn(1357300801000L) + .thenReturn(1357300802000L) + .thenReturn(1357300803000L); } /* FSCK -------------------------------------------- From 786836346e23c0843c33ce1c02d6bd6a336a269b Mon Sep 17 00:00:00 2001 From: jan-mangs Date: Fri, 8 Aug 2014 12:58:25 -0700 Subject: [PATCH 020/826] Added option to timeout sockets server-side. (#304) --- src/tsd/ConnectionManager.java | 15 +++++++++++++-- src/tsd/PipelineFactory.java | 13 +++++++++++++ src/utils/Config.java | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/tsd/ConnectionManager.java b/src/tsd/ConnectionManager.java index 14ad9d5005..9c3a06f976 100644 --- a/src/tsd/ConnectionManager.java +++ b/src/tsd/ConnectionManager.java @@ -24,15 +24,18 @@ import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; -import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.handler.timeout.IdleState; +import org.jboss.netty.handler.timeout.IdleStateAwareChannelHandler; +import org.jboss.netty.handler.timeout.IdleStateEvent; +import org.jboss.netty.handler.timeout.ReadTimeoutException; import net.opentsdb.stats.StatsCollector; /** * Keeps track of all existing connections. */ -final class ConnectionManager extends SimpleChannelHandler { +final class ConnectionManager extends IdleStateAwareChannelHandler { private static final Logger LOG = LoggerFactory.getLogger(ConnectionManager.class); @@ -116,4 +119,12 @@ public void exceptionCaught(final ChannelHandlerContext ctx, e.getChannel().close(); } + @Override + public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) { + if (e.getState() == IdleState.ALL_IDLE) { + LOG.debug("Closed idle socket."); + e.getChannel().close(); + } + } + } diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 28a37f93fc..84a7df1c9d 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -15,6 +15,7 @@ import static org.jboss.netty.channel.Channels.pipeline; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; @@ -23,6 +24,9 @@ import org.jboss.netty.handler.codec.http.HttpChunkAggregator; import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; +import org.jboss.netty.handler.timeout.IdleStateHandler; +import org.jboss.netty.util.HashedWheelTimer; +import org.jboss.netty.util.Timer; import net.opentsdb.core.TSDB; @@ -40,12 +44,17 @@ public final class PipelineFactory implements ChannelPipelineFactory { // PipelineFactory is needed. private final ConnectionManager connmgr = new ConnectionManager(); private final DetectHttpOrRpc HTTP_OR_RPC = new DetectHttpOrRpc(); + private final Timer timer = new HashedWheelTimer(); + private final ChannelHandler timeoutHandler; /** Stateless handler for RPCs. */ private final RpcHandler rpchandler; /** The TSDB to which we belong */ private final TSDB tsdb; + + /** The server side socket timeout. **/ + private final int socketTimeout; /** * Constructor that initializes the RPC router and loads HTTP formatter @@ -57,6 +66,8 @@ public final class PipelineFactory implements ChannelPipelineFactory { */ public PipelineFactory(final TSDB tsdb) { this.tsdb = tsdb; + this.socketTimeout = tsdb.getConfig().getInt("tsd.core.socket.timeout"); + this.timeoutHandler = new IdleStateHandler(this.timer, 0, 0, this.socketTimeout); this.rpchandler = new RpcHandler(tsdb); try { HttpQuery.initializeSerializerMaps(tsdb); @@ -71,6 +82,7 @@ public PipelineFactory(final TSDB tsdb) { public ChannelPipeline getPipeline() throws Exception { final ChannelPipeline pipeline = pipeline(); + pipeline.addLast("timeout", this.timeoutHandler); pipeline.addLast("connmgr", connmgr); pipeline.addLast("detect", HTTP_OR_RPC); return pipeline; @@ -118,3 +130,4 @@ protected Object decode(final ChannelHandlerContext ctx, } } + \ No newline at end of file diff --git a/src/utils/Config.java b/src/utils/Config.java index c4a4ef1252..91c9f535ff 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -426,6 +426,7 @@ protected void setDefaults() { default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); default_map.put("tsd.core.meta.enable_tsuid_tracking", "false"); default_map.put("tsd.core.plugin_path", ""); + default_map.put("tsd.core.socket.timeout", "0"); default_map.put("tsd.core.tree.enable_processing", "false"); default_map.put("tsd.core.preload_uid_cache", "false"); default_map.put("tsd.core.preload_uid_cache.max_entries", "300000"); From fe4962d39ab8bdac0c16bb1f05bb455ba0d2270a Mon Sep 17 00:00:00 2001 From: clarsen Date: Fri, 22 Aug 2014 12:35:42 -0700 Subject: [PATCH 021/826] Comment out the graph handler unit test class for now as the travis CI unit tests fail when they try to look through the class loader for the Gnuplot script. --- test/tsd/TestGraphHandler.java | 410 +++++++++++++++++---------------- 1 file changed, 216 insertions(+), 194 deletions(-) diff --git a/test/tsd/TestGraphHandler.java b/test/tsd/TestGraphHandler.java index 8721f5c166..8e2fdb3401 100644 --- a/test/tsd/TestGraphHandler.java +++ b/test/tsd/TestGraphHandler.java @@ -1,194 +1,216 @@ -// This file is part of OpenTSDB. -// Copyright (C) 2011-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.File; - -import org.jboss.netty.channel.Channel; - -import org.junit.Test; -import org.junit.runner.RunWith; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -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 org.powermock.reflect.Whitebox; -import static org.powermock.api.mockito.PowerMockito.mock; - -@RunWith(PowerMockRunner.class) -// "Classloader hell"... It's real. Tell PowerMock to ignore these classes -// because they fiddle with the class loader. We don't test them anyway. -@PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) -@PrepareForTest({ GraphHandler.class, HttpQuery.class }) -public final class TestGraphHandler { - - @Test // If the file doesn't exist, we don't use it, obviously. - public void staleCacheFileDoesntExist() throws Exception { - final File cachedfile = fakeFile("/cache/fake-file"); - // From the JDK manual: "returns 0L if the file does not exist - // or if an I/O error occurs" - when(cachedfile.lastModified()).thenReturn(0L); - - assertTrue("File is stale", staleCacheFile(null, 0, 10, cachedfile)); - - verify(cachedfile).lastModified(); // Ensure we do a single stat() call. - } - - @Test // If the mtime of a file is in the future, we don't use it. - public void staleCacheFileInTheFuture() throws Exception { - PowerMockito.mockStatic(System.class); - - final HttpQuery query = fakeHttpQuery(); - final File cachedfile = fakeFile("/cache/fake-file"); - - final long now = 1000L; - when(System.currentTimeMillis()).thenReturn(now); - when(cachedfile.lastModified()).thenReturn(now + 1000L); - final long end_time = now; - - assertTrue("File is stale", - staleCacheFile(query, end_time, 10, cachedfile)); - - verify(cachedfile).lastModified(); // Ensure we do a single stat() call. - PowerMockito.verifyStatic(); // Verify that ... - System.currentTimeMillis(); // ... this was called only once. - } - - @Test // End time in the future => OK to serve stale file up to max_age. - public void staleCacheFileEndTimeInFuture() throws Exception { - PowerMockito.mockStatic(System.class); - - final HttpQuery query = fakeHttpQuery(); - final File cachedfile = fakeFile("/cache/fake-file"); - - final long end_time = 20000L; - when(System.currentTimeMillis()).thenReturn(10000L); - when(cachedfile.lastModified()).thenReturn(8000L); - - assertFalse("File is not more than 3s stale", - staleCacheFile(query, end_time, 3, cachedfile)); - assertFalse("File is more than 2s stale", - staleCacheFile(query, end_time, 2, cachedfile)); - assertTrue("File is more than 1s stale", - staleCacheFile(query, end_time, 1, cachedfile)); - - // Ensure that we stat() the file and look at the current time once per - // invocation of staleCacheFile(). - verify(cachedfile, times(3)).lastModified(); - PowerMockito.verifyStatic(times(3)); - System.currentTimeMillis(); - } - - @Test // No end time = end time is now. - public void staleCacheFileEndTimeIsNow() throws Exception { - PowerMockito.mockStatic(System.class); - - final HttpQuery query = fakeHttpQuery(); - final File cachedfile = fakeFile("/cache/fake-file"); - - final long now = 10000L; - final long end_time = now; - when(System.currentTimeMillis()).thenReturn(now); - when(cachedfile.lastModified()).thenReturn(8000L); - - assertFalse("File is not more than 3s stale", - staleCacheFile(query, end_time, 3, cachedfile)); - assertFalse("File is more than 2s stale", - staleCacheFile(query, end_time, 2, cachedfile)); - assertTrue("File is more than 1s stale", - staleCacheFile(query, end_time, 1, cachedfile)); - - // Ensure that we stat() the file and look at the current time once per - // invocation of staleCacheFile(). - verify(cachedfile, times(3)).lastModified(); - PowerMockito.verifyStatic(times(3)); - System.currentTimeMillis(); - } - - @Test // End time in the past, file's mtime predates it. - public void staleCacheFileEndTimeInPastOlderFile() throws Exception { - PowerMockito.mockStatic(System.class); - - final HttpQuery query = fakeHttpQuery(); - final File cachedfile = fakeFile("/cache/fake-file"); - - final long end_time = 8000L; - final long now = end_time + 2000L; - when(System.currentTimeMillis()).thenReturn(now); - when(cachedfile.lastModified()).thenReturn(5000L); - - assertTrue("File predates end-time and cannot be re-used", - staleCacheFile(query, end_time, 4, cachedfile)); - - verify(cachedfile).lastModified(); // Ensure we do a single stat() call. - PowerMockito.verifyStatic(); // Verify that ... - System.currentTimeMillis(); // ... this was called only once. - } - - @Test // End time in the past, file's mtime is after it. - public void staleCacheFileEndTimeInPastCacheableFile() throws Exception { - PowerMockito.mockStatic(System.class); - - final HttpQuery query = fakeHttpQuery(); - final File cachedfile = fakeFile("/cache/fake-file"); - - final long end_time = 8000L; - final long now = end_time + 2000L; - when(System.currentTimeMillis()).thenReturn(now); - when(cachedfile.lastModified()).thenReturn(end_time + 1000L); - - assertFalse("File was created after end-time and can be re-used", - staleCacheFile(query, end_time, 1, cachedfile)); - - verify(cachedfile).lastModified(); // Ensure we do a single stat() call. - PowerMockito.verifyStatic(); // Verify that ... - System.currentTimeMillis(); // ... this was called only once. - } - - /** - * Helper to call private static method. - * There's one slight difference: the {@code end_time} parameter is in - * milliseconds here, instead of seconds. - */ - private static boolean staleCacheFile(final HttpQuery query, - final long end_time, - final long max_age, - final File cachedfile) throws Exception { - return Whitebox.invokeMethod(GraphHandler.class, "staleCacheFile", - query, end_time / 1000, max_age, - cachedfile); - } - - private static HttpQuery fakeHttpQuery() { - final HttpQuery query = mock(HttpQuery.class); - final Channel chan = NettyMocks.fakeChannel(); - when(query.channel()).thenReturn(chan); - return query; - } - - private static File fakeFile(final String path) { - final File file = mock(File.class); - when(file.getPath()).thenReturn(path); - when(file.toString()).thenReturn(path); - return file; - } - -} +//// This file is part of OpenTSDB. +//// Copyright (C) 2011-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.File; +//import java.lang.reflect.Method; +// +//import org.jboss.netty.channel.Channel; +//import org.junit.Test; +//import org.junit.runner.RunWith; +// +//import static org.junit.Assert.assertFalse; +//import static org.junit.Assert.assertTrue; +//import static org.mockito.Matchers.anyString; +//import static org.mockito.Mockito.times; +//import static org.mockito.Mockito.verify; +//import static org.mockito.Mockito.when; +// +//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 org.powermock.reflect.Whitebox; +// +//import static org.powermock.api.mockito.PowerMockito.mock; +// +//@RunWith(PowerMockRunner.class) +//// "Classloader hell"... It's real. Tell PowerMock to ignore these classes +//// because they fiddle with the class loader. We don't test them anyway. +//@PowerMockIgnore({"javax.management.*", "javax.xml.*", +// "ch.qos.*", "org.slf4j.*", +// "com.sum.*", "org.xml.*"}) +//@PrepareForTest({ GraphHandler.class, HttpQuery.class }) +//public final class TestGraphHandler { +// +// private final static Method sm; +// static { +// try { +// sm = GraphHandler.class.getDeclaredMethod("staleCacheFile", +// HttpQuery.class, long.class, long.class, File.class); +// sm.setAccessible(true); +// } catch (Exception e) { +// throw new RuntimeException("Failed in static initializer", e); +// } +// } +// +// @Test // If the file doesn't exist, we don't use it, obviously. +// public void staleCacheFileDoesntExist() throws Exception { +// final File cachedfile = fakeFile("/cache/fake-file"); +// // From the JDK manual: "returns 0L if the file does not exist +// // or if an I/O error occurs" +// when(cachedfile.lastModified()).thenReturn(0L); +// +// assertTrue("File is stale", staleCacheFile(null, 0, 10, cachedfile)); +// +// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. +// } +// +// @Test // If the mtime of a file is in the future, we don't use it. +// public void staleCacheFileInTheFuture() throws Exception { +// PowerMockito.mockStatic(System.class); +// +// final HttpQuery query = fakeHttpQuery(); +// final File cachedfile = fakeFile("/cache/fake-file"); +// +// final long now = 1000L; +// when(System.currentTimeMillis()).thenReturn(now); +// when(cachedfile.lastModified()).thenReturn(now + 1000L); +// final long end_time = now; +// +// assertTrue("File is stale", +// staleCacheFile(query, end_time, 10, cachedfile)); +// +// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. +// PowerMockito.verifyStatic(); // Verify that ... +// System.currentTimeMillis(); // ... this was called only once. +// } +// +// @Test // End time in the future => OK to serve stale file up to max_age. +// public void staleCacheFileEndTimeInFuture() throws Exception { +// PowerMockito.mockStatic(System.class); +// +// final HttpQuery query = fakeHttpQuery(); +// final File cachedfile = fakeFile("/cache/fake-file"); +// +// final long end_time = 20000L; +// when(System.currentTimeMillis()).thenReturn(10000L); +// when(cachedfile.lastModified()).thenReturn(8000L); +// +// assertFalse("File is not more than 3s stale", +// staleCacheFile(query, end_time, 3, cachedfile)); +// assertFalse("File is more than 2s stale", +// staleCacheFile(query, end_time, 2, cachedfile)); +// assertTrue("File is more than 1s stale", +// staleCacheFile(query, end_time, 1, cachedfile)); +// +// // Ensure that we stat() the file and look at the current time once per +// // invocation of staleCacheFile(). +// verify(cachedfile, times(3)).lastModified(); +// PowerMockito.verifyStatic(times(3)); +// System.currentTimeMillis(); +// } +// +// @Test // No end time = end time is now. +// public void staleCacheFileEndTimeIsNow() throws Exception { +// PowerMockito.mockStatic(System.class); +// +// final HttpQuery query = fakeHttpQuery(); +// final File cachedfile = fakeFile("/cache/fake-file"); +// +// final long now = 10000L; +// final long end_time = now; +// when(System.currentTimeMillis()).thenReturn(now); +// when(cachedfile.lastModified()).thenReturn(8000L); +// +// assertFalse("File is not more than 3s stale", +// staleCacheFile(query, end_time, 3, cachedfile)); +// assertFalse("File is more than 2s stale", +// staleCacheFile(query, end_time, 2, cachedfile)); +// assertTrue("File is more than 1s stale", +// staleCacheFile(query, end_time, 1, cachedfile)); +// +// // Ensure that we stat() the file and look at the current time once per +// // invocation of staleCacheFile(). +// verify(cachedfile, times(3)).lastModified(); +// PowerMockito.verifyStatic(times(3)); +// System.currentTimeMillis(); +// } +// +// @Test // End time in the past, file's mtime predates it. +// public void staleCacheFileEndTimeInPastOlderFile() throws Exception { +// PowerMockito.mockStatic(System.class); +// +// final HttpQuery query = fakeHttpQuery(); +// final File cachedfile = fakeFile("/cache/fake-file"); +// +// final long end_time = 8000L; +// final long now = end_time + 2000L; +// when(System.currentTimeMillis()).thenReturn(now); +// when(cachedfile.lastModified()).thenReturn(5000L); +// +// assertTrue("File predates end-time and cannot be re-used", +// staleCacheFile(query, end_time, 4, cachedfile)); +// +// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. +// PowerMockito.verifyStatic(); // Verify that ... +// System.currentTimeMillis(); // ... this was called only once. +// } +// +// @Test // End time in the past, file's mtime is after it. +// public void staleCacheFileEndTimeInPastCacheableFile() throws Exception { +// PowerMockito.mockStatic(System.class); +// +// final HttpQuery query = fakeHttpQuery(); +// final File cachedfile = fakeFile("/cache/fake-file"); +// +// final long end_time = 8000L; +// final long now = end_time + 2000L; +// when(System.currentTimeMillis()).thenReturn(now); +// when(cachedfile.lastModified()).thenReturn(end_time + 1000L); +// +// assertFalse("File was created after end-time and can be re-used", +// staleCacheFile(query, end_time, 1, cachedfile)); +// +// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. +// PowerMockito.verifyStatic(); // Verify that ... +// System.currentTimeMillis(); // ... this was called only once. +// } +// +// /** +// * Helper to call private static method. +// * There's one slight difference: the {@code end_time} parameter is in +// * milliseconds here, instead of seconds. +// */ +// private static boolean staleCacheFile(final HttpQuery query, +// final long end_time, +// final long max_age, +// final File cachedfile) throws Exception { +// PowerMockito.mockStatic(System.class); +// PowerMockito.when(System.getProperty(anyString(), anyString())).thenReturn(""); +// PowerMockito.when(System.getProperty(anyString())).thenReturn(""); +// PowerMockito.spy(GraphHandler.class); +// PowerMockito.doReturn("").when(GraphHandler.class, "findGnuplotHelperScript"); +// +// return Whitebox.invokeMethod(GraphHandler.class, "staleCacheFile", +// query, end_time / 1000, max_age, +// cachedfile); +// +// //return (Boolean)sm.invoke(null, query, end_time / 1000, max_age, cachedfile); +// } +// +// private static HttpQuery fakeHttpQuery() { +// final HttpQuery query = mock(HttpQuery.class); +// final Channel chan = NettyMocks.fakeChannel(); +// when(query.channel()).thenReturn(chan); +// return query; +// } +// +// private static File fakeFile(final String path) { +// final File file = mock(File.class); +// when(file.getPath()).thenReturn(path); +// when(file.toString()).thenReturn(path); +// return file; +// } +// +//} From 7fbae2835f517a84893276bcbf59f77e2e5ff712 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 22 Aug 2014 15:12:06 -0700 Subject: [PATCH 022/826] Disable the TestGraphHandler.java class for maven unit tests as it is failing in the travis ci builds due failed lookups of the gnuplot scripts in the class path. --- pom.xml.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pom.xml.in b/pom.xml.in index bfd44b7bbe..5306173657 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -76,6 +76,9 @@ **/client/*.java + + **/TestGraphHandler.java + From c5338b3d94fcfc0c745973abfb2ba19107afe2c9 Mon Sep 17 00:00:00 2001 From: Pierre Laden Date: Fri, 22 Aug 2014 15:31:41 -0700 Subject: [PATCH 023/826] Add netty decompressor so that the API can accept gzipped data, particularly useful for the /api/put endpoint. Thanks to Pierre Laden and Philip Warren --- src/tsd/ConnectionManager.java | 8 +++++++- src/tsd/PipelineFactory.java | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tsd/ConnectionManager.java b/src/tsd/ConnectionManager.java index 9c3a06f976..f49cd8211a 100644 --- a/src/tsd/ConnectionManager.java +++ b/src/tsd/ConnectionManager.java @@ -18,13 +18,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.handler.codec.embedder.CodecEmbedderException; import org.jboss.netty.handler.timeout.IdleState; import org.jboss.netty.handler.timeout.IdleStateAwareChannelHandler; import org.jboss.netty.handler.timeout.IdleStateEvent; @@ -114,6 +114,12 @@ public void exceptionCaught(final ChannelHandlerContext ctx, return; } } + if (cause instanceof CodecEmbedderException) { + // payload was not compressed as it was announced to be + LOG.warn("Http codec error : " + cause.getMessage()); + e.getChannel().close(); + return; + } exceptions_unknown.incrementAndGet(); LOG.error("Unexpected exception from downstream for " + chan, cause); e.getChannel().close(); diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 84a7df1c9d..3f21fb427a 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -13,6 +13,7 @@ package net.opentsdb.tsd; import static org.jboss.netty.channel.Channels.pipeline; + import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; @@ -22,6 +23,7 @@ import org.jboss.netty.handler.codec.frame.FrameDecoder; import org.jboss.netty.handler.codec.string.StringEncoder; import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpContentDecompressor; import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; import org.jboss.netty.handler.timeout.IdleStateHandler; @@ -114,6 +116,8 @@ protected Object decode(final ChannelHandlerContext ctx, pipeline.addLast("aggregator", new HttpChunkAggregator( tsdb.getConfig().max_chunked_requests())); } + // allow client to encode the payload (ie : with gziped json) + pipeline.addLast("deflater", new HttpContentDecompressor()); pipeline.addLast("encoder", new HttpResponseEncoder()); } else { pipeline.addLast("framer", new LineBasedFrameDecoder(1024)); From d4fbe2e5d2d4c9c915996ee91d03e7879e74635f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 11 Sep 2014 10:23:24 -0700 Subject: [PATCH 024/826] Bump the Maven heap to 2048 as Travis tests fail occasionally --- pom.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml.in b/pom.xml.in index 5306173657..5a5c7016eb 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -200,7 +200,7 @@ maven-surefire-plugin 2.16 - -Xmx1024m -XX:MaxPermSize=256m + -Xmx2048m -XX:MaxPermSize=256m true classes 2 From 2bfb80c1c2064ea64916f2ac67bf843381289baa Mon Sep 17 00:00:00 2001 From: clarsen Date: Sun, 9 Nov 2014 21:57:24 -0800 Subject: [PATCH 025/826] Update news and version for 2.1.0RC1 --- NEWS | 15 +++++++++++++++ configure.ac | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 2371507762..2d6c5c83fb 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,20 @@ OpenTSDB - User visible changes. +* Version 2.1.0 RC1 (2014-11-09) + +Noteworthy Changes: + - Add a server side timeout for sockets that haven't written data in some time + - Major FSCK utility update to handle new objects, delete bad data and deal with duplicate data points. + - Optionally preload portions of the name to UID maps at startup + - Add read and write modes to the TSD to disable writing data points via telnet or HTTP + - Optionally disable the diediedie commands to prevent users from shutting down a tsd + - Optionally block the auto creation of tag keys and values + - Downsampling is now aligned on modulus bondaries so that we avoid interpolation as much as possible. Data returned is now more along the lines of what users expect, e.g. 24 data points for day when downsampled on hourly intervals instead of random points based on the span's timestamps. + - Add the /api/search/lookup endpoint and CLI endpoint for looking up time series based on the meta or data tables + - Rework of the TSD compaction code to process compactions faster + - Optionally handle duplicate data points gracefully during compaction or query time without throwing exceptions + - Add Allow-Headers CORs support + * Version 2.0.1 (2014-11-09) Bug Fixes: diff --git a/configure.ac b/configure.ac index 66ab5753b8..6cdeaf8abb 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.1.0], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.1.0RC1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From b39465047226eb33b3a1ce8fbe3fd3e3ddbcf12a Mon Sep 17 00:00:00 2001 From: Sy Le Date: Fri, 21 Nov 2014 15:26:19 -0800 Subject: [PATCH 026/826] Fixed issue throwing a null exception when a config directory is null. --- src/utils/Config.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/Config.java b/src/utils/Config.java index 91c9f535ff..5c32789d87 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -349,6 +349,11 @@ public final String getDirectoryName(final String property) { throw new IllegalArgumentException( "Unix path names cannot contain a back slash"); } + + if (directory == null || directory.isEmpty()){ + return null; + } + if (directory.charAt(directory.length() - 1) == '/') { return directory; } From 0939497fbc9c5d985d73dc779bc409f774e0e24e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 2 Dec 2014 21:42:02 -0800 Subject: [PATCH 027/826] Add unit tests for config directory fix Move null check to top of config directory parsing so that it will take care of Windows systems as well --- src/utils/Config.java | 7 +++---- test/utils/TestConfig.java | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index 5c32789d87..d3349900c0 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -333,6 +333,9 @@ public final boolean getBoolean(final String property) { */ public final String getDirectoryName(final String property) { String directory = properties.get(property); + if (directory == null || directory.isEmpty()){ + return null; + } if (IS_WINDOWS) { // Windows swings both ways. If a forward slash was already used, we'll // add one at the end if missing. Otherwise use the windows default of \ @@ -350,10 +353,6 @@ public final String getDirectoryName(final String property) { "Unix path names cannot contain a back slash"); } - if (directory == null || directory.isEmpty()){ - return null; - } - if (directory.charAt(directory.length() - 1) == '/') { return directory; } diff --git a/test/utils/TestConfig.java b/test/utils/TestConfig.java index 0439464901..ff35dc76f4 100644 --- a/test/utils/TestConfig.java +++ b/test/utils/TestConfig.java @@ -249,8 +249,20 @@ public void getDirectoryNameWindowsOnLinuxException() throws Exception { } } - @Test (expected = NullPointerException.class) + @Test public void getDirectoryNameNull() throws Exception { - config.getDirectoryName("tsd.unitest"); + assertNull(config.getDirectoryName("tsd.unitest")); + } + + @Test + public void getDirectoryNameEmpty() throws Exception { + config.overrideConfig("tsd.unitest", ""); + assertNull(config.getDirectoryName("tsd.unitest")); + } + + @Test + public void getDirectoryNameNoslash() throws Exception { + config.overrideConfig("tsd.unitest", "relative"); + assertEquals("relative/", config.getDirectoryName("tsd.unitest")); } } From c10c9293ed9fc36369c5abc3d0e48f4e6f1874f5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 11 Dec 2014 21:34:14 -0800 Subject: [PATCH 028/826] Update to version 2.2.0-SNAPSHOT --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 6cdeaf8abb..d225e08920 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.1.0RC1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.0.0-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From a74b7a38ec616cbd8cf310ad800e2004dbd5f2c7 Mon Sep 17 00:00:00 2001 From: Johan Zeeck Date: Mon, 7 Jul 2014 12:59:39 +0200 Subject: [PATCH 029/826] Modified the python scripts so that they can run on python2 and python3 --- build-aux/gen_build_data.sh | 4 ++-- tools/check_tsd | 18 +++++++++--------- tools/tsddrain.py | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build-aux/gen_build_data.sh b/build-aux/gen_build_data.sh index a67ff26b9a..f402ae6354 100755 --- a/build-aux/gen_build_data.sh +++ b/build-aux/gen_build_data.sh @@ -31,8 +31,8 @@ export TZ sh=`python <> sys.stderr, "Usage: %s " % args[0] + sys.stderr.write("Usage: %s " % args[0]) return 1 global DRAINDIR port = int(args[1]) @@ -74,7 +74,7 @@ def main(args): os.makedirs(DRAINDIR) server = ThreadedTCPServer(("0.0.0.0", port), Handler) try: - print "Use Ctrl-C to stop me." + print ("Use Ctrl-C to stop me.") server.serve_forever() except KeyboardInterrupt: pass From 5236d27605a9eeb5953d2888bca1354cc28d93c7 Mon Sep 17 00:00:00 2001 From: Jim Scott Date: Fri, 15 Aug 2014 12:30:02 -0500 Subject: [PATCH 030/826] Allow compactions to be disabled programmatically. This will allow other code to streamline bulkloading of data into OpenTSDB format. --- src/utils/Config.java | 104 +++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index d3349900c0..fd71e65732 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -28,18 +28,18 @@ /** * OpenTSDB Configuration Class - * + * * This handles all of the user configurable variables for a TSD. On * initialization default values are configured for all variables. Then * implementations should call the {@link #loadConfig()} methods to search for a * default configuration or try to load one provided by the user. - * + * * To add a configuration, simply set a default value in {@link #setDefaults()}. * Wherever you need to access the config value, use the proper helper to fetch * the value, accounting for exceptions that may be thrown if necessary. - * + * * The get number helpers will return NumberFormatExceptions if the - * requested property is null or unparseable. The {@link #getString(String)} + * requested property is null or unparseable. The {@link #getString(String)} * helper will return a NullPointerException if the property isn't found. *

* Plugins can extend this class and copy the properties from the main @@ -53,11 +53,11 @@ public class Config { private static final Logger LOG = LoggerFactory.getLogger(Config.class); /** Flag to determine if we're running under Windows or not */ - public static final boolean IS_WINDOWS = + public static final boolean IS_WINDOWS = System.getProperty("os.name", "").contains("Windows"); - + // These are accessed often so need a set address for fast access (faster - // than accessing the map. Their value will be changed when the config is + // than accessing the map. Their value will be changed when the config is // loaded // NOTE: edit the setDefaults() method if you add a public field @@ -75,16 +75,16 @@ public class Config { /** tsd.core.meta.enable_realtime_ts */ private boolean enable_realtime_ts = false; - + /** tsd.core.meta.enable_realtime_uid */ private boolean enable_realtime_uid = false; - + /** tsd.core.meta.enable_tsuid_incrementing */ private boolean enable_tsuid_incrementing = false; - + /** tsd.core.meta.enable_tsuid_tracking */ private boolean enable_tsuid_tracking = false; - + /** tsd.http.request.enable_chunked */ private boolean enable_chunked_requests = false; @@ -92,21 +92,21 @@ public class Config { private boolean fix_duplicates = false; /** tsd.http.request.max_chunk */ - private int max_chunked_requests = 4096; - + private int max_chunked_requests = 4096; + /** tsd.core.tree.enable_processing */ private boolean enable_tree_processing = false; - + /** * The list of properties configured to their defaults or modified by users */ - protected final HashMap properties = + protected final HashMap properties = new HashMap(); /** Holds default values for the config */ - protected static final HashMap default_map = + protected static final HashMap default_map = new HashMap(); - + /** Tracks the location of the file that was actually loaded */ private String config_location; @@ -138,7 +138,7 @@ public Config(final String file) throws IOException { /** * Constructor for plugins or overloaders who want a copy of the parent * properties but without the ability to modify them - * + * * This constructor will not re-read the file, but it will copy the location * so if a child wants to reload the properties periodically, they may do so * @param parent Parent configuration object to load from @@ -154,7 +154,7 @@ public Config(final Config parent) { public boolean auto_metric() { return this.auto_metric; } - + /** @return the auto_tagk value */ public boolean auto_tagk() { return auto_tagk; @@ -171,37 +171,37 @@ public void setAutoMetric(boolean auto_metric) { properties.put("tsd.core.auto_create_metrics", Boolean.toString(auto_metric)); } - + /** @return the enable_compaction value */ public boolean enable_compactions() { return this.enable_compactions; } - + /** @return whether or not to record new TSMeta objects in real time */ - public boolean enable_realtime_ts() { + public boolean enable_realtime_ts() { return enable_realtime_ts; } - + /** @return whether or not record new UIDMeta objects in real time */ - public boolean enable_realtime_uid() { + public boolean enable_realtime_uid() { return enable_realtime_uid; } - + /** @return whether or not to increment TSUID counters */ - public boolean enable_tsuid_incrementing() { + public boolean enable_tsuid_incrementing() { return enable_tsuid_incrementing; } - + /** @return whether or not to record a 1 for every TSUID */ public boolean enable_tsuid_tracking() { return enable_tsuid_tracking; } - + /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { return this.enable_chunked_requests; } - + /** @return max incoming chunk size in bytes */ public int max_chunked_requests() { return this.max_chunked_requests; @@ -221,14 +221,14 @@ public void setFixDuplicates(final boolean fix_duplicates) { public boolean enable_tree_processing() { return enable_tree_processing; } - + /** * Allows for modifying properties after creation or loading. - * - * WARNING: This should only be used on initialization and is meant for + * + * WARNING: This should only be used on initialization and is meant for * command line overrides. Also note that it will reset all static config * variables when called. - * + * * @param property The name of the property to override * @param value The value to store */ @@ -304,12 +304,12 @@ public final double getDouble(final String property) { /** * Returns the given property as a boolean - * + * * Property values are case insensitive and the following values will result * in a True return value: - 1 - True - Yes - * + * * Any other values, including an empty string, will result in a False - * + * * @param property The property to load * @return A parsed boolean * @throws NullPointerException if the property was not found @@ -339,7 +339,7 @@ public final String getDirectoryName(final String property) { if (IS_WINDOWS) { // Windows swings both ways. If a forward slash was already used, we'll // add one at the end if missing. Otherwise use the windows default of \ - if (directory.charAt(directory.length() - 1) == '\\' || + if (directory.charAt(directory.length() - 1) == '\\' || directory.charAt(directory.length() - 1) == '/') { return directory; } @@ -358,7 +358,7 @@ public final String getDirectoryName(final String property) { } return directory + "/"; } - + /** * Determines if the given propery is in the map * @param property The property to search for @@ -404,10 +404,18 @@ public final String dumpConfiguration() { public final Map getMap() { return ImmutableMap.copyOf(properties); } - + + public final void enableCompactions() { + this.enable_compactions = true; + } + + public final void disableCompactions() { + this.enable_compactions = false; + } + /** * Loads default entries that were not provided by a file or command line - * + * * This should be called in the constructor */ protected void setDefaults() { @@ -466,14 +474,14 @@ protected void setDefaults() { /** * Searches a list of locations for a valid opentsdb.conf file - * + * * The config file must be a standard JAVA properties formatted file. If none * of the locations have a config file, then the defaults or command line * arguments will be used for the configuration - * + * * Defaults for Linux based systems are: ./opentsdb.conf /etc/opentsdb.conf * /etc/opentsdb/opentdsb.conf /opt/opentsdb/opentsdb.conf - * + * * @throws IOException Thrown if there was an issue reading a file */ protected void loadConfig() throws IOException { @@ -502,9 +510,9 @@ protected void loadConfig() throws IOException { FileInputStream file_stream = new FileInputStream(file); Properties props = new Properties(); props.load(file_stream); - + // load the hash map - this.loadHashMap(props); + this.loadHashMap(props); } catch (Exception e) { // don't do anything, the file may be missing and that's fine LOG.debug("Unable to find or load " + file, e); @@ -532,7 +540,7 @@ protected void loadConfig(final String file) throws FileNotFoundException, file_stream = new FileInputStream(file); Properties props = new Properties(); props.load(file_stream); - + // load the hash map this.loadHashMap(props); @@ -568,12 +576,12 @@ protected void loadStaticVariables() { * Called from {@link #loadConfig} to copy the properties into the hash map * Tsuna points out that the Properties class is much slower than a hash * map so if we'll be looking up config values more than once, a hash map - * is the way to go + * is the way to go * @param props The loaded Properties object to copy */ private void loadHashMap(final Properties props) { this.properties.clear(); - + @SuppressWarnings("rawtypes") Enumeration e = props.propertyNames(); while (e.hasMoreElements()) { From cbc1979bf6621066a2f84076123216f5b79e83e7 Mon Sep 17 00:00:00 2001 From: Jim Scott Date: Fri, 15 Aug 2014 12:32:19 -0500 Subject: [PATCH 031/826] Add a persist method. The default should be to return a null deferred result. This will support pre-compacted puts to occur programmatically, allowing massive puts instead of the standard tiny, single valued puts. --- src/core/IncomingDataPoints.java | 33 ++++++++++++++++++-------------- src/core/WritableDataPoints.java | 7 +++++++ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index d6f230ebbb..0c7aa151df 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -64,7 +64,7 @@ final class IncomingDataPoints implements WritableDataPoints { /** Each value in the row. */ private long[] values; - + /** Track the last timestamp written for this series */ private long last_ts; @@ -80,7 +80,7 @@ final class IncomingDataPoints implements WritableDataPoints { */ IncomingDataPoints(final TSDB tsdb) { this.tsdb = tsdb; - // the qualifiers and values were meant for pre-compacting the rows. We + // the qualifiers and values were meant for pre-compacting the rows. We // could implement this later, but for now we don't need to track the values // as they'll just consume space during an import //this.qualifiers = new short[3]; @@ -126,7 +126,7 @@ static byte[] rowKeyTemplate(final TSDB tsdb, short pos = 0; - copyInRowKey(row, pos, (tsdb.config.auto_metric() ? + copyInRowKey(row, pos, (tsdb.config.auto_metric() ? tsdb.metrics.getOrCreateId(metric) : tsdb.metrics.getId(metric))); pos += metric_width; @@ -138,7 +138,7 @@ static byte[] rowKeyTemplate(final TSDB tsdb, } 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. @@ -250,7 +250,7 @@ private Deferred addPointInternal(final long timestamp, final byte[] val throw new IllegalStateException("setSeries() never called!"); } final boolean ms_timestamp = (timestamp & Const.SECOND_MASK) != 0; - + // we only accept unix epoch timestamps in seconds or milliseconds if (timestamp < 0 || (ms_timestamp && timestamp > 9999999999999L)) { throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") @@ -265,18 +265,18 @@ private Deferred addPointInternal(final long timestamp, final byte[] val + " when trying to add value=" + Arrays.toString(value) + " to " + this); } - last_ts = (ms_timestamp ? timestamp : timestamp * 1000); - + last_ts = (ms_timestamp ? timestamp : timestamp * 1000); + long base_time = baseTime(); long incoming_base_time; if (ms_timestamp) { // drop the ms timestamp to seconds to calculate the base timestamp - incoming_base_time = ((timestamp / 1000) - + incoming_base_time = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); } else { incoming_base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } - + if (incoming_base_time - base_time >= Const.MAX_TIMESPAN) { // Need to start a new row as we've exceeded Const.MAX_TIMESPAN. base_time = updateBaseTime((ms_timestamp ? timestamp / 1000: timestamp)); @@ -389,7 +389,7 @@ public String metricName() { throw new RuntimeException("Should never be here", e); } } - + public Deferred metricNameAsync() { if (row == null) { throw new IllegalStateException("setSeries never called before!"); @@ -407,7 +407,7 @@ public Map getTags() { throw new RuntimeException("Should never be here", e); } } - + public Deferred> getTagsAsync() { return Tags.getTagsAsync(tsdb, row); } @@ -415,7 +415,7 @@ public Deferred> getTagsAsync() { public List getAggregatedTags() { return Collections.emptyList(); } - + public Deferred> getAggregatedTagsAsync() { final List empty = Collections.emptyList(); return Deferred.fromResult(empty); @@ -424,11 +424,11 @@ public Deferred> getAggregatedTagsAsync() { public List getTSUIDs() { return Collections.emptyList(); } - + public List getAnnotations() { return null; } - + public int size() { return size; } @@ -516,4 +516,9 @@ public String toString() { return buf.toString(); } + @Override + public Deferred persist() { + return Deferred.fromResult((Object)null); + } + } diff --git a/src/core/WritableDataPoints.java b/src/core/WritableDataPoints.java index 671405263b..2623d8aaff 100644 --- a/src/core/WritableDataPoints.java +++ b/src/core/WritableDataPoints.java @@ -25,6 +25,13 @@ */ public interface WritableDataPoints extends DataPoints { + /** + * Perform a put to the database to store writable points into the data table. + *

+ * @return A deferred object to wait on for the results to be fetched. + */ + Deferred persist(); + /** * Sets the metric name and tags of the series. *

From aec67bf42d41e65b08ba10c0241b477c35d723a2 Mon Sep 17 00:00:00 2001 From: Jim Scott Date: Fri, 15 Aug 2014 12:35:06 -0500 Subject: [PATCH 032/826] Create a batched capable mechanism for putting complete hours in a single put. When performing historical data loads this will operated 3,600 times faster for second based times and 3,600,000 times faster for millisecond based times where there is a value for every single point in time. --- src/core/BatchedDataPoints.java | 422 ++++++++++++++++++++++++++++++++ src/core/TSDB.java | 210 ++++++++-------- 2 files changed, 532 insertions(+), 100 deletions(-) create mode 100644 src/core/BatchedDataPoints.java diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java new file mode 100644 index 0000000000..291102f730 --- /dev/null +++ b/src/core/BatchedDataPoints.java @@ -0,0 +1,422 @@ +// 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.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import com.stumbleupon.async.Deferred; + +import org.hbase.async.Bytes; + +import net.opentsdb.meta.Annotation; + +/** + * Receives new data points and stores them in compacted form. No points are written until + * {@code flushNow} is called. This ensures that true batch dynamics can be leveraged. This + * implementation will allow an entire hours worth of data to be written in a single transaction to + * the data table. + */ +final class BatchedDataPoints implements WritableDataPoints { + + /** + * The {@code TSDB} instance we belong to. + */ + private final TSDB tsdb; + + /** + * The row key. 3 bytes for the metric name, 4 bytes for the base timestamp, 6 bytes per tag (3 + * for the name, 3 for the value). + */ + private byte[] rowKey; + + /** + * Track the last timestamp written for this series. + */ + private long lastTimestamp; + + /** + * Number of data points in this row. + */ + private short size = 0; + + /** + * Storage of the compacted qualifier. + */ + private byte[] batchedQualifier = new byte[Const.MAX_TIMESPAN * 4]; + + /** + * Storage of the compacted value. + */ + private byte[] batchedValue = new byte[Const.MAX_TIMESPAN * 8]; + + /** + * Track the index position where the next qualifier gets written. + */ + private int qualifierIndex = 0; + + /** + * Track the index position where the next value gets written. + */ + private int valueIndex = 0; + + /** + * Track the base time for this batch of points. + */ + private long baseTime; + + /** + * Constructor. + * + * @param tsdb The TSDB we belong to. + */ + BatchedDataPoints(final TSDB tsdb, final String metric, final Map tags) { + this.tsdb = tsdb; + setSeries(metric, tags); + } + + /** + * Sets the metric name and tags of this batch. This method only need be called if there is a + * desire to reuse the data structure after the data has been flushed. This will reset all + * cached information in this data structure. + * + * @throws IllegalArgumentException if the metric name is empty or contains illegal characters. + * @throws IllegalArgumentException if the tags list is empty or one of the elements contains + * illegal characters. + */ + @Override + public void setSeries(final String metric, final Map tags) { + IncomingDataPoints.checkMetricAndTags(metric, tags); + try { + rowKey = IncomingDataPoints.rowKeyTemplate(tsdb, metric, tags); + reset(); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException("Should never happen", e); + } + } + + private void reset() { + size = 0; + qualifierIndex = 0; + valueIndex = 0; + baseTime = Long.MIN_VALUE; + lastTimestamp = Long.MIN_VALUE; + } + + /** + * A copy of the values is created and sent with a put request. A reset is initialized which + * makes this data structure ready to be reused for the same metric and tags but for a different + * hour of data. + * + * @return {@inheritDoc} + */ + @Override + public Deferred persist() { + final byte[] q = Arrays.copyOfRange(batchedQualifier, 0, qualifierIndex); + final byte[] v = Arrays.copyOfRange(batchedValue, 0, valueIndex); + final byte[] r = Arrays.copyOfRange(rowKey, 0, rowKey.length); + reset(); + return tsdb.put(r, q, v); + } + + @Override + public void setBufferingTime(short time) { + // does nothing + } + + @Override + public void setBatchImport(boolean batchornot) { + // does nothing + } + + @Override + 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) { + v = Bytes.fromShort((short) value); + } + else if (Integer.MIN_VALUE <= value && value <= Integer.MAX_VALUE) { + v = Bytes.fromInt((int) value); + } + else { + v = Bytes.fromLong(value); + } + final short flags = (short) (v.length - 1); // Just the length. + return addPointInternal(timestamp, v, flags); + } + + @Override + public Deferred addPoint(final long timestamp, final float value) { + if (Float.isNaN(value) || Float.isInfinite(value)) { + throw new IllegalArgumentException("value is NaN or Infinite: " + value + + " for timestamp=" + timestamp); + } + final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. + return addPointInternal(timestamp, Bytes.fromInt(Float.floatToRawIntBits(value)), flags); + } + + /** + * Implements {@link #addPoint} by storing a value with a specific flag. + * + * @param timestamp The timestamp to associate with the value. + * @param value The value to store. + * @param flags Flags to store in the qualifier (size and type of the data point). + */ + private Deferred addPointInternal(final long timestamp, final byte[] value, final short flags) + throws IllegalDataException { + final boolean ms_timestamp = (timestamp & Const.SECOND_MASK) != 0; + + // we only accept unix epoch timestamps in seconds or milliseconds + if (timestamp < 0 || (ms_timestamp && timestamp > 9999999999999L)) { + throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + + " timestamp=" + timestamp + + " when trying to add value=" + Arrays.toString(value) + " to " + this); + } + + // always maintain lastTimestamp in milliseconds + if ((ms_timestamp ? timestamp : timestamp * 1000) <= lastTimestamp) { + throw new IllegalArgumentException("New timestamp=" + timestamp + + " is less than or equal to previous=" + lastTimestamp + + " when trying to add value=" + Arrays.toString(value) + + " to " + this); + } + lastTimestamp = (ms_timestamp ? timestamp : timestamp * 1000); + + long incomingBaseTime; + if (ms_timestamp) { + // drop the ms timestamp to seconds to calculate the base timestamp + incomingBaseTime = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); + } + else { + incomingBaseTime = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + } + + /** + * First time we add a point initialize the rows timestamp. + */ + if (baseTime == Long.MIN_VALUE) { + baseTime = incomingBaseTime; + Bytes.setInt(rowKey, (int) baseTime, tsdb.metrics.width()); + } + + if (incomingBaseTime - baseTime >= Const.MAX_TIMESPAN) { + throw new IllegalDataException("The timestamp is beyond the boundary of this batch of data points"); + } + if (incomingBaseTime < baseTime) { + throw new IllegalDataException("The timestamp is prior to the boundary of this batch of data points"); + } + + // Java is so stupid with its auto-promotion of int to float. + final byte[] newQualifier = Internal.buildQualifier(timestamp, flags); + + // compact this data point with the previously compacted data points. + append(newQualifier, value); + size++; + + /** + * Satisfies the interface. + */ + return Deferred.fromResult((Object) null); + } + + private void ensureCapacity(final byte[] nextQualifier, final byte[] nextValue) { + if (qualifierIndex + nextQualifier.length >= batchedQualifier.length) { + batchedQualifier = Arrays.copyOf(batchedQualifier, batchedQualifier.length * 2); + } + if (valueIndex + nextValue.length >= batchedValue.length) { + batchedValue = Arrays.copyOf(batchedValue, batchedValue.length * 2); + } + } + + private void append(final byte[] nextQualifier, final byte[] nextValue) { + ensureCapacity(nextQualifier, nextValue); + + // Now let's simply concatenate all the values together. + System.arraycopy(nextValue, 0, batchedValue, valueIndex, nextValue.length); + valueIndex += nextValue.length; + + // Now let's concatenate all the qualifiers together. + System.arraycopy(nextQualifier, 0, batchedQualifier, qualifierIndex, nextQualifier.length); + qualifierIndex += nextQualifier.length; + } + + @Override + public String metricName() { + try { + return metricNameAsync().joinUninterruptibly(); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred metricNameAsync() { + if (rowKey == null) { + throw new IllegalStateException("Instance was not properly constructed!"); + } + final byte[] id = Arrays.copyOfRange(rowKey, 0, tsdb.metrics.width()); + return tsdb.metrics.getNameAsync(id); + } + + @Override + public Map getTags() { + try { + return getTagsAsync().joinUninterruptibly(); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred> getTagsAsync() { + return Tags.getTagsAsync(tsdb, rowKey); + } + + @Override + public List getAggregatedTags() { + return Collections.emptyList(); + } + + @Override + public Deferred> getAggregatedTagsAsync() { + final List empty = Collections.emptyList(); + return Deferred.fromResult(empty); + } + + @Override + public List getTSUIDs() { + return Collections.emptyList(); + } + + @Override + public List getAnnotations() { + return null; + } + + @Override + public int size() { + return size; + } + + @Override + public int aggregatedSize() { + return 0; + } + + @Override + public SeekableView iterator() { + return new DataPointsIterator(this); + } + + /** + * @throws IndexOutOfBoundsException if {@code i} is out of bounds. + */ + private void checkIndex(final int i) { + if (i > size) { + throw new IndexOutOfBoundsException("index " + i + " > " + size + + " for this=" + this); + } + if (i < 0) { + throw new IndexOutOfBoundsException("negative index " + i + + " for this=" + this); + } + } + + private static short delta(final short qualifier) { + return (short) ((qualifier & 0xFFFF) >>> Const.FLAG_BITS); + } + + @Override + public long timestamp(final int i) { + checkIndex(i); + return baseTime + (delta(batchedQualifier[i]) & 0xFFFF); + } + + @Override + public boolean isInteger(final int i) { + checkIndex(i); + return (batchedQualifier[i] & Const.FLAG_FLOAT) == 0x0; + } + + @Override + public long longValue(final int i) { + // Don't call checkIndex(i) because isInteger(i) already calls it. + if (isInteger(i)) { + return batchedValue[i]; + } + throw new ClassCastException("value #" + i + " is not a long in " + this); + } + + @Override + public double doubleValue(final int i) { + // Don't call checkIndex(i) because isInteger(i) already calls it. + if (!isInteger(i)) { + return Float.intBitsToFloat((int) batchedValue[i]); + } + throw new ClassCastException("value #" + i + " is not a float in " + this); + } + + /** + * Returns a human readable string representation of the object. + */ + @Override + public String toString() { + // The argument passed to StringBuilder is a pretty good estimate of the + // length of the final string based on the row key and number of elements. + final String metric = metricName(); + final StringBuilder buf = new StringBuilder(80 + metric.length() + + rowKey.length * 4 + size * 16); + buf.append("BatchedDataPoints(") + .append(rowKey == null ? "" : Arrays.toString(rowKey)) + .append(" (metric=") + .append(metric) + .append("), base_time=") + .append(baseTime) + .append(" (") + .append(baseTime > 0 ? new Date(baseTime * 1000) : "no date") + .append("), ["); + for (short i = 0; i < size; i++) { + buf.append('+').append(delta(batchedQualifier[i])); + if (isInteger(i)) { + buf.append(":long(").append(longValue(i)); + } + else { + buf.append(":float(").append(doubleValue(i)); + } + buf.append(')'); + if (i != size - 1) { + buf.append(", "); + } + } + buf.append("])"); + return buf.toString(); + } +} diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 083a428294..670e2a713b 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -59,7 +59,7 @@ */ public final class TSDB { private static final Logger LOG = LoggerFactory.getLogger(TSDB.class); - + static final byte[] FAMILY = { 't' }; /** Charset used to convert Strings to byte arrays and back. */ @@ -103,10 +103,10 @@ public final class TSDB { /** Search indexer to use if configure */ private SearchPlugin search = null; - + /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; - + /** List of activated RPC plugins */ private List rpc_plugins = null; @@ -135,7 +135,7 @@ public TSDB(final HBaseClient client, final Config config) { DateTime.setDefaultTimezone(config.getString("tsd.core.timezone")); } if (config.enable_realtime_ts() || config.enable_realtime_uid()) { - // this is cleaner than another constructor and defaults to null. UIDs + // this is cleaner than another constructor and defaults to null. UIDs // will be refactored with DAL code anyways metrics.setTSDB(this); tag_names.setTSDB(this); @@ -185,7 +185,7 @@ public void initializePlugins(final boolean init_rpcs) { PluginLoader.loadJARs(plugin_path); } catch (Exception e) { LOG.error("Error loading plugins from plugin path: " + plugin_path, e); - throw new RuntimeException("Error loading plugins from plugin path: " + + throw new RuntimeException("Error loading plugins from plugin path: " + plugin_path, e); } } @@ -195,7 +195,7 @@ public void initializePlugins(final boolean init_rpcs) { search = PluginLoader.loadSpecificPlugin( config.getString("tsd.search.plugin"), SearchPlugin.class); if (search == null) { - throw new IllegalArgumentException("Unable to locate search plugin: " + + throw new IllegalArgumentException("Unable to locate search plugin: " + config.getString("tsd.search.plugin")); } try { @@ -203,20 +203,20 @@ public void initializePlugins(final boolean init_rpcs) { } catch (Exception e) { throw new RuntimeException("Failed to initialize search plugin", e); } - LOG.info("Successfully initialized search plugin [" + - search.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized search plugin [" + + search.getClass().getCanonicalName() + "] version: " + search.version()); } else { search = null; } - + // load the real time publisher plugin if enabled if (config.getBoolean("tsd.rtpublisher.enable")) { rt_publisher = PluginLoader.loadSpecificPlugin( config.getString("tsd.rtpublisher.plugin"), RTPublisher.class); if (rt_publisher == null) { throw new IllegalArgumentException( - "Unable to locate real time publisher plugin: " + + "Unable to locate real time publisher plugin: " + config.getString("tsd.rtpublisher.plugin")); } try { @@ -225,17 +225,17 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize real time publisher plugin", e); } - LOG.info("Successfully initialized real time publisher plugin [" + - rt_publisher.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized real time publisher plugin [" + + rt_publisher.getClass().getCanonicalName() + "] version: " + rt_publisher.version()); } else { rt_publisher = null; } - + if (init_rpcs && config.hasProperty("tsd.rpc.plugins")) { final String[] plugins = config.getString("tsd.rpc.plugins").split(","); for (final String plugin : plugins) { - final RpcPlugin rpc = PluginLoader.loadSpecificPlugin(plugin.trim(), + final RpcPlugin rpc = PluginLoader.loadSpecificPlugin(plugin.trim(), RpcPlugin.class); if (rpc == null) { throw new IllegalArgumentException( @@ -247,31 +247,31 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize RPC plugin", e); } - + if (rpc_plugins == null) { rpc_plugins = new ArrayList(1); } rpc_plugins.add(rpc); - LOG.info("Successfully initialized RPC plugin [" + - rpc.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized RPC plugin [" + + rpc.getClass().getCanonicalName() + "] version: " + rpc.version()); } } } - - /** - * Returns the configured HBase client + + /** + * Returns the configured HBase client * @return The HBase client - * @since 2.0 + * @since 2.0 */ public final HBaseClient getClient() { return this.client; } - - /** + + /** * Getter that returns the configuration object * @return The configuration object - * @since 2.0 + * @since 2.0 */ public final Config getConfig() { return this.config; @@ -302,7 +302,7 @@ public Deferred getUidName(final UniqueIdType type, final byte[] uid) { throw new IllegalArgumentException("Unrecognized UID type"); } } - + /** * Attempts to find the UID matching a given name * @param type The type of UID @@ -326,7 +326,7 @@ public byte[] getUID(final UniqueIdType type, final String name) { throw new IllegalArgumentException("Unrecognized UID type"); } } - + /** * Verifies that the data and UID tables exist in HBase and optionally the * tree and meta data tables if the user has enabled meta tracking or tree @@ -336,7 +336,7 @@ public byte[] getUID(final UniqueIdType type, final String name) { * @since 2.0 */ public Deferred> checkNecessaryTablesExist() { - final ArrayList> checks = + final ArrayList> checks = new ArrayList>(2); checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.data_table"))); @@ -346,14 +346,14 @@ public Deferred> checkNecessaryTablesExist() { checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.tree_table"))); } - if (config.enable_realtime_ts() || config.enable_realtime_uid() || + if (config.enable_realtime_ts() || config.enable_realtime_uid() || config.enable_tsuid_incrementing()) { checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.meta_table"))); } return Deferred.group(checks); } - + /** Number of cache hits during lookups involving UIDs. */ public int uidCacheHits() { return (metrics.cacheHits() + tag_names.cacheHits() @@ -377,36 +377,36 @@ public int uidCacheSize() { * @param collector The collector to use. */ public void collectStats(final StatsCollector collector) { - final byte[][] kinds = { - METRICS_QUAL.getBytes(CHARSET), - TAG_NAME_QUAL.getBytes(CHARSET), - TAG_VALUE_QUAL.getBytes(CHARSET) + final byte[][] kinds = { + METRICS_QUAL.getBytes(CHARSET), + TAG_NAME_QUAL.getBytes(CHARSET), + TAG_VALUE_QUAL.getBytes(CHARSET) }; try { final Map used_uids = UniqueId.getUsedUIDs(this, kinds) .joinUninterruptibly(); - + collectUidStats(metrics, collector); - collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), + collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), "kind=" + METRICS_QUAL); - collector.record("uid.ids-available", - (metrics.maxPossibleId() - used_uids.get(METRICS_QUAL)), + collector.record("uid.ids-available", + (metrics.maxPossibleId() - used_uids.get(METRICS_QUAL)), "kind=" + METRICS_QUAL); - + collectUidStats(tag_names, collector); - collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), + collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), "kind=" + TAG_NAME_QUAL); - collector.record("uid.ids-available", - (tag_names.maxPossibleId() - used_uids.get(TAG_NAME_QUAL)), + collector.record("uid.ids-available", + (tag_names.maxPossibleId() - used_uids.get(TAG_NAME_QUAL)), "kind=" + TAG_NAME_QUAL); - + collectUidStats(tag_values, collector); - collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), + collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), "kind=" + TAG_VALUE_QUAL); - collector.record("uid.ids-available", - (tag_values.maxPossibleId() - used_uids.get(TAG_VALUE_QUAL)), + collector.record("uid.ids-available", + (tag_values.maxPossibleId() - used_uids.get(TAG_VALUE_QUAL)), "kind=" + TAG_VALUE_QUAL); - + } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } @@ -459,7 +459,7 @@ public void collectStats(final StatsCollector collector) { rt_publisher.collectStats(collector); } finally { collector.clearExtraTag("plugin"); - } + } } if (search != null) { try { @@ -467,18 +467,18 @@ public void collectStats(final StatsCollector collector) { search.collectStats(collector); } finally { collector.clearExtraTag("plugin"); - } + } } if (rpc_plugins != null) { try { collector.addExtraTag("plugin", "rpc"); for(RpcPlugin rpc: rpc_plugins) { rpc.collectStats(collector); - } + } } finally { collector.clearExtraTag("plugin"); - } - } + } + } } /** Returns a latency histogram for Put RPCs used to store data points. */ @@ -507,17 +507,17 @@ private static void collectUidStats(final UniqueId uid, public static short metrics_width() { return METRICS_WIDTH; } - + /** @return the width, in bytes, of tagk UIDs */ public static short tagk_width() { return TAG_NAME_WIDTH; } - + /** @return the width, in bytes, of tagv UIDs */ public static short tagv_width() { return TAG_VALUE_WIDTH; } - + /** * Returns a new {@link Query} instance suitable for this TSDB. */ @@ -535,6 +535,16 @@ public WritableDataPoints newDataPoints() { return new IncomingDataPoints(this); } + /** + * + * @param metric Every data point that gets appended must be associated to this metric. + * @param tags The associated tags for all data points being added. + * @return data structure which can have data points appended. + */ + public WritableDataPoints newBatch(String metric, Map tags) { + return new BatchedDataPoints(this, metric, tags); + } + /** * Adds a single integer value data point in the TSDB. * @param metric A non-empty string. @@ -652,7 +662,7 @@ private Deferred addPointInternal(final String metric, final Map tags, final short flags) { // we only accept positive unix epoch timestamps in seconds or milliseconds - if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && + if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && timestamp > 9999999999999L)) { throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + " timestamp=" + timestamp @@ -664,41 +674,41 @@ private Deferred addPointInternal(final String metric, 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) - + 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); final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); - + // TODO(tsuna): Add a callback to time the latency of HBase and store the // timing in a moving Histogram (once we have a class for this). Deferred result = client.put(point); - if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && + if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && !config.enable_tsuid_tracking() && rt_publisher == null) { return result; } - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, + + 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, + 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); } @@ -748,9 +758,9 @@ public String toString() { * recoverable by retrying, some are not. */ public Deferred shutdown() { - final ArrayList> deferreds = + final ArrayList> deferreds = new ArrayList>(); - + final class HClientShutdown implements Callback> { public Object call(final ArrayList args) { return client.shutdown(); @@ -759,7 +769,7 @@ public String toString() { return "shutdown HBase client"; } } - + final class ShutdownErrback implements Callback { public Object call(final Exception e) { final Logger LOG = LoggerFactory.getLogger(ShutdownErrback.class); @@ -779,36 +789,36 @@ public String toString() { return "shutdown HBase client after error"; } } - + final class CompactCB implements Callback> { public Object call(ArrayList compactions) throws Exception { return null; } } - + if (config.enable_compactions()) { LOG.info("Flushing compaction queue"); deferreds.add(compactionq.flush().addCallback(new CompactCB())); } if (search != null) { - LOG.info("Shutting down search plugin: " + + LOG.info("Shutting down search plugin: " + search.getClass().getCanonicalName()); deferreds.add(search.shutdown()); } if (rt_publisher != null) { - LOG.info("Shutting down RT plugin: " + + LOG.info("Shutting down RT plugin: " + rt_publisher.getClass().getCanonicalName()); deferreds.add(rt_publisher.shutdown()); } - + if (rpc_plugins != null && !rpc_plugins.isEmpty()) { for (final RpcPlugin rpc : rpc_plugins) { - LOG.info("Shutting down RPC plugin: " + + LOG.info("Shutting down RPC plugin: " + rpc.getClass().getCanonicalName()); deferreds.add(rpc.shutdown()); } } - + // wait for plugins to shutdown before we close the client return deferreds.size() > 0 ? Deferred.group(deferreds).addCallbacks(new HClientShutdown(), @@ -823,14 +833,14 @@ public Object call(ArrayList compactions) throws Exception { public List suggestMetrics(final String search) { return metrics.suggest(search); } - + /** * Given a prefix search, returns matching metric names. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestMetrics(final String search, + public List suggestMetrics(final String search, final int max_results) { return metrics.suggest(search, max_results); } @@ -842,14 +852,14 @@ public List suggestMetrics(final String search, public List suggestTagNames(final String search) { return tag_names.suggest(search); } - + /** * Given a prefix search, returns matching tagk names. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestTagNames(final String search, + public List suggestTagNames(final String search, final int max_results) { return tag_names.suggest(search, max_results); } @@ -861,14 +871,14 @@ public List suggestTagNames(final String search, public List suggestTagValues(final String search) { return tag_values.suggest(search); } - + /** * Given a prefix search, returns matching tag values. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestTagValues(final String search, + public List suggestTagValues(final String search, final int max_results) { return tag_values.suggest(search, max_results); } @@ -885,14 +895,14 @@ public void dropCaches() { /** * Attempts to assign a UID to a name for the given type - * Used by the UniqueIdRpc call to generate IDs for new metrics, tagks or + * Used by the UniqueIdRpc call to generate IDs for new metrics, tagks or * tagvs. The name must pass validation and if it's already assigned a UID, * this method will throw an error with the proper UID. Otherwise if it can * create the UID, it will be returned * @param type The type of uid to assign, metric, tagk or tagv * @param name The name of the uid object * @return A byte array with the UID if the assignment was successful - * @throws IllegalArgumentException if the name is invalid or it already + * @throws IllegalArgumentException if the name is invalid or it already * exists * @since 2.0 */ @@ -927,22 +937,22 @@ public byte[] assignUid(final String type, final String name) { throw new IllegalArgumentException("Unknown type name"); } } - + /** @return the name of the UID table as a byte array for client requests */ public byte[] uidTable() { return this.uidtable; } - + /** @return the name of the data table as a byte array for client requests */ public byte[] dataTable() { return this.table; } - + /** @return the name of the tree table as a byte array for client requests */ public byte[] treeTable() { return this.treetable; } - + /** @return the name of the meta table as a byte array for client requests */ public byte[] metaTable() { return this.meta_table; @@ -958,7 +968,7 @@ public void indexTSMeta(final TSMeta meta) { search.indexTSMeta(meta).addErrback(new PluginError()); } } - + /** * Delete the timeseries meta object from the search index * @param tsuid The TSUID to delete @@ -969,7 +979,7 @@ public void deleteTSMeta(final String tsuid) { search.deleteTSMeta(tsuid).addErrback(new PluginError()); } } - + /** * Index the given UID meta object via the configured search plugin * @param meta The meta data object to index @@ -980,7 +990,7 @@ public void indexUIDMeta(final UIDMeta meta) { search.indexUIDMeta(meta).addErrback(new PluginError()); } } - + /** * Delete the UID meta object from the search index * @param meta The UID meta object to delete @@ -991,7 +1001,7 @@ public void deleteUIDMeta(final UIDMeta meta) { search.deleteUIDMeta(meta).addErrback(new PluginError()); } } - + /** * Index the given Annotation object via the configured search plugin * @param note The annotation object to index @@ -1005,7 +1015,7 @@ public void indexAnnotation(final Annotation note) { rt_publisher.publishAnnotation(note); } } - + /** * Delete the annotation object from the search index * @param note The annotation object to delete @@ -1016,7 +1026,7 @@ public void deleteAnnotation(final Annotation note) { search.deleteAnnotation(note).addErrback(new PluginError()); } } - + /** * Processes the TSMeta through all of the trees if configured to do so * @param meta The meta data to process @@ -1028,7 +1038,7 @@ public Deferred processTSMetaThroughTrees(final TSMeta meta) { } return Deferred.fromResult(false); } - + /** * Executes a search query using the search plugin * @param query The query to execute @@ -1042,13 +1052,13 @@ public Deferred executeSearch(final SearchQuery query) { throw new IllegalStateException( "Searching has not been enabled on this TSD"); } - + return search.executeQuery(query); } - + /** - * Simply logs plugin errors when they're thrown by attaching as an errorback. - * Without this, exceptions will just disappear (unless logged by the plugin) + * Simply logs plugin errors when they're thrown by attaching as an errorback. + * Without this, exceptions will just disappear (unless logged by the plugin) * since we don't wait for a result. */ final class PluginError implements Callback { @@ -1058,12 +1068,12 @@ public Object call(final Exception e) throws Exception { return null; } } - + // ------------------ // // Compaction helpers // // ------------------ // - final KeyValue compact(final ArrayList row, + final KeyValue compact(final ArrayList row, List annotations) { return compactionq.compact(row, annotations); } From 3154a7cb791c2f4a90bc21fae10a7a53555c5320 Mon Sep 17 00:00:00 2001 From: haden dude Date: Mon, 15 Sep 2014 07:12:33 -0700 Subject: [PATCH 033/826] Cleanup BatchedDataPoints and add unit tests --- Makefile.am | 1 + src/core/BatchedDataPoints.java | 852 +++++++++++++++------------ src/core/IncomingDataPoints.java | 196 +++--- src/core/TSDB.java | 221 +++---- src/utils/Config.java | 112 ++-- test/core/TestBatchedDataPoints.java | 190 ++++++ 6 files changed, 923 insertions(+), 649 deletions(-) create mode 100644 test/core/TestBatchedDataPoints.java diff --git a/Makefile.am b/Makefile.am index 0b87f6558e..06628b2dca 100644 --- a/Makefile.am +++ b/Makefile.am @@ -33,6 +33,7 @@ tsdb_SRC := \ src/core/AggregationIterator.java \ src/core/Aggregator.java \ src/core/Aggregators.java \ + src/core/BatchedDataPoints.java \ src/core/ByteBufferList.java \ src/core/ColumnDatapointIterator.java \ src/core/CompactionQueue.java \ diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 291102f730..80b3a92120 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// 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 @@ -25,398 +25,466 @@ import net.opentsdb.meta.Annotation; /** - * Receives new data points and stores them in compacted form. No points are written until - * {@code flushNow} is called. This ensures that true batch dynamics can be leveraged. This - * implementation will allow an entire hours worth of data to be written in a single transaction to - * the data table. + * Receives new data points and stores them in compacted form. No points are + * written until {@code flushNow} is called. This ensures that true batch + * dynamics can be leveraged. This implementation will allow an entire hours + * worth of data to be written in a single transaction to the data table. */ final class BatchedDataPoints implements WritableDataPoints { - /** - * The {@code TSDB} instance we belong to. - */ - private final TSDB tsdb; - - /** - * The row key. 3 bytes for the metric name, 4 bytes for the base timestamp, 6 bytes per tag (3 - * for the name, 3 for the value). - */ - private byte[] rowKey; - - /** - * Track the last timestamp written for this series. - */ - private long lastTimestamp; - - /** - * Number of data points in this row. - */ - private short size = 0; - - /** - * Storage of the compacted qualifier. - */ - private byte[] batchedQualifier = new byte[Const.MAX_TIMESPAN * 4]; - - /** - * Storage of the compacted value. - */ - private byte[] batchedValue = new byte[Const.MAX_TIMESPAN * 8]; - - /** - * Track the index position where the next qualifier gets written. - */ - private int qualifierIndex = 0; - - /** - * Track the index position where the next value gets written. - */ - private int valueIndex = 0; - - /** - * Track the base time for this batch of points. - */ - private long baseTime; - - /** - * Constructor. - * - * @param tsdb The TSDB we belong to. - */ - BatchedDataPoints(final TSDB tsdb, final String metric, final Map tags) { - this.tsdb = tsdb; - setSeries(metric, tags); - } - - /** - * Sets the metric name and tags of this batch. This method only need be called if there is a - * desire to reuse the data structure after the data has been flushed. This will reset all - * cached information in this data structure. - * - * @throws IllegalArgumentException if the metric name is empty or contains illegal characters. - * @throws IllegalArgumentException if the tags list is empty or one of the elements contains - * illegal characters. - */ - @Override - public void setSeries(final String metric, final Map tags) { - IncomingDataPoints.checkMetricAndTags(metric, tags); - try { - rowKey = IncomingDataPoints.rowKeyTemplate(tsdb, metric, tags); - reset(); - } - catch (RuntimeException e) { - throw e; - } - catch (Exception e) { - throw new RuntimeException("Should never happen", e); - } - } - - private void reset() { - size = 0; - qualifierIndex = 0; - valueIndex = 0; - baseTime = Long.MIN_VALUE; - lastTimestamp = Long.MIN_VALUE; - } - - /** - * A copy of the values is created and sent with a put request. A reset is initialized which - * makes this data structure ready to be reused for the same metric and tags but for a different - * hour of data. - * - * @return {@inheritDoc} - */ - @Override - public Deferred persist() { - final byte[] q = Arrays.copyOfRange(batchedQualifier, 0, qualifierIndex); - final byte[] v = Arrays.copyOfRange(batchedValue, 0, valueIndex); - final byte[] r = Arrays.copyOfRange(rowKey, 0, rowKey.length); - reset(); - return tsdb.put(r, q, v); - } - - @Override - public void setBufferingTime(short time) { - // does nothing - } - - @Override - public void setBatchImport(boolean batchornot) { - // does nothing - } - - @Override - 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) { - v = Bytes.fromShort((short) value); - } - else if (Integer.MIN_VALUE <= value && value <= Integer.MAX_VALUE) { - v = Bytes.fromInt((int) value); - } - else { - v = Bytes.fromLong(value); - } - final short flags = (short) (v.length - 1); // Just the length. - return addPointInternal(timestamp, v, flags); - } - - @Override - public Deferred addPoint(final long timestamp, final float value) { - if (Float.isNaN(value) || Float.isInfinite(value)) { - throw new IllegalArgumentException("value is NaN or Infinite: " + value - + " for timestamp=" + timestamp); - } - final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. - return addPointInternal(timestamp, Bytes.fromInt(Float.floatToRawIntBits(value)), flags); - } - - /** - * Implements {@link #addPoint} by storing a value with a specific flag. - * - * @param timestamp The timestamp to associate with the value. - * @param value The value to store. - * @param flags Flags to store in the qualifier (size and type of the data point). - */ - private Deferred addPointInternal(final long timestamp, final byte[] value, final short flags) - throws IllegalDataException { - final boolean ms_timestamp = (timestamp & Const.SECOND_MASK) != 0; - - // we only accept unix epoch timestamps in seconds or milliseconds - if (timestamp < 0 || (ms_timestamp && timestamp > 9999999999999L)) { - throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") - + " timestamp=" + timestamp - + " when trying to add value=" + Arrays.toString(value) + " to " + this); - } - - // always maintain lastTimestamp in milliseconds - if ((ms_timestamp ? timestamp : timestamp * 1000) <= lastTimestamp) { - throw new IllegalArgumentException("New timestamp=" + timestamp - + " is less than or equal to previous=" + lastTimestamp - + " when trying to add value=" + Arrays.toString(value) - + " to " + this); - } - lastTimestamp = (ms_timestamp ? timestamp : timestamp * 1000); - - long incomingBaseTime; - if (ms_timestamp) { - // drop the ms timestamp to seconds to calculate the base timestamp - incomingBaseTime = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); - } - else { - incomingBaseTime = (timestamp - (timestamp % Const.MAX_TIMESPAN)); - } - - /** - * First time we add a point initialize the rows timestamp. - */ - if (baseTime == Long.MIN_VALUE) { - baseTime = incomingBaseTime; - Bytes.setInt(rowKey, (int) baseTime, tsdb.metrics.width()); - } - - if (incomingBaseTime - baseTime >= Const.MAX_TIMESPAN) { - throw new IllegalDataException("The timestamp is beyond the boundary of this batch of data points"); - } - if (incomingBaseTime < baseTime) { - throw new IllegalDataException("The timestamp is prior to the boundary of this batch of data points"); - } - - // Java is so stupid with its auto-promotion of int to float. - final byte[] newQualifier = Internal.buildQualifier(timestamp, flags); - - // compact this data point with the previously compacted data points. - append(newQualifier, value); - size++; - - /** - * Satisfies the interface. - */ - return Deferred.fromResult((Object) null); - } - - private void ensureCapacity(final byte[] nextQualifier, final byte[] nextValue) { - if (qualifierIndex + nextQualifier.length >= batchedQualifier.length) { - batchedQualifier = Arrays.copyOf(batchedQualifier, batchedQualifier.length * 2); - } - if (valueIndex + nextValue.length >= batchedValue.length) { - batchedValue = Arrays.copyOf(batchedValue, batchedValue.length * 2); - } - } - - private void append(final byte[] nextQualifier, final byte[] nextValue) { - ensureCapacity(nextQualifier, nextValue); - - // Now let's simply concatenate all the values together. - System.arraycopy(nextValue, 0, batchedValue, valueIndex, nextValue.length); - valueIndex += nextValue.length; - - // Now let's concatenate all the qualifiers together. - System.arraycopy(nextQualifier, 0, batchedQualifier, qualifierIndex, nextQualifier.length); - qualifierIndex += nextQualifier.length; - } - - @Override - public String metricName() { - try { - return metricNameAsync().joinUninterruptibly(); - } - catch (RuntimeException e) { - throw e; - } - catch (Exception e) { - throw new RuntimeException("Should never be here", e); - } - } - - @Override - public Deferred metricNameAsync() { - if (rowKey == null) { - throw new IllegalStateException("Instance was not properly constructed!"); - } - final byte[] id = Arrays.copyOfRange(rowKey, 0, tsdb.metrics.width()); - return tsdb.metrics.getNameAsync(id); - } - - @Override - public Map getTags() { - try { - return getTagsAsync().joinUninterruptibly(); - } - catch (RuntimeException e) { - throw e; - } - catch (Exception e) { - throw new RuntimeException("Should never be here", e); - } - } - - @Override - public Deferred> getTagsAsync() { - return Tags.getTagsAsync(tsdb, rowKey); - } - - @Override - public List getAggregatedTags() { - return Collections.emptyList(); - } - - @Override - public Deferred> getAggregatedTagsAsync() { - final List empty = Collections.emptyList(); - return Deferred.fromResult(empty); - } - - @Override - public List getTSUIDs() { - return Collections.emptyList(); - } - - @Override - public List getAnnotations() { - return null; - } - - @Override - public int size() { - return size; - } - - @Override - public int aggregatedSize() { - return 0; - } - - @Override - public SeekableView iterator() { - return new DataPointsIterator(this); - } - - /** - * @throws IndexOutOfBoundsException if {@code i} is out of bounds. - */ - private void checkIndex(final int i) { - if (i > size) { - throw new IndexOutOfBoundsException("index " + i + " > " + size - + " for this=" + this); - } - if (i < 0) { - throw new IndexOutOfBoundsException("negative index " + i - + " for this=" + this); - } - } - - private static short delta(final short qualifier) { - return (short) ((qualifier & 0xFFFF) >>> Const.FLAG_BITS); - } - - @Override - public long timestamp(final int i) { - checkIndex(i); - return baseTime + (delta(batchedQualifier[i]) & 0xFFFF); - } - - @Override - public boolean isInteger(final int i) { - checkIndex(i); - return (batchedQualifier[i] & Const.FLAG_FLOAT) == 0x0; - } - - @Override - public long longValue(final int i) { - // Don't call checkIndex(i) because isInteger(i) already calls it. - if (isInteger(i)) { - return batchedValue[i]; - } - throw new ClassCastException("value #" + i + " is not a long in " + this); - } - - @Override - public double doubleValue(final int i) { - // Don't call checkIndex(i) because isInteger(i) already calls it. - if (!isInteger(i)) { - return Float.intBitsToFloat((int) batchedValue[i]); - } - throw new ClassCastException("value #" + i + " is not a float in " + this); - } - - /** - * Returns a human readable string representation of the object. - */ - @Override - public String toString() { - // The argument passed to StringBuilder is a pretty good estimate of the - // length of the final string based on the row key and number of elements. - final String metric = metricName(); - final StringBuilder buf = new StringBuilder(80 + metric.length() - + rowKey.length * 4 + size * 16); - buf.append("BatchedDataPoints(") - .append(rowKey == null ? "" : Arrays.toString(rowKey)) - .append(" (metric=") - .append(metric) - .append("), base_time=") - .append(baseTime) - .append(" (") - .append(baseTime > 0 ? new Date(baseTime * 1000) : "no date") - .append("), ["); - for (short i = 0; i < size; i++) { - buf.append('+').append(delta(batchedQualifier[i])); - if (isInteger(i)) { - buf.append(":long(").append(longValue(i)); - } - else { - buf.append(":float(").append(doubleValue(i)); - } - buf.append(')'); - if (i != size - 1) { - buf.append(", "); - } - } - buf.append("])"); - return buf.toString(); - } + /** + * The {@code TSDB} instance we belong to. + */ + private final TSDB tsdb; + + /** + * The row key. 3 bytes for the metric name, 4 bytes for the base timestamp, + * 6 bytes per tag (3 for the name, 3 for the value). + */ + private byte[] row_key; + + /** + * Track the last timestamp written for this series. + */ + private long last_timestamp; + + /** + * Number of data points in this row. + */ + private int size = 0; + + /** + * Storage of the compacted qualifier. + */ + private byte[] batched_qualifier = new byte[Const.MAX_TIMESPAN * 4]; + + /** + * Storage of the compacted value. + */ + private byte[] batched_value = new byte[Const.MAX_TIMESPAN * 8]; + + /** + * Track the index position where the next qualifier gets written. + */ + private int qualifier_index = 0; + + /** + * Track the index position where the next value gets written. + */ + private int value_index = 0; + + /** + * Track the base time for this batch of points. + */ + private long base_time; + + /** + * Constructor. + * + * @param tsdb The TSDB we belong to. + */ + BatchedDataPoints(final TSDB tsdb, final String metric, + final Map tags) { + this.tsdb = tsdb; + setSeries(metric, tags); + } + + /** + * Sets the metric name and tags of this batch. This method only need be + * called if there is a desire to reuse the data structure after the data has + * been flushed. This will reset all cached information in this data structure. + * @throws IllegalArgumentException if the metric name is empty or contains + * illegal characters or if the tags list is empty or one of the elements + * contains illegal characters. + */ + @Override + public void setSeries(final String metric, final Map tags) { + IncomingDataPoints.checkMetricAndTags(metric, tags); + try { + row_key = IncomingDataPoints.rowKeyTemplate(tsdb, metric, tags); + reset(); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException("Should never happen", e); + } + } + + /** + * Resets the indices without overwriting the buffers. So the same amount of + * space will remain allocated. + */ + private void reset() { + size = 0; + qualifier_index = 0; + value_index = 0; + base_time = Long.MIN_VALUE; + last_timestamp = Long.MIN_VALUE; + } + + /** + * A copy of the values is created and sent with a put request. A reset is + * initialized which makes this data structure ready to be reused for the same + * metric and tags but for a different hour of data. + * @return {@inheritDoc} + */ + @Override + public Deferred persist() { + final byte[] q = Arrays.copyOfRange(batched_qualifier, 0, qualifier_index); + final byte[] v = Arrays.copyOfRange(batched_value, 0, value_index); + final byte[] r = Arrays.copyOfRange(row_key, 0, row_key.length); + reset(); + return tsdb.put(r, q, v); + } + + @Override + public void setBufferingTime(short time) { + // does nothing + } + + @Override + public void setBatchImport(boolean batchornot) { + // does nothing + } + + @Override + 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) { + v = Bytes.fromShort((short) value); + } + else if (Integer.MIN_VALUE <= value && value <= Integer.MAX_VALUE) { + v = Bytes.fromInt((int) value); + } + else { + v = Bytes.fromLong(value); + } + final short flags = (short) (v.length - 1); // Just the length. + return addPointInternal(timestamp, v, flags); + } + + @Override + public Deferred addPoint(final long timestamp, final float value) { + if (Float.isNaN(value) || Float.isInfinite(value)) { + throw new IllegalArgumentException("value is NaN or Infinite: " + value + + " for timestamp=" + timestamp); + } + final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. + return addPointInternal(timestamp, + Bytes.fromInt(Float.floatToRawIntBits(value)), flags); + } + + /** + * Implements {@link #addPoint} by storing a value with a specific flag. + * + * @param timestamp The timestamp to associate with the value. + * @param value The value to store. + * @param flags Flags to store in the qualifier (size and type of the data point). + */ + private Deferred addPointInternal(final long timestamp, + final byte[] value, final short flags) throws IllegalDataException { + final boolean ms_timestamp = (timestamp & Const.SECOND_MASK) != 0; + + // we only accept unix epoch timestamps in seconds or milliseconds + if (timestamp < 0 || (ms_timestamp && timestamp > 9999999999999L)) { + throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + + " timestamp=" + timestamp + + " when trying to add value=" + Arrays.toString(value) + " to " + this); + } + + // always maintain lastTimestamp in milliseconds + if ((ms_timestamp ? timestamp : timestamp * 1000) <= last_timestamp) { + throw new IllegalArgumentException("New timestamp=" + timestamp + + " is less than or equal to previous=" + last_timestamp + + " when trying to add value=" + Arrays.toString(value) + + " to " + this); + } + last_timestamp = (ms_timestamp ? timestamp : timestamp * 1000); + + long incomingBaseTime; + if (ms_timestamp) { + // drop the ms timestamp to seconds to calculate the base timestamp + incomingBaseTime = + ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); + } + else { + incomingBaseTime = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + } + + /** + * First time we add a point initialize the rows timestamp. + */ + if (base_time == Long.MIN_VALUE) { + base_time = incomingBaseTime; + Bytes.setInt(row_key, (int) base_time, tsdb.metrics.width()); + } + + if (incomingBaseTime - base_time >= Const.MAX_TIMESPAN) { + throw new IllegalDataException( + "The timestamp is beyond the boundary of this batch of data points"); + } + if (incomingBaseTime < base_time) { + throw new IllegalDataException( + "The timestamp is prior to the boundary of this batch of data points"); + } + + // Java is so stupid with its auto-promotion of int to float. + final byte[] new_qualifier = Internal.buildQualifier(timestamp, flags); + + // compact this data point with the previously compacted data points. + append(new_qualifier, value); + size++; + + /** + * Satisfies the interface. + */ + return Deferred.fromResult((Object) null); + } + + /** + * Checks the size of the qualifier and value arrays to make sure we have + * space. If not then we double the size of the arrays. This way a row + * allocates space for a full hour of second data but if the user requires + * millisecond storage with more than 3600 points, it will expand. + * @param next_qualifier The next qualifier to use for it's length + * @param next_value The next value to use for it's length + */ + private void ensureCapacity(final byte[] next_qualifier, + final byte[] next_value) { + if (qualifier_index + next_qualifier.length >= batched_qualifier.length) { + batched_qualifier = Arrays.copyOf(batched_qualifier, + batched_qualifier.length * 2); + } + if (value_index + next_value.length >= batched_value.length) { + batched_value = Arrays.copyOf(batched_value, batched_value.length * 2); + } + } + + /** + * Appends the value and qualifier to the appropriate arrays + * @param next_qualifier The next qualifier to append + * @param next_value The next value to append + */ + private void append(final byte[] next_qualifier, final byte[] next_value) { + ensureCapacity(next_qualifier, next_value); + + // Now let's simply concatenate all the values together. + System.arraycopy(next_value, 0, batched_value, value_index, next_value.length); + value_index += next_value.length; + + // Now let's concatenate all the qualifiers together. + System.arraycopy(next_qualifier, 0, batched_qualifier, qualifier_index, + next_qualifier.length); + qualifier_index += next_qualifier.length; + } + + @Override + public String metricName() { + try { + return metricNameAsync().joinUninterruptibly(); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred metricNameAsync() { + if (row_key == null) { + throw new IllegalStateException("Instance was not properly constructed!"); + } + final byte[] id = Arrays.copyOfRange(row_key, 0, tsdb.metrics.width()); + return tsdb.metrics.getNameAsync(id); + } + + @Override + public Map getTags() { + try { + return getTagsAsync().joinUninterruptibly(); + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred> getTagsAsync() { + return Tags.getTagsAsync(tsdb, row_key); + } + + @Override + public List getAggregatedTags() { + return Collections.emptyList(); + } + + @Override + public Deferred> getAggregatedTagsAsync() { + final List empty = Collections.emptyList(); + return Deferred.fromResult(empty); + } + + @Override + public List getTSUIDs() { + return Collections.emptyList(); + } + + @Override + public List getAnnotations() { + return null; + } + + @Override + public int size() { + return size; + } + + @Override + public int aggregatedSize() { + return 0; + } + + @Override + public SeekableView iterator() { + return new DataPointsIterator(this); + } + + /** + * @throws IndexOutOfBoundsException if {@code i} is out of bounds. + */ + private void checkIndex(final int i) { + if (i > size) { + throw new IndexOutOfBoundsException("index " + i + " > " + size + + " for this=" + this); + } + if (i < 0) { + throw new IndexOutOfBoundsException("negative index " + i + + " for this=" + this); + } + } + + /** + * Computes the proper offset to reach qualifier + * @param i + * @return + */ + private int qualifierOffset(final int i) { + int offset = 0; + for (int j = 0; j < i; j++) { + offset += Internal.getQualifierLength(batched_qualifier, offset); + } + return offset; + } + + @Override + public long timestamp(final int i) { + checkIndex(i); + return Internal.getTimestampFromQualifier(batched_qualifier, base_time, qualifierOffset(i)); + } + + @Override + public boolean isInteger(final int i) { + checkIndex(i); + return isInteger(i, qualifierOffset(i)); + } + + /** + * Tells whether or not the ith value is integer. Uses pre-computed qualifier offset. + * @param i + * @param q_offset qualifier offset + * @return + */ + private boolean isInteger(final int i, final int q_offset) { + final short flags = Internal.getFlagsFromQualifier(batched_qualifier, q_offset); + return (flags & Const.FLAG_FLOAT) == 0x0; + } + + @Override + public long longValue(final int i) { + checkIndex(i); + // compute the prope value and qualifier offsets + int v_offset = 0; + int q_offset = 0; + for (int j = 0; j < i; j++) { + v_offset += Internal.getValueLengthFromQualifier(batched_qualifier, q_offset); + q_offset += Internal.getQualifierLength(batched_qualifier, q_offset); + } + + if (isInteger(i, q_offset)) { + final short flags = Internal.getFlagsFromQualifier(batched_qualifier, q_offset); + return Internal.extractIntegerValue(batched_value, v_offset, (byte)flags); + } + throw new ClassCastException("value #" + i + " is not a long in " + this); + } + + @Override + public double doubleValue(final int i) { + checkIndex(i); + // compute the proper value and qualifier offsets + int v_offset = 0; + int q_offset = 0; + for (int j = 0; j < i; j++) { + v_offset += Internal.getValueLengthFromQualifier(batched_qualifier, q_offset); + q_offset += Internal.getQualifierLength(batched_qualifier, q_offset); + } + + if (!isInteger(i, q_offset)) { + final short flags = Internal.getFlagsFromQualifier(batched_qualifier, q_offset); + return Internal.extractFloatingPointValue(batched_value, v_offset, (byte)flags); + } + throw new ClassCastException("value #" + i + " is not a float in " + this); + } + + /** + * Returns a human readable string representation of the object. + */ + @Override + public String toString() { + // The argument passed to StringBuilder is a pretty good estimate of the + // length of the final string based on the row key and number of elements. + final String metric = metricName(); + final StringBuilder buf = new StringBuilder(80 + metric.length() + + row_key.length * 4 + size * 16); + buf.append("BatchedDataPoints(") + .append(row_key == null ? "" : Arrays.toString(row_key)) + .append(" (metric=") + .append(metric) + .append("), base_time=") + .append(base_time) + .append(" (") + .append(base_time > 0 ? new Date(base_time * 1000) : "no date") + .append("), ["); + int q_offset = 0; + int v_offset = 0; + for (int i = 0; i < size; i++) { + buf.append('+').append(Internal.getOffsetFromQualifier(batched_qualifier, q_offset)); + final short flags = Internal.getFlagsFromQualifier(batched_qualifier, q_offset); + if (isInteger(i, q_offset)) { + buf.append(":long(") + .append(Internal.extractIntegerValue(batched_value, v_offset, (byte)flags)); + } + else { + buf.append(":float(") + .append(Internal.extractFloatingPointValue(batched_value, v_offset, (byte)flags)); + } + buf.append(')'); + if (i != size - 1) { + buf.append(", "); + } + v_offset += Internal.getValueLengthFromQualifier(batched_qualifier, q_offset); + q_offset += Internal.getQualifierLength(batched_qualifier, q_offset); + } + buf.append("])"); + return buf.toString(); + } } diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 0c7aa151df..5b0fde19a7 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -33,13 +33,13 @@ */ final class IncomingDataPoints implements WritableDataPoints { - /** For how long to buffer edits when doing batch imports (in ms). */ + /** For how long to buffer edits when doing batch imports (in ms). */ private static final short DEFAULT_BATCH_IMPORT_BUFFER_INTERVAL = 5000; /** - * Keep track of the latency (in ms) we perceive sending edits to HBase. - * We want buckets up to 16s, with 2 ms interval between each bucket up to - * 100 ms after we which we switch to exponential buckets. + * Keep track of the latency (in ms) we perceive sending edits to HBase. We + * want buckets up to 16s, with 2 ms interval between each bucket up to 100 ms + * after we which we switch to exponential buckets. */ static final Histogram putlatency = new Histogram(16000, (short) 2, 100); @@ -47,18 +47,16 @@ final class IncomingDataPoints implements WritableDataPoints { private final TSDB tsdb; /** - * The row key. - * 3 bytes for the metric name, 4 bytes for the base timestamp, 6 bytes per - * tag (3 for the name, 3 for the value). + * The row key. 3 bytes for the metric name, 4 bytes for the base timestamp, 6 + * bytes per tag (3 for the name, 3 for the value). */ private byte[] row; /** - * Qualifiers for individual data points. - * The last Const.FLAG_BITS bits are used to store flags (the type of the - * data point - integer or floating point - and the size of the data point - * in bytes). The remaining MSBs store a delta in seconds from the base - * timestamp stored in the row key. + * Qualifiers for individual data points. The last Const.FLAG_BITS bits are + * used to store flags (the type of the data point - integer or floating point + * - and the size of the data point in bytes). The remaining MSBs store a + * delta in seconds from the base timestamp stored in the row key. */ private short[] qualifiers; @@ -76,22 +74,27 @@ final class IncomingDataPoints implements WritableDataPoints { /** * Constructor. - * @param tsdb The TSDB we belong to. + * + * @param tsdb + * The TSDB we belong to. */ IncomingDataPoints(final TSDB tsdb) { this.tsdb = tsdb; // the qualifiers and values were meant for pre-compacting the rows. We // could implement this later, but for now we don't need to track the values // as they'll just consume space during an import - //this.qualifiers = new short[3]; - //this.values = new long[3]; + // this.qualifiers = new short[3]; + // this.values = new long[3]; } /** * Validates the given metric and tags. - * @throws IllegalArgumentException if any of the arguments aren't valid. + * + * @throws IllegalArgumentException + * if any of the arguments aren't valid. */ - static void checkMetricAndTags(final String metric, final Map tags) { + static void checkMetricAndTags(final String metric, + final Map tags) { if (tags.size() <= 0) { throw new IllegalArgumentException("Need at least one tags (metric=" + metric + ", tags=" + tags + ')'); @@ -108,31 +111,30 @@ static void checkMetricAndTags(final String metric, final Map ta } /** - * Returns a partially initialized row key for this metric and these tags. - * The only thing left to fill in is the base timestamp. - */ - static byte[] rowKeyTemplate(final TSDB tsdb, - final String metric, - final Map tags) { + * Returns a partially initialized row key for this metric and these tags. The + * only thing left to fill in is the base timestamp. + */ + static byte[] rowKeyTemplate(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 = (metric_width + Const.TIMESTAMP_BYTES - + tag_name_width * num_tags - + tag_value_width * num_tags); + int row_size = (metric_width + Const.TIMESTAMP_BYTES + tag_name_width + * num_tags + tag_value_width * num_tags); final byte[] row = new byte[row_size]; short pos = 0; - copyInRowKey(row, pos, (tsdb.config.auto_metric() ? - tsdb.metrics.getOrCreateId(metric) : tsdb.metrics.getId(metric))); + copyInRowKey(row, pos, + (tsdb.config.auto_metric() ? tsdb.metrics.getOrCreateId(metric) + : tsdb.metrics.getId(metric))); pos += metric_width; pos += Const.TIMESTAMP_BYTES; - for(final byte[] tag : Tags.resolveOrCreateAll(tsdb, tags)) { + for (final byte[] tag : Tags.resolveOrCreateAll(tsdb, tags)) { copyInRowKey(row, pos, tag); pos += tag.length; } @@ -140,21 +142,20 @@ static byte[] rowKeyTemplate(final TSDB tsdb, } /** - * Returns a partially initialized row key for this metric and these tags. - * The only thing left to fill in is the base timestamp. + * 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 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 = (metric_width + Const.TIMESTAMP_BYTES - + tag_name_width * num_tags - + tag_value_width * num_tags); + int row_size = (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. @@ -174,8 +175,8 @@ public byte[] call(final byte[] metricid) { } // Copy the tag IDs in the row key. - class CopyTagsInRowKeyCB - implements Callback, ArrayList> { + class CopyTagsInRowKeyCB implements + Callback, ArrayList> { public Deferred call(final ArrayList tags) { short pos = metric_width; pos += Const.TIMESTAMP_BYTES; @@ -190,8 +191,8 @@ public Deferred call(final ArrayList tags) { } // Kick off the resolution of all tags. - return Tags.resolveOrCreateAllAsync(tsdb, tags) - .addCallbackDeferring(new CopyTagsInRowKeyCB()); + return Tags.resolveOrCreateAllAsync(tsdb, tags).addCallbackDeferring( + new CopyTagsInRowKeyCB()); } public void setSeries(final String metric, final Map tags) { @@ -208,26 +209,33 @@ public void setSeries(final String metric, final Map tags) { /** * Copies the specified byte array at the specified offset in the row key. - * @param row The row key into which to copy the bytes. - * @param offset The offset in the row key to start writing at. - * @param bytes The bytes to copy. + * + * @param row + * The row key into which to copy the bytes. + * @param offset + * The offset in the row key to start writing at. + * @param bytes + * The bytes to copy. */ - private static void copyInRowKey(final byte[] row, final short offset, final byte[] bytes) { + private static void copyInRowKey(final byte[] row, final short offset, + final byte[] bytes) { System.arraycopy(bytes, 0, row, offset, bytes.length); } /** * Updates the base time in the row key. - * @param timestamp The timestamp from which to derive the new base time. + * + * @param timestamp + * The timestamp from which to derive the new base time. * @return The updated base time. */ private long updateBaseTime(final long timestamp) { // We force the starting timestamp to be on a MAX_TIMESPAN boundary - // so that all TSDs create rows with the same base time. Otherwise + // so that all TSDs create rows with the same base time. Otherwise // we'd need to coordinate TSDs to avoid creating rows that cover // overlapping time periods. final long base_time = timestamp - (timestamp % Const.MAX_TIMESPAN); - // Clone the row key since we're going to change it. We must clone it + // Clone the row key since we're going to change it. We must clone it // because the HBase client may still hold a reference to it in its // internal datastructures. row = Arrays.copyOf(row, row.length); @@ -238,14 +246,17 @@ private long updateBaseTime(final long timestamp) { /** * Implements {@link #addPoint} by storing a value with a specific flag. - * @param timestamp The timestamp to associate with the value. - * @param value The value to store. - * @param flags Flags to store in the qualifier (size and type of the data - * point). + * + * @param timestamp + * The timestamp to associate with the value. + * @param value + * The value to store. + * @param flags + * Flags to store in the qualifier (size and type of the data point). * @return A deferred object that indicates the completion of the request. */ - private Deferred addPointInternal(final long timestamp, final byte[] value, - final short flags) { + private Deferred addPointInternal(final long timestamp, + final byte[] value, final short flags) { if (row == null) { throw new IllegalStateException("setSeries() never called!"); } @@ -254,16 +265,16 @@ private Deferred addPointInternal(final long timestamp, final byte[] val // we only accept unix epoch timestamps in seconds or milliseconds if (timestamp < 0 || (ms_timestamp && timestamp > 9999999999999L)) { throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") - + " timestamp=" + timestamp - + " when trying to add value=" + Arrays.toString(value) + " to " + this); + + " timestamp=" + timestamp + " when trying to add value=" + + Arrays.toString(value) + " to " + this); } // always maintain last_ts in milliseconds if ((ms_timestamp ? timestamp : timestamp * 1000) <= last_ts) { throw new IllegalArgumentException("New timestamp=" + timestamp + " is less than or equal to previous=" + last_ts - + " when trying to add value=" + Arrays.toString(value) - + " to " + this); + + " when trying to add value=" + Arrays.toString(value) + " to " + + this); } last_ts = (ms_timestamp ? timestamp : timestamp * 1000); @@ -271,44 +282,43 @@ private Deferred addPointInternal(final long timestamp, final byte[] val long incoming_base_time; if (ms_timestamp) { // drop the ms timestamp to seconds to calculate the base timestamp - incoming_base_time = ((timestamp / 1000) - - ((timestamp / 1000) % Const.MAX_TIMESPAN)); + incoming_base_time = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); } else { incoming_base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } if (incoming_base_time - base_time >= Const.MAX_TIMESPAN) { // Need to start a new row as we've exceeded Const.MAX_TIMESPAN. - base_time = updateBaseTime((ms_timestamp ? timestamp / 1000: timestamp)); + base_time = updateBaseTime((ms_timestamp ? timestamp / 1000 : timestamp)); } // Java is so stupid with its auto-promotion of int to float. final byte[] qualifier = Internal.buildQualifier(timestamp, flags); final PutRequest point = new PutRequest(tsdb.table, row, TSDB.FAMILY, - qualifier, value); - // TODO(tsuna): The following timing is rather useless. First of all, + qualifier, value); + // TODO(tsuna): The following timing is rather useless. First of all, // the histogram never resets, so it tends to converge to a certain - // distribution and never changes. What we really want is a moving + // distribution and never changes. What we really want is a moving // histogram so we can see how the latency distribution varies over time. // The other problem is that the Histogram class isn't thread-safe and // here we access it from a callback that runs in an unknown thread, so - // we might miss some increments. So let's comment this out until we + // we might miss some increments. So let's comment this out until we // have a proper thread-safe moving histogram. - //final long start_put = System.nanoTime(); - //final Callback cb = new Callback() { - // public Object call(final Object arg) { - // putlatency.add((int) ((System.nanoTime() - start_put) / 1000000)); - // return arg; - // } - // public String toString() { - // return "time put request"; - // } - //}; + // final long start_put = System.nanoTime(); + // final Callback cb = new Callback() { + // public Object call(final Object arg) { + // putlatency.add((int) ((System.nanoTime() - start_put) / 1000000)); + // return arg; + // } + // public String toString() { + // return "time put request"; + // } + // }; // TODO(tsuna): Add an errback to handle some error cases here. point.setDurable(!batch_import); - return tsdb.client.put(point)/*.addBoth(cb)*/; + return tsdb.client.put(point)/* .addBoth(cb) */; } private void grow() { @@ -337,19 +347,18 @@ public Deferred addPoint(final long timestamp, final long value) { } else { v = Bytes.fromLong(value); } - final short flags = (short) (v.length - 1); // Just the length. + final short flags = (short) (v.length - 1); // Just the length. return addPointInternal(timestamp, v, flags); } public Deferred addPoint(final long timestamp, final float value) { if (Float.isNaN(value) || Float.isInfinite(value)) { throw new IllegalArgumentException("value is NaN or Infinite: " + value - + " for timestamp=" + timestamp); + + " for timestamp=" + timestamp); } - final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. + final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. return addPointInternal(timestamp, - Bytes.fromInt(Float.floatToRawIntBits(value)), - flags); + Bytes.fromInt(Float.floatToRawIntBits(value)), flags); } public void setBufferingTime(final short time) { @@ -441,15 +450,18 @@ public SeekableView iterator() { return new DataPointsIterator(this); } - /** @throws IndexOutOfBoundsException if {@code i} is out of bounds. */ + /** + * @throws IndexOutOfBoundsException + * if {@code i} is out of bounds. + */ private void checkIndex(final int i) { if (i > size) { throw new IndexOutOfBoundsException("index " + i + " > " + size + " for this=" + this); } if (i < 0) { - throw new IndexOutOfBoundsException("negative index " + i - + " for this=" + this); + throw new IndexOutOfBoundsException("negative index " + i + " for this=" + + this); } } @@ -489,17 +501,14 @@ public String toString() { // length of the final string based on the row key and number of elements. final String metric = metricName(); final StringBuilder buf = new StringBuilder(80 + metric.length() - + row.length * 4 + size * 16); + + row.length * 4 + size * 16); final long base_time = baseTime(); buf.append("IncomingDataPoints(") - .append(row == null ? "" : Arrays.toString(row)) - .append(" (metric=") - .append(metric) - .append("), base_time=") - .append(base_time) - .append(" (") - .append(base_time > 0 ? new Date(base_time * 1000) : "no date") - .append("), ["); + .append(row == null ? "" : Arrays.toString(row)) + .append(" (metric=").append(metric).append("), base_time=") + .append(base_time).append(" (") + .append(base_time > 0 ? new Date(base_time * 1000) : "no date") + .append("), ["); for (short i = 0; i < size; i++) { buf.append('+').append(delta(qualifiers[i])); if (isInteger(i)) { @@ -518,7 +527,6 @@ public String toString() { @Override public Deferred persist() { - return Deferred.fromResult((Object)null); + return Deferred.fromResult((Object) null); } - } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 670e2a713b..c96dc5122c 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -59,7 +59,7 @@ */ public final class TSDB { private static final Logger LOG = LoggerFactory.getLogger(TSDB.class); - + static final byte[] FAMILY = { 't' }; /** Charset used to convert Strings to byte arrays and back. */ @@ -103,13 +103,13 @@ public final class TSDB { /** Search indexer to use if configure */ private SearchPlugin search = null; - + /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; - + /** List of activated RPC plugins */ private List rpc_plugins = null; - + /** * Constructor * @param client An initialized HBase client object @@ -135,7 +135,7 @@ public TSDB(final HBaseClient client, final Config config) { DateTime.setDefaultTimezone(config.getString("tsd.core.timezone")); } if (config.enable_realtime_ts() || config.enable_realtime_uid()) { - // this is cleaner than another constructor and defaults to null. UIDs + // this is cleaner than another constructor and defaults to null. UIDs // will be refactored with DAL code anyways metrics.setTSDB(this); tag_names.setTSDB(this); @@ -151,7 +151,7 @@ public TSDB(final HBaseClient client, final Config config) { } LOG.debug(config.dumpConfiguration()); } - + /** * Constructor * @param config An initialized configuration object @@ -185,7 +185,7 @@ public void initializePlugins(final boolean init_rpcs) { PluginLoader.loadJARs(plugin_path); } catch (Exception e) { LOG.error("Error loading plugins from plugin path: " + plugin_path, e); - throw new RuntimeException("Error loading plugins from plugin path: " + + throw new RuntimeException("Error loading plugins from plugin path: " + plugin_path, e); } } @@ -195,7 +195,7 @@ public void initializePlugins(final boolean init_rpcs) { search = PluginLoader.loadSpecificPlugin( config.getString("tsd.search.plugin"), SearchPlugin.class); if (search == null) { - throw new IllegalArgumentException("Unable to locate search plugin: " + + throw new IllegalArgumentException("Unable to locate search plugin: " + config.getString("tsd.search.plugin")); } try { @@ -203,20 +203,20 @@ public void initializePlugins(final boolean init_rpcs) { } catch (Exception e) { throw new RuntimeException("Failed to initialize search plugin", e); } - LOG.info("Successfully initialized search plugin [" + - search.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized search plugin [" + + search.getClass().getCanonicalName() + "] version: " + search.version()); } else { search = null; } - + // load the real time publisher plugin if enabled if (config.getBoolean("tsd.rtpublisher.enable")) { rt_publisher = PluginLoader.loadSpecificPlugin( config.getString("tsd.rtpublisher.plugin"), RTPublisher.class); if (rt_publisher == null) { throw new IllegalArgumentException( - "Unable to locate real time publisher plugin: " + + "Unable to locate real time publisher plugin: " + config.getString("tsd.rtpublisher.plugin")); } try { @@ -225,17 +225,17 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize real time publisher plugin", e); } - LOG.info("Successfully initialized real time publisher plugin [" + - rt_publisher.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized real time publisher plugin [" + + rt_publisher.getClass().getCanonicalName() + "] version: " + rt_publisher.version()); } else { rt_publisher = null; } - + if (init_rpcs && config.hasProperty("tsd.rpc.plugins")) { final String[] plugins = config.getString("tsd.rpc.plugins").split(","); for (final String plugin : plugins) { - final RpcPlugin rpc = PluginLoader.loadSpecificPlugin(plugin.trim(), + final RpcPlugin rpc = PluginLoader.loadSpecificPlugin(plugin.trim(), RpcPlugin.class); if (rpc == null) { throw new IllegalArgumentException( @@ -247,31 +247,31 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize RPC plugin", e); } - + if (rpc_plugins == null) { rpc_plugins = new ArrayList(1); } rpc_plugins.add(rpc); - LOG.info("Successfully initialized RPC plugin [" + - rpc.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized RPC plugin [" + + rpc.getClass().getCanonicalName() + "] version: " + rpc.version()); } } } - - /** - * Returns the configured HBase client + + /** + * Returns the configured HBase client * @return The HBase client - * @since 2.0 + * @since 2.0 */ public final HBaseClient getClient() { return this.client; } - - /** + + /** * Getter that returns the configuration object * @return The configuration object - * @since 2.0 + * @since 2.0 */ public final Config getConfig() { return this.config; @@ -302,7 +302,7 @@ public Deferred getUidName(final UniqueIdType type, final byte[] uid) { throw new IllegalArgumentException("Unrecognized UID type"); } } - + /** * Attempts to find the UID matching a given name * @param type The type of UID @@ -326,7 +326,7 @@ public byte[] getUID(final UniqueIdType type, final String name) { throw new IllegalArgumentException("Unrecognized UID type"); } } - + /** * Verifies that the data and UID tables exist in HBase and optionally the * tree and meta data tables if the user has enabled meta tracking or tree @@ -336,7 +336,7 @@ public byte[] getUID(final UniqueIdType type, final String name) { * @since 2.0 */ public Deferred> checkNecessaryTablesExist() { - final ArrayList> checks = + final ArrayList> checks = new ArrayList>(2); checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.data_table"))); @@ -346,14 +346,14 @@ public Deferred> checkNecessaryTablesExist() { checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.tree_table"))); } - if (config.enable_realtime_ts() || config.enable_realtime_uid() || + if (config.enable_realtime_ts() || config.enable_realtime_uid() || config.enable_tsuid_incrementing()) { checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.meta_table"))); } return Deferred.group(checks); } - + /** Number of cache hits during lookups involving UIDs. */ public int uidCacheHits() { return (metrics.cacheHits() + tag_names.cacheHits() @@ -377,36 +377,36 @@ public int uidCacheSize() { * @param collector The collector to use. */ public void collectStats(final StatsCollector collector) { - final byte[][] kinds = { - METRICS_QUAL.getBytes(CHARSET), - TAG_NAME_QUAL.getBytes(CHARSET), - TAG_VALUE_QUAL.getBytes(CHARSET) + final byte[][] kinds = { + METRICS_QUAL.getBytes(CHARSET), + TAG_NAME_QUAL.getBytes(CHARSET), + TAG_VALUE_QUAL.getBytes(CHARSET) }; try { final Map used_uids = UniqueId.getUsedUIDs(this, kinds) .joinUninterruptibly(); - + collectUidStats(metrics, collector); - collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), + collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), "kind=" + METRICS_QUAL); - collector.record("uid.ids-available", - (metrics.maxPossibleId() - used_uids.get(METRICS_QUAL)), + collector.record("uid.ids-available", + (metrics.maxPossibleId() - used_uids.get(METRICS_QUAL)), "kind=" + METRICS_QUAL); - + collectUidStats(tag_names, collector); - collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), + collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), "kind=" + TAG_NAME_QUAL); - collector.record("uid.ids-available", - (tag_names.maxPossibleId() - used_uids.get(TAG_NAME_QUAL)), + collector.record("uid.ids-available", + (tag_names.maxPossibleId() - used_uids.get(TAG_NAME_QUAL)), "kind=" + TAG_NAME_QUAL); - + collectUidStats(tag_values, collector); - collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), + collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), "kind=" + TAG_VALUE_QUAL); - collector.record("uid.ids-available", - (tag_values.maxPossibleId() - used_uids.get(TAG_VALUE_QUAL)), + collector.record("uid.ids-available", + (tag_values.maxPossibleId() - used_uids.get(TAG_VALUE_QUAL)), "kind=" + TAG_VALUE_QUAL); - + } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } @@ -459,7 +459,7 @@ public void collectStats(final StatsCollector collector) { rt_publisher.collectStats(collector); } finally { collector.clearExtraTag("plugin"); - } + } } if (search != null) { try { @@ -467,18 +467,18 @@ public void collectStats(final StatsCollector collector) { search.collectStats(collector); } finally { collector.clearExtraTag("plugin"); - } + } } if (rpc_plugins != null) { try { collector.addExtraTag("plugin", "rpc"); for(RpcPlugin rpc: rpc_plugins) { rpc.collectStats(collector); - } + } } finally { collector.clearExtraTag("plugin"); - } - } + } + } } /** Returns a latency histogram for Put RPCs used to store data points. */ @@ -507,17 +507,17 @@ private static void collectUidStats(final UniqueId uid, public static short metrics_width() { return METRICS_WIDTH; } - + /** @return the width, in bytes, of tagk UIDs */ public static short tagk_width() { return TAG_NAME_WIDTH; } - + /** @return the width, in bytes, of tagv UIDs */ public static short tagv_width() { return TAG_VALUE_WIDTH; } - + /** * Returns a new {@link Query} instance suitable for this TSDB. */ @@ -535,14 +535,15 @@ public WritableDataPoints newDataPoints() { return new IncomingDataPoints(this); } - /** - * - * @param metric Every data point that gets appended must be associated to this metric. - * @param tags The associated tags for all data points being added. - * @return data structure which can have data points appended. - */ - public WritableDataPoints newBatch(String metric, Map tags) { - return new BatchedDataPoints(this, metric, tags); + /** + * Returns a new {@link BatchedDataPoints} instance suitable for this TSDB. + * + * @param metric Every data point that gets appended must be associated to this metric. + * @param tags The associated tags for all data points being added. + * @return data structure which can have data points appended. + */ + public WritableDataPoints newBatch(String metric, Map tags) { + return new BatchedDataPoints(this, metric, tags); } /** @@ -662,7 +663,7 @@ private Deferred addPointInternal(final String metric, final Map tags, final short flags) { // we only accept positive unix epoch timestamps in seconds or milliseconds - if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && + if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && timestamp > 9999999999999L)) { throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + " timestamp=" + timestamp @@ -674,41 +675,41 @@ private Deferred addPointInternal(final String metric, 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) - + 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); final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); - + // TODO(tsuna): Add a callback to time the latency of HBase and store the // timing in a moving Histogram (once we have a class for this). Deferred result = client.put(point); - if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && + if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && !config.enable_tsuid_tracking() && rt_publisher == null) { return result; } - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, + + 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, + 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); } @@ -758,9 +759,9 @@ public String toString() { * recoverable by retrying, some are not. */ public Deferred shutdown() { - final ArrayList> deferreds = + final ArrayList> deferreds = new ArrayList>(); - + final class HClientShutdown implements Callback> { public Object call(final ArrayList args) { return client.shutdown(); @@ -769,7 +770,7 @@ public String toString() { return "shutdown HBase client"; } } - + final class ShutdownErrback implements Callback { public Object call(final Exception e) { final Logger LOG = LoggerFactory.getLogger(ShutdownErrback.class); @@ -789,36 +790,36 @@ public String toString() { return "shutdown HBase client after error"; } } - + final class CompactCB implements Callback> { public Object call(ArrayList compactions) throws Exception { return null; } } - + if (config.enable_compactions()) { LOG.info("Flushing compaction queue"); deferreds.add(compactionq.flush().addCallback(new CompactCB())); } if (search != null) { - LOG.info("Shutting down search plugin: " + + LOG.info("Shutting down search plugin: " + search.getClass().getCanonicalName()); deferreds.add(search.shutdown()); } if (rt_publisher != null) { - LOG.info("Shutting down RT plugin: " + + LOG.info("Shutting down RT plugin: " + rt_publisher.getClass().getCanonicalName()); deferreds.add(rt_publisher.shutdown()); } - + if (rpc_plugins != null && !rpc_plugins.isEmpty()) { for (final RpcPlugin rpc : rpc_plugins) { - LOG.info("Shutting down RPC plugin: " + + LOG.info("Shutting down RPC plugin: " + rpc.getClass().getCanonicalName()); deferreds.add(rpc.shutdown()); } } - + // wait for plugins to shutdown before we close the client return deferreds.size() > 0 ? Deferred.group(deferreds).addCallbacks(new HClientShutdown(), @@ -833,14 +834,14 @@ public Object call(ArrayList compactions) throws Exception { public List suggestMetrics(final String search) { return metrics.suggest(search); } - + /** * Given a prefix search, returns matching metric names. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestMetrics(final String search, + public List suggestMetrics(final String search, final int max_results) { return metrics.suggest(search, max_results); } @@ -852,14 +853,14 @@ public List suggestMetrics(final String search, public List suggestTagNames(final String search) { return tag_names.suggest(search); } - + /** * Given a prefix search, returns matching tagk names. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestTagNames(final String search, + public List suggestTagNames(final String search, final int max_results) { return tag_names.suggest(search, max_results); } @@ -871,14 +872,14 @@ public List suggestTagNames(final String search, public List suggestTagValues(final String search) { return tag_values.suggest(search); } - + /** * Given a prefix search, returns matching tag values. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestTagValues(final String search, + public List suggestTagValues(final String search, final int max_results) { return tag_values.suggest(search, max_results); } @@ -895,14 +896,14 @@ public void dropCaches() { /** * Attempts to assign a UID to a name for the given type - * Used by the UniqueIdRpc call to generate IDs for new metrics, tagks or + * Used by the UniqueIdRpc call to generate IDs for new metrics, tagks or * tagvs. The name must pass validation and if it's already assigned a UID, * this method will throw an error with the proper UID. Otherwise if it can * create the UID, it will be returned * @param type The type of uid to assign, metric, tagk or tagv * @param name The name of the uid object * @return A byte array with the UID if the assignment was successful - * @throws IllegalArgumentException if the name is invalid or it already + * @throws IllegalArgumentException if the name is invalid or it already * exists * @since 2.0 */ @@ -937,22 +938,22 @@ public byte[] assignUid(final String type, final String name) { throw new IllegalArgumentException("Unknown type name"); } } - + /** @return the name of the UID table as a byte array for client requests */ public byte[] uidTable() { return this.uidtable; } - + /** @return the name of the data table as a byte array for client requests */ public byte[] dataTable() { return this.table; } - + /** @return the name of the tree table as a byte array for client requests */ public byte[] treeTable() { return this.treetable; } - + /** @return the name of the meta table as a byte array for client requests */ public byte[] metaTable() { return this.meta_table; @@ -968,7 +969,7 @@ public void indexTSMeta(final TSMeta meta) { search.indexTSMeta(meta).addErrback(new PluginError()); } } - + /** * Delete the timeseries meta object from the search index * @param tsuid The TSUID to delete @@ -979,7 +980,7 @@ public void deleteTSMeta(final String tsuid) { search.deleteTSMeta(tsuid).addErrback(new PluginError()); } } - + /** * Index the given UID meta object via the configured search plugin * @param meta The meta data object to index @@ -990,7 +991,7 @@ public void indexUIDMeta(final UIDMeta meta) { search.indexUIDMeta(meta).addErrback(new PluginError()); } } - + /** * Delete the UID meta object from the search index * @param meta The UID meta object to delete @@ -1001,7 +1002,7 @@ public void deleteUIDMeta(final UIDMeta meta) { search.deleteUIDMeta(meta).addErrback(new PluginError()); } } - + /** * Index the given Annotation object via the configured search plugin * @param note The annotation object to index @@ -1015,7 +1016,7 @@ public void indexAnnotation(final Annotation note) { rt_publisher.publishAnnotation(note); } } - + /** * Delete the annotation object from the search index * @param note The annotation object to delete @@ -1026,7 +1027,7 @@ public void deleteAnnotation(final Annotation note) { search.deleteAnnotation(note).addErrback(new PluginError()); } } - + /** * Processes the TSMeta through all of the trees if configured to do so * @param meta The meta data to process @@ -1038,7 +1039,7 @@ public Deferred processTSMetaThroughTrees(final TSMeta meta) { } return Deferred.fromResult(false); } - + /** * Executes a search query using the search plugin * @param query The query to execute @@ -1052,13 +1053,13 @@ public Deferred executeSearch(final SearchQuery query) { throw new IllegalStateException( "Searching has not been enabled on this TSD"); } - + return search.executeQuery(query); } - + /** - * Simply logs plugin errors when they're thrown by attaching as an errorback. - * Without this, exceptions will just disappear (unless logged by the plugin) + * Simply logs plugin errors when they're thrown by attaching as an errorback. + * Without this, exceptions will just disappear (unless logged by the plugin) * since we don't wait for a result. */ final class PluginError implements Callback { @@ -1068,12 +1069,12 @@ public Object call(final Exception e) throws Exception { return null; } } - + // ------------------ // // Compaction helpers // // ------------------ // - final KeyValue compact(final ArrayList row, + final KeyValue compact(final ArrayList row, List annotations) { return compactionq.compact(row, annotations); } diff --git a/src/utils/Config.java b/src/utils/Config.java index fd71e65732..f757d063f0 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -28,18 +28,18 @@ /** * OpenTSDB Configuration Class - * + * * This handles all of the user configurable variables for a TSD. On * initialization default values are configured for all variables. Then * implementations should call the {@link #loadConfig()} methods to search for a * default configuration or try to load one provided by the user. - * + * * To add a configuration, simply set a default value in {@link #setDefaults()}. * Wherever you need to access the config value, use the proper helper to fetch * the value, accounting for exceptions that may be thrown if necessary. - * + * * The get number helpers will return NumberFormatExceptions if the - * requested property is null or unparseable. The {@link #getString(String)} + * requested property is null or unparseable. The {@link #getString(String)} * helper will return a NullPointerException if the property isn't found. *

* Plugins can extend this class and copy the properties from the main @@ -53,11 +53,11 @@ public class Config { private static final Logger LOG = LoggerFactory.getLogger(Config.class); /** Flag to determine if we're running under Windows or not */ - public static final boolean IS_WINDOWS = + public static final boolean IS_WINDOWS = System.getProperty("os.name", "").contains("Windows"); - + // These are accessed often so need a set address for fast access (faster - // than accessing the map. Their value will be changed when the config is + // than accessing the map. Their value will be changed when the config is // loaded // NOTE: edit the setDefaults() method if you add a public field @@ -72,41 +72,41 @@ public class Config { /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; - + /** tsd.core.meta.enable_realtime_ts */ private boolean enable_realtime_ts = false; - + /** tsd.core.meta.enable_realtime_uid */ private boolean enable_realtime_uid = false; - + /** tsd.core.meta.enable_tsuid_incrementing */ private boolean enable_tsuid_incrementing = false; - + /** tsd.core.meta.enable_tsuid_tracking */ private boolean enable_tsuid_tracking = false; - + /** tsd.http.request.enable_chunked */ private boolean enable_chunked_requests = false; - + /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; /** tsd.http.request.max_chunk */ - private int max_chunked_requests = 4096; - + private int max_chunked_requests = 4096; + /** tsd.core.tree.enable_processing */ private boolean enable_tree_processing = false; - + /** * The list of properties configured to their defaults or modified by users */ - protected final HashMap properties = + protected final HashMap properties = new HashMap(); /** Holds default values for the config */ - protected static final HashMap default_map = + protected static final HashMap default_map = new HashMap(); - + /** Tracks the location of the file that was actually loaded */ private String config_location; @@ -138,7 +138,7 @@ public Config(final String file) throws IOException { /** * Constructor for plugins or overloaders who want a copy of the parent * properties but without the ability to modify them - * + * * This constructor will not re-read the file, but it will copy the location * so if a child wants to reload the properties periodically, they may do so * @param parent Parent configuration object to load from @@ -154,7 +154,7 @@ public Config(final Config parent) { public boolean auto_metric() { return this.auto_metric; } - + /** @return the auto_tagk value */ public boolean auto_tagk() { return auto_tagk; @@ -171,42 +171,42 @@ public void setAutoMetric(boolean auto_metric) { properties.put("tsd.core.auto_create_metrics", Boolean.toString(auto_metric)); } - + /** @return the enable_compaction value */ public boolean enable_compactions() { return this.enable_compactions; } - + /** @return whether or not to record new TSMeta objects in real time */ - public boolean enable_realtime_ts() { + public boolean enable_realtime_ts() { return enable_realtime_ts; } - + /** @return whether or not record new UIDMeta objects in real time */ - public boolean enable_realtime_uid() { + public boolean enable_realtime_uid() { return enable_realtime_uid; } - + /** @return whether or not to increment TSUID counters */ - public boolean enable_tsuid_incrementing() { + public boolean enable_tsuid_incrementing() { return enable_tsuid_incrementing; } - + /** @return whether or not to record a 1 for every TSUID */ public boolean enable_tsuid_tracking() { return enable_tsuid_tracking; } - + /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { return this.enable_chunked_requests; } - + /** @return max incoming chunk size in bytes */ public int max_chunked_requests() { return this.max_chunked_requests; } - + /** @return true if duplicate values should be fixed */ public boolean fix_duplicates() { return fix_duplicates; @@ -221,14 +221,14 @@ public void setFixDuplicates(final boolean fix_duplicates) { public boolean enable_tree_processing() { return enable_tree_processing; } - + /** * Allows for modifying properties after creation or loading. - * - * WARNING: This should only be used on initialization and is meant for + * + * WARNING: This should only be used on initialization and is meant for * command line overrides. Also note that it will reset all static config * variables when called. - * + * * @param property The name of the property to override * @param value The value to store */ @@ -304,12 +304,12 @@ public final double getDouble(final String property) { /** * Returns the given property as a boolean - * + * * Property values are case insensitive and the following values will result * in a True return value: - 1 - True - Yes - * + * * Any other values, including an empty string, will result in a False - * + * * @param property The property to load * @return A parsed boolean * @throws NullPointerException if the property was not found @@ -339,7 +339,7 @@ public final String getDirectoryName(final String property) { if (IS_WINDOWS) { // Windows swings both ways. If a forward slash was already used, we'll // add one at the end if missing. Otherwise use the windows default of \ - if (directory.charAt(directory.length() - 1) == '\\' || + if (directory.charAt(directory.length() - 1) == '\\' || directory.charAt(directory.length() - 1) == '/') { return directory; } @@ -358,7 +358,7 @@ public final String getDirectoryName(final String property) { } return directory + "/"; } - + /** * Determines if the given propery is in the map * @param property The property to search for @@ -405,17 +405,23 @@ public final Map getMap() { return ImmutableMap.copyOf(properties); } + /** + * set enable_compactions to true + */ public final void enableCompactions() { - this.enable_compactions = true; + this.enable_compactions = true; } + /** + * set enable_compactions to false + */ public final void disableCompactions() { - this.enable_compactions = false; + this.enable_compactions = false; } - + /** * Loads default entries that were not provided by a file or command line - * + * * This should be called in the constructor */ protected void setDefaults() { @@ -474,14 +480,14 @@ protected void setDefaults() { /** * Searches a list of locations for a valid opentsdb.conf file - * + * * The config file must be a standard JAVA properties formatted file. If none * of the locations have a config file, then the defaults or command line * arguments will be used for the configuration - * + * * Defaults for Linux based systems are: ./opentsdb.conf /etc/opentsdb.conf * /etc/opentsdb/opentdsb.conf /opt/opentsdb/opentsdb.conf - * + * * @throws IOException Thrown if there was an issue reading a file */ protected void loadConfig() throws IOException { @@ -510,9 +516,9 @@ protected void loadConfig() throws IOException { FileInputStream file_stream = new FileInputStream(file); Properties props = new Properties(); props.load(file_stream); - + // load the hash map - this.loadHashMap(props); + this.loadHashMap(props); } catch (Exception e) { // don't do anything, the file may be missing and that's fine LOG.debug("Unable to find or load " + file, e); @@ -540,7 +546,7 @@ protected void loadConfig(final String file) throws FileNotFoundException, file_stream = new FileInputStream(file); Properties props = new Properties(); props.load(file_stream); - + // load the hash map this.loadHashMap(props); @@ -576,12 +582,12 @@ protected void loadStaticVariables() { * Called from {@link #loadConfig} to copy the properties into the hash map * Tsuna points out that the Properties class is much slower than a hash * map so if we'll be looking up config values more than once, a hash map - * is the way to go + * is the way to go * @param props The loaded Properties object to copy */ private void loadHashMap(final Properties props) { this.properties.clear(); - + @SuppressWarnings("rawtypes") Enumeration e = props.propertyNames(); while (e.hasMoreElements()) { diff --git a/test/core/TestBatchedDataPoints.java b/test/core/TestBatchedDataPoints.java new file mode 100644 index 0000000000..38e3c472c9 --- /dev/null +++ b/test/core/TestBatchedDataPoints.java @@ -0,0 +1,190 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.HBaseClient; +import org.junit.Assert; +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 com.stumbleupon.async.Deferred; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, + IncomingDataPoints.class }) +public class TestBatchedDataPoints { + private static Config config; + private static TSDB tsdb = null; + private HBaseClient client = mock(HBaseClient.class); + private UniqueId metrics = mock(UniqueId.class); + private UniqueId tag_names = mock(UniqueId.class); + private UniqueId tag_values = mock(UniqueId.class); + private BatchedDataPoints bdp = null; + + @SuppressWarnings("unchecked") + @Before + public void before() throws Exception { + PowerMockito.whenNew(HBaseClient.class) + .withArguments(anyString(), anyString()).thenReturn(client); + config = new Config(false); + tsdb = new TSDB(config); + + // replace the "real" field objects with mocks + Field met = tsdb.getClass().getDeclaredField("metrics"); + met.setAccessible(true); + met.set(tsdb, metrics); + + Field tagk = tsdb.getClass().getDeclaredField("tag_names"); + tagk.setAccessible(true); + tagk.set(tsdb, tag_names); + + Field tagv = tsdb.getClass().getDeclaredField("tag_values"); + tagv.setAccessible(true); + tagv.set(tsdb, tag_values); + + when(metrics.width()).thenReturn((short) 3); + when(tag_names.width()).thenReturn((short) 3); + when(tag_values.width()).thenReturn((short) 3); + + when(metrics.getId("foo")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })).thenReturn( + Deferred.fromResult("foo")); + + PowerMockito.mockStatic(IncomingDataPoints.class); + final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1 }; + PowerMockito.doAnswer(new Answer() { + public byte[] answer(final InvocationOnMock unused) throws Exception { + return row; + } + }).when(IncomingDataPoints.class, "rowKeyTemplate", (TSDB) any(), + anyString(), (Map) any()); + + Map tags = new HashMap(); + tags.put("host", "web01"); + bdp = new BatchedDataPoints(tsdb, "foo", tags); + } + + @Test + public void timestamp() { + bdp.addPoint(1388534400L, 1); + bdp.addPoint(1388534400500L, 2); + bdp.addPoint(1388534400750L, 2); + + assertEquals(1388534400000L, bdp.timestamp(0)); + assertEquals(1388534400500L, bdp.timestamp(1)); + assertEquals(1388534400750L, bdp.timestamp(2)); + } + + @Test + public void isInteger() { + bdp.addPoint(1388534400L, 1); + bdp.addPoint(1388534400500L, Short.MIN_VALUE); + bdp.addPoint(1388534401L, Integer.MIN_VALUE); + bdp.addPoint(1388534401750L, 2.0f); + + assertTrue(bdp.isInteger(0)); + assertTrue(bdp.isInteger(1)); + assertTrue(bdp.isInteger(2)); + assertFalse(bdp.isInteger(3)); + } + + @Test + public void longValue() { + bdp.addPoint(1388534400L, 1); + bdp.addPoint(1388534400500L, Short.MIN_VALUE); + bdp.addPoint(1388534401L, Integer.MIN_VALUE); + bdp.addPoint(1388534401750L, 2.0f); + + assertEquals(1, bdp.longValue(0)); + assertEquals(Short.MIN_VALUE, bdp.longValue(1)); + assertEquals(Integer.MIN_VALUE, bdp.longValue(2)); + } + + @Test + public void doubleValue() { + bdp.addPoint(1388534400L, 1); + bdp.addPoint(1388534400500L, Short.MIN_VALUE); + bdp.addPoint(1388534401L, Integer.MIN_VALUE); + bdp.addPoint(1388534401750L, 2.0f); + + Assert.assertEquals(2.0f, bdp.doubleValue(3), 0.00001f); + } + + @Test + public void inBounds() { + long timestamp = 1388534400L; + long base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + + // test that the limit values are fine + bdp.addPoint(base_time, 2); + bdp.addPoint(base_time + Const.MAX_TIMESPAN - 1, 3); + } + + @Test(expected = IllegalArgumentException.class) + public void addPrevTimestamp() { + bdp.addPoint(1388534400L, 1); + bdp.addPoint(1388534350L, 2); + } + + @Test(expected = IllegalDataException.class) + public void outOfBounds() { + long timestamp = 1388534400L; + long base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + + bdp.addPoint(timestamp, 1); // this set's the batch's baseTime + + // outside the limits addPoint throws an IllegalArgumentException + bdp.addPoint(base_time + Const.MAX_TIMESPAN, 2); + } + + @Test + public void fullLoad() { + long timestamp = 1388534400L; + long base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + + for (int i = 0; i < Const.MAX_TIMESPAN; i++) { + bdp.addPoint(base_time + i, i); + } + } + + @Test + public void fullLoadMs() { + long timestamp = 1388534400L; + long base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + + for (long i = 0; i < Const.MAX_TIMESPAN * 1000; i++) { + bdp.addPoint(base_time * 1000 + i, i); + } + } +} From d430ab057ae8163e21c12e56c422b78223bd9178 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 11 Dec 2014 21:49:02 -0800 Subject: [PATCH 034/826] Add TestBatchedDataPoints.java to the Makefile --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index 06628b2dca..9dc5cdcf6e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -146,6 +146,7 @@ test_SRC := \ test/core/SeekableViewsForTest.java \ test/core/TestAggregationIterator.java \ test/core/TestAggregators.java \ + test/core/TestBatchedDataPoints.java \ test/core/TestCompactionQueue.java \ test/core/TestDownsampler.java \ test/core/TestInternal.java \ From ed0ec520f74cc684725a2ad5e88440939cfc754a Mon Sep 17 00:00:00 2001 From: Benoit Sigoure Date: Sun, 11 Jan 2015 14:56:48 -0800 Subject: [PATCH 035/826] Introduce /api/annotations, to fetch multiple global annotations at once. Signed-off-by: Chris Larsen --- src/tsd/AnnotationRpc.java | 38 +++++++++++++++++++++++++++------ src/tsd/RpcHandler.java | 4 +++- test/tsd/TestAnnotationRpc.java | 15 +++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/tsd/AnnotationRpc.java b/src/tsd/AnnotationRpc.java index d4c72050d5..2ac6c23903 100644 --- a/src/tsd/AnnotationRpc.java +++ b/src/tsd/AnnotationRpc.java @@ -65,14 +65,11 @@ public void execute(final TSDB tsdb, HttpQuery query) throws IOException { // GET if (method == HttpMethod.GET) { try { - final Annotation stored_annotation = - Annotation.getAnnotation(tsdb, note.getTSUID(), note.getStartTime()) - .joinUninterruptibly(); - if (stored_annotation == null) { - throw new BadRequestException(HttpResponseStatus.NOT_FOUND, - "Unable to locate annotation in storage"); + if ("annotations".toLowerCase().equals(uri[0])) { + fetchMultipleAnnotations(tsdb, note, query); + } else { + fetchSingleAnnotation(tsdb, note, query); } - query.sendReply(query.serializer().formatAnnotationV1(stored_annotation)); } catch (BadRequestException e) { throw e; } catch (Exception e) { @@ -345,6 +342,33 @@ private Annotation parseQS(final HttpQuery query) { return note; } + private void fetchSingleAnnotation(final TSDB tsdb, final Annotation note, + final HttpQuery query) throws Exception { + final Annotation stored_annotation = + Annotation.getAnnotation(tsdb, note.getTSUID(), note.getStartTime()) + .joinUninterruptibly(); + if (stored_annotation == null) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to locate annotation in storage"); + } + query.sendReply(query.serializer().formatAnnotationV1(stored_annotation)); + } + + private void fetchMultipleAnnotations(final TSDB tsdb, final Annotation note, + final HttpQuery query) throws Exception { + if (note.getEndTime() == 0) { + note.setEndTime(System.currentTimeMillis()); + } + final List annotations = + Annotation.getGlobalAnnotations(tsdb, note.getStartTime(), note.getEndTime()) + .joinUninterruptibly(); + if (annotations == null) { + throw new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to locate annotations in storage"); + } + query.sendReply(query.serializer().formatAnnotationsV1(annotations)); + } + /** * Parses a query string for a bulk delet request * @param query The query to parse diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index e8146e3826..1b10f8b067 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -146,7 +146,9 @@ public RpcHandler(final TSDB tsdb) { http_commands.put("api/uid", new UniqueIdRpc()); http_commands.put("api/query", new QueryRpc()); http_commands.put("api/tree", new TreeRpc()); - http_commands.put("api/annotation", new AnnotationRpc()); + final AnnotationRpc annotation_rpc = new AnnotationRpc(); + http_commands.put("api/annotation", annotation_rpc); + http_commands.put("api/annotations", annotation_rpc); http_commands.put("api/search", new SearchRpc()); http_commands.put("api/config", new ShowConfig()); } diff --git a/test/tsd/TestAnnotationRpc.java b/test/tsd/TestAnnotationRpc.java index 299e58be1e..0060c1c637 100644 --- a/test/tsd/TestAnnotationRpc.java +++ b/test/tsd/TestAnnotationRpc.java @@ -134,6 +134,14 @@ public void getGlobal() throws Exception { assertEquals(HttpResponseStatus.OK, query.response().getStatus()); } + @Test + public void getGlobals() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/annotations?start_time=1328140800"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + } + @Test (expected = BadRequestException.class) public void getNotFound() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, @@ -148,6 +156,13 @@ public void getGlobalNotFound() throws Exception { rpc.execute(tsdb, query); } + @Test (expected = BadRequestException.class) + public void getGlobalsNotFound() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/annotation?start_time=1388450563"); + rpc.execute(tsdb, query); + } + @Test (expected = BadRequestException.class) public void getMissingStart() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, From cd396a53e3b4e151667ac897db39c698934ba92d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jan 2015 14:32:57 -0800 Subject: [PATCH 036/826] Cleanup the Config class a bit. Make sure to close the conig file after opening and add more unit tests. Signed-off-by: Chris Larsen --- src/utils/Config.java | 92 ++++---- test/utils/TestConfig.java | 455 ++++++++++++++++++++++++++++++------- 2 files changed, 423 insertions(+), 124 deletions(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index f757d063f0..68eb9ff723 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -108,7 +108,7 @@ public class Config { new HashMap(); /** Tracks the location of the file that was actually loaded */ - private String config_location; + protected String config_location; /** * Constructor that initializes default configuration values. May attempt to @@ -119,9 +119,10 @@ public class Config { * config files */ public Config(final boolean auto_load_config) throws IOException { - if (auto_load_config) - this.loadConfig(); - this.setDefaults(); + if (auto_load_config) { + loadConfig(); + } + setDefaults(); } /** @@ -131,8 +132,8 @@ public Config(final boolean auto_load_config) throws IOException { * @throws IOException Thrown if unable to read or parse the file */ public Config(final String file) throws IOException { - this.loadConfig(file); - this.setDefaults(); + loadConfig(file); + setDefaults(); } /** @@ -145,14 +146,14 @@ public Config(final String file) throws IOException { */ public Config(final Config parent) { // copy so changes to the local props by the plugin don't affect the master - this.properties.putAll(parent.properties); - this.config_location = parent.config_location; - this.setDefaults(); + properties.putAll(parent.properties); + config_location = parent.config_location; + setDefaults(); } /** @return the auto_metric value */ public boolean auto_metric() { - return this.auto_metric; + return auto_metric; } /** @return the auto_tagk value */ @@ -174,7 +175,7 @@ public void setAutoMetric(boolean auto_metric) { /** @return the enable_compaction value */ public boolean enable_compactions() { - return this.enable_compactions; + return enable_compactions; } /** @return whether or not to record new TSMeta objects in real time */ @@ -199,12 +200,12 @@ public boolean enable_tsuid_tracking() { /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { - return this.enable_chunked_requests; + return enable_chunked_requests; } /** @return max incoming chunk size in bytes */ public int max_chunked_requests() { - return this.max_chunked_requests; + return max_chunked_requests; } /** @return true if duplicate values should be fixed */ @@ -233,7 +234,7 @@ public boolean enable_tree_processing() { * @param value The value to store */ public void overrideConfig(final String property, final String value) { - this.properties.put(property, value); + properties.put(property, value); loadStaticVariables(); } @@ -244,7 +245,7 @@ public void overrideConfig(final String property, final String value) { * @throws NullPointerException if the property did not exist */ public final String getString(final String property) { - return this.properties.get(property); + return properties.get(property); } /** @@ -255,7 +256,7 @@ public final String getString(final String property) { * @throws NullPointerException if the property did not exist */ public final int getInt(final String property) { - return Integer.parseInt(this.properties.get(property)); + return Integer.parseInt(properties.get(property)); } /** @@ -266,7 +267,7 @@ public final int getInt(final String property) { * @throws NullPointerException if the property did not exist */ public final short getShort(final String property) { - return Short.parseShort(this.properties.get(property)); + return Short.parseShort(properties.get(property)); } /** @@ -277,7 +278,7 @@ public final short getShort(final String property) { * @throws NullPointerException if the property did not exist */ public final long getLong(final String property) { - return Long.parseLong(this.properties.get(property)); + return Long.parseLong(properties.get(property)); } /** @@ -288,7 +289,7 @@ public final long getLong(final String property) { * @throws NullPointerException if the property did not exist */ public final float getFloat(final String property) { - return Float.parseFloat(this.properties.get(property)); + return Float.parseFloat(properties.get(property)); } /** @@ -299,7 +300,7 @@ public final float getFloat(final String property) { * @throws NullPointerException if the property did not exist */ public final double getDouble(final String property) { - return Double.parseDouble(this.properties.get(property)); + return Double.parseDouble(properties.get(property)); } /** @@ -315,7 +316,7 @@ public final double getDouble(final String property) { * @throws NullPointerException if the property was not found */ public final boolean getBoolean(final String property) { - final String val = this.properties.get(property).toUpperCase(); + final String val = properties.get(property).toUpperCase(); if (val.equals("1")) return true; if (val.equals("TRUE")) @@ -328,8 +329,8 @@ public final boolean getBoolean(final String property) { /** * Returns the directory name, making sure the end is an OS dependent slash * @param property The property to load - * @return The property value with a forward or back slash appended - * @throws NullPointerException if the property was not found + * @return The property value with a forward or back slash appended or null + * if the property wasn't found or the directory was empty. */ public final String getDirectoryName(final String property) { String directory = properties.get(property); @@ -365,7 +366,7 @@ public final String getDirectoryName(final String property) { * @return True if the property exists and has a value, not an empty string */ public final boolean hasProperty(final String property) { - final String val = this.properties.get(property); + final String val = properties.get(property); if (val == null) return false; if (val.isEmpty()) @@ -378,13 +379,13 @@ public final boolean hasProperty(final String property) { * @return A string with information about the config */ public final String dumpConfiguration() { - if (this.properties.isEmpty()) + if (properties.isEmpty()) return "No configuration settings stored"; StringBuilder response = new StringBuilder("TSD Configuration:\n"); - response.append("File [" + this.config_location + "]\n"); + response.append("File [" + config_location + "]\n"); int line = 0; - for (Map.Entry entry : this.properties.entrySet()) { + for (Map.Entry entry : properties.entrySet()) { if (line > 0) { response.append("\n"); } @@ -491,8 +492,8 @@ protected void setDefaults() { * @throws IOException Thrown if there was an issue reading a file */ protected void loadConfig() throws IOException { - if (this.config_location != null && !this.config_location.isEmpty()) { - this.loadConfig(this.config_location); + if (config_location != null && !config_location.isEmpty()) { + loadConfig(config_location); return; } @@ -518,7 +519,7 @@ protected void loadConfig() throws IOException { props.load(file_stream); // load the hash map - this.loadHashMap(props); + loadHashMap(props); } catch (Exception e) { // don't do anything, the file may be missing and that's fine LOG.debug("Unable to find or load " + file, e); @@ -527,7 +528,7 @@ protected void loadConfig() throws IOException { // no exceptions thrown, so save the valid path and exit LOG.info("Successfully loaded configuration file: " + file); - this.config_location = file; + config_location = file; return; } @@ -542,17 +543,20 @@ protected void loadConfig() throws IOException { */ protected void loadConfig(final String file) throws FileNotFoundException, IOException { - FileInputStream file_stream; - file_stream = new FileInputStream(file); - Properties props = new Properties(); - props.load(file_stream); - - // load the hash map - this.loadHashMap(props); - - // no exceptions thrown, so save the valid path and exit - LOG.info("Successfully loaded configuration file: " + file); - this.config_location = file; + final FileInputStream file_stream = new FileInputStream(file); + try { + final Properties props = new Properties(); + props.load(file_stream); + + // load the hash map + loadHashMap(props); + + // no exceptions thrown, so save the valid path and exit + LOG.info("Successfully loaded configuration file: " + file); + config_location = file; + } finally { + file_stream.close(); + } } /** @@ -586,13 +590,13 @@ protected void loadStaticVariables() { * @param props The loaded Properties object to copy */ private void loadHashMap(final Properties props) { - this.properties.clear(); + properties.clear(); @SuppressWarnings("rawtypes") Enumeration e = props.propertyNames(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); - this.properties.put(key, props.getProperty(key)); + properties.put(key, props.getProperty(key)); } } } diff --git a/test/utils/TestConfig.java b/test/utils/TestConfig.java index ff35dc76f4..b30e17ddbb 100644 --- a/test/utils/TestConfig.java +++ b/test/utils/TestConfig.java @@ -17,20 +17,22 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.util.Properties; -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; +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Config.class, FileInputStream.class }) public final class TestConfig { - private Config config; - - @Before - public void before() throws Exception { - config = new Config(false); - } - + @Test public void constructor() throws Exception { assertNotNull(new Config(false)); @@ -38,7 +40,7 @@ public void constructor() throws Exception { @Test public void constructorDefault() throws Exception { - assertEquals("0.0.0.0", config.getString("tsd.network.bind")); + assertEquals("0.0.0.0", new Config(false).getString("tsd.network.bind")); } @Test @@ -60,154 +62,421 @@ public void constructorChildCopy() throws Exception { assertEquals("Child", ch.getString("MyProp")); } + @Test(expected = NullPointerException.class) + public void constructorNullChild() throws Exception { + new Config((Config) null); + } + + @Test + public void constructorWithFile() throws Exception { + PowerMockito.whenNew(FileInputStream.class).withAnyArguments() + .thenReturn(mock(FileInputStream.class)); + final Properties props = new Properties(); + props.setProperty("tsd.test", "val1"); + PowerMockito.whenNew(Properties.class).withNoArguments().thenReturn(props); + + final Config config = new Config("/tmp/config.file"); + assertNotNull(config); + assertEquals("/tmp/config.file", config.config_location); + assertEquals("val1", config.getString("tsd.test")); + } + + @Test(expected = FileNotFoundException.class) + public void constructorFileNotFound() throws Exception { + new Config("/tmp/filedoesnotexist.conf"); + } + + @Test(expected = NullPointerException.class) + public void constructorNullFile() throws Exception { + new Config((String) null); + } + + @Test(expected = FileNotFoundException.class) + public void constructorEmptyFile() throws Exception { + new Config(""); + } + @Test (expected = FileNotFoundException.class) public void loadConfigNotFound() throws Exception { Config c = new Config(false); c.loadConfig("/tmp/filedoesnotexist.conf"); } - + + @Test(expected = NullPointerException.class) + public void loadConfigNull() throws Exception { + final Config config = new Config(false); + config.loadConfig(null); + } + + @Test(expected = FileNotFoundException.class) + public void loadConfigEmpty() throws Exception { + final Config config = new Config(false); + config.loadConfig(""); + } + @Test public void overrideConfig() throws Exception { + final Config config = new Config(false); config.overrideConfig("tsd.core.bind", "127.0.0.1"); assertEquals("127.0.0.1", config.getString("tsd.core.bind")); } @Test public void getString() throws Exception { + final Config config = new Config(false); assertEquals("1000", config.getString("tsd.storage.flush_interval")); } @Test public void getStringNull() throws Exception { + final Config config = new Config(false); assertNull(config.getString("tsd.blarg")); } @Test public void getInt() throws Exception { - assertEquals(1000, config.getInt("tsd.storage.flush_interval")); + final Config config = new Config(false); + config.overrideConfig("tsd.int", + Integer.toString(Integer.MAX_VALUE)); + assertEquals(Integer.MAX_VALUE, + config.getInt("tsd.int")); } - - @Test (expected = NumberFormatException.class) + + @Test + public void getIntNegative() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.int", + Integer.toString(Integer.MIN_VALUE)); + assertEquals(Integer.MIN_VALUE, + config.getInt("tsd.int")); + } + + @Test(expected = NumberFormatException.class) public void getIntNull() throws Exception { - config.getInt("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + config.getInt("tsd.null"); } - - @Test (expected = NumberFormatException.class) + + @Test(expected = NumberFormatException.class) + public void getIntDoesNotExist() throws Exception { + final Config config = new Config(false); + config.getInt("tsd.nosuchkey"); + } + + @Test(expected = NumberFormatException.class) public void getIntNFE() throws Exception { - config.overrideConfig("tsd.blarg", "this can't be parsed to int"); - config.getInt("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.int", + "this can't be parsed to int"); + config.getInt("tsd.int"); } - + @Test public void getShort() throws Exception { - assertEquals(1000, config.getShort("tsd.storage.flush_interval")); + final Config config = new Config(false); + config.overrideConfig("tsd.short", + Short.toString(Short.MAX_VALUE)); + assertEquals(Short.MAX_VALUE, + config.getShort("tsd.short")); } - - @Test (expected = NumberFormatException.class) + + @Test + public void getShortNegative() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.short", + Short.toString(Short.MIN_VALUE)); + assertEquals(Short.MIN_VALUE, + config.getShort("tsd.short")); + } + + @Test(expected = NumberFormatException.class) public void getShortNull() throws Exception { - assertEquals(1000, config.getShort("tsd.blarg")); + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + config.getShort("tsd.null"); } - - @Test (expected = NumberFormatException.class) + + @Test(expected = NumberFormatException.class) + public void getShortDoesNotExist() throws Exception { + final Config config = new Config(false); + config.getShort("tsd.nosuchkey"); + } + + @Test(expected = NumberFormatException.class) public void getShortNFE() throws Exception { - config.overrideConfig("tsd.blarg", "this can't be parsed to short"); - config.getShort("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.short", + "this can't be parsed to short"); + config.getShort("tsd.short"); } - + @Test public void getLong() throws Exception { - assertEquals(1000, config.getLong("tsd.storage.flush_interval")); + final Config config = new Config(false); + config.overrideConfig("tsd.long", Long.toString(Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, config.getLong("tsd.long")); } - - @Test (expected = NumberFormatException.class) + + @Test + public void getLongNegative() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.long", Long.toString(Long.MIN_VALUE)); + assertEquals(Long.MIN_VALUE, + config.getLong("tsd.long")); + } + + @Test(expected = NumberFormatException.class) public void getLongNull() throws Exception { - config.getLong("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + config.getLong("tsd.null"); } - - @Test (expected = NumberFormatException.class) + + @Test(expected = NumberFormatException.class) + public void getLongDoesNotExist() throws Exception { + final Config config = new Config(false); + config.getLong("tsd.nosuchkey"); + } + + @Test(expected = NumberFormatException.class) public void getLongNullNFE() throws Exception { - config.overrideConfig("tsd.blarg", "this can't be parsed to long"); - config.getLong("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.long", "this can't be parsed to long"); + config.getLong("tsd.long"); } - + @Test public void getFloat() throws Exception { - config.overrideConfig("tsd.unitest", "42.5"); - assertEquals(42.5, config.getFloat("tsd.unitest"), 0.000001); + final Config config = new Config(false); + config.overrideConfig("tsd.float", Float.toString(Float.MAX_VALUE)); + assertEquals(Float.MAX_VALUE, + config.getFloat("tsd.float"), 0.000001); } - - @Test (expected = NullPointerException.class) + + @Test + public void getFloatNegative() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.float", Float.toString(Float.MIN_VALUE)); + assertEquals(Float.MIN_VALUE, + config.getFloat("tsd.float"), 0.000001); + } + + @Test + public void getFloatNaN() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.float", "NaN"); + assertEquals(Float.NaN, + config.getDouble("tsd.float"), 0.000001); + } + + @Test(expected = NumberFormatException.class) + public void getFloatNaNBadCase() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.float", "nan"); + assertEquals(Float.NaN, + config.getDouble("tsd.float"), 0.000001); + } + + @Test + public void getFloatPIfinity() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.float", "Infinity"); + assertEquals(Float.POSITIVE_INFINITY, + config.getDouble("tsd.float"), 0.000001); + } + + @Test + public void getFloatNIfinity() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.float", "-Infinity"); + assertEquals(Float.NEGATIVE_INFINITY, + config.getDouble("tsd.float"), 0.000001); + } + + @Test(expected = NullPointerException.class) public void getFloatNull() throws Exception { - config.getFloat("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + config.getFloat("tsd.null"); } - - @Test (expected = NumberFormatException.class) + + @Test(expected = NullPointerException.class) + public void getFloatDoesNotExist() throws Exception { + final Config config = new Config(false); + config.getFloat("tsd.nosuchkey"); + } + + @Test(expected = NumberFormatException.class) public void getFloatNFE() throws Exception { - config.overrideConfig("tsd.unitest", "this can't be parsed to float"); - config.getFloat("tsd.unitest"); + final Config config = new Config(false); + config.overrideConfig("tsd.float", "this can't be parsed to float"); + config.getFloat("tsd.float"); } - + @Test public void getDouble() throws Exception { - config.overrideConfig("tsd.unitest", "42.5"); - assertEquals(42.5, config.getDouble("tsd.unitest"), 0.000001); + final Config config = new Config(false); + config.overrideConfig("tsd.double", Double.toString(Double.MAX_VALUE)); + assertEquals(Double.MAX_VALUE, + config.getDouble("tsd.double"), 0.000001); } - - @Test (expected = NullPointerException.class) + + @Test + public void getDoubleNegative() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.double", Double.toString(Double.MIN_VALUE)); + assertEquals(Double.MIN_VALUE, + config.getDouble("tsd.double"), 0.000001); + } + + @Test + public void getDoubleNaN() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.double", "NaN"); + assertEquals(Double.NaN, + config.getDouble("tsd.double"), 0.000001); + } + + @Test(expected = NumberFormatException.class) + public void getDoubleNaNBadCase() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.double", "nan"); + assertEquals(Double.NaN, + config.getDouble("tsd.double"), 0.000001); + } + + @Test + public void getDoublePIfinity() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.double", "Infinity"); + assertEquals(Double.POSITIVE_INFINITY, + config.getDouble("tsd.double"), 0.000001); + } + + @Test + public void getDoubleNIfinity() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.double", "-Infinity"); + assertEquals(Double.NEGATIVE_INFINITY, + config.getDouble("tsd.double"), 0.000001); + } + + @Test(expected = NullPointerException.class) public void getDoubleNull() throws Exception { - config.getDouble("tsd.blarg"); + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + config.getDouble("tsd.null"); } - - @Test (expected = NumberFormatException.class) + + @Test(expected = NullPointerException.class) + public void getDoubleDoesNotExist() throws Exception { + final Config config = new Config(false); + config.getDouble("tsd.nosuchkey"); + } + + @Test(expected = NumberFormatException.class) public void getDoubleNFE() throws Exception { - config.overrideConfig("tsd.unitest", "this can't be parsed to double"); - config.getDouble("tsd.unitest"); + final Config config = new Config(false); + config.overrideConfig("tsd.double", + "this can't be parsed to double"); + config.getDouble("tsd.double"); } - + @Test - public void getBool1() throws Exception { - config.overrideConfig("tsd.unitest", "1"); - assertTrue(config.getBoolean("tsd.unitest")); + public void getBool() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "true"); + assertTrue(config.getBoolean("tsd.bool")); } - + @Test - public void getBoolTrue1() throws Exception { - config.overrideConfig("tsd.unitest", "True"); - assertTrue(config.getBoolean("tsd.unitest")); + public void getBool1() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "1"); + assertTrue(config.getBoolean("tsd.bool")); } - + @Test - public void getBoolTrue2() throws Exception { - config.overrideConfig("tsd.unitest", "true"); - assertTrue(config.getBoolean("tsd.unitest")); + public void getBoolTrueCaseInsensitive() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "TrUe"); + assertTrue(config.getBoolean("tsd.bool")); } - + @Test public void getBoolYes() throws Exception { - config.overrideConfig("tsd.unitest", "yes"); - assertTrue(config.getBoolean("tsd.unitest")); + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "yes"); + assertTrue(config.getBoolean("tsd.bool")); } - + + @Test + public void getBoolYesCaseInsensitive() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "YeS"); + assertTrue(config.getBoolean("tsd.bool")); + } + + @Test + public void getBoolFalse() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "false"); + assertFalse(config.getBoolean("tsd.bool")); + } + + @Test + public void getBoolFalse0() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "0"); + assertFalse(config.getBoolean("tsd.bool")); + } + + @Test + public void getBoolFalse2() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "2"); + assertFalse(config.getBoolean("tsd.bool")); + } + + @Test + public void getBoolFalseNo() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "no"); + assertFalse(config.getBoolean("tsd.bool")); + } + @Test public void getBoolFalseEmpty() throws Exception { - config.overrideConfig("tsd.unitest", ""); - assertFalse(config.getBoolean("tsd.unitest")); + final Config config = new Config(false); + config.overrideConfig("tsd.bool", ""); + assertFalse(config.getBoolean("tsd.bool")); } - - @Test (expected = NullPointerException.class) + + @Test(expected = NullPointerException.class) public void getBoolFalseNull() throws Exception { - config.getBoolean("tsd.unitest"); + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + config.getBoolean("tsd.null"); } - + + @Test (expected = NullPointerException.class) + public void getBoolFalseDoesNotExist() throws Exception { + final Config config = new Config(false); + assertFalse(config.getBoolean("tsd.nosuchkey")); + } + @Test public void getBoolFalseOther() throws Exception { - config.overrideConfig("tsd.unitest", "blarg"); - assertFalse(config.getBoolean("tsd.unitest")); + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "blarg"); + assertFalse(config.getBoolean("tsd.bool")); } @Test public void getDirectoryNameAddSlash() throws Exception { // same for Windows && Unix + final Config config = new Config(false); config.overrideConfig("tsd.unitest", "/my/dir"); assertEquals("/my/dir/", config.getDirectoryName("tsd.unitest")); } @@ -215,6 +484,7 @@ public void getDirectoryNameAddSlash() throws Exception { @Test public void getDirectoryNameHasSlash() throws Exception { // same for Windows && Unix + final Config config = new Config(false); config.overrideConfig("tsd.unitest", "/my/dir/"); assertEquals("/my/dir/", config.getDirectoryName("tsd.unitest")); } @@ -222,6 +492,7 @@ public void getDirectoryNameHasSlash() throws Exception { @Test public void getDirectoryNameWindowsAddSlash() throws Exception { if (Config.IS_WINDOWS) { + final Config config = new Config(false); config.overrideConfig("tsd.unitest", "C:\\my\\dir"); assertEquals("C:\\my\\dir\\", config.getDirectoryName("tsd.unitest")); } else { @@ -232,6 +503,7 @@ public void getDirectoryNameWindowsAddSlash() throws Exception { @Test public void getDirectoryNameWindowsHasSlash() throws Exception { if (Config.IS_WINDOWS) { + final Config config = new Config(false); config.overrideConfig("tsd.unitest", "C:\\my\\dir\\"); assertEquals("C:\\my\\dir\\", config.getDirectoryName("tsd.unitest")); } else { @@ -244,6 +516,7 @@ public void getDirectoryNameWindowsOnLinuxException() throws Exception { if (Config.IS_WINDOWS) { throw new IllegalArgumentException("Can't run this on Windows"); } else { + final Config config = new Config(false); config.overrideConfig("tsd.unitest", "C:\\my\\dir"); config.getDirectoryName("tsd.unitest"); } @@ -251,18 +524,40 @@ public void getDirectoryNameWindowsOnLinuxException() throws Exception { @Test public void getDirectoryNameNull() throws Exception { + final Config config = new Config(false); assertNull(config.getDirectoryName("tsd.unitest")); } @Test public void getDirectoryNameEmpty() throws Exception { + final Config config = new Config(false); config.overrideConfig("tsd.unitest", ""); assertNull(config.getDirectoryName("tsd.unitest")); } @Test public void getDirectoryNameNoslash() throws Exception { + final Config config = new Config(false); config.overrideConfig("tsd.unitest", "relative"); assertEquals("relative/", config.getDirectoryName("tsd.unitest")); } + + @Test + public void hasProperty() throws Exception { + final Config config = new Config(false); + assertTrue(config.hasProperty("tsd.network.bind")); + } + + @Test + public void hasPropertyNull() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.null", null); + assertFalse(config.hasProperty("tsd.null")); + } + + @Test + public void hasPropertyNot() throws Exception { + final Config config = new Config(false); + assertFalse(config.hasProperty("tsd.nosuchkey")); + } } From 46c2b90c0c97163cd0712cd8240b2aa25b4897b7 Mon Sep 17 00:00:00 2001 From: Adrien Mogenet Date: Sat, 16 Feb 2013 08:47:08 +0100 Subject: [PATCH 037/826] Added count aggregator. Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index c387410f15..1d956eaa9c 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -66,6 +66,9 @@ public enum Interpolation { public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); + /** Aggregator that returns the number of data points. */ + public static final Aggregator COUNT = new Count(); + /** Maps an aggregator name to its instance. */ private static final HashMap aggregators; @@ -76,6 +79,7 @@ public enum Interpolation { aggregators.put("max", MAX); aggregators.put("avg", AVG); aggregators.put("dev", DEV); + aggregators.put("count", COUNT); aggregators.put("zimsum", ZIMSUM); aggregators.put("mimmin", MIMMIN); aggregators.put("mimmax", MIMMAX); @@ -332,4 +336,30 @@ public Interpolation interpolationMethod() { } + private static final class Count implements Aggregator { + + @Override + public long runLong(Longs values) { + long result = 0; + while (values.hasNextValue()) { + values.nextLongValue(); + result++; + } + return result; + } + + @Override + public double runDouble(Doubles values) { + double result = 0; + while (values.hasNextValue()) { + values.nextDoubleValue(); + result++; + } + return result; + } + + public String toString() { + return "count"; + } + } } From 88089bade7d39f6ca27f280b9885bba79d98ab4a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 19 Jan 2015 18:01:05 -0800 Subject: [PATCH 038/826] Fix up the count aggregator for 2.2 compatibility and add some unit tests. WARNING: Until we support NaNs for avoiding interpolation, if the count aggregator is used to agg multiple series, the results will be fairly off. Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 23 +++++- test/core/TestTsdbQuery.java | 110 +++++++++++++++++++++++++ test/core/TestTsdbQueryDownsample.java | 76 +++++++++++++++++ 3 files changed, 205 insertions(+), 4 deletions(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 1d956eaa9c..2a28de8663 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -66,8 +66,12 @@ public enum Interpolation { public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); - /** Aggregator that returns the number of data points. */ - public static final Aggregator COUNT = new Count(); + /** Aggregator that returns the number of data points. + * WARNING: This currently interpolates with zero-if-missing. In this case + * counts will be off when counting multiple time series. Only use this when + * downsampling until we support NaNs. + * @since 2.2 */ + public static final Aggregator COUNT = new Count(Interpolation.ZIM, "count"); /** Maps an aggregator name to its instance. */ private static final HashMap aggregators; @@ -337,7 +341,14 @@ public Interpolation interpolationMethod() { } private static final class Count implements Aggregator { - + private final Interpolation method; + private final String name; + + public Count(final Interpolation method, final String name) { + this.method = method; + this.name = name; + } + @Override public long runLong(Longs values) { long result = 0; @@ -359,7 +370,11 @@ public double runDouble(Doubles values) { } public String toString() { - return "count"; + return name; + } + + public Interpolation interpolationMethod() { + return method; } } } diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 52e0d6a1a4..6bfc0bb2b4 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -2587,6 +2587,116 @@ public void runMimMaxFloatOffset() throws Exception { assertEquals(600, dps[0].size()); } + @Test + public void runCount() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + HashMap tags = new HashMap(0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(2, dp.longValue()); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runCountFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + HashMap tags = new HashMap(0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(2, dp.doubleValue(), 0.001); + } + assertEquals(300, dps[0].size()); + } + + // TODO - The count agg is inaccurate until we implement NaNs. + @Test + public void runCountOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + HashMap tags = new HashMap(0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter == 0 || counter == 599) { + assertEquals(1, dp.longValue()); + } else { + assertEquals(2, dp.longValue()); + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + // TODO - The count agg is inaccurate until we implement NaNs. + @Test + public void runCountFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + HashMap tags = new HashMap(0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter == 0 || counter == 599) { + assertEquals(1, dp.doubleValue(), 0.0001); + } else { + assertEquals(2, dp.doubleValue(), 0.0001); + } + counter++; + } + assertEquals(600, dps[0].size()); + } + // ----------------- // // Helper functions. // // ----------------- // diff --git a/test/core/TestTsdbQueryDownsample.java b/test/core/TestTsdbQueryDownsample.java index cc0007fcd8..dcf549eb80 100644 --- a/test/core/TestTsdbQueryDownsample.java +++ b/test/core/TestTsdbQueryDownsample.java @@ -513,6 +513,82 @@ public void runFloatSingleTSDownsampleAndRateMs() throws Exception { assertEquals(150, dps[0].size()); } + @Test + public void runLongSingleTSDownsampleCount() throws Exception { + storeLongTimeSeriesSeconds(true, false);; + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.downsample(60000, Aggregators.COUNT); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + // Timeseries in intervals: (1), (2, 3), (4, 5), ... (298, 299), (300) + int i = 0; + for (DataPoint dp : dps[0]) { + // Downsampler outputs just doubles. + assertFalse(dp.isInteger()); + if (i == 0 || i == 150) { + assertEquals(1, dp.doubleValue(), 0.00001); + } else { + assertEquals(2, dp.doubleValue(), 0.00001); + } + ++i; + } + // Out of 300 values, the first and the last intervals have one value each, + // and the 149 intervals in the middle have two values for each. + assertEquals(151, dps[0].size()); + } + + // this could happen. + @Test + public void runFloatSingleTSDownsampleAndRateAndCount() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.downsample(60000, Aggregators.COUNT); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + // Timeseries in intervals: (1.25), (1.5, 1.75), (2, 2.25), ... + // (75.5, 75.75), (76). + // After downsampling: 1.25, 1.625, 2.125, ... 75.625, 76 + long expected_timestamp = 1356998460000L; + int i = 0; + for (DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + if (i == 0) { + // switching from 1 value to 2 on the first point + assertEquals(0.016666F, dp.doubleValue(), 0.00001); + } else if (i == 149) { + // switching from 2 values to 1 on the last point + assertEquals(-0.016666F, dp.doubleValue(), 0.00001); + } else { + // no difference between the value counts for most of these so zero + assertEquals(0.000F, dp.doubleValue(), 0.00001); + } + // Timestamp of an interval should be aligned by the interval. + assertEquals(0, dp.timestamp() % 60000); + assertEquals(expected_timestamp, dp.timestamp()); + expected_timestamp += 60000; + ++i; + } + assertEquals(150, dps[0].size()); + } + // ----------------- // // Helper functions. // // ----------------- // From 61a840a003ab6c8a7ef8ef2cea55413152b68424 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Jan 2015 13:52:33 -0800 Subject: [PATCH 039/826] Fix version number in configure.ac --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index d225e08920..29d2615325 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.0.0-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.0-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From d475d8490b14ecff15a1f78702734960aa05c01c Mon Sep 17 00:00:00 2001 From: Mike Kobyakov Date: Mon, 8 Dec 2014 12:11:27 -0800 Subject: [PATCH 040/826] Added p50-75-95-99 exact and estimated aggregators with test Signed-off-by: Chris Larsen --- .gitignore | 1 + Makefile.am | 4 +- pom.xml.in | 6 ++ src/core/Aggregators.java | 94 +++++++++++++++++++ test/core/TestAggregators.java | 29 ++++++ test/core/TestTsdbQuery.java | 45 ++++++++- .../apache/commons-math3-3.4.1.jar.md5 | 1 + third_party/apache/include.mk | 32 +++++++ third_party/include.mk | 1 + 9 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 third_party/apache/commons-math3-3.4.1.jar.md5 create mode 100644 third_party/apache/include.mk diff --git a/.gitignore b/.gitignore index 8fdaa6b007..24afdb80df 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ guava-rpm-maker/\.project src-main src-test plugin_test.jar +/bin/ diff --git a/Makefile.am b/Makefile.am index 9dc5cdcf6e..a88138e1e8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -140,7 +140,8 @@ tsdb_DEPS = \ $(PROTOBUF) \ $(SLF4J_API) \ $(SUASYNC) \ - $(ZOOKEEPER) + $(ZOOKEEPER) \ + $(APACHE_MATH) test_SRC := \ test/core/SeekableViewsForTest.java \ @@ -619,6 +620,7 @@ pom.xml: pom.xml.in Makefile -e 's/@SLF4J_API_VERSION@/$(SLF4J_API_VERSION)/' \ -e 's/@SUASYNC_VERSION@/$(SUASYNC_VERSION)/' \ -e 's/@ZOOKEEPER_VERSION@/$(ZOOKEEPER_VERSION)/' \ + -e 's/@APACHE_MATH_VERSION@/$(APACHE_MATH_VERSION)/' \ -e 's/@spec_title@/$(spec_title)/' \ -e 's/@spec_vendor@/$(spec_vendor)/' \ -e 's/@spec_version@/$(PACKAGE_VERSION)/' \ diff --git a/pom.xml.in b/pom.xml.in index 5a5c7016eb..2ade149892 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -370,6 +370,12 @@ @ASYNCHBASE_VERSION@ + + org.apache.commons + commons-math3 + @APACHE_MATH_VERSION@ + + diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 2a28de8663..5c4f8520f1 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -16,6 +16,12 @@ import java.util.NoSuchElementException; import java.util.Set; +import org.apache.commons.math3.stat.descriptive.rank.Percentile; +import org.apache.commons.math3.stat.descriptive.rank.Percentile.EstimationType; +import org.apache.commons.math3.util.ResizableDoubleArray; + +import com.google.common.base.Preconditions; + /** * Utility class that provides common, generally useful aggregators. */ @@ -76,6 +82,32 @@ public enum Interpolation { /** Maps an aggregator name to its instance. */ private static final HashMap aggregators; + /** Aggregator that returns 99.9th percentile. */ + public static final PercentileAgg p999 = new PercentileAgg(99.9d, "p999"); + /** Aggregator that returns 99th percentile. */ + public static final PercentileAgg p99 = new PercentileAgg(99d, "p99"); + /** Aggregator that returns 95th percentile. */ + public static final PercentileAgg p95 = new PercentileAgg(95d, "p95"); + /** Aggregator that returns 99th percentile. */ + public static final PercentileAgg p90 = new PercentileAgg(90d, "p90"); + /** Aggregator that returns 75th percentile. */ + public static final PercentileAgg p75 = new PercentileAgg(75d, "p75"); + /** Aggregator that returns 50th percentile. */ + public static final PercentileAgg p50 = new PercentileAgg(50d, "p50"); + + /** Aggregator that returns estimated 99.9th percentile. */ + public static final PercentileAgg ep999 = new PercentileAgg(99.9d, "ep999", EstimationType.R_3); + /** Aggregator that returns estimated 99th percentile. */ + public static final PercentileAgg ep99 = new PercentileAgg(99d, "ep99", EstimationType.R_3); + /** Aggregator that returns estimated 95th percentile. */ + public static final PercentileAgg ep95 = new PercentileAgg(95d, "ep95", EstimationType.R_3); + /** Aggregator that returns estimated 75th percentile. */ + public static final PercentileAgg ep90 = new PercentileAgg(90d, "ep90", EstimationType.R_3); + /** Aggregator that returns estimated 50th percentile. */ + public static final PercentileAgg ep75 = new PercentileAgg(75d, "ep75", EstimationType.R_3); + /** Aggregator that returns estimated 50th percentile. */ + public static final PercentileAgg ep50 = new PercentileAgg(50d, "ep50", EstimationType.R_3); + static { aggregators = new HashMap(8); aggregators.put("sum", SUM); @@ -87,6 +119,13 @@ public enum Interpolation { aggregators.put("zimsum", ZIMSUM); aggregators.put("mimmin", MIMMIN); aggregators.put("mimmax", MIMMAX); + + PercentileAgg[] percentiles = { + p999, p99, p95, p90, p75, p50, ep999, ep99, ep95, ep90, ep75, ep50 + }; + for (PercentileAgg agg : percentiles) { + aggregators.put(agg.getName(), agg); + } } private Aggregators() { @@ -377,4 +416,59 @@ public Interpolation interpolationMethod() { return method; } } + + /** + * Percentile aggregator based on apache commons math3 implementation + */ + private static final class PercentileAgg implements Aggregator { + private final Double percentile; + private final String name; + private final EstimationType estimation; + + PercentileAgg(final Double percentile, final String name) { + this(percentile, name, null); + } + public String getName() { + return name; + } + PercentileAgg(final Double percentile, final String name, final EstimationType est) { + Preconditions.checkArgument(percentile > 0 && percentile <= 100, "Invalid percentile value"); + this.percentile = percentile; + this.name = name; + this.estimation = est; + } + + public long runLong(final Longs values) { + final Percentile percentile = + this.estimation == null + ? new Percentile(this.percentile) + : new Percentile(this.percentile).withEstimationType(estimation); + final ResizableDoubleArray local_values = new ResizableDoubleArray(); + while(values.hasNextValue()) { + local_values.addElement(values.nextLongValue()); + } + percentile.setData(local_values.getElements()); + return (long) percentile.evaluate(); + } + + public double runDouble(final Doubles values) { + final Percentile percentile = new Percentile(this.percentile); + final ResizableDoubleArray local_values = new ResizableDoubleArray(); + while(values.hasNextValue()) { + local_values.addElement(values.nextDoubleValue()); + } + percentile.setData(local_values.getElements()); + return percentile.evaluate(); + } + + public String toString() { + return name; + } + + @Override + public Interpolation interpolationMethod() { + return Aggregators.Interpolation.LERP; + } + + } } diff --git a/test/core/TestAggregators.java b/test/core/TestAggregators.java index 6d01e0bd2c..3efe5ce450 100644 --- a/test/core/TestAggregators.java +++ b/test/core/TestAggregators.java @@ -134,4 +134,33 @@ private static double naiveStdDev(long[] values) { return Math.sqrt(variance); } + @Test + public void testPercentiles() { + final long[] longValues = new long[1000]; + for (int i = 0; i < longValues.length; i++) { + longValues[i] = i+1; + } + + Numbers values = new Numbers(longValues); + assertEquals(500, Aggregators.get("p50"), values); + assertEquals(750, Aggregators.get("p75"), values); + assertEquals(900, Aggregators.get("p90"), values); + assertEquals(950, Aggregators.get("p95"), values); + assertEquals(990, Aggregators.get("p99"), values); + assertEquals(999, Aggregators.get("p999"), values); + + assertEquals(500, Aggregators.get("ep50"), values); + assertEquals(750, Aggregators.get("ep75"), values); + assertEquals(900, Aggregators.get("ep90"), values); + assertEquals(950, Aggregators.get("ep95"), values); + assertEquals(990, Aggregators.get("ep99"), values); + assertEquals(999, Aggregators.get("ep999"), values); + } + + private void assertEquals(long value, Aggregator agg, Numbers numbers) { + Assert.assertEquals(value, agg.runLong(numbers)); + numbers.reset(); + Assert.assertEquals((double)value, agg.runDouble(numbers), 1.0); + numbers.reset(); + } } diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 6bfc0bb2b4..649b9fa16c 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -2586,8 +2586,51 @@ public void runMimMaxFloatOffset() throws Exception { } assertEquals(600, dps[0].size()); } - + @Test + public void runPercentiles() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + // These are not accurate at all when data points only contain 2 values + // so we are just testing constructor logic, rather than precision + testPercentile(Aggregators.p50, 150, 150); + testPercentile(Aggregators.p75, 150, 150); + testPercentile(Aggregators.p90, 150, 150); + testPercentile(Aggregators.p95, 150, 150); + testPercentile(Aggregators.p99, 150, 150); + testPercentile(Aggregators.p999, 150, 150); + testPercentile(Aggregators.ep50, 150, 150); + testPercentile(Aggregators.ep75, 150, 150); + testPercentile(Aggregators.ep90, 150, 150); + testPercentile(Aggregators.ep95, 150, 150); + testPercentile(Aggregators.ep99, 150, 150); + testPercentile(Aggregators.ep999, 150, 150); + } + +private void testPercentile(Aggregator agg, long value, double delta) { + HashMap tags = new HashMap(0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("sys.cpu.user", tags, agg, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + long ts = 1356998430000L; + int counter = 0; + int size = dps[0].size(); + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals("counter " + counter, value, dp.longValue(), delta); + counter++; + } + assertEquals(600, size); +} + public void runCount() throws Exception { storeLongTimeSeriesSeconds(false, false); diff --git a/third_party/apache/commons-math3-3.4.1.jar.md5 b/third_party/apache/commons-math3-3.4.1.jar.md5 new file mode 100644 index 0000000000..9939ae9c15 --- /dev/null +++ b/third_party/apache/commons-math3-3.4.1.jar.md5 @@ -0,0 +1 @@ +14a218d0ee57907dd2c7ef944b6c0afd diff --git a/third_party/apache/include.mk b/third_party/apache/include.mk new file mode 100644 index 0000000000..a97b81a366 --- /dev/null +++ b/third_party/apache/include.mk @@ -0,0 +1,32 @@ +# Copyright (C) 2011-2013 The OpenTSDB Authors. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# - Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# - Neither the name of the StumbleUpon nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +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): $(APACHE_MATH).md5 + set dummy "$(APACHE_MATH_BASE_URL)" "$(APACHE_MATH)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(APACHE_MATH) diff --git a/third_party/include.mk b/third_party/include.mk index c6b7fe2326..56649734d1 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -35,3 +35,4 @@ include third_party/slf4j/include.mk include third_party/suasync/include.mk include third_party/validation-api/include.mk include third_party/zookeeper/include.mk +include third_party/apache/include.mk From e36927b00e5c13a1c9ef00895ccbb0ee58d5c7c4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Jan 2015 13:24:43 -0800 Subject: [PATCH 041/826] Rename the estimated aggregators and add the R7 agg for Excel users who want the same formula. Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 46 ++++++++++++++++++++++++++++------ test/core/TestAggregators.java | 19 +++++++++----- test/core/TestTsdbQuery.java | 26 +++++++++++-------- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 5c4f8520f1..eaa529300b 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -96,17 +96,42 @@ public enum Interpolation { public static final PercentileAgg p50 = new PercentileAgg(50d, "p50"); /** Aggregator that returns estimated 99.9th percentile. */ - public static final PercentileAgg ep999 = new PercentileAgg(99.9d, "ep999", EstimationType.R_3); + public static final PercentileAgg ep999r3 = + new PercentileAgg(99.9d, "ep999r3", EstimationType.R_3); /** Aggregator that returns estimated 99th percentile. */ - public static final PercentileAgg ep99 = new PercentileAgg(99d, "ep99", EstimationType.R_3); + public static final PercentileAgg ep99r3 = + new PercentileAgg(99d, "ep99r3", EstimationType.R_3); /** Aggregator that returns estimated 95th percentile. */ - public static final PercentileAgg ep95 = new PercentileAgg(95d, "ep95", EstimationType.R_3); + public static final PercentileAgg ep95r3 = + new PercentileAgg(95d, "ep95r3", EstimationType.R_3); /** Aggregator that returns estimated 75th percentile. */ - public static final PercentileAgg ep90 = new PercentileAgg(90d, "ep90", EstimationType.R_3); + public static final PercentileAgg ep90r3 = + new PercentileAgg(90d, "ep90r3", EstimationType.R_3); /** Aggregator that returns estimated 50th percentile. */ - public static final PercentileAgg ep75 = new PercentileAgg(75d, "ep75", EstimationType.R_3); + public static final PercentileAgg ep75r3 = + new PercentileAgg(75d, "ep75r3", EstimationType.R_3); /** Aggregator that returns estimated 50th percentile. */ - public static final PercentileAgg ep50 = new PercentileAgg(50d, "ep50", EstimationType.R_3); + public static final PercentileAgg ep50r3 = + new PercentileAgg(50d, "ep50r3", EstimationType.R_3); + + /** Aggregator that returns estimated 99.9th percentile. */ + public static final PercentileAgg ep999r7 = + new PercentileAgg(99.9d, "ep999r7", EstimationType.R_7); + /** Aggregator that returns estimated 99th percentile. */ + public static final PercentileAgg ep99r7 = + new PercentileAgg(99d, "ep99r7", EstimationType.R_7); + /** Aggregator that returns estimated 95th percentile. */ + public static final PercentileAgg ep95r7 = + new PercentileAgg(95d, "ep95r7", EstimationType.R_7); + /** Aggregator that returns estimated 75th percentile. */ + public static final PercentileAgg ep90r7 = + new PercentileAgg(90d, "ep90r7", EstimationType.R_7); + /** Aggregator that returns estimated 50th percentile. */ + public static final PercentileAgg ep75r7 = + new PercentileAgg(75d, "ep75r7", EstimationType.R_7); + /** Aggregator that returns estimated 50th percentile. */ + public static final PercentileAgg ep50r7 = + new PercentileAgg(50d, "ep50r7", EstimationType.R_7); static { aggregators = new HashMap(8); @@ -121,7 +146,9 @@ public enum Interpolation { aggregators.put("mimmax", MIMMAX); PercentileAgg[] percentiles = { - p999, p99, p95, p90, p75, p50, ep999, ep99, ep95, ep90, ep75, ep50 + p999, p99, p95, p90, p75, p50, + ep999r3, ep99r3, ep95r3, ep90r3, ep75r3, ep50r3, + ep999r7, ep99r7, ep95r7, ep90r7, ep75r7, ep50r7 }; for (PercentileAgg agg : percentiles) { aggregators.put(agg.getName(), agg); @@ -419,6 +446,11 @@ public Interpolation interpolationMethod() { /** * Percentile aggregator based on apache commons math3 implementation + * The default calculation is: + * index=(N+1)p + * estimate=x⌈h−1/2⌉ + * minLimit=0 + * maxLimit=1 */ private static final class PercentileAgg implements Aggregator { private final Double percentile; diff --git a/test/core/TestAggregators.java b/test/core/TestAggregators.java index 3efe5ce450..a254e23101 100644 --- a/test/core/TestAggregators.java +++ b/test/core/TestAggregators.java @@ -149,12 +149,19 @@ public void testPercentiles() { assertEquals(990, Aggregators.get("p99"), values); assertEquals(999, Aggregators.get("p999"), values); - assertEquals(500, Aggregators.get("ep50"), values); - assertEquals(750, Aggregators.get("ep75"), values); - assertEquals(900, Aggregators.get("ep90"), values); - assertEquals(950, Aggregators.get("ep95"), values); - assertEquals(990, Aggregators.get("ep99"), values); - assertEquals(999, Aggregators.get("ep999"), values); + assertEquals(500, Aggregators.get("ep50r3"), values); + assertEquals(750, Aggregators.get("ep75r3"), values); + assertEquals(900, Aggregators.get("ep90r3"), values); + assertEquals(950, Aggregators.get("ep95r3"), values); + assertEquals(990, Aggregators.get("ep99r3"), values); + assertEquals(999, Aggregators.get("ep999r3"), values); + + assertEquals(500, Aggregators.get("ep50r7"), values); + assertEquals(750, Aggregators.get("ep75r7"), values); + assertEquals(900, Aggregators.get("ep90r7"), values); + assertEquals(950, Aggregators.get("ep95r7"), values); + assertEquals(990, Aggregators.get("ep99r7"), values); + assertEquals(999, Aggregators.get("ep999r7"), values); } private void assertEquals(long value, Aggregator agg, Numbers numbers) { diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 649b9fa16c..dfc66673cd 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -2599,15 +2599,21 @@ public void runPercentiles() throws Exception { testPercentile(Aggregators.p95, 150, 150); testPercentile(Aggregators.p99, 150, 150); testPercentile(Aggregators.p999, 150, 150); - testPercentile(Aggregators.ep50, 150, 150); - testPercentile(Aggregators.ep75, 150, 150); - testPercentile(Aggregators.ep90, 150, 150); - testPercentile(Aggregators.ep95, 150, 150); - testPercentile(Aggregators.ep99, 150, 150); - testPercentile(Aggregators.ep999, 150, 150); - } - -private void testPercentile(Aggregator agg, long value, double delta) { + testPercentile(Aggregators.ep50r3, 150, 150); + testPercentile(Aggregators.ep75r3, 150, 150); + testPercentile(Aggregators.ep90r3, 150, 150); + testPercentile(Aggregators.ep95r3, 150, 150); + testPercentile(Aggregators.ep99r3, 150, 150); + testPercentile(Aggregators.ep999r3, 150, 150); + testPercentile(Aggregators.ep50r7, 150, 150); + testPercentile(Aggregators.ep75r7, 150, 150); + testPercentile(Aggregators.ep90r7, 150, 150); + testPercentile(Aggregators.ep95r7, 150, 150); + testPercentile(Aggregators.ep99r7, 150, 150); + testPercentile(Aggregators.ep999r7, 150, 150); + } + + private void testPercentile(Aggregator agg, long value, double delta) { HashMap tags = new HashMap(0); query.setStartTime(1356998400); query.setEndTime(1357041600); @@ -2629,7 +2635,7 @@ private void testPercentile(Aggregator agg, long value, double delta) { counter++; } assertEquals(600, size); -} + } public void runCount() throws Exception { storeLongTimeSeriesSeconds(false, false); From 2a4cc0b6390584ef983e500036a79bd34904f124 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Jan 2015 13:28:10 -0800 Subject: [PATCH 042/826] Strip "Random seed" printline from TestAggregators Signed-off-by: Chris Larsen --- test/core/TestAggregators.java | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core/TestAggregators.java b/test/core/TestAggregators.java index a254e23101..5f51327046 100644 --- a/test/core/TestAggregators.java +++ b/test/core/TestAggregators.java @@ -22,7 +22,6 @@ public final class TestAggregators { private static final Random random; static { final long seed = System.nanoTime(); - System.out.println("Random seed: " + seed); random = new Random(seed); } From 1803d3a38128bfd9483818f1f8f2666d9b41fa56 Mon Sep 17 00:00:00 2001 From: Mike Kobyakov Date: Tue, 10 Feb 2015 14:24:48 -0800 Subject: [PATCH 043/826] sort the aggregators in the web form Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index 4143fbfd66..1b5d9f35f2 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -13,6 +13,8 @@ package tsd.client; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; @@ -275,7 +277,9 @@ public void setMetricChangeHandler(final MetricChangeHandler handler) { } public void setAggregators(final ArrayList aggs) { - for (final String agg : aggs) { + final String[] agg_rray = (String[])aggs.toArray(); + Arrays.sort(agg_rray); + for (final String agg : agg_rray) { aggregators.addItem(agg); downsampler.addItem(agg); } From 57b546414c3f28c5167116a55f06e87fe9cbba93 Mon Sep 17 00:00:00 2001 From: Nitin Aggarwal Date: Sat, 7 Feb 2015 12:57:23 -0800 Subject: [PATCH 044/826] Make compaction parameters configurable Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 45 ++++++++++++++++++----------------- src/opentsdb.conf | 15 +++++++++++- src/utils/Config.java | 4 ++++ 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 06385cfc15..9aee8f9ab8 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -76,6 +76,18 @@ final class CompactionQueue extends ConcurrentSkipListMap { /** On how many bytes do we encode metrics IDs. */ private final short metric_width; + /** How frequently the compaction thread wakes up to flush stuff. */ + private final int flush_interval; // seconds + + /** Minimum number of rows we'll attempt to compact at once. */ + private final int min_flush_threshold; // rows + + /** Maximum number of rows we'll compact concurrently. */ + private final int max_concurrent_flushes; // rows + + /** If this is X then we'll flush X times faster than we really need. */ + private final int flush_speed; // multiplicative factor + /** * Constructor. * @param tsdb The TSDB we belong to. @@ -84,6 +96,10 @@ public CompactionQueue(final TSDB tsdb) { super(new Cmp(tsdb)); this.tsdb = tsdb; metric_width = tsdb.metrics.width(); + flush_interval = tsdb.config.getInt("tsd.storage.compaction.flush_interval"); + min_flush_threshold = tsdb.config.getInt("tsd.storage.compaction.min_flush_threshold"); + max_concurrent_flushes = tsdb.config.getInt("tsd.storage.compaction.max_concurrent_flushes"); + flush_speed = tsdb.config.getInt("tsd.storage.compaction.flush_speed"); if (tsdb.config.enable_compactions()) { startCompactionThread(); } @@ -153,8 +169,7 @@ private Deferred> flush(final long cut_off, int maxflushes) { return Deferred.fromResult(new ArrayList(0)); } final ArrayList> ds = - new ArrayList>(Math.min(maxflushes, - MAX_CONCURRENT_FLUSHES)); + new ArrayList>(Math.min(maxflushes, max_concurrent_flushes)); int nflushes = 0; int seed = (int) (System.nanoTime() % 3); for (final byte[] row : this.keySet()) { @@ -167,7 +182,7 @@ private Deferred> flush(final long cut_off, int maxflushes) { final long base_time = Bytes.getUnsignedInt(row, metric_width); if (base_time > cut_off) { break; - } else if (nflushes == MAX_CONCURRENT_FLUSHES) { + } else if (nflushes == max_concurrent_flushes) { // We kicked off the compaction of too many rows already, let's wait // until they're done before kicking off more. break; @@ -185,7 +200,7 @@ private Deferred> flush(final long cut_off, int maxflushes) { ds.add(tsdb.get(row).addCallbacks(compactcb, handle_read_error)); } final Deferred> group = Deferred.group(ds); - if (nflushes == MAX_CONCURRENT_FLUSHES && maxflushes > 0) { + if (nflushes == max_concurrent_flushes && maxflushes > 0) { // We're not done yet. Once this group of flushes completes, we need // to kick off more. tsdb.flush(); // Speed up this batch by telling the client to flush. @@ -650,21 +665,7 @@ private void startCompactionThread() { thread.start(); } - /** How frequently the compaction thread wakes up flush stuff. */ - // TODO(tsuna): Make configurable? - private static final int FLUSH_INTERVAL = 10; // seconds - - /** Minimum number of rows we'll attempt to compact at once. */ - // TODO(tsuna): Make configurable? - private static final int MIN_FLUSH_THRESHOLD = 100; // rows - - /** Maximum number of rows we'll compact concurrently. */ - // TODO(tsuna): Make configurable? - private static final int MAX_CONCURRENT_FLUSHES = 10000; // rows - /** If this is X then we'll flush X times faster than we really need. */ - // TODO(tsuna): Make configurable? - private static final int FLUSH_SPEED = 2; // multiplicative factor /** * Background thread to trigger periodic compactions. @@ -682,7 +683,7 @@ public void run() { // Flush if we have too many rows to recompact. // Note that in we might not be able to actually // flush anything if the rows aren't old enough. - if (size > MIN_FLUSH_THRESHOLD) { + if (size > min_flush_threshold) { // How much should we flush during this iteration? This scheme is // adaptive and flushes at a rate that is proportional to the size // of the queue, so we flush more aggressively if the queue is big. @@ -701,8 +702,8 @@ public void run() { // FLUSH_SPEED is 2, then instead of taking 1h to flush what we have // for the previous hour, we'll take only 30m. This is desirable so // that we evict old entries from the queue a bit faster. - final int maxflushes = Math.max(MIN_FLUSH_THRESHOLD, - size * FLUSH_INTERVAL * FLUSH_SPEED / Const.MAX_TIMESPAN); + final int maxflushes = Math.max(min_flush_threshold, + size * flush_interval * flush_speed / Const.MAX_TIMESPAN); final long now = System.currentTimeMillis(); flush(now / 1000 - Const.MAX_TIMESPAN - 1, maxflushes); if (LOG.isDebugEnabled()) { @@ -740,7 +741,7 @@ public void run() { return; } try { - Thread.sleep(FLUSH_INTERVAL * 1000); + Thread.sleep(flush_interval * 1000); } catch (InterruptedException e) { LOG.error("Compaction thread interrupted, doing one last flush", e); flush(); diff --git a/src/opentsdb.conf b/src/opentsdb.conf index bed259d587..11d2a911df 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -57,4 +57,17 @@ tsd.http.cachedir = # A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" -#tsd.storage.hbase.zk_quorum = localhost \ No newline at end of file +#tsd.storage.hbase.zk_quorum = localhost + +# --------- COMPACTIONS --------------------------------- +# Frequency at which compaction thread wakes up to flush stuff in seconds, default 10 +# tsd.storage.compaction.flush_interval = 10 + +# Minimum rows attempted to compact at once, default 100 +# tsd.storage.compaction.min_flush_threshold = 100 + +# Maximum number of rows, compacted concirrently, default 10000 +# tsd.storage.compaction.max_concurrent_flushes = 10000 + +# Compaction flush speed multiplier, default 2 +# tsd.storage.compaction.flush_speed = 2 diff --git a/src/utils/Config.java b/src/utils/Config.java index 7c251bb68c..eca904279e 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -467,6 +467,10 @@ protected void setDefaults() { default_map.put("tsd.storage.hbase.zk_quorum", "localhost"); default_map.put("tsd.storage.hbase.zk_basedir", "/hbase"); 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"); + default_map.put("tsd.storage.compaction.max_concurrent_flushes", "10000"); + default_map.put("tsd.storage.compaction.flush_speed", "2"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.request.enable_chunked", "false"); default_map.put("tsd.http.request.max_chunk", "4096"); From 9621c31f8e726deed2d76c9ebc1933c1ca8700fc Mon Sep 17 00:00:00 2001 From: Jan Mangs Date: Fri, 6 Feb 2015 09:29:59 -0500 Subject: [PATCH 045/826] Updated I/O worker to handle IdleEvents. Signed-off-by: Chris Larsen --- src/tsd/ConnectionManager.java | 15 ++------------- src/tsd/PipelineFactory.java | 3 ++- src/tsd/RpcHandler.java | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/tsd/ConnectionManager.java b/src/tsd/ConnectionManager.java index f49cd8211a..35c3288bad 100644 --- a/src/tsd/ConnectionManager.java +++ b/src/tsd/ConnectionManager.java @@ -23,19 +23,16 @@ import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.jboss.netty.handler.codec.embedder.CodecEmbedderException; -import org.jboss.netty.handler.timeout.IdleState; -import org.jboss.netty.handler.timeout.IdleStateAwareChannelHandler; -import org.jboss.netty.handler.timeout.IdleStateEvent; -import org.jboss.netty.handler.timeout.ReadTimeoutException; import net.opentsdb.stats.StatsCollector; /** * Keeps track of all existing connections. */ -final class ConnectionManager extends IdleStateAwareChannelHandler { +final class ConnectionManager extends SimpleChannelHandler { private static final Logger LOG = LoggerFactory.getLogger(ConnectionManager.class); @@ -125,12 +122,4 @@ public void exceptionCaught(final ChannelHandlerContext ctx, e.getChannel().close(); } - @Override - public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) { - if (e.getState() == IdleState.ALL_IDLE) { - LOG.debug("Closed idle socket."); - e.getChannel().close(); - } - } - } diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 947e7a0e5a..95e553f1a9 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -85,7 +85,6 @@ public PipelineFactory(final TSDB tsdb) { public ChannelPipeline getPipeline() throws Exception { final ChannelPipeline pipeline = pipeline(); - pipeline.addLast("timeout", this.timeoutHandler); pipeline.addLast("connmgr", connmgr); pipeline.addLast("detect", HTTP_OR_RPC); return pipeline; @@ -126,6 +125,8 @@ protected Object decode(final ChannelHandlerContext ctx, pipeline.addLast("encoder", ENCODER); pipeline.addLast("decoder", DECODER); } + + pipeline.addLast("timeout", timeoutHandler); pipeline.remove(this); pipeline.addLast("handler", rpchandler); diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index 1b10f8b067..a3aeeb34c4 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -28,10 +28,12 @@ import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; -import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.timeout.IdleState; +import org.jboss.netty.handler.timeout.IdleStateAwareChannelUpstreamHandler; +import org.jboss.netty.handler.timeout.IdleStateEvent; import net.opentsdb.BuildData; import net.opentsdb.core.Aggregators; @@ -42,7 +44,7 @@ /** * Stateless handler for RPCs (telnet-style or HTTP). */ -final class RpcHandler extends SimpleChannelUpstreamHandler { +final class RpcHandler extends IdleStateAwareChannelUpstreamHandler { private static final Logger LOG = LoggerFactory.getLogger(RpcHandler.class); @@ -578,6 +580,14 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { } } + + @Override + public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) { + if (e.getState() == IdleState.ALL_IDLE) { + LOG.debug("Closed idle socket."); + e.getChannel().close(); + } + } // ---------------- // // Logging helpers. // From f5d8168b1e47186e46c682191ead7a19a6ee03fa Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Feb 2015 16:15:01 -0800 Subject: [PATCH 046/826] Modify the idle state connection close to log the connection that is being closed Signed-off-by: Chris Larsen --- src/tsd/RpcHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index a3aeeb34c4..7dd006aa51 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -584,8 +584,10 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { @Override public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) { if (e.getState() == IdleState.ALL_IDLE) { - LOG.debug("Closed idle socket."); + final String channel_info = e.getChannel().toString(); + LOG.debug("Closing idle socket: " + channel_info); e.getChannel().close(); + LOG.info("Closed idle socket: " + channel_info); } } From 4a8cc386f645b7ad4dd428056cb092ec094534ee Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Mon, 16 Feb 2015 23:28:04 -0800 Subject: [PATCH 047/826] Reimplementation of sorted aggregators drop-down. Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index 1b5d9f35f2..e409d3e0fa 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -14,8 +14,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.List; - import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; @@ -277,11 +275,11 @@ public void setMetricChangeHandler(final MetricChangeHandler handler) { } public void setAggregators(final ArrayList aggs) { - final String[] agg_rray = (String[])aggs.toArray(); - Arrays.sort(agg_rray); - for (final String agg : agg_rray) { - aggregators.addItem(agg); - downsampler.addItem(agg); + Object[] aggsSortedArray = aggs.toArray(); + Arrays.sort(aggsSortedArray); + for (final Object agg : aggsSortedArray) { + aggregators.addItem((String)agg); + downsampler.addItem((String)agg); } setSelectedItem(aggregators, "sum"); setSelectedItem(downsampler, "avg"); From 0b680dc4d077de7ce6c977ce22fce1ad084d5f03 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Wed, 18 Feb 2015 00:15:55 -0800 Subject: [PATCH 048/826] Simplify fsck dedupe logic, fix accounting problem, tighten tests. Correctied another accounting problem. Corrected yet another accounting bug (dup resolution on compaction not accounted for). Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 140 +++++++++------- test/tools/TestFsck.java | 338 ++++++++++++++++++++------------------- 2 files changed, 254 insertions(+), 224 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index c1b0905c49..cc9b02a5e3 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -83,7 +83,7 @@ final class Fsck { /** Options to use while iterating over rows */ private final FsckOptions options; - /** Counters incremented during processing. They have to be atomic countsers + /** Counters incremented during processing. They have to be atomic counters * as we may be running multiple fsck threads. */ final AtomicLong kvs_processed = new AtomicLong(); final AtomicLong rows_processed = new AtomicLong(); @@ -93,6 +93,7 @@ final class Fsck { final AtomicLong bad_key_fixed = new AtomicLong(); final AtomicLong duplicates = new AtomicLong(); final AtomicLong duplicates_fixed = new AtomicLong(); + final AtomicLong duplicates_fixed_comp = new AtomicLong(); final AtomicLong orphans = new AtomicLong(); final AtomicLong orphans_fixed = new AtomicLong(); final AtomicLong future = new AtomicLong(); @@ -592,7 +593,9 @@ private void fsckDataPoints(final Map> datapoints) // or newest Collections.sort(time_map.getValue()); has_duplicates = true; - + // We want to keep either the first or the last incoming datapoint + // and ignore delete the middle. + final StringBuilder buf = new StringBuilder(); buf.append("More than one column had a value for the same timestamp: ") .append("(") @@ -600,66 +603,68 @@ private void fsckDataPoints(final Map> datapoints) .append(")\n row key: (") .append(UniqueId.uidToString(key)) .append(")\n"); - int index = 0; - DP last_dp = null; - for (DP dp : time_map.getValue()) { + + int num_dupes = time_map.getValue().size(); + + final int deleteRangeStart; + final int deleteRangeStop; + final DP dpToKeep; + if (options.lastWriteWins()) { + // Save the latest datapoint from extinction. + deleteRangeStart = 0; + deleteRangeStop = num_dupes - 1; + dpToKeep = time_map.getValue().get(num_dupes - 1); + } else { + // Save the oldest datapoint from extinction. + deleteRangeStart = 1; + deleteRangeStop = num_dupes; + dpToKeep = time_map.getValue().get(0); + appendDatapointInfo(buf, dpToKeep, " <--- keep oldest").append("\n"); + } + + unique_columns.put(dpToKeep.kv.qualifier(), dpToKeep.kv.value()); + valid_datapoints.getAndIncrement(); + has_uncorrected_value_error |= Internal.isFloat(dpToKeep.qualifier()) ? + fsckFloat(dpToKeep) : fsckInteger(dpToKeep); + + if (Internal.inMilliseconds(dpToKeep.qualifier())) { + has_milliseconds = true; + } else { + has_seconds = true; + } + + for (int dpIndex = deleteRangeStart; dpIndex < deleteRangeStop; dpIndex++) { + duplicates.getAndIncrement(); + DP dp = time_map.getValue().get(dpIndex); buf.append(" ") - .append("write time: (") - .append(dp.kv.timestamp()) - .append(") ") - .append(" compacted: (") - .append(dp.compacted) - .append(") qualifier: ") - .append(Arrays.toString(dp.kv.qualifier())); + .append("write time: (") + .append(dp.kv.timestamp()) + .append(") ") + .append(" compacted: (") + .append(dp.compacted) + .append(") qualifier: ") + .append(Arrays.toString(dp.kv.qualifier())) + .append("\n"); unique_columns.put(dp.kv.qualifier(), dp.kv.value()); - if (options.lastWriteWins()) { - if (index == time_map.getValue().size() - 1) { - buf.append(" <--- Keep latest"); - valid_datapoints.getAndIncrement(); - has_uncorrected_value_error |= Internal.isFloat(dp.qualifier()) ? - fsckFloat(dp) : fsckInteger(dp); - if (Internal.inMilliseconds(dp.qualifier())) { - has_milliseconds = true; - } else { - has_seconds = true; - } - } - if (last_dp != null && options.fix() && options.resolveDupes()) { - if (!compact_row && options.fix() && options.resolveDupes() && - !last_dp.compacted) { - final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), - last_dp.kv.key(), last_dp.kv.family(), last_dp.qualifier()); - tsdb.getClient().delete(delete); - } - } - } else if (!options.lastWriteWins() && index == 0) { - buf.append(" <--- Keep oldest"); - valid_datapoints.getAndIncrement(); - has_uncorrected_value_error |= Internal.isFloat(dp.qualifier()) ? - fsckFloat(dp) : fsckInteger(dp); - if (Internal.inMilliseconds(dp.qualifier())) { - has_milliseconds = true; - } else { - has_seconds = true; - } - } else if (options.fix() && options.resolveDupes()) { - // don't want this dp - LOG.error("Delete: " + dp.kv); - if (!compact_row && options.fix() && options.resolveDupes() && - !dp.compacted) { - final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), - dp.kv.key(), dp.kv.family(), dp.qualifier()); - tsdb.getClient().delete(delete); + if (options.fix() && options.resolveDupes()) { + if (compact_row) { + // Scheduled for deletion by compaction. + duplicates_fixed_comp.getAndIncrement(); + } else if (!dp.compacted) { + LOG.debug("REMOVING: " + dp.kv); + tsdb.getClient().delete( + new DeleteRequest( + tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() + ) + ); + duplicates_fixed.getAndIncrement(); } } - index++; - if (index < time_map.getValue().size()) { - buf.append("\n"); - } - last_dp = dp; - duplicates.getAndIncrement(); } - LOG.error(buf.toString()); + if (options.lastWriteWins()) { + appendDatapointInfo(buf, dpToKeep, " <--- keep latest").append("\n"); + } + LOG.info(buf.toString()); } // if an error was found in this row that was not marked for repair, then @@ -713,6 +718,8 @@ private void fsckDataPoints(final Map> datapoints) TSDB.FAMILY(), qualifier); tsdb.getClient().delete(delete); } + duplicates_fixed.getAndAdd(duplicates_fixed_comp.longValue()); + duplicates_fixed_comp.set(0); } } @@ -944,7 +951,24 @@ private void appendDP(final byte[] new_qual, final byte[] new_value, System.arraycopy(new_value, 0, compact_value, value_index, value_length); value_index += value_length; } - + /** + * Appends a representation of a datapoint to a string buffer + * @param buf + * @param extraMessage + */ + private StringBuilder appendDatapointInfo(StringBuilder buf, DP dp, String extraMessage) { + buf.append(" ") + .append("write time: (") + .append(dp.kv.timestamp()) + .append(") ") + .append(" compacted: (") + .append(dp.compacted) + .append(") qualifier: ") + .append(Arrays.toString(dp.kv.qualifier())) + .append(extraMessage); + return buf; + } + /** * Resets the running compaction variables. This should be called AFTER a * {@link fsckDataPoints()} has been run and before the next row of values diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index dda615a323..31b078b1ee 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -1563,7 +1563,7 @@ public void integerVle1ByteNegative() throws Exception { @Test public void integerVle1ByteNegativeFix() throws Exception { when(options.fix()).thenReturn(true); - + final byte[] qual1 = { 0x00, 0x00 }; final byte[] val1 = new byte[] { 1 }; final byte[] qual2 = { 0x00, 0x27 }; @@ -1869,9 +1869,9 @@ public void dupesSinglesSeconds() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -1895,9 +1895,10 @@ public void dupesSinglesSecondsFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(1, fsck.totalFixed()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -1920,9 +1921,10 @@ public void dupesSinglesSecondsLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(0, fsck.totalFixed()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -1947,9 +1949,10 @@ public void dupesSinglesSecondsLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); + assertEquals(1, fsck.totalFixed()); assertNull(storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -1976,9 +1979,9 @@ public void dupeTimestampsMultipleSinglesSeconds() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(4, fsck.duplicates.get()); - assertEquals(4, fsck.totalErrors()); - assertEquals(4, fsck.correctable()); + assertEquals(3, fsck.duplicates.get()); + assertEquals(3, fsck.totalErrors()); + assertEquals(3, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2010,9 +2013,9 @@ public void dupeTimestampsMultipleSinglesSecondsFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(4, fsck.duplicates.get()); - assertEquals(4, fsck.totalErrors()); - assertEquals(4, fsck.correctable()); + assertEquals(3, fsck.duplicates.get()); + assertEquals(3, fsck.totalErrors()); + assertEquals(3, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -2043,9 +2046,9 @@ public void dupeTimestampsMultipleSinglesSecondsLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(4, fsck.duplicates.get()); - assertEquals(4, fsck.totalErrors()); - assertEquals(4, fsck.correctable()); + assertEquals(3, fsck.duplicates.get()); + assertEquals(3, fsck.totalErrors()); + assertEquals(3, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2078,9 +2081,9 @@ public void dupeTimestampsMultipleSinglesSecondsLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(4, fsck.duplicates.get()); - assertEquals(4, fsck.totalErrors()); - assertEquals(4, fsck.correctable()); + assertEquals(3, fsck.duplicates.get()); + assertEquals(3, fsck.totalErrors()); + assertEquals(3, fsck.correctable()); assertNull(storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -2103,9 +2106,9 @@ public void dupeSinglesTimestampsMs() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2129,9 +2132,9 @@ public void dupeSinglesTimestampsMsFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -2154,9 +2157,9 @@ public void dupeSinglesTimestampsMsLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2181,9 +2184,9 @@ public void dupeSinglesTimestampsMsFixLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2210,9 +2213,9 @@ public void dupeTimestampsMultipleSinglesMs() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2244,9 +2247,9 @@ public void dupeTimestampsMultipleSinglesMsFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -2277,9 +2280,9 @@ public void dupeTimestampsMultipleSinglesMsLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2312,9 +2315,9 @@ public void dupeTimestampsMultipleSinglesMsLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(5, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -2337,9 +2340,9 @@ public void dupesSinglesMixed() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2363,9 +2366,9 @@ public void dupesSinglesMixedFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -2388,9 +2391,9 @@ public void dupesSinglesMixedLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2415,9 +2418,9 @@ public void dupesSinglesMixedLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -2446,9 +2449,9 @@ public void twoCompactedColumnsWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -2477,9 +2480,9 @@ public void twoCompactedColumnsWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual3, qual4))); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val4, @@ -2509,9 +2512,9 @@ public void twoCompactedColumnsWSameTSLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -2542,9 +2545,9 @@ public void twoCompactedColumnsWSameTSFixLLW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual3, qual4))); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val4, @@ -2572,9 +2575,9 @@ public void twoCompactedColumnsMSWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -2604,9 +2607,9 @@ public void twoCompactedColumnsMSWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual3, qual4))); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val4, new byte[] { 0 }), @@ -2635,9 +2638,9 @@ public void twoCompactedColumnsMSWSameTSLLW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -2668,9 +2671,9 @@ public void twoCompactedColumnsMSWSameTSLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual3, qual4))); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val4, new byte[] { 0 }), @@ -2701,9 +2704,9 @@ public void twoCompactedPlusSingleWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -2737,9 +2740,9 @@ public void twoCompactedPlusSingleWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val4, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual4))); @@ -2774,9 +2777,9 @@ public void twoCompactedPlusSingleWSameTSLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -2811,9 +2814,9 @@ public void twoCompactedPlusSingleWSameTSLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val5, val4, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual5, qual4))); @@ -2839,9 +2842,9 @@ public void compactedAndSingleWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertArrayEquals(val4, storage.getColumn(ROW, qual4)); @@ -2867,9 +2870,9 @@ public void compactedAndSingleWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertNull(storage.getColumn(ROW, qual4)); @@ -2895,9 +2898,9 @@ public void compactedAndSingleWSameTSLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertArrayEquals(val4, storage.getColumn(ROW, qual4)); @@ -2924,9 +2927,9 @@ public void compactedAndSingleWSameTSLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val4, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual4))); assertNull(storage.getColumn(ROW, qual3)); @@ -2950,9 +2953,9 @@ public void compactedAndSingleMSWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertArrayEquals(val4, storage.getColumn(ROW, qual4)); @@ -2979,9 +2982,9 @@ public void compactedAndSingleMSWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertNull(storage.getColumn(ROW, qual4)); @@ -3007,9 +3010,9 @@ public void compactedAndSingleMSWSameTSLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertArrayEquals(val4, storage.getColumn(ROW, qual4)); @@ -3037,9 +3040,9 @@ public void compactedAndSingleMSWSameTSLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val4, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual4))); assertNull(storage.getColumn(ROW, qual4)); @@ -3062,9 +3065,9 @@ public void compactedAndSingleMixedWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 1 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertArrayEquals(val4, storage.getColumn(ROW, qual4)); @@ -3090,9 +3093,9 @@ public void compactedAndSingleMixedWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] {1 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertNull(storage.getColumn(ROW, qual4)); @@ -3116,9 +3119,9 @@ public void compactedAndSingleMixedWSameTSLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, new byte[] { 1 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual3))); assertArrayEquals(val4, storage.getColumn(ROW, qual4)); @@ -3145,9 +3148,9 @@ public void compactedAndSingleMixedWSameTSLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(2, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val4, val3, new byte[] {1 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual4, qual3))); assertNull(storage.getColumn(ROW, qual4)); @@ -3181,9 +3184,9 @@ public void tripleCompactedColumnsWSameTS() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -3222,9 +3225,9 @@ public void tripleCompactedColumnsWSameTSFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val4, val6, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2, qual4, qual6))); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); @@ -3261,9 +3264,9 @@ public void tripleCompactedColumnsWSameTSLWW() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(MockBase.concatByteArrays(val3, val4, new byte[] { 0 }), @@ -3303,9 +3306,9 @@ public void tripleCompactedColumnsWSameTSLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.duplicates.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); assertArrayEquals(MockBase.concatByteArrays(val1, val5, val4, val6, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual5, qual4, qual6))); assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); @@ -3336,9 +3339,9 @@ public void compactAndNotFixDupes() throws Exception { fsck.runFullTable(); storage.dumpToSystemOut(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertArrayEquals(val1, storage.getColumn(ROW, qual1)); assertArrayEquals(val2, storage.getColumn(ROW, qual2)); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -3363,9 +3366,9 @@ public void compactAndFixDupes() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(2, fsck.duplicates.get()); - assertEquals(2, fsck.totalErrors()); - assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); + assertEquals(1, fsck.totalErrors()); + assertEquals(1, fsck.correctable()); assertNull(storage.getColumn(ROW, qual1)); assertNull(storage.getColumn(ROW, qual2)); assertNull(storage.getColumn(ROW, qual3)); @@ -3583,9 +3586,9 @@ public void compactedAndBadValuesNotFixAndDupesNotFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); - assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); assertEquals(1, fsck.bad_values.get()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); @@ -3616,9 +3619,9 @@ public void compactedAndBadValuesFixAndDupesNotFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); - assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); assertEquals(1, fsck.bad_values.get()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); @@ -3650,10 +3653,11 @@ public void compactedAndBadValuesNotFixAndDupesFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); - assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); assertEquals(1, fsck.bad_values.get()); + assertEquals(0, fsck.totalFixed()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertArrayEquals(val3, storage.getColumn(ROW, qual3)); @@ -3683,10 +3687,11 @@ public void compactedAndBadValuesFixAndDupesFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); - assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); assertEquals(1, fsck.bad_values.get()); + assertEquals(2, fsck.totalFixed()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertNull(storage.getColumn(ROW, qual3)); @@ -3717,10 +3722,11 @@ public void compactedAndBadValuesFixAndDupesLWWFix() throws Exception { final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(3, fsck.kvs_processed.get()); - assertEquals(3, fsck.totalErrors()); - assertEquals(3, fsck.correctable()); - assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertEquals(1, fsck.duplicates.get()); assertEquals(1, fsck.bad_values.get()); + assertEquals(2, fsck.totalFixed()); assertArrayEquals(MockBase.concatByteArrays(val1, val4, new byte[] { 0 }), storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); assertNull(storage.getColumn(ROW, qual3)); From 821e55da7e2831b122241b5d656bb65f42ceaa77 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 Mar 2015 13:12:21 -0800 Subject: [PATCH 049/826] Tweaks to fsck fix coding style Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 77 +++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index cc9b02a5e3..640e43014f 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -606,52 +606,53 @@ private void fsckDataPoints(final Map> datapoints) int num_dupes = time_map.getValue().size(); - final int deleteRangeStart; - final int deleteRangeStop; - final DP dpToKeep; + final int delete_range_start; + final int delete_range_stop; + final DP dp_to_keep; if (options.lastWriteWins()) { // Save the latest datapoint from extinction. - deleteRangeStart = 0; - deleteRangeStop = num_dupes - 1; - dpToKeep = time_map.getValue().get(num_dupes - 1); + delete_range_start = 0; + delete_range_stop = num_dupes - 1; + dp_to_keep = time_map.getValue().get(num_dupes - 1); } else { // Save the oldest datapoint from extinction. - deleteRangeStart = 1; - deleteRangeStop = num_dupes; - dpToKeep = time_map.getValue().get(0); - appendDatapointInfo(buf, dpToKeep, " <--- keep oldest").append("\n"); + delete_range_start = 1; + delete_range_stop = num_dupes; + dp_to_keep = time_map.getValue().get(0); + appendDatapointInfo(buf, dp_to_keep, " <--- keep oldest").append("\n"); } - unique_columns.put(dpToKeep.kv.qualifier(), dpToKeep.kv.value()); + unique_columns.put(dp_to_keep.kv.qualifier(), dp_to_keep.kv.value()); valid_datapoints.getAndIncrement(); - has_uncorrected_value_error |= Internal.isFloat(dpToKeep.qualifier()) ? - fsckFloat(dpToKeep) : fsckInteger(dpToKeep); + has_uncorrected_value_error |= Internal.isFloat(dp_to_keep.qualifier()) ? + fsckFloat(dp_to_keep) : fsckInteger(dp_to_keep); - if (Internal.inMilliseconds(dpToKeep.qualifier())) { + if (Internal.inMilliseconds(dp_to_keep.qualifier())) { has_milliseconds = true; } else { has_seconds = true; } - for (int dpIndex = deleteRangeStart; dpIndex < deleteRangeStop; dpIndex++) { + for (int dp_index = delete_range_start; dp_index < delete_range_stop; + dp_index++) { duplicates.getAndIncrement(); - DP dp = time_map.getValue().get(dpIndex); + DP dp = time_map.getValue().get(dp_index); buf.append(" ") - .append("write time: (") - .append(dp.kv.timestamp()) - .append(") ") - .append(" compacted: (") - .append(dp.compacted) - .append(") qualifier: ") - .append(Arrays.toString(dp.kv.qualifier())) - .append("\n"); + .append("write time: (") + .append(dp.kv.timestamp()) + .append(") ") + .append(" compacted: (") + .append(dp.compacted) + .append(") qualifier: ") + .append(Arrays.toString(dp.kv.qualifier())) + .append("\n"); unique_columns.put(dp.kv.qualifier(), dp.kv.value()); if (options.fix() && options.resolveDupes()) { if (compact_row) { // Scheduled for deletion by compaction. duplicates_fixed_comp.getAndIncrement(); } else if (!dp.compacted) { - LOG.debug("REMOVING: " + dp.kv); + LOG.debug("Removing duplicate data point: " + dp.kv); tsdb.getClient().delete( new DeleteRequest( tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() @@ -662,7 +663,7 @@ private void fsckDataPoints(final Map> datapoints) } } if (options.lastWriteWins()) { - appendDatapointInfo(buf, dpToKeep, " <--- keep latest").append("\n"); + appendDatapointInfo(buf, dp_to_keep, " <--- keep latest").append("\n"); } LOG.info(buf.toString()); } @@ -951,21 +952,23 @@ private void appendDP(final byte[] new_qual, final byte[] new_value, System.arraycopy(new_value, 0, compact_value, value_index, value_length); value_index += value_length; } + /** * Appends a representation of a datapoint to a string buffer - * @param buf - * @param extraMessage + * @param buf The buffer to modify + * @param msg An optional message to append */ - private StringBuilder appendDatapointInfo(StringBuilder buf, DP dp, String extraMessage) { + private StringBuilder appendDatapointInfo(final StringBuilder buf, + final DP dp, final String msg) { buf.append(" ") - .append("write time: (") - .append(dp.kv.timestamp()) - .append(") ") - .append(" compacted: (") - .append(dp.compacted) - .append(") qualifier: ") - .append(Arrays.toString(dp.kv.qualifier())) - .append(extraMessage); + .append("write time: (") + .append(dp.kv.timestamp()) + .append(") ") + .append(" compacted: (") + .append(dp.compacted) + .append(") qualifier: ") + .append(Arrays.toString(dp.kv.qualifier())) + .append(msg); return buf; } From 21fb7fe9193e4908dad976323b4a055756618e60 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Tue, 24 Feb 2015 02:28:51 -0800 Subject: [PATCH 050/826] Fix for #412 insufficient tag validation + improved testability of TestTSDB Signed-off-by: Chris Larsen --- src/core/TSDB.java | 1 - src/core/Tags.java | 2 ++ test/core/TestTSDB.java | 43 ++++++++++++++++++++++------------------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index c96dc5122c..7e0e7be54c 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -670,7 +670,6 @@ private Deferred addPointInternal(final String metric, + " 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; diff --git a/src/core/Tags.java b/src/core/Tags.java index 5c96bf7809..5422baeaab 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -401,6 +401,8 @@ public Map call(final ArrayList names) public static void validateString(final String what, final String s) { if (s == null) { throw new IllegalArgumentException("Invalid " + what + ": null"); + } else if ("".equals(s)) { + throw new IllegalArgumentException("Invalid " + what + ": empty string"); } final int n = s.length(); for (int i = 0; i < n; i++) { diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index f296cfd1b2..0d9ede48b2 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -56,7 +56,7 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, - Scanner.class, AtomicIncrementRequest.class, IncomingDataPoints.class}) + Scanner.class, AtomicIncrementRequest.class}) public final class TestTSDB { private Config config; private TSDB tsdb; @@ -526,9 +526,8 @@ public void addPointLongOverwrite() throws Exception { @Test (expected = NoSuchUniqueName.class) public void addPointNoAutoMetric() throws Exception { setupAddPointStorage(); - when(IncomingDataPoints.rowKeyTemplate((TSDB)any(), anyString(), - (Map)any())) - .thenThrow(new NoSuchUniqueName("sys.cpu.user", "metric")); + when(metrics.getId(anyString())).thenThrow(new NoSuchUniqueName("sys.cpu.user", "metric")); + HashMap tags = new HashMap(1); tags.put("host", "web01"); tsdb.addPoint("sys.cpu.user", 1356998400, 42, tags).joinUninterruptibly(); @@ -584,6 +583,15 @@ public void addPointSecondNegative() throws Exception { tsdb.addPoint("sys.cpu.user", -2147483648, 42, tags).joinUninterruptibly(); } + @Test (expected = IllegalArgumentException.class) + public void emptyTagValue() throws Exception { + setupAddPointStorage(); + HashMap tags = new HashMap() {{ + put("host", ""); + }}; + tsdb.addPoint("sys.cpu.user", 1234567890, 42, tags).joinUninterruptibly(); + } + @Test public void addPointMS1970() throws Exception { // Since it's just over Integer.MAX_VALUE, OpenTSDB will treat this as @@ -843,29 +851,24 @@ private void setGetUidName() { when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })).thenThrow( new NoSuchUniqueId("tag_values", new byte[] { 0, 0, 2})); } - /** * Configures storage for the addPoint() tests to validate that we're storing * data points correctly. */ - private void setupAddPointStorage() throws Exception { + @Test + public void setupAddPointStorage() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); - - PowerMockito.mockStatic(IncomingDataPoints.class); - final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - PowerMockito.doAnswer( - new Answer() { - @Override - public byte[] answer(final InvocationOnMock unused) - throws Exception { - return row; - } - } - ).when(IncomingDataPoints.class, "rowKeyTemplate", any(), anyString(), - any()); - when(metrics.width()).thenReturn((short)3); + when(metrics.getId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); when(tag_names.width()).thenReturn((short)3); when(tag_values.width()).thenReturn((short)3); + when(tag_values.getId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getOrCreateId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getOrCreateId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); + + HashMap tags = new HashMap() {{ + put("host", "web01"); + }}; } } From eecd448e93c3a0422cfdbe2ffcf5cf468b02dec6 Mon Sep 17 00:00:00 2001 From: Nitin Aggarwal Date: Sat, 7 Feb 2015 01:51:33 -0800 Subject: [PATCH 051/826] Avoid multiple compactionq flushes with tsdb flush Flushing compaction queue from tsdb flush, is creating an endless chain of compaction queue flushes, as compaction queue flush calls tsdb flush to flush HBaseClient. Also we don't really need to call compaction queue flush from tsdb.flush(), as compaction background thread flush anyways every FLUSH_INTERVAL seconds. But to avoid API breaking changes, just calling flush on tsdb's HBaseClient, rather than TSDB. Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 9aee8f9ab8..617a03943a 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -203,7 +203,7 @@ private Deferred> flush(final long cut_off, int maxflushes) { if (nflushes == max_concurrent_flushes && maxflushes > 0) { // We're not done yet. Once this group of flushes completes, we need // to kick off more. - tsdb.flush(); // Speed up this batch by telling the client to flush. + tsdb.getClient().flush(); // Speed up this batch by telling the client to flush. final int maxflushez = maxflushes; // Make it final for closure. final class FlushMoreCB implements Callback>, ArrayList> { From 6f9aa4b1f9fb7a514c3388e90cc4556a19b4a5bf Mon Sep 17 00:00:00 2001 From: James Royalty Date: Sun, 24 Aug 2014 18:00:33 -0400 Subject: [PATCH 052/826] Create HttpPlugins that run in the main Netty loop. Signed-off-by: Chris Larsen --- Makefile.am | 8 + pom.xml.in | 6 + src/core/TSDB.java | 57 +- src/tools/TSDMain.java | 32 +- src/tsd/AbstractHttpQuery.java | 404 ++++++++++ src/tsd/BadRequestException.java | 2 +- src/tsd/HttpQuery.java | 377 ++------- src/tsd/HttpRpc.java | 4 +- src/tsd/HttpRpcPlugin.java | 113 +++ src/tsd/HttpRpcPluginQuery.java | 53 ++ src/tsd/PipelineFactory.java | 19 +- src/tsd/RpcHandler.java | 605 +++++---------- src/tsd/RpcManager.java | 730 ++++++++++++++++++ src/tsd/StatsRpc.java | 2 + .../services/net.opentsdb.tsd.HttpRpcPlugin | 1 + test/tsd/DummyHttpRpcPlugin.java | 56 ++ test/tsd/TestHttpRpcPluginQuery.java | 73 ++ test/tsd/TestRpcHandler.java | 116 ++- test/tsd/TestRpcManager.java | 203 +++++ test/utils/TestPluginLoader.java | 9 + 20 files changed, 2042 insertions(+), 828 deletions(-) create mode 100644 src/tsd/AbstractHttpQuery.java create mode 100644 src/tsd/HttpRpcPlugin.java create mode 100644 src/tsd/HttpRpcPluginQuery.java create mode 100644 src/tsd/RpcManager.java create mode 100644 test/META-INF/services/net.opentsdb.tsd.HttpRpcPlugin create mode 100644 test/tsd/DummyHttpRpcPlugin.java create mode 100644 test/tsd/TestHttpRpcPluginQuery.java create mode 100644 test/tsd/TestRpcManager.java diff --git a/Makefile.am b/Makefile.am index a88138e1e8..d719a31755 100644 --- a/Makefile.am +++ b/Makefile.am @@ -90,6 +90,7 @@ tsdb_SRC := \ src/tree/Tree.java \ src/tree/TreeBuilder.java \ src/tree/TreeRule.java \ + src/tsd/AbstractHttpQuery.java \ src/tsd/AnnotationRpc.java \ src/tsd/BadRequestException.java \ src/tsd/ConnectionManager.java \ @@ -99,6 +100,8 @@ tsdb_SRC := \ src/tsd/HttpSerializer.java \ src/tsd/HttpQuery.java \ src/tsd/HttpRpc.java \ + src/tsd/HttpRpcPlugin.java \ + src/tsd/HttpRpcPluginQuery.java \ src/tsd/LineBasedFrameDecoder.java \ src/tsd/LogsRpc.java \ src/tsd/PipelineFactory.java \ @@ -106,6 +109,7 @@ tsdb_SRC := \ src/tsd/QueryRpc.java \ src/tsd/RpcHandler.java \ src/tsd/RpcPlugin.java \ + src/tsd/RpcManager.java \ src/tsd/RTPublisher.java \ src/tsd/SearchRpc.java \ src/tsd/StaticFileRpc.java \ @@ -185,10 +189,12 @@ test_SRC := \ test/tsd/TestGraphHandler.java \ test/tsd/TestHttpJsonSerializer.java \ test/tsd/TestHttpQuery.java \ + test/tsd/TestHttpRpcPluginQuery.java \ test/tsd/TestPutRpc.java \ test/tsd/TestQueryRpc.java \ test/tsd/TestRpcHandler.java \ test/tsd/TestRpcPlugin.java \ + test/tsd/TestRpcManager.java \ test/tsd/TestRTPublisher.java \ test/tsd/TestSearchRpc.java \ test/tsd/TestSuggestRpc.java \ @@ -208,6 +214,7 @@ test_plugin_SRC := \ test/plugin/DummyPluginB.java \ test/search/DummySearchPlugin.java \ test/tsd/DummyHttpSerializer.java \ + test/tsd/DummyHttpRpcPlugin.java \ test/tsd/DummyRpcPlugin.java \ test/tsd/DummyRTPublisher.java @@ -216,6 +223,7 @@ test_plugin_SVCS := \ META-INF/services/net.opentsdb.plugin.DummyPlugin \ META-INF/services/net.opentsdb.search.SearchPlugin \ META-INF/services/net.opentsdb.tsd.HttpSerializer \ + META-INF/services/net.opentsdb.tsd.HttpRpcPlugin \ META-INF/services/net.opentsdb.tsd.RpcPlugin \ META-INF/services/net.opentsdb.tsd.RTPublisher diff --git a/pom.xml.in b/pom.xml.in index 2ade149892..f62d5442fc 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -126,6 +126,9 @@ net/opentsdb/tsd/DummyHttpSerializer.class -C target/test-classes + net/opentsdb/tsd/DummyHttpRpcPlugin.class + -C + target/test-classes net/opentsdb/tsd/DummyRpcPlugin.class -C target/test-classes @@ -141,6 +144,9 @@ META-INF/services/net.opentsdb.tsd.HttpSerializer -C test + META-INF/services/net.opentsdb.tsd.HttpRpcPlugin + -C + test META-INF/services/net.opentsdb.tsd.RpcPlugin -C test diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 7e0e7be54c..1b035018e2 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -36,7 +36,6 @@ import net.opentsdb.tree.TreeBuilder; import net.opentsdb.tsd.RTPublisher; -import net.opentsdb.tsd.RpcPlugin; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; @@ -107,8 +106,6 @@ public final class TSDB { /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; - /** List of activated RPC plugins */ - private List rpc_plugins = null; /** * Constructor @@ -231,32 +228,6 @@ public void initializePlugins(final boolean init_rpcs) { } else { rt_publisher = null; } - - if (init_rpcs && config.hasProperty("tsd.rpc.plugins")) { - final String[] plugins = config.getString("tsd.rpc.plugins").split(","); - for (final String plugin : plugins) { - final RpcPlugin rpc = PluginLoader.loadSpecificPlugin(plugin.trim(), - RpcPlugin.class); - if (rpc == null) { - throw new IllegalArgumentException( - "Unable to locate RPC plugin: " + plugin.trim()); - } - try { - rpc.initialize(this); - } catch (Exception e) { - throw new RuntimeException( - "Failed to initialize RPC plugin", e); - } - - if (rpc_plugins == null) { - rpc_plugins = new ArrayList(1); - } - rpc_plugins.add(rpc); - LOG.info("Successfully initialized RPC plugin [" + - rpc.getClass().getCanonicalName() + "] version: " - + rpc.version()); - } - } } /** @@ -455,30 +426,20 @@ public void collectStats(final StatsCollector collector) { // Collect Stats from Plugins if (rt_publisher != null) { try { - collector.addExtraTag("plugin", "publish"); + collector.addExtraTag("plugin", "publish"); rt_publisher.collectStats(collector); } finally { - collector.clearExtraTag("plugin"); + collector.clearExtraTag("plugin"); } } if (search != null) { try { - collector.addExtraTag("plugin", "search"); - search.collectStats(collector); + collector.addExtraTag("plugin", "search"); + search.collectStats(collector); } finally { - collector.clearExtraTag("plugin"); + collector.clearExtraTag("plugin"); } } - if (rpc_plugins != null) { - try { - collector.addExtraTag("plugin", "rpc"); - for(RpcPlugin rpc: rpc_plugins) { - rpc.collectStats(collector); - } - } finally { - collector.clearExtraTag("plugin"); - } - } } /** Returns a latency histogram for Put RPCs used to store data points. */ @@ -811,14 +772,6 @@ public Object call(ArrayList compactions) throws Exception { deferreds.add(rt_publisher.shutdown()); } - if (rpc_plugins != null && !rpc_plugins.isEmpty()) { - for (final RpcPlugin rpc : rpc_plugins) { - LOG.info("Shutting down RPC plugin: " + - rpc.getClass().getCanonicalName()); - deferreds.add(rpc.shutdown()); - } - } - // wait for plugins to shutdown before we close the client return deferreds.size() > 0 ? Deferred.group(deferreds).addCallbacks(new HClientShutdown(), diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index cf269ce12e..2923b8a23c 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -18,17 +18,17 @@ import java.net.InetSocketAddress; import java.util.concurrent.Executors; +import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.socket.ServerSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.jboss.netty.bootstrap.ServerBootstrap; -import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; - import net.opentsdb.BuildData; import net.opentsdb.core.TSDB; import net.opentsdb.tsd.PipelineFactory; +import net.opentsdb.tsd.RpcManager; import net.opentsdb.utils.Config; /** @@ -52,7 +52,9 @@ static void usage(final ArgP argp, final String errmsg, final int retval) { private static final boolean DONT_CREATE = false; private static final boolean CREATE_IF_NEEDED = true; private static final boolean MUST_BE_WRITEABLE = true; - + + private static TSDB tsdb = null; + public static void main(String[] args) throws IOException { Logger log = LoggerFactory.getLogger(TSDMain.class); log.info("Starting."); @@ -138,18 +140,21 @@ public static void main(String[] args) throws IOException { Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); } - TSDB tsdb = null; try { tsdb = new TSDB(config); tsdb.initializePlugins(true); // Make sure we don't even start if we can't find our tables. tsdb.checkNecessaryTablesExist().joinUninterruptibly(); - - registerShutdownHook(tsdb); + + registerShutdownHook(); final ServerBootstrap server = new ServerBootstrap(factory); + + // This manager is capable of lazy init, but we force an init + // here to fail fast. + final RpcManager manager = RpcManager.instance(tsdb); - server.setPipelineFactory(new PipelineFactory(tsdb)); + server.setPipelineFactory(new PipelineFactory(tsdb, manager)); if (config.hasProperty("tsd.network.backlog")) { server.setOption("backlog", config.getInt("tsd.network.backlog")); } @@ -184,14 +189,21 @@ public static void main(String[] args) throws IOException { // The server is now running in separate threads, we can exit main. } - private static void registerShutdownHook(final TSDB tsdb) { + private static void registerShutdownHook() { final class TSDBShutdown extends Thread { public TSDBShutdown() { super("TSDBShutdown"); } public void run() { try { - tsdb.shutdown().join(); + if (RpcManager.isInitialized()) { + // Check that its actually been initialized. We don't want to + // create a new instance only to shutdown! + RpcManager.instance(tsdb).shutdown().join(); + } + if (tsdb != null) { + tsdb.shutdown().join(); + } } catch (Exception e) { LoggerFactory.getLogger(TSDBShutdown.class) .error("Uncaught exception during shutdown", e); diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java new file mode 100644 index 0000000000..27fcace893 --- /dev/null +++ b/src/tsd/AbstractHttpQuery.java @@ -0,0 +1,404 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.tsd; + +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Objects; +import com.stumbleupon.async.Deferred; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.jboss.netty.handler.codec.http.QueryStringDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.opentsdb.core.TSDB; + +/** + * Abstract base class for HTTP queries. + * + * @since 2.2 + */ +public abstract class AbstractHttpQuery { + private static final Logger LOG = LoggerFactory.getLogger(AbstractHttpQuery.class); + + /** When the query was started (useful for timing). */ + private final long start_time = System.nanoTime(); + + /** The request in this HTTP query. */ + private final HttpRequest request; + + /** The channel on which the request was received. */ + private final Channel chan; + + /** Shortcut to the request method */ + private final HttpMethod method; + + /** Parsed query string (lazily built on first access). */ + private Map> querystring; + + /** Deferred result of this query, to allow asynchronous processing. + * (Optional.) */ + protected final Deferred deferred = new Deferred(); + + /** The response object we'll fill with data */ + private final DefaultHttpResponse response = + new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + + /** The {@code TSDB} instance we belong to */ + protected final TSDB tsdb; + + /** + * Set up required internal state. For subclasses. + * + * @param request the incoming HTTP request + * @param chan the {@link Channel} the request was received on + */ + protected AbstractHttpQuery(final TSDB tsdb, final HttpRequest request, final Channel chan) { + this.tsdb = tsdb; + this.request = request; + this.chan = chan; + this.method = request.getMethod(); + } + + /** + * Returns the underlying Netty {@link HttpRequest} of this query. + */ + public HttpRequest request() { + return request; + } + + /** Returns the HTTP method/verb for the request */ + public HttpMethod method() { + return this.method; + } + + /** Returns the response object, allowing serializers to set headers */ + public DefaultHttpResponse response() { + return this.response; + } + + /** + * Returns the underlying Netty {@link Channel} of this query. + */ + public Channel channel() { + return chan; + } + + /** Return the time in nanoseconds that this query object was + * created. + */ + public long startTimeNanos() { + return start_time; + } + + /** Returns how many ms have elapsed since this query was created. */ + public int processingTimeMillis() { + return (int) ((System.nanoTime() - start_time) / 1000000); + } + + /** + * Returns the query string parameters passed in the URI. + */ + public Map> getQueryString() { + if (querystring == null) { + try { + querystring = new QueryStringDecoder(request.getUri()).getParameters(); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Bad query string: " + e.getMessage()); + } + } + return querystring; + } + + /** + * Returns the value of the given query string parameter. + *

+ * If this parameter occurs multiple times in the URL, only the last value + * is returned and others are silently ignored. + * @param paramname Name of the query string parameter to get. + * @return The value of the parameter or {@code null} if this parameter + * wasn't passed in the URI. + */ + public String getQueryStringParam(final String paramname) { + final List params = getQueryString().get(paramname); + return params == null ? null : params.get(params.size() - 1); + } + + /** + * Returns the non-empty value of the given required query string parameter. + *

+ * If this parameter occurs multiple times in the URL, only the last value + * is returned and others are silently ignored. + * @param paramname Name of the query string parameter to get. + * @return The value of the parameter. + * @throws BadRequestException if this query string parameter wasn't passed + * or if its last occurrence had an empty value ({@code &a=}). + */ + public String getRequiredQueryStringParam(final String paramname) + throws BadRequestException { + final String value = getQueryStringParam(paramname); + if (value == null || value.isEmpty()) { + throw BadRequestException.missingParameter(paramname); + } + return value; + } + + /** + * Returns whether or not the given query string parameter was passed. + * @param paramname Name of the query string parameter to get. + * @return {@code true} if the parameter + */ + public boolean hasQueryStringParam(final String paramname) { + return getQueryString().get(paramname) != null; + } + + /** + * Returns all the values of the given query string parameter. + *

+ * In case this parameter occurs multiple times in the URL, this method is + * useful to get all the values. + * @param paramname Name of the query string parameter to get. + * @return The values of the parameter or {@code null} if this parameter + * wasn't passed in the URI. + */ + public List getQueryStringParams(final String paramname) { + return getQueryString().get(paramname); + } + + + /** + * Returns only the path component of the URI as a string + * This call strips the protocol, host, port and query string parameters + * leaving only the path e.g. "/path/starts/here" + *

+ * Note that for slightly quicker performance you can call request().getUri() + * to get the full path as a string but you'll have to strip query string + * parameters manually. + * @return The path component of the URI + * @throws NullPointerException if the URI is null + */ + public String getQueryPath() { + return new QueryStringDecoder(request.getUri()).getPath(); + } + + /** + * Returns the path component of the URI as an array of strings, split on the + * forward slash + * Similar to the {@link #getQueryPath} call, this returns only the path + * without the protocol, host, port or query string params. E.g. + * "/path/starts/here" will return an array of {"path", "starts", "here"} + *

+ * Note that for maximum speed you may want to parse the query path manually. + * @return An array with 1 or more components, note the first item may be + * an empty string. + * @throws BadRequestException if the URI is empty or does not start with a + * slash + * @throws NullPointerException if the URI is null + */ + public String[] explodePath() { + final String path = getQueryPath(); + if (path.isEmpty()) { + throw new BadRequestException("Query path is empty"); + } + if (path.charAt(0) != '/') { + throw new BadRequestException("Query path doesn't start with a slash"); + } + // split may be a tad slower than other methods, but since the URIs are + // usually pretty short and not every request will make this call, we + // probably don't need any premature optimization + return path.substring(1).split("/"); + } + + /** + * Parses the query string to determine the base route for handing a query + * off to an RPC handler. + * @return the base route + * @throws BadRequestException if some necessary part of the query cannot + * be parsed. + */ + public abstract String getQueryBaseRoute(); + + /** + * Attempts to parse the character set from the request header. If not set + * defaults to UTF-8 + * @return A Charset object + * @throws UnsupportedCharsetException if the parsed character set is invalid + */ + public Charset getCharset() { + // RFC2616 3.7 + for (String type : this.request.headers().getAll("Content-Type")) { + int idx = type.toUpperCase().indexOf("CHARSET="); + if (idx > 1) { + String charset = type.substring(idx+8); + return Charset.forName(charset); + } + } + return Charset.forName("UTF-8"); + } + + /** @return True if the request has content, false if not. */ + public boolean hasContent() { + return this.request.getContent() != null && + this.request.getContent().readable(); + } + + /** + * Decodes the request content to a string using the appropriate character set + * @return Decoded content or an empty string if the request did not include + * content + * @throws UnsupportedCharsetException if the parsed character set is invalid + */ + public String getContent() { + return this.request.getContent().toString(this.getCharset()); + } + + /** + * Method to call after writing the HTTP response to the wire. The default + * is to simply log the request info. Can be overridden by subclasses. + */ + public void done() { + final int processing_time = processingTimeMillis(); + logInfo("HTTP " + request.getUri() + " done in " + processing_time + "ms"); + } + + /** + * Sends 500/Internal Server Error to the client. + * @param cause The unexpected exception that caused this error. + */ + public void internalError(final Exception cause) { + logError("Internal Server Error on " + request().getUri(), cause); + sendStatusOnly(HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Sends 400/Bad Request status to the client. + * @param exception The exception that was thrown + */ + public void badRequest(final BadRequestException exception) { + logWarn("Bad Request on " + request().getUri() + ": " + exception.getMessage()); + sendStatusOnly(HttpResponseStatus.BAD_REQUEST); + } + + /** + * Sends 404/Not Found to the client. + */ + public void notFound() { + logWarn("Not Found: " + request().getUri()); + sendStatusOnly(HttpResponseStatus.NOT_FOUND); + } + + /** + * Send just the status code without a body, used for 204 or 304 + * @param status The response code to reply with + */ + public void sendStatusOnly(final HttpResponseStatus status) { + if (!chan.isConnected()) { + done(); + return; + } + + response.setStatus(status); + final boolean keepalive = HttpHeaders.isKeepAlive(request); + if (keepalive) { + HttpHeaders.setContentLength(response, 0); + } + final ChannelFuture future = chan.write(response); + if (!keepalive) { + future.addListener(ChannelFutureListener.CLOSE); + } + done(); + } + + /** + * Sends an HTTP reply to the client. + * @param status The status of the request (e.g. 200 OK or 404 Not Found). + * @param buf The content of the reply to send. + */ + public void sendBuffer(final HttpResponseStatus status, + final ChannelBuffer buf, + final String contentType) { + if (!chan.isConnected()) { + done(); + return; + } + response.headers().set(HttpHeaders.Names.CONTENT_TYPE, contentType); + + // TODO(tsuna): Server, X-Backend, etc. headers. + // only reset the status if we have the default status, otherwise the user + // already set it + response.setStatus(status); + response.setContent(buf); + final boolean keepalive = HttpHeaders.isKeepAlive(request); + if (keepalive) { + HttpHeaders.setContentLength(response, buf.readableBytes()); + } + final ChannelFuture future = chan.write(response); + if (!keepalive) { + future.addListener(ChannelFutureListener.CLOSE); + } + done(); + } + + /** @return Information about the query */ + public String toString() { + return Objects.toStringHelper(this) + .add("start_time", start_time) + .add("request", request) + .add("chan", chan) + .add("querystring", querystring) + .toString(); + } + + // ---------------- // + // Logging helpers. // + // ---------------- // + + /** + * Logger for the query instance. + */ + protected Logger logger() { + return LOG; + } + + protected final void logInfo(final String msg) { + if (logger().isInfoEnabled()) { + logger().info(chan.toString() + ' ' + msg); + } + } + + protected final void logWarn(final String msg) { + if (logger().isWarnEnabled()) { + logger().warn(chan.toString() + ' ' + msg); + } + } + + protected final void logError(final String msg, final Exception e) { + if (logger().isErrorEnabled()) { + logger().error(chan.toString() + ' ' + msg, e); + } + } + +} diff --git a/src/tsd/BadRequestException.java b/src/tsd/BadRequestException.java index b221a3c9da..24975e7338 100644 --- a/src/tsd/BadRequestException.java +++ b/src/tsd/BadRequestException.java @@ -22,7 +22,7 @@ * optional detailed response. The default "message" field is still used for * short error descriptions, typically one sentence long. */ -final class BadRequestException extends RuntimeException { +public final class BadRequestException extends RuntimeException { /** The HTTP status code to return to the user * @since 2.0 */ diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index f1308c5e2f..bea01cb6f2 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -20,20 +20,17 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; -import ch.qos.logback.classic.spi.ThrowableProxy; -import ch.qos.logback.classic.spi.ThrowableProxyUtil; - -import com.stumbleupon.async.Deferred; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.graph.Plot; +import net.opentsdb.stats.Histogram; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.utils.PluginLoader; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; @@ -41,22 +38,18 @@ import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.DefaultFileRegion; -import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; -import org.jboss.netty.handler.codec.http.HttpVersion; -import org.jboss.netty.handler.codec.http.QueryStringDecoder; import org.jboss.netty.util.CharsetUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import net.opentsdb.core.Const; -import net.opentsdb.core.TSDB; -import net.opentsdb.graph.Plot; -import net.opentsdb.stats.Histogram; -import net.opentsdb.stats.StatsCollector; -import net.opentsdb.tsd.HttpSerializer; -import net.opentsdb.utils.PluginLoader; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxyUtil; + +import com.stumbleupon.async.Deferred; /** * Binds together an HTTP request and the channel on which it was received. @@ -64,7 +57,7 @@ * It makes it easier to provide a few utility methods to respond to the * requests. */ -final class HttpQuery { +final class HttpQuery extends AbstractHttpQuery { private static final Logger LOG = LoggerFactory.getLogger(HttpQuery.class); @@ -90,37 +83,12 @@ final class HttpQuery { /** Caches serializer implementation information for user access */ private static ArrayList> serializer_status = null; - /** When the query was started (useful for timing). */ - private final long start_time = System.nanoTime(); - - /** The request in this HTTP query. */ - private final HttpRequest request; - - /** The channel on which the request was received. */ - private final Channel chan; - - /** Shortcut to the request method */ - private final HttpMethod method; - - /** Parsed query string (lazily built on first access). */ - private Map> querystring; - /** API version parsed from the incoming request */ private int api_version = 0; /** The serializer to use for parsing input and responding */ private HttpSerializer serializer = null; - /** Deferred result of this query, to allow asynchronous processing. */ - private final Deferred deferred = new Deferred(); - - /** The response object we'll fill with data */ - private final DefaultHttpResponse response = - new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); - - /** The {@code TSDB} instance we belong to */ - private final TSDB tsdb; - /** Whether or not to show stack traces in the output */ private final boolean show_stack_trace; @@ -130,12 +98,9 @@ final class HttpQuery { * @param chan The channel on which the request was received. */ public HttpQuery(final TSDB tsdb, final HttpRequest request, final Channel chan) { - this.tsdb = tsdb; - this.request = request; - this.chan = chan; + super(tsdb, request, chan); this.show_stack_trace = tsdb.getConfig().getBoolean("tsd.http.show_stack_trace"); - this.method = request.getMethod(); this.serializer = new HttpJsonSerializer(this); } @@ -147,30 +112,6 @@ public static void collectStats(final StatsCollector collector) { collector.record("http.latency", httplatency, "type=all"); } - /** - * Returns the underlying Netty {@link HttpRequest} of this query. - */ - public HttpRequest request() { - return request; - } - - /** Returns the HTTP method/verb for the request */ - public HttpMethod method() { - return this.method; - } - - /** Returns the response object, allowing serializers to set headers */ - public DefaultHttpResponse response() { - return this.response; - } - - /** - * Returns the underlying Netty {@link Channel} of this query. - */ - public Channel channel() { - return chan; - } - /** * Returns the version for an API request. If the request was for a deprecated * API call (such as /q, /suggest, /logs) this value will be 0. If the request @@ -194,131 +135,12 @@ public Deferred getDeferred() { return deferred; } - /** Returns how many ms have elapsed since this query was created. */ - public int processingTimeMillis() { - return (int) ((System.nanoTime() - start_time) / 1000000); - } - /** @return The selected seralizer. Will return null if {@link #setSerializer} * hasn't been called yet @since 2.0 */ public HttpSerializer serializer() { return this.serializer; } - /** - * Returns the query string parameters passed in the URI. - */ - public Map> getQueryString() { - if (querystring == null) { - try { - querystring = new QueryStringDecoder(request.getUri()).getParameters(); - } catch (IllegalArgumentException e) { - throw new BadRequestException("Bad query string: " + e.getMessage()); - } - } - return querystring; - } - - /** - * Returns the value of the given query string parameter. - *

- * If this parameter occurs multiple times in the URL, only the last value - * is returned and others are silently ignored. - * @param paramname Name of the query string parameter to get. - * @return The value of the parameter or {@code null} if this parameter - * wasn't passed in the URI. - */ - public String getQueryStringParam(final String paramname) { - final List params = getQueryString().get(paramname); - return params == null ? null : params.get(params.size() - 1); - } - - /** - * Returns the non-empty value of the given required query string parameter. - *

- * If this parameter occurs multiple times in the URL, only the last value - * is returned and others are silently ignored. - * @param paramname Name of the query string parameter to get. - * @return The value of the parameter. - * @throws BadRequestException if this query string parameter wasn't passed - * or if its last occurrence had an empty value ({@code &a=}). - */ - public String getRequiredQueryStringParam(final String paramname) - throws BadRequestException { - final String value = getQueryStringParam(paramname); - if (value == null || value.isEmpty()) { - throw BadRequestException.missingParameter(paramname); - } - return value; - } - - /** - * Returns whether or not the given query string parameter was passed. - * @param paramname Name of the query string parameter to get. - * @return {@code true} if the parameter - */ - public boolean hasQueryStringParam(final String paramname) { - return getQueryString().get(paramname) != null; - } - - /** - * Returns all the values of the given query string parameter. - *

- * In case this parameter occurs multiple times in the URL, this method is - * useful to get all the values. - * @param paramname Name of the query string parameter to get. - * @return The values of the parameter or {@code null} if this parameter - * wasn't passed in the URI. - */ - public List getQueryStringParams(final String paramname) { - return getQueryString().get(paramname); - } - - /** - * Returns only the path component of the URI as a string - * This call strips the protocol, host, port and query string parameters - * leaving only the path e.g. "/path/starts/here" - *

- * Note that for slightly quicker performance you can call request().getUri() - * to get the full path as a string but you'll have to strip query string - * parameters manually. - * @return The path component of the URI - * @throws NullPointerException if the URI is null - * @since 2.0 - */ - public String getQueryPath() { - return new QueryStringDecoder(request.getUri()).getPath(); - } - - /** - * Returns the path component of the URI as an array of strings, split on the - * forward slash - * Similar to the {@link #getQueryPath} call, this returns only the path - * without the protocol, host, port or query string params. E.g. - * "/path/starts/here" will return an array of {"path", "starts", "here"} - *

- * Note that for maximum speed you may want to parse the query path manually. - * @return An array with 1 or more components, note the first item may be - * an empty string. - * @throws BadRequestException if the URI is empty or does not start with a - * slash - * @throws NullPointerException if the URI is null - * @since 2.0 - */ - public String[] explodePath() { - final String path = this.getQueryPath(); - if (path.isEmpty()) { - throw new BadRequestException("Query path is empty"); - } - if (path.charAt(0) != '/') { - throw new BadRequestException("Query path doesn't start with a slash"); - } - // split may be a tad slower than other methods, but since the URIs are - // usually pretty short and not every request will make this call, we - // probably don't need any premature optimization - return path.substring(1).split("/"); - } - /** * Helper that strips the api and optional version from the URI array since * api calls only care about what comes after. @@ -364,8 +186,6 @@ public String[] explodeAPIPath() { } /** - * Parses the query string to determine the base route for handing a query - * off to an RPC handler. * This method splits the query path component and returns a string suitable * for routing by {@link RpcHandler}. The resulting route is always lower case * and will consist of either an empty string, a deprecated API call or an @@ -383,8 +203,9 @@ public String[] explodeAPIPath() { * max or the version # can't be parsed * @since 2.0 */ + @Override public String getQueryBaseRoute() { - final String[] split = this.explodePath(); + final String[] split = explodePath(); if (split.length < 1) { return ""; } @@ -423,42 +244,6 @@ public String getQueryBaseRoute() { return "api/" + split[2].toLowerCase(); } - /** - * Attempts to parse the character set from the request header. If not set - * defaults to UTF-8 - * @return A Charset object - * @throws UnsupportedCharsetException if the parsed character set is invalid - * @since 2.0 - */ - public Charset getCharset() { - // RFC2616 3.7 - for (String type : this.request.headers().getAll("Content-Type")) { - int idx = type.toUpperCase().indexOf("CHARSET="); - if (idx > 1) { - String charset = type.substring(idx+8); - return Charset.forName(charset); - } - } - return Charset.forName("UTF-8"); - } - - /** @return True if the request has content, false if not @since 2.0 */ - public boolean hasContent() { - return this.request.getContent() != null && - this.request.getContent().readable(); - } - - /** - * Decodes the request content to a string using the appropriate character set - * @return Decoded content or an empty string if the request did not include - * content - * @throws UnsupportedCharsetException if the parsed character set is invalid - * @since 2.0 - */ - public String getContent() { - return this.request.getContent().toString(this.getCharset()); - } - /** * Determines the requested HttpMethod via VERB and QS override. * If the request is a {@code GET} and the user provides a valid override @@ -538,7 +323,7 @@ public void setSerializer() throws InvocationTargetException, // attempt to parse the Content-Type string. We only want the first part, // not the character set. And if the CT is missing, we'll use the default // serializer - String content_type = this.request.headers().get("Content-Type"); + String content_type = request().headers().get("Content-Type"); if (content_type == null || content_type.isEmpty()) { return; } @@ -560,8 +345,9 @@ public void setSerializer() throws InvocationTargetException, * API calls * @param cause The unexpected exception that caused this error. */ + @Override public void internalError(final Exception cause) { - logError("Internal Server Error on " + request.getUri(), cause); + logError("Internal Server Error on " + request().getUri(), cause); if (this.api_version > 0) { // always default to the latest version of the error formatter since we @@ -611,12 +397,13 @@ public void badRequest(final String explain) { } /** - * Sends an error message to the client with the proeper status code and - * optional details stored in the exception + * Sends an error message to the client. Handles responses from + * deprecated API calls. * @param exception The exception that was thrown */ + @Override public void badRequest(final BadRequestException exception) { - logWarn("Bad Request on " + request.getUri() + ": " + exception.getMessage()); + logWarn("Bad Request on " + request().getUri() + ": " + exception.getMessage()); if (this.api_version > 0) { // always default to the latest version of the error formatter since we // need to return something @@ -649,9 +436,13 @@ public void badRequest(final BadRequestException exception) { } } - /** Sends a 404 error page to the client. */ + /** + * Sends a 404 error page to the client. + * Handles responses from deprecated API calls + */ + @Override public void notFound() { - logWarn("Not Found: " + request.getUri()); + logWarn("Not Found: " + request().getUri()); if (this.api_version > 0) { // always default to the latest version of the error formatter since we // need to return something @@ -675,7 +466,7 @@ public void notFound() { /** Redirects the client's browser to the given location. */ public void redirect(final String location) { // set the header AND a meta refresh just in case - response.headers().set("Location", location); + response().headers().set("Location", location); sendReply(HttpResponseStatus.OK, new StringBuilder( " 0) { - response.headers().set(HttpHeaders.Names.AGE, + response().headers().set(HttpHeaders.Names.AGE, (System.currentTimeMillis() - mtime) / 1000); } else { logWarn("Found a file with mtime=" + mtime + ": " + path); } - response.headers().set(HttpHeaders.Names.CACHE_CONTROL, + response().headers().set(HttpHeaders.Names.CACHE_CONTROL, "max-age=" + max_age); - HttpHeaders.setContentLength(response, length); - chan.write(response); + HttpHeaders.setContentLength(response(), length); + channel().write(response()); } final DefaultFileRegion region = new DefaultFileRegion(file.getChannel(), 0, length); - final ChannelFuture future = chan.write(region); + final ChannelFuture future = channel().write(region); future.addListener(new ChannelFutureListener() { public void operationComplete(final ChannelFuture future) { region.releaseExternalResources(); done(); } }); - if (!HttpHeaders.isKeepAlive(request)) { + if (!HttpHeaders.isKeepAlive(request())) { future.addListener(ChannelFutureListener.CLOSE); } } @@ -961,10 +731,11 @@ public void operationComplete(final ChannelFuture future) { /** * Method to call after writing the HTTP response to the wire. */ - private void done() { + @Override + public void done() { final int processing_time = processingTimeMillis(); httplatency.add(processing_time); - logInfo("HTTP " + request.getUri() + " done in " + processing_time + "ms"); + logInfo("HTTP " + request().getUri() + " done in " + processing_time + "ms"); deferred.callback(null); } @@ -975,28 +746,9 @@ private void done() { */ private void sendBuffer(final HttpResponseStatus status, final ChannelBuffer buf) { - if (!chan.isConnected()) { - done(); - return; - } - response.headers().set(HttpHeaders.Names.CONTENT_TYPE, - (api_version < 1 ? guessMimeType(buf) : - serializer.responseContentType())); - - // TODO(tsuna): Server, X-Backend, etc. headers. - // only reset the status if we have the default status, otherwise the user - // already set it - response.setStatus(status); - response.setContent(buf); - final boolean keepalive = HttpHeaders.isKeepAlive(request); - if (keepalive) { - HttpHeaders.setContentLength(response, buf.readableBytes()); - } - final ChannelFuture future = chan.write(response); - if (!keepalive) { - future.addListener(ChannelFutureListener.CLOSE); - } - done(); + final String contentType = (api_version < 1 ? guessMimeType(buf) : + serializer.responseContentType()); + sendBuffer(status, buf, contentType); } /** @@ -1004,7 +756,7 @@ private void sendBuffer(final HttpResponseStatus status, * @param buf The content of the reply to send. */ private String guessMimeType(final ChannelBuffer buf) { - final String mimetype = guessMimeTypeFromUri(request.getUri()); + final String mimetype = guessMimeTypeFromUri(request().getUri()); return mimetype == null ? guessMimeTypeFromContents(buf) : mimetype; } @@ -1243,33 +995,12 @@ public static StringBuilder makePage(final String htmlheader, .append(PAGE_FOOTER); return buf; } - - /** @return Information about the query */ - public String toString() { - return "HttpQuery" - + "(start_time=" + start_time - + ", request=" + request - + ", chan=" + chan - + ", querystring=" + querystring - + ')'; + + @Override + protected Logger logger() { + return LOG; } - - // ---------------- // - // Logging helpers. // - // ---------------- // - - private void logInfo(final String msg) { - LOG.info(chan.toString() + ' ' + msg); - } - - private void logWarn(final String msg) { - LOG.warn(chan.toString() + ' ' + msg); - } - - private void logError(final String msg, final Exception e) { - LOG.error(chan.toString() + ' ' + msg, e); - } - + // -------------------------------------------- // // Boilerplate (shamelessly stolen from Google) // // -------------------------------------------- // diff --git a/src/tsd/HttpRpc.java b/src/tsd/HttpRpc.java index fc1e3f0bcb..40dec97cc4 100644 --- a/src/tsd/HttpRpc.java +++ b/src/tsd/HttpRpc.java @@ -16,7 +16,9 @@ import net.opentsdb.core.TSDB; -/** Base interface for all HTTP query handlers. */ +/** + * Base interface for all built-in HTTP query handlers. + */ interface HttpRpc { /** diff --git a/src/tsd/HttpRpcPlugin.java b/src/tsd/HttpRpcPlugin.java new file mode 100644 index 0000000000..17c41dc195 --- /dev/null +++ b/src/tsd/HttpRpcPlugin.java @@ -0,0 +1,113 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.tsd; + +import java.io.IOException; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; + +/** + * A plugin that runs along side TSD's built-in HTTP endpoints (like the + * /api endpoints). There can be multiple implementations of + * such plugins per TSD. These plugins run on the same Netty server as + * built-in HTTP endpoints and thus are available on the same port as those + * endpoints. However, these plugins are mounted beneath a special base + * path called /plugin. + * + *

Notes on multi-threaded behavior: + *

    + *
  • Plugins are created and initialized once per instance + * of the TSD. Therefore, these plugins are effectively singletons. + *
  • Plugins will be executed from multiple threads so the {@link #execute} + * and {@link collectStats} methods must be thread safe + * with respect to the plugin's internal state and external resources. + *
+ * @since 2.2 + */ +public abstract class HttpRpcPlugin { + /** + * Called by TSDB to initialize the plugin. This is called once + * (and from a single thread) at the time the plugin in loaded. + * + *

Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception + */ + public abstract void initialize(TSDB tsdb); + + /** + * Called to gracefully shutdown the plugin. This is called once + * (and from a single thread) at the time the owning TSD is shutting down. + * + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. "2.0.1". The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * + *

Note: Must be thread-safe. + * + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(StatsCollector collector); + + /** + * The (web) path this plugin should be available at. This value + * should start with a /. However, it + * must not contain the system's plugin base path or the + * plugin will fail to load. + * + *

Here are some examples where + * path --(is available at)--> server path + *

    + *
  • /myAwesomePlugin --> /plugin/myAwesomePlugin + *
  • /myOtherPlugin/operation --> /plugin/myOtherPlugin/operation + *
+ * + * @return a slash separated path + */ + public abstract String getPath(); + + /** + * Executes the plugin for the given query received on the path derived from + * {@link #getPath()}. This method will be called by multiple threads + * simultaneously and must be thread-safe. + * + * @param tsdb the owning TSDB instance. + * @param query the parsed query + * @throws IOException + */ + public abstract void execute(TSDB tsdb, HttpRpcPluginQuery query) throws IOException; + +} diff --git a/src/tsd/HttpRpcPluginQuery.java b/src/tsd/HttpRpcPluginQuery.java new file mode 100644 index 0000000000..15e1cc928e --- /dev/null +++ b/src/tsd/HttpRpcPluginQuery.java @@ -0,0 +1,53 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.tsd; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpRequest; + +import net.opentsdb.core.TSDB; + +/** + * Query class for {@link HttpRpcPlugin}s. Binds together a request, its + * owning channel, and reponse helpers. + * + * @since 2.2 + */ +public final class HttpRpcPluginQuery extends AbstractHttpQuery { + public HttpRpcPluginQuery(final TSDB tsdb, final HttpRequest request, final Channel chan) { + super(tsdb, request, chan); + } + + /** + * Return the base route with no plugin prefix in it. This is matched with + * values returned by {@link HttpRpcPlugin#getPath()}. + * @return the base route path (no query parameters, etc.) + */ + @Override + public String getQueryBaseRoute() { + final String[] parts = explodePath(); + if (parts.length < 2) { // Must be at least something like: /plugin/blah + throw new BadRequestException("Invalid plugin request path: " + getQueryPath()); + } + // Lop off the first element (which is the "plugin" base path). + // The remaining elements are the base route. + final StringBuilder joined = new StringBuilder(); + for (int i=1; i. package net.opentsdb.tsd; -import java.io.IOException; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.concurrent.atomic.AtomicLong; +import com.google.common.base.Strings; import com.google.common.net.HttpHeaders; -import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; import org.jboss.netty.handler.timeout.IdleState; import org.jboss.netty.handler.timeout.IdleStateAwareChannelUpstreamHandler; import org.jboss.netty.handler.timeout.IdleStateEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import net.opentsdb.BuildData; -import net.opentsdb.core.Aggregators; import net.opentsdb.core.TSDB; import net.opentsdb.stats.StatsCollector; -import net.opentsdb.utils.JSON; /** - * Stateless handler for RPCs (telnet-style or HTTP). + * Stateless handler for all RPCs: telnet-style, built-in or plugin + * HTTP. */ final class RpcHandler extends IdleStateAwareChannelUpstreamHandler { private static final Logger LOG = LoggerFactory.getLogger(RpcHandler.class); - + private static final AtomicLong telnet_rpcs_received = new AtomicLong(); private static final AtomicLong http_rpcs_received = new AtomicLong(); + private static final AtomicLong http_plugin_rpcs_received = new AtomicLong(); private static final AtomicLong exceptions_caught = new AtomicLong(); - /** Commands we can serve on the simple, telnet-style RPC interface. */ - private final HashMap telnet_commands; /** RPC executed when there's an unknown telnet-style command. */ private final TelnetRpc unknown_cmd = new Unknown(); - /** Commands we serve on the HTTP interface. */ - private final HashMap http_commands; /** List of domains to allow access to HTTP. By default this will be empty and * all CORS headers will be ignored. */ private final HashSet cors_domains; /** List of headers allowed for access to HTTP. By default this will contain a * set of known-to-work headers */ private final String cors_headers; + /** RPC plugins. Contains the handlers we dispatch requests to. */ + private final RpcManager rpc_manager; /** The TSDB to use. */ private final TSDB tsdb; - + /** - * Constructor that loads the CORS domain list and configures the route maps - * for telnet and HTTP requests + * 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 */ public RpcHandler(final TSDB tsdb) { + this(tsdb, RpcManager.instance(tsdb)); + } + + /** + * Constructor that loads the CORS domain list and prepares for handling + * requests. + * @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 + */ + public RpcHandler(final TSDB tsdb, final RpcManager manager) { this.tsdb = tsdb; + this.rpc_manager = manager; final String cors = tsdb.getConfig().getString("tsd.http.request.cors_domains"); final String mode = tsdb.getConfig().getString("tsd.mode"); @@ -109,66 +121,6 @@ public RpcHandler(final TSDB tsdb) { } else { LOG.info("Loaded CORS headers (" + cors_headers + ")"); } - - telnet_commands = new HashMap(); - http_commands = new HashMap(); - if (mode.equals("rw") || mode.equals("wo")) { - final PutDataPointRpc put = new PutDataPointRpc(); - telnet_commands.put("put", put); - http_commands.put("api/put", put); - } - - if (mode.equals("rw") || mode.equals("ro")) { - http_commands.put("", new HomePage()); - final StaticFileRpc staticfile = new StaticFileRpc(); - http_commands.put("favicon.ico", staticfile); - http_commands.put("s", staticfile); - - final StatsRpc stats = new StatsRpc(); - telnet_commands.put("stats", stats); - http_commands.put("stats", stats); - http_commands.put("api/stats", stats); - - final DropCaches dropcaches = new DropCaches(); - telnet_commands.put("dropcaches", dropcaches); - http_commands.put("dropcaches", dropcaches); - http_commands.put("api/dropcaches", dropcaches); - - final ListAggregators aggregators = new ListAggregators(); - http_commands.put("aggregators", aggregators); - http_commands.put("api/aggregators", aggregators); - - final SuggestRpc suggest_rpc = new SuggestRpc(); - http_commands.put("suggest", suggest_rpc); - http_commands.put("api/suggest", suggest_rpc); - - http_commands.put("logs", new LogsRpc()); - http_commands.put("q", new GraphHandler()); - http_commands.put("api/serializers", new Serializers()); - http_commands.put("api/uid", new UniqueIdRpc()); - http_commands.put("api/query", new QueryRpc()); - http_commands.put("api/tree", new TreeRpc()); - final AnnotationRpc annotation_rpc = new AnnotationRpc(); - http_commands.put("api/annotation", annotation_rpc); - http_commands.put("api/annotations", annotation_rpc); - http_commands.put("api/search", new SearchRpc()); - http_commands.put("api/config", new ShowConfig()); - } - - if (tsdb.getConfig().getString("tsd.no_diediedie").equals("false")) { - final DieDieDie diediedie = new DieDieDie(); - telnet_commands.put("diediedie", diediedie); - http_commands.put("diediedie", diediedie); - } - { - final Version version = new Version(); - telnet_commands.put("version", version); - http_commands.put("version", version); - http_commands.put("api/version", version); - } - - telnet_commands.put("exit", new Exit()); - telnet_commands.put("help", new Help()); } @Override @@ -195,14 +147,14 @@ public void messageReceived(final ChannelHandlerContext ctx, exceptions_caught.incrementAndGet(); } } - + /** * Finds the right handler for a telnet-style RPC and executes it. * @param chan The channel on which the RPC was received. * @param command The split telnet-style command. */ private void handleTelnetRpc(final Channel chan, final String[] command) { - TelnetRpc rpc = telnet_commands.get(command[0]); + TelnetRpc rpc = rpc_manager.lookupTelnetRpc(command[0]); if (rpc == null) { rpc = unknown_cmd; } @@ -211,78 +163,159 @@ private void handleTelnetRpc(final Channel chan, final String[] command) { } /** - * Finds the right handler for an HTTP query and executes it. - * Also handles simple and pre-flight CORS requests if configured, rejecting - * requests that do not match a domain in the list. + * Using the request URI, creates a query instance capable of handling + * the given request. + * @param tsdb the TSDB instance we are running within + * @param request the incoming HTTP request + * @param chan the {@link Channel} the request came in on. + * @return a subclass of {@link AbstractHttpQuery} + * @throws BadRequestException if the request is invalid in a way that + * can be detected early, here. + */ + private AbstractHttpQuery createQueryInstance(final TSDB tsdb, + final HttpRequest request, + final Channel chan) + throws BadRequestException { + final String uri = request.getUri(); + if (Strings.isNullOrEmpty(uri)) { + throw new BadRequestException("Request URI is empty"); + } else if (uri.charAt(0) != '/') { + throw new BadRequestException("Request URI doesn't start with a slash"); + } else if (rpc_manager.isHttpRpcPluginPath(uri)) { + http_plugin_rpcs_received.incrementAndGet(); + return new HttpRpcPluginQuery(tsdb, request, chan); + } else { + http_rpcs_received.incrementAndGet(); + HttpQuery builtinQuery = new HttpQuery(tsdb, request, chan); + return builtinQuery; + } + } + + /** + * Helper method to apply CORS configuration to a request, either a built-in + * RPC or a user plugin. + * @return true if a status reply was sent (in the the case of + * certain HTTP methods); false otherwise. + */ + private boolean applyCorsConfig(final HttpRequest req, final AbstractHttpQuery query) + throws BadRequestException { + final String domain = req.headers().get("Origin"); + + // catch CORS requests and add the header or refuse them if the domain + // list has been configured + if (query.method() == HttpMethod.OPTIONS || + (cors_domains != null && domain != null && !domain.isEmpty())) { + if (cors_domains == null || domain == null || domain.isEmpty()) { + throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, + "Method not allowed", "The HTTP method [" + + query.method().getName() + "] is not permitted"); + } + + if (cors_domains.contains("*") || + cors_domains.contains(domain.toUpperCase())) { + + // when a domain has matched successfully, we need to add the header + query.response().headers().add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, + domain); + query.response().headers().add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, + "GET, POST, PUT, DELETE"); + query.response().headers().add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, + cors_headers); + + // if the method requested was for OPTIONS then we'll return an OK + // here and no further processing is needed. + if (query.method() == HttpMethod.OPTIONS) { + query.sendStatusOnly(HttpResponseStatus.OK); + return true; + } + } else { + // You'd think that they would want the server to return a 403 if + // the Origin wasn't in the CORS domain list, but they want a 200 + // without the allow origin header. We'll return an error in the + // body though. + throw new BadRequestException(HttpResponseStatus.OK, + "CORS domain not allowed", "The domain [" + domain + + "] is not permitted access"); + } + } + return false; + } + + /** + * Finds the right handler for an HTTP query (either built-in or user plugin) + * and executes it. Also handles simple and pre-flight CORS requests if + * configured, rejecting requests that do not match a domain in the list. * @param chan The channel on which the query was received. * @param req The parsed HTTP request. */ private void handleHttpQuery(final TSDB tsdb, final Channel chan, final HttpRequest req) { - http_rpcs_received.incrementAndGet(); - final HttpQuery query = new HttpQuery(tsdb, req, chan); - if (!tsdb.getConfig().enable_chunked_requests() && req.isChunked()) { - logError(query, "Received an unsupported chunked request: " - + query.request()); - query.badRequest("Chunked request not supported."); - return; - } + AbstractHttpQuery abstractQuery = null; try { - try { - final String route = query.getQueryBaseRoute(); - query.setSerializer(); - - final String domain = req.headers().get("Origin"); - - // catch CORS requests and add the header or refuse them if the domain - // list has been configured - if (query.method() == HttpMethod.OPTIONS || - (cors_domains != null && domain != null && !domain.isEmpty())) { - if (cors_domains == null || domain == null || domain.isEmpty()) { - throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, - "Method not allowed", "The HTTP method [" + - query.method().getName() + "] is not permitted"); - } - - if (cors_domains.contains("*") || - cors_domains.contains(domain.toUpperCase())) { - - // when a domain has matched successfully, we need to add the header - query.response().headers().add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, - domain); - query.response().headers().add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, - "GET, POST, PUT, DELETE"); - query.response().headers().add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, - cors_headers); - - // if the method requested was for OPTIONS then we'll return an OK - // here and no further processing is needed. - if (query.method() == HttpMethod.OPTIONS) { - query.sendStatusOnly(HttpResponseStatus.OK); - return; - } - } else { - // You'd think that they would want the server to return a 403 if - // the Origin wasn't in the CORS domain list, but they want a 200 - // without the allow origin header. We'll return an error in the - // body though. - throw new BadRequestException(HttpResponseStatus.OK, - "CORS domain not allowed", "The domain [" + domain + - "] is not permitted access"); - } + abstractQuery = createQueryInstance(tsdb, req, chan); + if (!tsdb.getConfig().enable_chunked_requests() && req.isChunked()) { + logError(abstractQuery, "Received an unsupported chunked request: " + + abstractQuery.request()); + abstractQuery.badRequest(new BadRequestException("Chunked request not supported.")); + return; + } + // NOTE: Some methods in HttpQuery have side-effects (getQueryBaseRoute and + // setSerializer for instance) so invocation order is important here. + final String route = abstractQuery.getQueryBaseRoute(); + if (abstractQuery.getClass().isAssignableFrom(HttpRpcPluginQuery.class)) { + if (applyCorsConfig(req, abstractQuery)) { + return; + } + final HttpRpcPluginQuery pluginQuery = (HttpRpcPluginQuery) abstractQuery; + final HttpRpcPlugin rpc = rpc_manager.lookupHttpRpcPlugin(route); + if (rpc != null) { + rpc.execute(tsdb, pluginQuery); + } else { + pluginQuery.notFound(); + } + } else if (abstractQuery.getClass().isAssignableFrom(HttpQuery.class)) { + final HttpQuery builtinQuery = (HttpQuery) abstractQuery; + builtinQuery.setSerializer(); + if (applyCorsConfig(req, abstractQuery)) { + return; } - - final HttpRpc rpc = http_commands.get(route); + final HttpRpc rpc = rpc_manager.lookupHttpRpc(route); if (rpc != null) { - rpc.execute(tsdb, query); + rpc.execute(tsdb, builtinQuery); } else { - query.notFound(); + builtinQuery.notFound(); } - } catch (BadRequestException ex) { - query.badRequest(ex); + } else { + throw new IllegalStateException("Unknown instance of AbstractHttpQuery: " + + abstractQuery.getClass().getName()); + } + } catch (BadRequestException ex) { + if (abstractQuery == null) { + LOG.warn("{} Unable to create query for {}. Reason: {}", chan, req, ex); + sendStatusAndClose(chan, HttpResponseStatus.BAD_REQUEST); + } else { + abstractQuery.badRequest(ex); } } catch (Exception ex) { - query.internalError(ex); exceptions_caught.incrementAndGet(); + if (abstractQuery == null) { + LOG.warn("{} Unexpected error handling HTTP request {}. Reason: {} ", chan, req, ex); + sendStatusAndClose(chan, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } else { + abstractQuery.internalError(ex); + } + } + } + + /** + * Helper method for sending a status-only HTTP response. This is used in cases where + * {@link #createQueryInstance(TSDB, HttpRequest, Channel)} failed to determine a query + * and we still want to return an error status to the client. + */ + private void sendStatusAndClose(final Channel chan, final HttpResponseStatus status) { + if (chan.isConnected()) { + final DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); + final ChannelFuture future = chan.write(response); + future.addListener(ChannelFutureListener.CLOSE); } } @@ -293,184 +326,13 @@ private void handleHttpQuery(final TSDB tsdb, final Channel chan, final HttpRequ public static void collectStats(final StatsCollector collector) { collector.record("rpc.received", telnet_rpcs_received, "type=telnet"); collector.record("rpc.received", http_rpcs_received, "type=http"); + collector.record("rpc.received", http_plugin_rpcs_received, "type=http_plugin"); collector.record("rpc.exceptions", exceptions_caught); HttpQuery.collectStats(collector); GraphHandler.collectStats(collector); PutDataPointRpc.collectStats(collector); } - // ---------------------------- // - // Individual command handlers. // - // ---------------------------- // - - /** The "diediedie" command and "/diediedie" endpoint. */ - private final class DieDieDie implements TelnetRpc, HttpRpc { - public Deferred execute(final TSDB tsdb, final Channel chan, - final String[] cmd) { - logWarn(chan, "shutdown requested"); - chan.write("Cleaning up and exiting now.\n"); - return doShutdown(tsdb, chan); - } - - public void execute(final TSDB tsdb, final HttpQuery query) { - logWarn(query, "shutdown requested"); - query.sendReply(HttpQuery.makePage("TSD Exiting", "You killed me", - "Cleaning up and exiting now.")); - doShutdown(tsdb, query.channel()); - } - - private Deferred doShutdown(final TSDB tsdb, final Channel chan) { - ((GraphHandler) http_commands.get("q")).shutdown(); - ConnectionManager.closeAllConnections(); - // Netty gets stuck in an infinite loop if we shut it down from within a - // NIO thread. So do this from a newly created thread. - final class ShutdownNetty extends Thread { - ShutdownNetty() { - super("ShutdownNetty"); - } - public void run() { - chan.getFactory().releaseExternalResources(); - } - } - new ShutdownNetty().start(); // Stop accepting new connections. - - // Log any error that might occur during shutdown. - final class ShutdownTSDB implements Callback { - public Exception call(final Exception arg) { - LOG.error("Unexpected exception while shutting down", arg); - return arg; - } - public String toString() { - return "shutdown callback"; - } - } - return tsdb.shutdown().addErrback(new ShutdownTSDB()); - } - } - - /** The "exit" command. */ - private static final class Exit implements TelnetRpc { - public Deferred execute(final TSDB tsdb, final Channel chan, - final String[] cmd) { - chan.disconnect(); - return Deferred.fromResult(null); - } - } - - /** The "help" command. */ - private final class Help implements TelnetRpc { - public Deferred execute(final TSDB tsdb, final Channel chan, - final String[] cmd) { - final StringBuilder buf = new StringBuilder(); - buf.append("available commands: "); - // TODO(tsuna): Maybe sort them? - for (final String command : telnet_commands.keySet()) { - buf.append(command).append(' '); - } - buf.append('\n'); - chan.write(buf.toString()); - return Deferred.fromResult(null); - } - } - - /** The home page ("GET /"). */ - private static final class HomePage implements HttpRpc { - public void execute(final TSDB tsdb, final HttpQuery query) - throws IOException { - final StringBuilder buf = new StringBuilder(2048); - buf.append("
" - + "" - + ""); - query.sendReply(HttpQuery.makePage( - "", - "TSD", "Time Series Database", buf.toString())); - } - } - - /** The "/aggregators" endpoint. */ - private static final class ListAggregators 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"); - } - - if (query.apiVersion() > 0) { - query.sendReply( - query.serializer().formatAggregatorsV1(Aggregators.set())); - } else { - query.sendReply(JSON.serializeToBytes(Aggregators.set())); - } - } - } - - /** For unknown commands. */ - private static final class Unknown implements TelnetRpc { - public Deferred execute(final TSDB tsdb, final Channel chan, - final String[] cmd) { - logWarn(chan, "unknown command : " + Arrays.toString(cmd)); - chan.write("unknown command: " + cmd[0] + ". Try `help'.\n"); - return Deferred.fromResult(null); - } - } - - /** The "version" command. */ - private static final class Version implements TelnetRpc, HttpRpc { - public Deferred execute(final TSDB tsdb, final Channel chan, - final String[] cmd) { - if (chan.isConnected()) { - chan.write(BuildData.revisionString() + '\n' - + BuildData.buildString() + '\n'); - } - return Deferred.fromResult(null); - } - - 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"); - } - - final HashMap version = new HashMap(); - version.put("version", BuildData.version); - version.put("short_revision", BuildData.short_revision); - version.put("full_revision", BuildData.full_revision); - version.put("timestamp", Long.toString(BuildData.timestamp)); - version.put("repo_status", BuildData.repo_status.toString()); - version.put("user", BuildData.user); - version.put("host", BuildData.host); - version.put("repo", BuildData.repo); - - if (query.apiVersion() > 0) { - query.sendReply(query.serializer().formatVersionV1(version)); - } else { - final boolean json = query.request().getUri().endsWith("json"); - if (json) { - query.sendReply(JSON.serializeToBytes(version)); - } else { - final String revision = BuildData.revisionString(); - final String build = BuildData.buildString(); - StringBuilder buf; - buf = new StringBuilder(2 // For the \n's - + revision.length() + build.length()); - buf.append(revision).append('\n').append(build).append('\n'); - query.sendReply(buf); - } - } - } - } - /** * Returns the directory path stored in the given system property. * @param prop The name of the system property. @@ -493,92 +355,19 @@ static String getDirectoryFromSystemProp(final String prop) { } return dir; } - - /** The "dropcaches" command. */ - private static final class DropCaches implements TelnetRpc, HttpRpc { + + // ---------------------------- // + // Individual command handlers. // + // ---------------------------- // + + /** For unknown commands. */ + private static final class Unknown implements TelnetRpc { public Deferred execute(final TSDB tsdb, final Channel chan, final String[] cmd) { - dropCaches(tsdb, chan); - chan.write("Caches dropped.\n"); + logWarn(chan, "unknown command : " + Arrays.toString(cmd)); + chan.write("unknown command: " + cmd[0] + ". Try `help'.\n"); return Deferred.fromResult(null); } - - public void execute(final TSDB tsdb, final HttpQuery query) - throws IOException { - dropCaches(tsdb, query.channel()); - - // 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"); - } - - if (query.apiVersion() > 0) { - final HashMap response = new HashMap(); - response.put("status", "200"); - response.put("message", "Caches dropped"); - query.sendReply(query.serializer().formatDropCachesV1(response)); - } else { // deprecated API - query.sendReply("Caches dropped.\n"); - } - } - - /** Drops in memory caches. */ - private void dropCaches(final TSDB tsdb, final Channel chan) { - LOG.warn(chan + " Dropping all in-memory caches."); - tsdb.dropCaches(); - } - } - - /** The /api/formatters endpoint - * @since 2.0 */ - private static final class Serializers 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"); - } - - switch (query.apiVersion()) { - case 0: - case 1: - query.sendReply(query.serializer().formatSerializersV1()); - break; - default: - throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, - "Requested API version not implemented", "Version " + - query.apiVersion() + " is not implemented"); - } - } - } - - private static final class ShowConfig implements HttpRpc { - - @Override - public void execute(TSDB tsdb, 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"); - } - - switch (query.apiVersion()) { - case 0: - case 1: - query.sendReply(query.serializer().formatConfigV1(tsdb.getConfig())); - break; - default: - throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, - "Requested API version not implemented", "Version " + - query.apiVersion() + " is not implemented"); - } - } - } @Override @@ -590,45 +379,23 @@ public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) { LOG.info("Closed idle socket: " + channel_info); } } - + // ---------------- // // Logging helpers. // // ---------------- // - //private static void logInfo(final HttpQuery query, final String msg) { - // LOG.info(query.channel().toString() + ' ' + msg); - //} - - private static void logWarn(final HttpQuery query, final String msg) { + private static void logWarn(final AbstractHttpQuery query, final String msg) { LOG.warn(query.channel().toString() + ' ' + msg); } - //private void logWarn(final HttpQuery query, final String msg, - // final Exception e) { - // LOG.warn(query.channel().toString() + ' ' + msg, e); - //} - - private void logError(final HttpQuery query, final String msg) { + private void logError(final AbstractHttpQuery query, final String msg) { LOG.error(query.channel().toString() + ' ' + msg); } - //private static void logError(final HttpQuery query, final String msg, - // final Exception e) { - // LOG.error(query.channel().toString() + ' ' + msg, e); - //} - - //private void logInfo(final Channel chan, final String msg) { - // LOG.info(chan.toString() + ' ' + msg); - //} - private static void logWarn(final Channel chan, final String msg) { LOG.warn(chan.toString() + ' ' + msg); } - //private void logWarn(final Channel chan, final String msg, final Exception e) { - // LOG.warn(chan.toString() + ' ' + msg, e); - //} - private void logError(final Channel chan, final String msg) { LOG.error(chan.toString() + ' ' + msg); } diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java new file mode 100644 index 0000000000..2003908ae9 --- /dev/null +++ b/src/tsd/RpcManager.java @@ -0,0 +1,730 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.tsd; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Atomics; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +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 net.opentsdb.BuildData; +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.JSON; +import net.opentsdb.utils.PluginLoader; + +/** + * Manager for the lifecycle of HttpRpcs, TelnetRpcs, + * RpcPlugins, and HttpRpcPlugin. This is a + * singleton. Its lifecycle must be managed by the "container". If you are + * launching via {@code TSDMain} then shutdown (and non-lazy initialization) + * is taken care of. Outside of the use of {@code TSDMain}, you are responsible + * for shutdown, at least. + * + *

Here's an example of how to correctly handle shutdown manually: + * + *

+ * // Startup our TSDB instance...
+ * TSDB tsdb_instance = ...;
+ * 
+ * // ... later, during shtudown ..
+ * 
+ * if (RpcManager.isInitialized()) {
+ *   // Check that its actually been initialized.  We don't want to
+ *   // create a new instance only to shutdown!
+ *   RpcManager.instance(tsdb_instance).shutdown().join();
+ * }
+ * 
+ * + * @since 2.2 + */ +public final class RpcManager { + private static final Logger LOG = LoggerFactory.getLogger(RpcManager.class); + + /** This is base path where {@link HttpRpcPlugin}s are rooted. It's used + * to match incoming requests. */ + @VisibleForTesting + protected static final String PLUGIN_BASE_WEBPATH = "plugin"; + + /** Splitter for web paths. Removes empty strings to handle trailing or + * leading slashes. For instance, all of /plugin/mytest, + * plugin/mytest/, and plugin/mytest will be + * split to [plugin, mytest]. */ + private static final Splitter WEBPATH_SPLITTER = Splitter.on('/') + .trimResults() + .omitEmptyStrings(); + + /** Matches paths declared by {@link HttpRpcPlugin}s that are rooted in + * the system's plugins path. */ + private static final Pattern HAS_PLUGIN_BASE_WEBPATH = Pattern.compile( + "^/?" + PLUGIN_BASE_WEBPATH + "/?.*", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + + /** Reference to our singleton instance. Set in {@link #initialize}. */ + private static final AtomicReference INSTANCE = Atomics.newReference(); + + /** Commands we can serve on the simple, telnet-style RPC interface. */ + private ImmutableMap telnet_commands; + /** Commands we serve on the HTTP interface. */ + private ImmutableMap http_commands; + /** HTTP commands from user plugins. */ + private ImmutableMap http_plugin_commands; + /** List of activated RPC plugins */ + private ImmutableList rpc_plugins; + + /** The TSDB that owns us. */ + private TSDB tsdb; + + /** + * Constructor used by singleton factory method. + * @param tsdb the owning TSDB instance. + */ + private RpcManager(final TSDB tsdb) { + this.tsdb = tsdb; + } + + /** + * Get or create the singleton instance of the manager, loading all the + * plugins enabled in the given TSDB's {@link Config}. + * @return the shared instance of {@link RpcManager}. It's okay to + * hold this reference once obtained. + */ + public static synchronized RpcManager instance(final TSDB tsdb) { + final RpcManager existing = INSTANCE.get(); + if (existing != null) { + return existing; + } + + final RpcManager manager = new RpcManager(tsdb); + final String mode = Strings.nullToEmpty(tsdb.getConfig().getString("tsd.mode")); + + // Load any plugins that are enabled via Config. Fail if any plugin cannot be loaded. + + final ImmutableList.Builder rpcBuilder = ImmutableList.builder(); + if (tsdb.getConfig().hasProperty("tsd.rpc.plugins")) { + final String[] plugins = tsdb.getConfig().getString("tsd.rpc.plugins").split(","); + manager.initializeRpcPlugins(plugins, rpcBuilder); + } + manager.rpc_plugins = rpcBuilder.build(); + + final ImmutableMap.Builder telnetBuilder = ImmutableMap.builder(); + final ImmutableMap.Builder httpBuilder = ImmutableMap.builder(); + manager.initializeBuiltinRpcs(mode, telnetBuilder, httpBuilder); + manager.telnet_commands = telnetBuilder.build(); + manager.http_commands = httpBuilder.build(); + + final ImmutableMap.Builder httpPluginsBuilder = ImmutableMap.builder(); + if (tsdb.getConfig().hasProperty("tsd.http.rpc.plugins")) { + final String[] plugins = tsdb.getConfig().getString("tsd.http.rpc.plugins").split(","); + manager.initializeHttpRpcPlugins(mode, plugins, httpPluginsBuilder); + } + manager.http_plugin_commands = httpPluginsBuilder.build(); + + INSTANCE.set(manager); + return manager; + } + + /** + * @return {@code true} if the shared instance has been initialized; + * {@code false} otherwise. + */ + public static synchronized boolean isInitialized() { + return INSTANCE.get() != null; + } + + /** + * @return list of loaded {@link RpcPlugin}s. Possibly empty but + * never {@code null}. + */ + @VisibleForTesting + protected ImmutableList getRpcPlugins() { + return rpc_plugins; + } + + /** + * Lookup a {@link TelnetRpc} based on given command name. Note that this + * lookup is case sensitive in that the {@code command} passed in must + * match a registered RPC command exactly. + * @param command a telnet API command name. + * @return the {@link TelnetRpc} for the given {@code command} or {@code null} + * if not found. + */ + TelnetRpc lookupTelnetRpc(final String command) { + return telnet_commands.get(command); + } + + /** + * Lookup a built-in {@link HttpRpc} based on the given {@code queryBaseRoute}. + * The lookup is based on exact match of the input parameter and the registered + * {@link HttpRpc}s. + * @param queryBaseRoute the HTTP query's base route, with no trailing or + * leading slashes. For example: {@code api/query} + * @return the {@link HttpRpc} for the given {@code queryBaseRoute} or + * {@code null} if not found. + */ + HttpRpc lookupHttpRpc(final String queryBaseRoute) { + return http_commands.get(queryBaseRoute); + } + + /** + * Lookup a user-supplied {@link HttpRpcPlugin} for the given + * {@code queryBaseRoute}. The lookup is based on exact match of the input + * parameter and the registered {@link HttpRpcPlugin}s. + * @param queryBaseRoute the value of {@link HttpRpcPlugin#getPath()} with no + * trailing or leading slashes. + * @return the {@link HttpRpcPlugin} for the given {@code queryBaseRoute} or + * {@code null} if not found. + */ + HttpRpcPlugin lookupHttpRpcPlugin(final String queryBaseRoute) { + return http_plugin_commands.get(queryBaseRoute); + } + + /** + * @param uri HTTP request URI, with or without query parameters. + * @return {@code true} if the URI represents a request for a + * {@link HttpRpcPlugin}; {@code false} otherwise. Note that this + * method returning true says nothing about + * whether or not there is a {@link HttpRpcPlugin} registered + * at the given URI, only that it's a valid RPC plugin request. + */ + boolean isHttpRpcPluginPath(final String uri) { + if (Strings.isNullOrEmpty(uri) || uri.length() <= PLUGIN_BASE_WEBPATH.length()) { + return false; + } else { + // Don't consider the query portion, if any. + int qmark = uri.indexOf('?'); + String path = uri; + if (qmark != -1) { + path = uri.substring(0, qmark); + } + + final List parts = WEBPATH_SPLITTER.splitToList(path); + return (parts.size() > 1 && parts.get(0).equals(PLUGIN_BASE_WEBPATH)); + } + } + + /** + * 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 http a map of API endpoints to {@link HttpRpc} instances. + */ + private void initializeBuiltinRpcs(final String mode, + final ImmutableMap.Builder telnet, + final ImmutableMap.Builder http) { + if (mode.equals("rw") || mode.equals("wo")) { + final PutDataPointRpc put = new PutDataPointRpc(); + telnet.put("put", put); + http.put("api/put", put); + } + + if (mode.equals("rw") || mode.equals("ro")) { + http.put("", new HomePage()); + final StaticFileRpc staticfile = new StaticFileRpc(); + http.put("favicon.ico", staticfile); + http.put("s", staticfile); + + final StatsRpc stats = new StatsRpc(); + telnet.put("stats", stats); + http.put("stats", stats); + http.put("api/stats", stats); + + final DropCaches dropcaches = new DropCaches(); + telnet.put("dropcaches", dropcaches); + http.put("dropcaches", dropcaches); + http.put("api/dropcaches", dropcaches); + + final ListAggregators aggregators = new ListAggregators(); + http.put("aggregators", aggregators); + http.put("api/aggregators", aggregators); + + final SuggestRpc suggest_rpc = new SuggestRpc(); + http.put("suggest", suggest_rpc); + http.put("api/suggest", suggest_rpc); + + http.put("logs", new LogsRpc()); + http.put("q", new GraphHandler()); + http.put("api/serializers", new Serializers()); + http.put("api/uid", new UniqueIdRpc()); + http.put("api/query", new QueryRpc()); + http.put("api/tree", new TreeRpc()); + { + final AnnotationRpc annotation_rpc = new AnnotationRpc(); + http.put("api/annotation", annotation_rpc); + http.put("api/annotations", annotation_rpc); + } + http.put("api/search", new SearchRpc()); + http.put("api/config", new ShowConfig()); + + if (tsdb.getConfig().getString("tsd.no_diediedie").equals("false")) { + final DieDieDie diediedie = new DieDieDie(); + telnet.put("diediedie", diediedie); + http.put("diediedie", diediedie); + } + { + final Version version = new Version(); + telnet.put("version", version); + http.put("version", version); + http.put("api/version", version); + } + + telnet.put("exit", new Exit()); + telnet.put("help", new Help()); + } + } + + /** + * Load and init the {@link HttpRpcPlugin}s provided as an array of + * {@code pluginClassNames}. + * @param mode is this TSD in read/write ("rw") or read-only ("ro") + * mode? + * @param pluginClassNames fully-qualified class names that are + * instances of {@link HttpRpcPlugin}s + * @param http a map of canonicalized paths + * (obtained via {@link #canonicalizePluginPath(String)}) + * to {@link HttpRpcPlugin} instance. + */ + @VisibleForTesting + protected void initializeHttpRpcPlugins(final String mode, + final String[] pluginClassNames, + final ImmutableMap.Builder http) { + for (final String plugin : pluginClassNames) { + final HttpRpcPlugin rpc = createAndInitialize(plugin, HttpRpcPlugin.class); + validateHttpRpcPluginPath(rpc.getPath()); + final String path = rpc.getPath().trim(); + final String canonicalized_path = canonicalizePluginPath(path); + http.put(canonicalized_path, rpc); + LOG.info("Mounted HttpRpcPlugin [{}] at path \"{}\"", rpc.getClass().getName(), canonicalized_path); + } + } + + /** + * Ensure that the given path for an {@link HttpRpcPlugin} is valid. This + * method simply returns for valid inputs; throws and exception otherwise. + * @param path a request path, no query parameters, etc. + * @throws IllegalArgumentException on invalid paths. + */ + @VisibleForTesting + protected void validateHttpRpcPluginPath(final String path) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(path), + "Invalid HttpRpcPlugin path. Path is null or empty."); + final String testPath = path.trim(); + Preconditions.checkArgument(!HAS_PLUGIN_BASE_WEBPATH.matcher(path).matches(), + "Invalid HttpRpcPlugin path %s. Path contains system's plugin base path.", + testPath); + + URI uri = URI.create(testPath); + Preconditions.checkArgument(!Strings.isNullOrEmpty(uri.getPath()), + "Invalid HttpRpcPlugin path %s. Parsed path is null or empty.", testPath); + Preconditions.checkArgument(!uri.getPath().equals("/"), + "Invalid HttpRpcPlugin path %s. Path is equal to root.", testPath); + Preconditions.checkArgument(Strings.isNullOrEmpty(uri.getQuery()), + "Invalid HttpRpcPlugin path %s. Path contains query parameters.", testPath); + } + + /** + * @param origPath a request path, no query parameters, etc. + * @return a canonical representation of the input, with trailing and leading + * slashes removed. + * @throws IllegalArgumentException if the given path is a root. + */ + @VisibleForTesting + protected String canonicalizePluginPath(final String origPath) { + Preconditions.checkArgument(!(Strings.isNullOrEmpty(origPath) || origPath.equals("/")), + "Path %s is a root.", origPath); + String new_path = origPath; + if (new_path.startsWith("/")) { + new_path = new_path.substring(1); + } + if (new_path.endsWith("/")) { + new_path = new_path.substring(0, new_path.length()-1); + } + return new_path; + } + + /** + * Load and init the {@link RpcPlugin}s provided as an array of + * {@code pluginClassNames}. + * @param pluginClassNames fully-qualified class names that are + * instances of {@link RpcPlugin}s + * @param rpcs a list of loaded and initialized plugins + */ + private void initializeRpcPlugins(final String[] pluginClassNames, + final ImmutableList.Builder rpcs) { + for (final String plugin : pluginClassNames) { + final RpcPlugin rpc = createAndInitialize(plugin, RpcPlugin.class); + rpcs.add(rpc); + } + } + + /** + * Helper method to load and initialize a given plugin class. This uses reflection + * because plugins share no common interfaces. (They could though!) + * @param pluginClassName the class name of the plugin to load + * @param pluginClass class of the plugin + * @return loaded an initialized instance of {@code pluginClass} + */ + @VisibleForTesting + protected T createAndInitialize(final String pluginClassName, final Class pluginClass) { + final T instance = PluginLoader.loadSpecificPlugin(pluginClassName, pluginClass); + Preconditions.checkState(instance != null, + "Unable to locate %s using name '%s", pluginClass, pluginClassName); + try { + final Method initMeth = instance.getClass().getMethod("initialize", TSDB.class); + initMeth.invoke(instance, tsdb); + final Method versionMeth = instance.getClass().getMethod("version"); + String version = (String) versionMeth.invoke(instance); + LOG.info("Successfully initialized plugin [{}] version: {}", + instance.getClass().getCanonicalName(), + version); + return instance; + } catch (Exception e) { + throw new RuntimeException("Failed to initialize " + instance.getClass(), e); + } + } + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public Deferred> shutdown() { + // Clear shared instance. + INSTANCE.set(null); + + final Collection> deferreds = Lists.newArrayList(); + + if (http_plugin_commands != null) { + for (final Map.Entry entry : http_plugin_commands.entrySet()) { + deferreds.add(entry.getValue().shutdown()); + } + } + + if (rpc_plugins != null) { + for (final RpcPlugin rpc : rpc_plugins) { + deferreds.add(rpc.shutdown()); + } + } + + return Deferred.groupInOrder(deferreds); + } + + /** + * Collect stats on the shared instance of {@link RpcManager}. + */ + static void collectStats(final StatsCollector collector) { + final RpcManager manager = INSTANCE.get(); + if (manager != null) { + if (manager.rpc_plugins != null) { + try { + collector.addExtraTag("plugin", "rpc"); + for (final RpcPlugin rpc : manager.rpc_plugins) { + rpc.collectStats(collector); + } + } finally { + collector.clearExtraTag("plugin"); + } + } + + if (manager.http_plugin_commands != null) { + try { + collector.addExtraTag("plugin", "httprpc"); + for (final Map.Entry entry + : manager.http_plugin_commands.entrySet()) { + entry.getValue().collectStats(collector); + } + } finally { + collector.clearExtraTag("plugin"); + } + } + } + } + + // ---------------------------- // + // Individual command handlers. // + // ---------------------------- // + + /** The "diediedie" command and "/diediedie" endpoint. */ + private final class DieDieDie implements TelnetRpc, HttpRpc { + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + LOG.warn("{} {}", chan, "shutdown requested"); + chan.write("Cleaning up and exiting now.\n"); + return doShutdown(tsdb, chan); + } + + public void execute(final TSDB tsdb, final HttpQuery query) { + LOG.warn("{} {}", query, "shutdown requested"); + query.sendReply(HttpQuery.makePage("TSD Exiting", "You killed me", + "Cleaning up and exiting now.")); + doShutdown(tsdb, query.channel()); + } + + private Deferred doShutdown(final TSDB tsdb, final Channel chan) { + ((GraphHandler) http_commands.get("q")).shutdown(); + ConnectionManager.closeAllConnections(); + // Netty gets stuck in an infinite loop if we shut it down from within a + // NIO thread. So do this from a newly created thread. + final class ShutdownNetty extends Thread { + ShutdownNetty() { + super("ShutdownNetty"); + } + public void run() { + chan.getFactory().releaseExternalResources(); + } + } + new ShutdownNetty().start(); // Stop accepting new connections. + + // Log any error that might occur during shutdown. + final class ShutdownTSDB implements Callback { + public Exception call(final Exception arg) { + LOG.error("Unexpected exception while shutting down", arg); + return arg; + } + public String toString() { + return "shutdown callback"; + } + } + return tsdb.shutdown().addErrback(new ShutdownTSDB()); + } + } + + /** The "exit" command. */ + private static final class Exit implements TelnetRpc { + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + chan.disconnect(); + return Deferred.fromResult(null); + } + } + + /** The "help" command. */ + private final class Help implements TelnetRpc { + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + final StringBuilder buf = new StringBuilder(); + buf.append("available commands: "); + // TODO(tsuna): Maybe sort them? + for (final String command : telnet_commands.keySet()) { + buf.append(command).append(' '); + } + buf.append('\n'); + chan.write(buf.toString()); + return Deferred.fromResult(null); + } + } + + /** The home page ("GET /"). */ + private static final class HomePage implements HttpRpc { + public void execute(final TSDB tsdb, final HttpQuery query) + throws IOException { + final StringBuilder buf = new StringBuilder(2048); + buf.append("
" + + "" + + ""); + query.sendReply(HttpQuery.makePage( + "", + "TSD", "Time Series Database", buf.toString())); + } + } + + /** The "/aggregators" endpoint. */ + private static final class ListAggregators 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"); + } + + if (query.apiVersion() > 0) { + query.sendReply( + query.serializer().formatAggregatorsV1(Aggregators.set())); + } else { + query.sendReply(JSON.serializeToBytes(Aggregators.set())); + } + } + } + + /** The "version" command. */ + private static final class Version implements TelnetRpc, HttpRpc { + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + if (chan.isConnected()) { + chan.write(BuildData.revisionString() + '\n' + + BuildData.buildString() + '\n'); + } + return Deferred.fromResult(null); + } + + 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"); + } + + final HashMap version = new HashMap(); + version.put("version", BuildData.version); + version.put("short_revision", BuildData.short_revision); + version.put("full_revision", BuildData.full_revision); + version.put("timestamp", Long.toString(BuildData.timestamp)); + version.put("repo_status", BuildData.repo_status.toString()); + version.put("user", BuildData.user); + version.put("host", BuildData.host); + version.put("repo", BuildData.repo); + + if (query.apiVersion() > 0) { + query.sendReply(query.serializer().formatVersionV1(version)); + } else { + final boolean json = query.request().getUri().endsWith("json"); + if (json) { + query.sendReply(JSON.serializeToBytes(version)); + } else { + final String revision = BuildData.revisionString(); + final String build = BuildData.buildString(); + StringBuilder buf; + buf = new StringBuilder(2 // For the \n's + + revision.length() + build.length()); + buf.append(revision).append('\n').append(build).append('\n'); + query.sendReply(buf); + } + } + } + } + + /** The "dropcaches" command. */ + private static final class DropCaches implements TelnetRpc, HttpRpc { + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + dropCaches(tsdb, chan); + chan.write("Caches dropped.\n"); + return Deferred.fromResult(null); + } + + public void execute(final TSDB tsdb, final HttpQuery query) + throws IOException { + dropCaches(tsdb, query.channel()); + + // 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"); + } + + if (query.apiVersion() > 0) { + final HashMap response = new HashMap(); + response.put("status", "200"); + response.put("message", "Caches dropped"); + query.sendReply(query.serializer().formatDropCachesV1(response)); + } else { // deprecated API + query.sendReply("Caches dropped.\n"); + } + } + + /** Drops in memory caches. */ + private void dropCaches(final TSDB tsdb, final Channel chan) { + LOG.warn(chan + " Dropping all in-memory caches."); + tsdb.dropCaches(); + } + } + + /** The /api/formatters endpoint + * @since 2.0 */ + private static final class Serializers 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"); + } + + switch (query.apiVersion()) { + case 0: + case 1: + query.sendReply(query.serializer().formatSerializersV1()); + break; + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + + query.apiVersion() + " is not implemented"); + } + } + } + + private static final class ShowConfig implements HttpRpc { + @Override + public void execute(TSDB tsdb, 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"); + } + + switch (query.apiVersion()) { + case 0: + case 1: + query.sendReply(query.serializer().formatConfigV1(tsdb.getConfig())); + break; + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + + query.apiVersion() + " is not implemented"); + } + } + } + +} diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index 28bd037c26..0ff6ec69fc 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -89,6 +89,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) { canonical); ConnectionManager.collectStats(collector); RpcHandler.collectStats(collector); + RpcManager.collectStats(collector); tsdb.collectStats(collector); query.sendReply(query.serializer().formatStatsV1(dps)); } @@ -103,6 +104,7 @@ private void doCollectStats(final TSDB tsdb, final StatsCollector collector, collector.addHostTag(canonical); ConnectionManager.collectStats(collector); RpcHandler.collectStats(collector); + RpcManager.collectStats(collector); tsdb.collectStats(collector); } diff --git a/test/META-INF/services/net.opentsdb.tsd.HttpRpcPlugin b/test/META-INF/services/net.opentsdb.tsd.HttpRpcPlugin new file mode 100644 index 0000000000..a673a90a34 --- /dev/null +++ b/test/META-INF/services/net.opentsdb.tsd.HttpRpcPlugin @@ -0,0 +1 @@ +net.opentsdb.tsd.DummyHttpRpcPlugin diff --git a/test/tsd/DummyHttpRpcPlugin.java b/test/tsd/DummyHttpRpcPlugin.java new file mode 100644 index 0000000000..8115ba1232 --- /dev/null +++ b/test/tsd/DummyHttpRpcPlugin.java @@ -0,0 +1,56 @@ +// 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.tsd; + +import java.io.IOException; + +import com.google.common.base.Preconditions; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; + +/** + * This is a dummy HTTP RPC plugin implementation for unit test purposes. + * @since 2.1 + */ +public class DummyHttpRpcPlugin extends HttpRpcPlugin { + @Override + public void initialize(TSDB tsdb) { + Preconditions.checkNotNull(tsdb); + } + + @Override + public Deferred shutdown() { + return Deferred.fromResult(null); + } + + @Override + public String version() { + return "2.0.0"; + } + + @Override + public void collectStats(StatsCollector collector) { + collector.record("http_rpcplugin.dummy.value", 1); + } + + @Override + public String getPath() { + return "/dummy/test"; + } + + @Override + public void execute(TSDB tsdb, HttpRpcPluginQuery query) throws IOException { + } +} diff --git a/test/tsd/TestHttpRpcPluginQuery.java b/test/tsd/TestHttpRpcPluginQuery.java new file mode 100644 index 0000000000..346055abeb --- /dev/null +++ b/test/tsd/TestHttpRpcPluginQuery.java @@ -0,0 +1,73 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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.tsd; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.DefaultHttpRequest; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpVersion; +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; + +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({TSDB.class, Config.class, HttpRpcPluginQuery.class}) +public final class TestHttpRpcPluginQuery { + private TSDB mockTsdb; + private Channel mockChannel; + + @Before + public void before() { + mockTsdb = mock(TSDB.class); + mockChannel = NettyMocks.fakeChannel(); + } + + @Test + public void getQueryBaseRoute() { + assertEquals("test/this/path", makeQuery("/plugin/test/this/path").getQueryBaseRoute()); + assertEquals("test", makeQuery("/plugin/test/").getQueryBaseRoute()); + assertEquals("test", makeQuery("/plugin/test?some=else&this=that").getQueryBaseRoute()); + } + + @Test(expected=BadRequestException.class) + public void getQueryBaseRouteNoSlash() { + makeQuery("plugin/test?some=else&this=that").getQueryBaseRoute(); + } + + @Test(expected=BadRequestException.class) + public void getQueryBaseRouteNoPluginBase() { + makeQuery("/test?some=else&this=that").getQueryBaseRoute(); + } + + @Test(expected=BadRequestException.class) + public void getQueryBaseRouteNoPath() { + makeQuery("/plugin?some=else&this=that").getQueryBaseRoute(); + } + + private HttpRpcPluginQuery makeQuery(final String uriString) { + HttpRequest req = new DefaultHttpRequest( + HttpVersion.HTTP_1_1, + HttpMethod.GET, + uriString); + return new HttpRpcPluginQuery(mockTsdb, req, mockChannel); + } +} \ No newline at end of file diff --git a/test/tsd/TestRpcHandler.java b/test/tsd/TestRpcHandler.java index 1528cfc9ea..ec26b0a34f 100644 --- a/test/tsd/TestRpcHandler.java +++ b/test/tsd/TestRpcHandler.java @@ -15,25 +15,29 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.powermock.api.mockito.PowerMockito.mock; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; -import net.opentsdb.core.TSDB; -import net.opentsdb.utils.Config; +import java.lang.reflect.Method; + +import com.google.common.net.HttpHeaders; import org.hbase.async.HBaseClient; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SucceededChannelFuture; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,18 +47,21 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; -import com.google.common.net.HttpHeaders; +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.Config; @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @RunWith(PowerMockRunner.class) -@PrepareForTest({ TSDB.class, Config.class, HBaseClient.class, RpcHandler.class, +@PrepareForTest({ TSDB.class, Config.class, HBaseClient.class, RpcHandler.class, HttpQuery.class, MessageEvent.class, DefaultHttpResponse.class, ChannelHandlerContext.class }) public final class TestRpcHandler { private TSDB tsdb = null; + private RpcManager rpc_manager; private ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); private HBaseClient client = mock(HBaseClient.class); private MessageEvent message = mock(MessageEvent.class); @@ -65,18 +72,24 @@ public void before() throws Exception { PowerMockito.whenNew(HBaseClient.class) .withArguments(anyString(), anyString()).thenReturn(client); tsdb = new TSDB(config); + rpc_manager = RpcManager.instance(tsdb); + } + + @After + public void after() { + rpc_manager.shutdown(); } @Test public void ctorDefaults() { - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); assertNotNull(rpc); } @Test public void ctorCORSPublic() { tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "*"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); assertNotNull(rpc); } @@ -84,7 +97,7 @@ public void ctorCORSPublic() { public void ctorCORSSeparated() { tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "aurther.com,dent.net,beeblebrox.org"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); assertNotNull(rpc); } @@ -92,7 +105,7 @@ public void ctorCORSSeparated() { public void ctorCORSPublicAndDomains() { tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "*,aurther.com,dent.net,beeblebrox.org"); - new RpcHandler(tsdb); + new RpcHandler(tsdb, rpc_manager); } @Test @@ -114,7 +127,7 @@ public ChannelFuture answer(final InvocationOnMock args) } ); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -139,7 +152,7 @@ public ChannelFuture answer(final InvocationOnMock args) ); tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "*"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -165,7 +178,7 @@ public ChannelFuture answer(final InvocationOnMock args) tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "aurther.com,dent.net,42.com,beeblebrox.org"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -190,7 +203,7 @@ public ChannelFuture answer(final InvocationOnMock args) tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "aurther.com,dent.net,beeblebrox.org"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -212,7 +225,7 @@ public ChannelFuture answer(final InvocationOnMock args) } ); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -235,7 +248,7 @@ public ChannelFuture answer(final InvocationOnMock args) } ); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -260,7 +273,7 @@ public ChannelFuture answer(final InvocationOnMock args) ); tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "*"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -286,7 +299,7 @@ public ChannelFuture answer(final InvocationOnMock args) tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "aurther.com,dent.net,42.com,beeblebrox.org"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } @@ -311,14 +324,79 @@ public ChannelFuture answer(final InvocationOnMock args) tsdb.getConfig().overrideConfig("tsd.http.request.cors_domains", "aurther.com,dent.net,beeblebrox.org"); - final RpcHandler rpc = new RpcHandler(tsdb); + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); rpc.messageReceived(ctx, message); } - private void handleHttpRpc(final HttpRequest req, final Answer answer) { + @Test + public void createQueryInstanceForBuiltin() throws Exception { + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); + final Channel mockChan = NettyMocks.fakeChannel(); + final Method meth = Whitebox.getMethod(RpcHandler.class, "createQueryInstance", + TSDB.class, HttpRequest.class, Channel.class); + AbstractHttpQuery query = (AbstractHttpQuery) meth.invoke( + rpc, tsdb, + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/api/v1/version"), + mockChan); + assertTrue(query instanceof HttpQuery); + + query = (AbstractHttpQuery) meth.invoke( + rpc, tsdb, + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/api/version"), + mockChan); + assertTrue(query instanceof HttpQuery); + + query = (AbstractHttpQuery) meth.invoke( + rpc, tsdb, + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/q"), + mockChan); + assertTrue(query instanceof HttpQuery); + + query = (AbstractHttpQuery) meth.invoke( + rpc, tsdb, + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/"), + mockChan); + assertTrue(query instanceof HttpQuery); + } + + @Test(expected=BadRequestException.class) + public void createQueryInstanceEmptyRequestInvalid() throws Exception { + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); + final Channel mockChan = NettyMocks.fakeChannel(); + final Method meth = Whitebox.getMethod(RpcHandler.class, "createQueryInstance", + TSDB.class, HttpRequest.class, Channel.class); + meth.invoke( + rpc, tsdb, + new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, ""), + mockChan); + } + + @Test + public void emptyPathIsBadRequest() throws Exception { + final HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.GET, ""); + + final Channel mockChan = handleHttpRpc(req, + new Answer() { + public ChannelFuture answer(final InvocationOnMock args) + throws Throwable { + DefaultHttpResponse response = + (DefaultHttpResponse)args.getArguments()[0]; + assertEquals(HttpResponseStatus.BAD_REQUEST, response.getStatus()); + return new SucceededChannelFuture((Channel) args.getMock()); + } + } + ); + + final RpcHandler rpc = new RpcHandler(tsdb, rpc_manager); + Whitebox.invokeMethod(rpc, "handleHttpQuery", tsdb, mockChan, req); + } + + private Channel handleHttpRpc(final HttpRequest req, final Answer answer) { final Channel channel = NettyMocks.fakeChannel(); when(message.getMessage()).thenReturn(req); when(message.getChannel()).thenReturn(channel); when(channel.write((DefaultHttpResponse)any())).thenAnswer(answer); + return channel; } } diff --git a/test/tsd/TestRpcManager.java b/test/tsd/TestRpcManager.java new file mode 100644 index 0000000000..58a8193487 --- /dev/null +++ b/test/tsd/TestRpcManager.java @@ -0,0 +1,203 @@ +// 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.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.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import org.hbase.async.HBaseClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.PluginLoader; + +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, Config.class, HBaseClient.class, RpcManager.class }) +public class TestRpcManager { + private TSDB mock_tsdb_no_plugins; + + // Set in individual test methods; shutdown by after() if set. + private RpcManager mgr_under_test; + + @Before + public void before() { + Config config = mock(Config.class); + TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + mock_tsdb_no_plugins = tsdb; + } + + @After + public void after() throws Exception { + if (mgr_under_test != null) { + mgr_under_test.shutdown().join(); + } + } + + @Test + public void loadHttpRpcPlugins() throws Exception { + Config config = mock(Config.class); + when(config.hasProperty("tsd.http.rpc.plugins")) + .thenReturn(true); + when(config.getString("tsd.http.rpc.plugins")) + .thenReturn("net.opentsdb.tsd.DummyHttpRpcPlugin"); + + TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + + PluginLoader.loadJAR("plugin_test.jar"); + mgr_under_test = RpcManager.instance(tsdb); + + HttpRpcPlugin plugin = mgr_under_test.lookupHttpRpcPlugin("dummy/test"); + assertNotNull(plugin); + } + + @Test + public void loadRpcPlugin() throws Exception { + Config config = mock(Config.class); + when(config.hasProperty("tsd.rpc.plugins")) + .thenReturn(true); + when(config.getString("tsd.rpc.plugins")) + .thenReturn("net.opentsdb.tsd.DummyRpcPlugin"); + + when(config.hasProperty("tsd.rpcplugin.DummyRPCPlugin.hosts")) + .thenReturn(true); + when(config.getString("tsd.rpcplugin.DummyRPCPlugin.hosts")) + .thenReturn("blah"); + when(config.getInt("tsd.rpcplugin.DummyRPCPlugin.port")) + .thenReturn(1000); + + TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + + PluginLoader.loadJAR("plugin_test.jar"); + mgr_under_test= RpcManager.instance(tsdb); + + assertFalse(mgr_under_test.getRpcPlugins().isEmpty()); + } + + @Test + public void isHttpRpcPluginPathValid() { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + assertTrue(mgr_under_test.isHttpRpcPluginPath("/plugin/my/http/plugin")); + assertTrue(mgr_under_test.isHttpRpcPluginPath("plugin/my/http/plugin")); + assertTrue(mgr_under_test.isHttpRpcPluginPath("/plugin/my?hey=hi&howdy=ho")); + assertTrue(mgr_under_test.isHttpRpcPluginPath("plugin/my?hey=hi&howdy=ho")); + assertTrue(mgr_under_test.isHttpRpcPluginPath("plugin/my/?hey=hi&howdy=ho")); + } + + @Test + public void isHttpRpcPluginPathInvalid() { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + assertFalse(mgr_under_test.isHttpRpcPluginPath("/plugin/")); + assertFalse(mgr_under_test.isHttpRpcPluginPath("plugin/")); + assertFalse(mgr_under_test.isHttpRpcPluginPath("plugin")); + assertFalse(mgr_under_test.isHttpRpcPluginPath("/plugin")); + assertFalse(mgr_under_test.isHttpRpcPluginPath("/plugin?howdy=ho")); + assertFalse(mgr_under_test.isHttpRpcPluginPath("/plugin/?howdy=ho")); + assertFalse(mgr_under_test.isHttpRpcPluginPath("api/query")); + } + + @Test + public void validateHttpRpcPluginPathValid() { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + mgr_under_test.validateHttpRpcPluginPath("/my/test/path"); + mgr_under_test.validateHttpRpcPluginPath("my/test/path"); + mgr_under_test.validateHttpRpcPluginPath("my/test/path"); + mgr_under_test.validateHttpRpcPluginPath("api/query"); + } + + @Test + public void validateHttpRpcPluginPathInvalid() { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + try { + mgr_under_test.validateHttpRpcPluginPath("/plugin/my/test"); + assertTrue(false); + } catch (IllegalArgumentException e) { } + try { + mgr_under_test.validateHttpRpcPluginPath("plugin/my/test"); + assertTrue(false); + } catch (IllegalArgumentException e) { } + try { + mgr_under_test.validateHttpRpcPluginPath("plugin/"); + assertTrue(false); + } catch (IllegalArgumentException e) { } + try { + mgr_under_test.validateHttpRpcPluginPath("/plugin/"); + assertTrue(false); + } catch (IllegalArgumentException e) { } + try { + mgr_under_test.validateHttpRpcPluginPath("/plugin"); + assertTrue(false); + } catch (IllegalArgumentException e) { } + try { + mgr_under_test.validateHttpRpcPluginPath("plugin"); + assertTrue(false); + } catch (IllegalArgumentException e) { } + } + + @Test + public void canonicalizePluginPathsValid() throws Exception { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + assertEquals("my/test/path", + mgr_under_test.canonicalizePluginPath("/my/test/path")); + assertEquals("my/test/path", + mgr_under_test.canonicalizePluginPath("/my/test/path/")); + assertEquals("my/test/path", + mgr_under_test.canonicalizePluginPath("my/test/path/")); + assertEquals("my/test/path", + mgr_under_test.canonicalizePluginPath("my/test/path")); + + assertEquals("my", + mgr_under_test.canonicalizePluginPath("/my/")); + assertEquals("my", + mgr_under_test.canonicalizePluginPath("my/")); + assertEquals("my", + mgr_under_test.canonicalizePluginPath("my")); + } + + @Test(expected=IllegalArgumentException.class) + public void canonicalizePluginPathIsRoot() { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + assertEquals(RpcManager.PLUGIN_BASE_WEBPATH + "/", + mgr_under_test.canonicalizePluginPath("")); + } + + @Test + public void validHttpPathEndToEnd() { + mgr_under_test = RpcManager.instance(mock_tsdb_no_plugins); + mgr_under_test.validateHttpRpcPluginPath("myplugin"); + assertEquals("myplugin", mgr_under_test.canonicalizePluginPath("myplugin")); + mgr_under_test.validateHttpRpcPluginPath("/myplugin"); + assertEquals("myplugin", mgr_under_test.canonicalizePluginPath("/myplugin")); + + mgr_under_test.validateHttpRpcPluginPath("myplugin/subcommand"); + assertEquals("myplugin/subcommand", mgr_under_test.canonicalizePluginPath("myplugin/subcommand")); + mgr_under_test.validateHttpRpcPluginPath("/myplugin/subcommand"); + assertEquals("myplugin/subcommand", mgr_under_test.canonicalizePluginPath("/myplugin/subcommand")); + } +} diff --git a/test/utils/TestPluginLoader.java b/test/utils/TestPluginLoader.java index 393a524a64..bf6450e2fa 100644 --- a/test/utils/TestPluginLoader.java +++ b/test/utils/TestPluginLoader.java @@ -20,6 +20,7 @@ import java.util.List; import net.opentsdb.plugin.DummyPlugin; +import net.opentsdb.tsd.HttpRpcPlugin; import net.opentsdb.utils.PluginLoader; import org.junit.Test; @@ -109,6 +110,14 @@ public void loadPlugins() throws Exception { assertEquals(2, plugins.size()); } + @Test + public void loadHttpRpcPlugin() throws Exception { + PluginLoader.loadJAR("plugin_test.jar"); + HttpRpcPlugin plugin = PluginLoader.loadSpecificPlugin("net.opentsdb.tsd.DummyHttpRpcPlugin", HttpRpcPlugin.class); + assertNotNull(plugin); + assertEquals("/dummy/test", plugin.getPath()); + } + @Test public void loadPluginsNotFound() throws Exception { List plugins = PluginLoader.loadPlugins( From 54303ce20668a1fb3e3c444ce2c577a064ac6147 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Sat, 13 Sep 2014 18:07:44 -0700 Subject: [PATCH 053/826] Moving BuildData to net.opentsdb.tools Signed-off-by: Chris Larsen --- Makefile.am | 5 +++-- build-aux/gen_build_data.sh | 5 +++++ pom.xml.in | 4 ++-- src/tools/TSDMain.java | 2 +- src/tsd/RpcManager.java | 2 +- tsdb.in | 5 ++++- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Makefile.am b/Makefile.am index d719a31755..7fb8068f1f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -18,11 +18,12 @@ ACLOCAL_AMFLAGS = -I build-aux all-am: jar staticroot package = net.opentsdb +builddata_subpackage = tools spec_title = OpenTSDB spec_vendor = The OpenTSDB Authors jar := tsdb-$(PACKAGE_VERSION).jar plugin_test_jar := plugin_test.jar -builddata_SRC := src/BuildData.java +builddata_SRC := src/tools/BuildData.java BUILT_SOURCES = $(builddata_SRC) nodist_bin_SCRIPTS = tsdb dist_noinst_SCRIPTS = src/create_table.sh src/upgrade_1to2.sh src/mygnuplot.sh \ @@ -308,7 +309,7 @@ install-exec-hook: rm -f tsdb.tmp $(builddata_SRC): .git/HEAD $(tsdb_SRC) $(top_srcdir)/build-aux/gen_build_data.sh - $(srcdir)/build-aux/gen_build_data.sh $(builddata_SRC) $(package) $(PACKAGE_VERSION) + $(srcdir)/build-aux/gen_build_data.sh $(builddata_SRC) $(package).$(builddata_subpackage) $(PACKAGE_VERSION) jar: $(jar) .javac-unittests-stamp .gwtc-stamp diff --git a/build-aux/gen_build_data.sh b/build-aux/gen_build_data.sh index f402ae6354..927f2e63b1 100755 --- a/build-aux/gen_build_data.sh +++ b/build-aux/gen_build_data.sh @@ -143,5 +143,10 @@ public final class $CLASS { // Can't instantiate. private $CLASS() {} + + public static void main(String[] args) { + System.out.println(revisionString()); + System.out.println(buildString()); + } } EOF diff --git a/pom.xml.in b/pom.xml.in index f62d5442fc..be1dabb7c0 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -93,8 +93,8 @@ build-aux/gen_build_data.sh - target/generated-sources/net/opentsdb/BuildData.java - net.opentsdb + target/generated-sources/net/opentsdb/tools/BuildData.java + net.opentsdb.tools BuildData diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 2923b8a23c..5c0b69bdb2 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -25,7 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.BuildData; +import net.opentsdb.tools.BuildData; import net.opentsdb.core.TSDB; import net.opentsdb.tsd.PipelineFactory; import net.opentsdb.tsd.RpcManager; diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 2003908ae9..603fd3e3f3 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -40,7 +40,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.BuildData; +import net.opentsdb.tools.BuildData; import net.opentsdb.core.Aggregators; import net.opentsdb.core.TSDB; import net.opentsdb.stats.StatsCollector; diff --git a/tsdb.in b/tsdb.in index 641498cee1..94077301d6 100644 --- a/tsdb.in +++ b/tsdb.in @@ -62,7 +62,7 @@ CLASSPATH="${CLASSPATH#:}" usage() { echo >&2 "usage: $me [args]" - echo 'Valid commands: fsck, import, mkmetric, query, tsd, scan, uid' + echo 'Valid commands: fsck, import, mkmetric, query, tsd, scan, search, uid, version' exit 1 } @@ -93,6 +93,9 @@ case $1 in (uid) MAINCLASS=UidManager ;; + (version) + MAINCLASS=BuildData + ;; (*) echo >&2 "$me: error: unknown command '$1'" usage From b98f645878025695036d3188947e243897b7774c Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Thu, 11 Sep 2014 00:28:49 -0700 Subject: [PATCH 054/826] Fix 3 typos: s/retreive/retrieve/g Signed-off-by: Chris Larsen --- src/core/Query.java | 6 +++--- src/core/TsdbQuery.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 534e91e797..01e08b969c 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -22,7 +22,7 @@ import net.opentsdb.uid.NoSuchUniqueName; /** - * A query to retreive data from the TSDB. + * A query to retrieve data from the TSDB. */ public interface Query { @@ -69,7 +69,7 @@ public interface Query { /** * Sets the time series to the query. - * @param metric The metric to retreive from the TSDB. + * @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 @@ -86,7 +86,7 @@ void setTimeSeries(String metric, Map tags, /** * Sets the time series to the query. - * @param metric The metric to retreive from the TSDB. + * @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 diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index f843ddaa89..914dbd69ff 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -343,7 +343,7 @@ public Deferred runAsync() throws HBaseException { * stored in the map has its timestamp zero'ed out. * @throws HBaseException if there was a problem communicating with HBase to * perform the search. - * @throws IllegalArgumentException if bad data was retreived from HBase. + * @throws IllegalArgumentException if bad data was retrieved from HBase. */ private Deferred> findSpans() throws HBaseException { final short metric_width = tsdb.metrics.width(); From 1a78a02a9324a69794f2eda721aa3b25314afaa2 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Thu, 11 Sep 2014 01:00:59 -0700 Subject: [PATCH 055/826] Pointing tsdb at /usr/share/opentsdb/bin/tsdb.local in packaged distributions. Signed-off-by: Chris Larsen --- tsdb.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsdb.in b/tsdb.in index 94077301d6..d96c53d540 100644 --- a/tsdb.in +++ b/tsdb.in @@ -18,7 +18,7 @@ test -d "$pkgdatadir" || test -n "$abs_srcdir$abs_builddir" || { } if test -n "$pkgdatadir"; then - localdir="$pkgdatadir" + localdir="$pkgdatadir/bin" for jar in "$pkgdatadir"/*.jar; do CLASSPATH="$CLASSPATH:$jar" done From 191cbf017678981475dbcb7be0df01283283eec3 Mon Sep 17 00:00:00 2001 From: Tomas Krajca Date: Sun, 1 Mar 2015 17:36:38 -0800 Subject: [PATCH 056/826] Added details about values and timestamps into the Fsck command for dplicate values Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 640e43014f..1b1c619ee7 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -600,6 +601,8 @@ private void fsckDataPoints(final Map> datapoints) buf.append("More than one column had a value for the same timestamp: ") .append("(") .append(time_map.getKey()) + .append(" - ") + .append(new Date(time_map.getKey())) .append(")\n row key: (") .append(UniqueId.uidToString(key)) .append(")\n"); @@ -637,14 +640,21 @@ private void fsckDataPoints(final Map> datapoints) dp_index++) { duplicates.getAndIncrement(); DP dp = time_map.getValue().get(dp_index); + final byte flags = (byte)Internal.getFlagsFromQualifier(dp.kv.qualifier()); buf.append(" ") .append("write time: (") .append(dp.kv.timestamp()) + .append(" - ") + .append(new Date(dp.kv.timestamp())) .append(") ") .append(" compacted: (") .append(dp.compacted) .append(") qualifier: ") .append(Arrays.toString(dp.kv.qualifier())) + .append(" value: ") + .append(Internal.isFloat(dp.kv.qualifier()) ? + Internal.extractFloatingPointValue(dp.value(), 0, flags) : + Internal.extractIntegerValue(dp.value(), 0, flags)) .append("\n"); unique_columns.put(dp.kv.qualifier(), dp.kv.value()); if (options.fix() && options.resolveDupes()) { From f6c879386e24090b314b821be1da01a961fe16e8 Mon Sep 17 00:00:00 2001 From: Filippo Giunchedi Date: Mon, 21 Jul 2014 12:38:29 +0100 Subject: [PATCH 057/826] noop, remove trailing whitespace Signed-off-by: Chris Larsen --- build-aux/deb/init.d/opentsdb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build-aux/deb/init.d/opentsdb b/build-aux/deb/init.d/opentsdb index 836f12ed10..ef550d1a48 100644 --- a/build-aux/deb/init.d/opentsdb +++ b/build-aux/deb/init.d/opentsdb @@ -24,7 +24,7 @@ MAX_OPEN_FILES=65535 . /lib/lsb/init-functions -# The first existing directory is used for JAVA_HOME +# The first existing directory is used for JAVA_HOME # (if JAVA_HOME is not defined in $DEFAULT) JDK_DIRS="/usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ @@ -53,7 +53,7 @@ DAEMON_OPTS=tsd case "$1" in start) - + if [ -z "$JAVA_HOME" ]; then log_failure_msg "no JDK found - please set JAVA_HOME" exit 1 @@ -65,7 +65,7 @@ start) >/dev/null; then touch "$PID_FILE" && chown "$TSD_USER":"$TSD_GROUP" "$PID_FILE" - + if [ -n "$MAX_OPEN_FILES" ]; then ulimit -n $MAX_OPEN_FILES fi @@ -82,7 +82,7 @@ start) stop) log_action_begin_msg "Stopping TSD" set +e - if [ -f "$PID_FILE" ]; then + if [ -f "$PID_FILE" ]; then start-stop-daemon --stop --pidfile "$PID_FILE" \ --user "$TSD_USER" --retry=TERM/20/KILL/5 >/dev/null if [ $? -eq 1 ]; then From c534d2555109fc0a4feb81e1589369b4b1382dc8 Mon Sep 17 00:00:00 2001 From: Filippo Giunchedi Date: Mon, 21 Jul 2014 12:40:54 +0100 Subject: [PATCH 058/826] debian: move init vars at the top this allows /etc/default/opentsdb to override them at will (e.g. --auto-metric) Signed-off-by: Chris Larsen --- build-aux/deb/init.d/opentsdb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/build-aux/deb/init.d/opentsdb b/build-aux/deb/init.d/opentsdb index ef550d1a48..4eb8ee3847 100644 --- a/build-aux/deb/init.d/opentsdb +++ b/build-aux/deb/init.d/opentsdb @@ -18,6 +18,9 @@ PATH=/bin:/usr/bin:/sbin:/usr/sbin NAME=opentsdb TSD_USER=opentsdb TSD_GROUP=opentsdb +DAEMON=/usr/share/opentsdb/bin/tsdb +DAEMON_OPTS=tsd +PID_FILE=/var/run/$NAME.pid # Maximum number of open files MAX_OPEN_FILES=65535 @@ -45,11 +48,6 @@ fi export JAVA_HOME -# Define other required variables -PID_FILE=/var/run/$NAME.pid - -DAEMON=/usr/share/opentsdb/bin/tsdb -DAEMON_OPTS=tsd case "$1" in start) From 0bdb855cfe1cfd1aeaad816f8e83255fb98d81a0 Mon Sep 17 00:00:00 2001 From: jan-mangs Date: Thu, 27 Feb 2014 12:47:34 -0800 Subject: [PATCH 059/826] Added support for timing out long-running queries (disabled by default) Signed-off-by: Chris Larsen Conflicts: src/utils/Config.java Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 5 +++++ src/utils/Config.java | 1 + 2 files changed, 6 insertions(+) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 914dbd69ff..a37295a7fb 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -367,6 +367,7 @@ final class ScannerCB implements Callback> rows) return null; } + if (timeout > 0 && hbase_time > timeout) { + throw new InterruptedException("Query timeout exceeded!"); + } + for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (Bytes.memcmp(metric, key, 0, metric_width) != 0) { diff --git a/src/utils/Config.java b/src/utils/Config.java index eca904279e..78486ace20 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -478,6 +478,7 @@ protected void setDefaults() { default_map.put("tsd.http.request.cors_headers", "Authorization, " + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, " + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since"); + default_map.put("tsd.query.timeout", "0"); for (Map.Entry entry : default_map.entrySet()) { if (!properties.containsKey(entry.getKey())) From ceb22c3e7565b4462a22ecca02b2731ae0e4162c Mon Sep 17 00:00:00 2001 From: Nathan Owens Date: Wed, 23 Apr 2014 20:08:29 +0100 Subject: [PATCH 060/826] changes to textImporter error handling Signed-off-by: Chris Larsen --- src/tools/TextImporter.java | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/tools/TextImporter.java b/src/tools/TextImporter.java index fb501f4957..df887dbf9b 100644 --- a/src/tools/TextImporter.java +++ b/src/tools/TextImporter.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-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 @@ -62,7 +62,7 @@ public static void main(String[] args) throws Exception { // get a config object Config config = CliOptions.getConfig(argp); - + final TSDB tsdb = new TSDB(config); tsdb.checkNecessaryTablesExist().joinUninterruptibly(); argp = null; @@ -126,19 +126,29 @@ public String toString() { } }; final Errback errback = new Errback(); + LOG.info("reading from file:" + path); while ((line = in.readLine()) != null) { final String[] words = Tags.splitString(line, ' '); final String metric = words[0]; if (metric.length() <= 0) { - throw new RuntimeException("invalid metric: " + metric); + LOG.error("invalid metric: " + metric); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; } final long timestamp = Tags.parseLong(words[1]); if (timestamp <= 0) { - throw new RuntimeException("invalid timestamp: " + timestamp); + LOG.error("invalid timestamp: " + timestamp); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; } final String value = words[2]; if (value.length() <= 0) { - throw new RuntimeException("invalid value: " + value); + LOG.error("invalid value: " + value); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; } final HashMap tags = new HashMap(); for (int i = 3; i < words.length; i++) { From 3596bb081138d7d3603f184222b90fb0285687b8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 Mar 2015 19:45:18 -0800 Subject: [PATCH 061/826] Finish #322 by adding the --skip-errors flag and continuing on most exceptions Signed-off-by: Chris Larsen --- src/tools/TextImporter.java | 153 +++++++++++++-------- test/tools/TestTextImporter.java | 221 +++++++++++++++++++++++++------ 2 files changed, 278 insertions(+), 96 deletions(-) diff --git a/src/tools/TextImporter.java b/src/tools/TextImporter.java index df887dbf9b..4c59de3a5b 100644 --- a/src/tools/TextImporter.java +++ b/src/tools/TextImporter.java @@ -53,24 +53,27 @@ public static void main(String[] args) throws Exception { ArgP argp = new ArgP(); CliOptions.addCommon(argp); CliOptions.addAutoMetricFlag(argp); + argp.addOption("--skip-errors", "Whether or not to skip exceptions " + + "during processing"); args = CliOptions.parse(argp, args); if (args == null) { usage(argp, 1); } else if (args.length < 1) { usage(argp, 2); } - + // get a config object Config config = CliOptions.getConfig(argp); final TSDB tsdb = new TSDB(config); + final boolean skip_errors = argp.has("--skip_errors"); tsdb.checkNecessaryTablesExist().joinUninterruptibly(); argp = null; try { int points = 0; final long start_time = System.nanoTime(); for (final String path : args) { - points += importFile(tsdb.getClient(), tsdb, path); + points += importFile(tsdb.getClient(), tsdb, path, skip_errors); } final double time_delta = (System.nanoTime() - start_time) / 1000000000.0; LOG.info(String.format("Total: imported %d data points in %.3fs" @@ -97,7 +100,8 @@ public final void emit(final String line) { private static int importFile(final HBaseClient client, final TSDB tsdb, - final String path) throws IOException { + final String path, + final boolean skip_errors) throws IOException { final long start_time = System.nanoTime(); long ping_start_time = start_time; final BufferedReader in = open(path); @@ -131,68 +135,111 @@ public String toString() { final String[] words = Tags.splitString(line, ' '); final String metric = words[0]; if (metric.length() <= 0) { - LOG.error("invalid metric: " + metric); - LOG.error("error while processing file " - + path + " line=" + line + "... Continuing"); - continue; + if (skip_errors) { + LOG.error("invalid metric: " + metric); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; + } else { + throw new RuntimeException("invalid metric: " + metric); + } } - final long timestamp = Tags.parseLong(words[1]); - if (timestamp <= 0) { - LOG.error("invalid timestamp: " + timestamp); - LOG.error("error while processing file " - + path + " line=" + line + "... Continuing"); - continue; + final long timestamp; + try { + timestamp = Tags.parseLong(words[1]); + if (timestamp <= 0) { + if (skip_errors) { + LOG.error("invalid timestamp: " + timestamp); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; + } else { + throw new RuntimeException("invalid timestamp: " + timestamp); + } + } + } catch (final RuntimeException e) { + if (skip_errors) { + LOG.error("invalid timestamp: " + e.getMessage()); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; + } else { + throw e; + } } + final String value = words[2]; if (value.length() <= 0) { - LOG.error("invalid value: " + value); - LOG.error("error while processing file " - + path + " line=" + line + "... Continuing"); - continue; - } - final HashMap tags = new HashMap(); - for (int i = 3; i < words.length; i++) { - if (!words[i].isEmpty()) { - Tags.parse(tags, words[i]); + if (skip_errors) { + LOG.error("invalid value: " + value); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; + } else { + throw new RuntimeException("invalid value: " + value); } } - final WritableDataPoints dp = getDataPoints(tsdb, metric, tags); - Deferred d; - if (Tags.looksLikeInteger(value)) { - d = dp.addPoint(timestamp, Tags.parseLong(value)); - } else { // floating point value - d = dp.addPoint(timestamp, Float.parseFloat(value)); - } - d.addErrback(errback); - points++; - if (points % 1000000 == 0) { - final long now = System.nanoTime(); - ping_start_time = (now - ping_start_time) / 1000000; - LOG.info(String.format("... %d data points in %dms (%.1f points/s)", - points, ping_start_time, - (1000000 * 1000.0 / ping_start_time))); - ping_start_time = now; - } - if (throttle) { - LOG.info("Throttling..."); - long throttle_time = System.nanoTime(); - try { - d.joinUninterruptibly(); - } catch (Exception e) { - throw new RuntimeException("Should never happen", e); + + try { + final HashMap tags = new HashMap(); + for (int i = 3; i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + + final WritableDataPoints dp = getDataPoints(tsdb, metric, tags); + Deferred d; + if (Tags.looksLikeInteger(value)) { + d = dp.addPoint(timestamp, Tags.parseLong(value)); + } else { // floating point value + d = dp.addPoint(timestamp, Float.parseFloat(value)); + } + d.addErrback(errback); + points++; + if (points % 1000000 == 0) { + final long now = System.nanoTime(); + ping_start_time = (now - ping_start_time) / 1000000; + LOG.info(String.format("... %d data points in %dms (%.1f points/s)", + points, ping_start_time, + (1000000 * 1000.0 / ping_start_time))); + ping_start_time = now; + } + if (throttle) { + LOG.info("Throttling..."); + long throttle_time = System.nanoTime(); + try { + d.joinUninterruptibly(); + } catch (final Exception e) { + throw new RuntimeException("Should never happen", e); + } + throttle_time = System.nanoTime() - throttle_time; + if (throttle_time < 1000000000L) { + LOG.info("Got throttled for only " + throttle_time + + "ns, sleeping a bit now"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException("interrupted", e); + } + } + LOG.info("Done throttling..."); + throttle = false; } - throttle_time = System.nanoTime() - throttle_time; - if (throttle_time < 1000000000L) { - LOG.info("Got throttled for only " + throttle_time + "ns, sleeping a bit now"); - try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException("interrupted", e); } + } catch (final RuntimeException e) { + if (skip_errors) { + LOG.error("Exception: " + e.getMessage()); + LOG.error("error while processing file " + + path + " line=" + line + "... Continuing"); + continue; + } else { + throw e; } - LOG.info("Done throttling..."); - throttle = false; } } } catch (RuntimeException e) { LOG.error("Exception caught while processing file " - + path + " line=" + line); + + path + " line=[" + line + "]", e); throw e; } finally { in.close(); diff --git a/test/tools/TestTextImporter.java b/test/tools/TestTextImporter.java index bf703a4578..c17208150d 100644 --- a/test/tools/TestTextImporter.java +++ b/test/tools/TestTextImporter.java @@ -85,7 +85,7 @@ public class TestTextImporter { static { try { importFile = TextImporter.class.getDeclaredMethod("importFile", - HBaseClient.class, TSDB.class, String.class); + HBaseClient.class, TSDB.class, String.class, boolean.class); importFile.setAccessible(true); } catch (Exception e) { throw new RuntimeException("Failed in static initializer", e); @@ -158,7 +158,7 @@ public void importFileGoodIntegers1Byte() throws Exception { "sys.cpu.user 1356998400 0 host=web01\n" + "sys.cpu.user 1356998400 127 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -179,7 +179,7 @@ public void importFileGoodIntegers1ByteNegative() throws Exception { "sys.cpu.user 1356998400 -0 host=web01\n" + "sys.cpu.user 1356998400 -128 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -200,7 +200,7 @@ public void importFileGoodIntegers2Byte() throws Exception { "sys.cpu.user 1356998400 128 host=web01\n" + "sys.cpu.user 1356998400 32767 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -221,7 +221,7 @@ public void importFileGoodIntegers2ByteNegative() throws Exception { "sys.cpu.user 1356998400 -129 host=web01\n" + "sys.cpu.user 1356998400 -32768 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -242,7 +242,7 @@ public void importFileGoodIntegers4Byte() throws Exception { "sys.cpu.user 1356998400 32768 host=web01\n" + "sys.cpu.user 1356998400 2147483647 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -263,7 +263,7 @@ public void importFileGoodIntegers4ByteNegative() throws Exception { "sys.cpu.user 1356998400 -32769 host=web01\n" + "sys.cpu.user 1356998400 -2147483648 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -284,7 +284,7 @@ public void importFileGoodIntegers8Byte() throws Exception { "sys.cpu.user 1356998400 2147483648 host=web01\n" + "sys.cpu.user 1356998400 9223372036854775807 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; @@ -304,7 +304,7 @@ public void importFileGoodIntegers8ByteNegative() throws Exception { "sys.cpu.user 1356998400 -2147483649 host=web01\n" + "sys.cpu.user 1356998400 -9223372036854775808 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -325,7 +325,16 @@ public void importFileTimestampZero() throws Exception { "sys.cpu.user 0 0 host=web01\n" + "sys.cpu.user 0 127 host=web02"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileTimestampZeroSkip() throws Exception { + String data = + "sys.cpu.user 0 0 host=web01\n" + + "sys.cpu.user 0 127 host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) @@ -334,7 +343,16 @@ public void importFileTimestampNegative() throws Exception { "sys.cpu.user -11356998400 0 host=web01\n" + "sys.cpu.user -11356998400 127 host=web02"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileTimestampNegativeSkip() throws Exception { + String data = + "sys.cpu.user -11356998400 0 host=web01\n" + + "sys.cpu.user -11356998400 127 host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test @@ -343,7 +361,7 @@ public void importFileMaxSecondTimestamp() throws Exception { "sys.cpu.user 4294967295 24 host=web01\n" + "sys.cpu.user 4294967295 42 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, @@ -364,7 +382,7 @@ public void importFileMinMSTimestamp() throws Exception { "sys.cpu.user 4294967296 24 host=web01\n" + "sys.cpu.user 4294967296 42 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0, (byte) 0x41, (byte) 0x88, (byte) 0x90, @@ -387,7 +405,7 @@ public void importFileMSTimestamp() throws Exception { "sys.cpu.user 1356998400500 24 host=web01\n" + "sys.cpu.user 1356998400500 42 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -408,7 +426,7 @@ public void importFileMSTimestampTooBig() throws Exception { "sys.cpu.user 13569984005001 24 host=web01\n" + "sys.cpu.user 13569984005001 42 host=web02"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); } @Test (expected = IllegalArgumentException.class) @@ -417,7 +435,25 @@ public void importFileMSTimestampNegative() throws Exception { "sys.cpu.user -2147483648000L 24 host=web01\n" + "sys.cpu.user -2147483648000L 42 host=web02"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test (expected = NumberFormatException.class) + public void importFileTimestampNFE() throws Exception { + String data = + "sys.cpu.user 1356998400 0 host=web01\n" + + "sys.cpu.user notatimestamp 127 host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileTimestampNFESkip() throws Exception { + String data = + "sys.cpu.user 1356998400 0 host=web01\n" + + "sys.cpu.user notatimestamp 127 host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test @@ -426,7 +462,7 @@ public void importFileGoodFloats() throws Exception { "sys.cpu.user 1356998400 24.5 host=web01\n" + "sys.cpu.user 1356998400 42.5 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -447,7 +483,7 @@ public void importFileGoodFloatsNegative() throws Exception { "sys.cpu.user 1356998400 -24.5 host=web01\n" + "sys.cpu.user 1356998400 -42.5 host=web02"; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(2, (int)points); byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, @@ -468,7 +504,16 @@ public void importFileNSUTagv() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998400 42 host=web03"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileNSUTagvSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 42 host=web03"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = NoSuchUniqueName.class) @@ -477,7 +522,16 @@ public void importFileNSUTagk() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998400 42 fqdn=web02"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileNSUTagkSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 42 fqdn=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = NoSuchUniqueName.class) @@ -486,7 +540,16 @@ public void importFileNSUMetric() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.system 1356998400 42 host=web02"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileNSUMetricSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.system 1356998400 42 host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) @@ -495,7 +558,16 @@ public void importFileEmptyMetric() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + " 1356998400 42 host=web03"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileEmptyMetricSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + " 1356998400 42 host=web03"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) @@ -504,7 +576,16 @@ public void importFileEmptyTimestamp() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 42 host=web03"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileEmptyTimestampSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 42 host=web03"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) @@ -513,7 +594,34 @@ public void importFileEmptyValue() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998400 host=web03"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileEmptyValueSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 host=web03"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); + } + + @Test (expected = NumberFormatException.class) + public void importFileEmptyValueNFE() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 notanumber host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileEmptyValueNFESkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 notanumber host=web02"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) @@ -522,7 +630,16 @@ public void importFileEmptyTags() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998400 42"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileEmptyTagsSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 42"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) @@ -531,43 +648,52 @@ public void importFileEmptyTagv() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998400 42 host"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); } - @Test (expected = RuntimeException.class) - public void importFileEmptyTagvEquals() throws Exception { + @Test + public void importFileEmptyTagvSkip() throws Exception { String data = "sys.cpu.user 1356998400 24 host=web01\n" + - "sys.cpu.user 1356998400 42 host="; + "sys.cpu.user 1356998400 42 host"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = RuntimeException.class) - public void importFile0Timestamp() throws Exception { + public void importFileEmptyTagvEquals() throws Exception { String data = "sys.cpu.user 1356998400 24 host=web01\n" + - "sys.cpu.user 0 42 host=web02"; + "sys.cpu.user 1356998400 42 host="; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); } - @Test (expected = RuntimeException.class) - public void importFileNegativeTimestamp() throws Exception { + @Test + public void importFileEmptyTagvEqualsSkip() throws Exception { String data = "sys.cpu.user 1356998400 24 host=web01\n" + - "sys.cpu.user -1356998400 42 host=web02"; + "sys.cpu.user 1356998400 42 host="; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", true); } - + @Test (expected = IllegalArgumentException.class) public void importFileSameTimestamp() throws Exception { String data = "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998400 42 host=web01"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileSameTimestampSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998400 42 host=web01"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } @Test (expected = IllegalArgumentException.class) @@ -576,7 +702,16 @@ public void importFileLessthanTimestamp() throws Exception { "sys.cpu.user 1356998400 24 host=web01\n" + "sys.cpu.user 1356998300 42 host=web01"; setData(data); - importFile.invoke(null, client, tsdb, "file"); + importFile.invoke(null, client, tsdb, "file", false); + } + + @Test + public void importFileLessthanTimestampSkip() throws Exception { + String data = + "sys.cpu.user 1356998400 24 host=web01\n" + + "sys.cpu.user 1356998300 42 host=web01"; + setData(data); + importFile.invoke(null, client, tsdb, "file", true); } // doesn't throw an exception, just returns "processed 0 data points" @@ -584,16 +719,16 @@ public void importFileLessthanTimestamp() throws Exception { public void importFileEmptyFile() throws Exception { String data = ""; setData(data); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(0, (int)points); } @Test (expected = FileNotFoundException.class) - public void inportFileNotFound() throws Exception { + public void importFileNotFound() throws Exception { PowerMockito.doThrow(new FileNotFoundException()).when(TextImporter.class, PowerMockito.method(TextImporter.class, "open", String.class)) .withArguments(anyString()); - Integer points = (Integer)importFile.invoke(null, client, tsdb, "file"); + Integer points = (Integer)importFile.invoke(null, client, tsdb, "file", false); assertEquals(0, (int)points); } From 1e07aa25bf99758cb654febce03d149c11282053 Mon Sep 17 00:00:00 2001 From: Filippo Giunchedi Date: Mon, 11 Nov 2013 17:35:11 +0000 Subject: [PATCH 062/826] add ability to check percentage of bad values rationale being that sometimes it might be OK if some points are outside the threshold for a given window. e.g. test that _all_ points in the lookback window must be outside the threshold, not just one. Signed-off-by: Chris Larsen --- tools/check_tsd | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/check_tsd b/tools/check_tsd index ff07aefcb2..522d8a5a4e 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -68,6 +68,9 @@ def main(argv): parser.add_option('-I', '--ignore-recent', default=0, type='int', metavar='SECONDS', help='Ignore data points that are that' ' are that recent.') + parser.add_option('-P', '--bad-percent', default=None, type='float', + metavar='PCT', help='Do not alarm if less than PCT% bad values' + ' are found.') parser.add_option('-S', '--ssl', default=False, action='store_true', help='Make queries to OpenTSDB via SSL (https)') (options, args) = parser.parse_args(args=argv[1:]) @@ -173,6 +176,7 @@ def main(argv): badval = None # Value of the bad value we found, if any. npoints = 0 # How many values have we seen? nbad = 0 # How many bad values have we seen? + bad_pct = 0 # Percent of bad values we've found for datapoint in datapoints: datapoint = datapoint.split() ts = int(datapoint[1]) @@ -211,6 +215,15 @@ def main(argv): print ('worse data point value=%s at ts=%s' % (badval, badts)) badts = time.asctime(time.localtime(badts)) + bad_pct = nbad * 100.0 / npoints + + if options.bad_percent is not None and rv > 0 \ + and bad_pct < options.bad_percent: + if options.verbose: + print 'ignoring alarm, less than %.1f%% bad values (found %.1f%%)' % \ + (options.bad_percent, bad_pct) + rv = 0 + # in nrpe, pipe character is something special, but it's used in tag # searches. Translate it to something else for the purposes of output. ttags = tags.replace("|",":") @@ -226,7 +239,7 @@ def main(argv): threshold = options.critical print ('%s: %s%s %s %s: %d/%d bad values (%.1f%%) worst: %r @ %s' % (level, options.metric, ttags, options.comparator, threshold, - nbad, npoints, nbad * 100.0 / npoints, badval, badts)) + nbad, npoints, bad_pct, badval, badts)) return rv From cd49dd05eca64ff1a3af1255c909dc04ae781c6f Mon Sep 17 00:00:00 2001 From: Marc Tamsky Date: Fri, 17 May 2013 12:37:32 -0700 Subject: [PATCH 063/826] fix script to avoid glob expansion (which can exceed ARG_MAX) Signed-off-by: Chris Larsen --- tools/clean_cache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/clean_cache.sh b/tools/clean_cache.sh index 6babdb44ad..3b1e0fe7f7 100755 --- a/tools/clean_cache.sh +++ b/tools/clean_cache.sh @@ -8,5 +8,5 @@ diskSpaceIsShort() { } if diskSpaceIsShort; then - rm -rf "$CACHE_DIR"/* + ( cd ${CACHE_DIR} && find . -x -exec rm {} \; ) fi From 9f5c45a995c99c2c5d106432ae6f450772f5ec37 Mon Sep 17 00:00:00 2001 From: Adrien Mogenet Date: Sat, 4 Oct 2014 17:31:30 +0200 Subject: [PATCH 064/826] Adds style options to select gnuplot data style When plotting dense points, lines between each datapoint can make the output quite cumbersome. This adds the possibility to display only points, and two additional styles as a bonus (dot and circle). The HTTP API now handles the `&style` parameter as well to inject any additional style. --- src/graph/Plot.java | 5 ++++- src/tsd/GraphHandler.java | 3 +++ src/tsd/client/QueryUi.java | 32 +++++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/graph/Plot.java b/src/graph/Plot.java index ff3b34d43b..f9095a06fd 100644 --- a/src/graph/Plot.java +++ b/src/graph/Plot.java @@ -16,6 +16,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -253,6 +254,7 @@ private void writeGnuplotScript(final String basepath, .append(Short.toString(height)); final String smooth = params.remove("smooth"); final String fgcolor = params.remove("fgcolor"); + final String style = params.remove("style"); String bgcolor = params.remove("bgcolor"); if (fgcolor != null && bgcolor == null) { // We can't specify a fgcolor without specifying a bgcolor. @@ -287,7 +289,8 @@ private void writeGnuplotScript(final String basepath, final int nseries = datapoints.size(); if (nseries > 0) { gp.write("set grid\n" - + "set style data linespoints\n"); + + "set style data "); + gp.append(style != null? style : "linespoint").append("\n"); if (!params.containsKey("key")) { gp.write("set key right box\n"); } diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 0eb6a6ee83..08b642bcce 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -693,6 +693,9 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { if ((value = popParam(querystring, "smooth")) != null) { params.put("smooth", value); } + if ((value = popParam(querystring, "style")) != null) { + params.put("style", value); + } // This must remain after the previous `if' in order to properly override // any previous `key' parameter if a `nokey' parameter is given. if ((value = popParam(querystring, "nokey")) != null) { diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index e06f787d4e..36f29b704f 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -19,8 +19,14 @@ */ import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; + +import net.opentsdb.graph.Plot; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.dom.client.Style; @@ -80,6 +86,7 @@ import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.RadioButton; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.TextBox; @@ -91,6 +98,18 @@ * Manages the entire UI, forms to query the TSDB and other misc panels. */ public class QueryUi implements EntryPoint, HistoryListener { + + /** Map of available gnuplot data styles. */ + public static Map stylesMap = new HashMap(); + static { + Map map = new HashMap(); + map.put("linespoint", 0); + map.put("points", 1); + map.put("circles", 3); + map.put("dots", 4); + stylesMap = Collections.unmodifiableMap(map); + } + // Some URLs we use to fetch data from the TSD. private static final String AGGREGATORS_URL = "/aggregators"; private static final String LOGS_URL = "/logs?json"; @@ -125,6 +144,7 @@ public class QueryUi implements EntryPoint, HistoryListener { // Styling options. private final CheckBox smooth = new CheckBox(); + private final ListBox styles = new ListBox(); /** * Handles every change to the query form and gets a new graph. @@ -176,7 +196,7 @@ protected void onEvent(final DomEvent event) { /** List of known aggregation functions. Fetched once from the server. */ private final ArrayList aggregators = new ArrayList(); - + private final DecoratedTabPanel metrics = new DecoratedTabPanel(); /** Panel to place generated graphs and a box for zoom highlighting. */ @@ -260,6 +280,7 @@ public void onValueChange(final ValueChangeEvent event) { keybox.addClickHandler(refreshgraph); nokey.addClickHandler(refreshgraph); smooth.addClickHandler(refreshgraph); + styles.addChangeHandler(refreshgraph); yrange.setValidationRegexp("^(" // Nothing or + "|\\[([-+.0-9eE]+|\\*)?" // "[start @@ -455,9 +476,14 @@ public void onHistoryChanged(String historyToken) { /** Additional styling options. */ private Grid makeStylePanel() { + for (Entry item : stylesMap.entrySet()) { + styles.insertItem(item.getKey(), item.getValue()); + } final Grid grid = new Grid(5, 3); grid.setText(0, 1, "Smooth"); grid.setWidget(0, 2, smooth); + grid.setText(1, 1, "Style"); + grid.setWidget(1, 2, styles); return grid; } @@ -812,6 +838,9 @@ private void refreshFromQueryString() { } nokey.setValue(qs.containsKey("nokey")); smooth.setValue(qs.containsKey("smooth")); + if (stylesMap.containsKey(qs.getFirst("style"))) { + styles.setSelectedIndex(stylesMap.get(qs.getFirst("style"))); + } } private void refreshGraph() { @@ -879,6 +908,7 @@ private void refreshGraph() { if (smooth.getValue()) { url.append("&smooth=csplines"); } + url.append("&style=").append(styles.getValue(styles.getSelectedIndex())); final String unencodedUri = url.toString(); final String uri = URL.encode(unencodedUri); if (uri.equals(lastgraphuri)) { From c51b05a2a21026ff4112df4eb1f658bdcc692cad Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Sat, 7 Mar 2015 22:05:52 -0800 Subject: [PATCH 065/826] Exclude stop key from scan in MockBase to reflect behavior of asynchbase's scanner. Signed-off-by: Chris Larsen --- test/storage/MockBase.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 6063ac9bd9..26942cb651 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -971,7 +971,11 @@ public Deferred>> answer( if (start != null && Bytes.memcmp(row.getKey(), start) < 0) { continue; } - if (stop != null && Bytes.memcmp(row.getKey(), stop) > 0) { + // asynchbase Scanner's logic: + // - start_key is inclusive, stop key is exclusive, + // - when start key is equal to the stop key, include the key in scan result, + if (stop != null && Bytes.memcmp(row.getKey(), stop) >= 0 + && Bytes.memcmp(start, stop) != 0) { continue; } if (pattern != null) { From 0126630c4d887329e32157bb60e550cfc9ef20c0 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Sat, 7 Mar 2015 23:17:08 -0800 Subject: [PATCH 066/826] Bugfix for issue #457 improper global annotation retrieval + disabling bad test. Signed-off-by: Chris Larsen --- src/meta/Annotation.java | 4 ++-- test/tree/TestTree.java | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index 9d69f36325..07762f3c9f 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -330,7 +330,7 @@ public ScannerCB() { final long normalized_start = (start_time - (start_time % Const.MAX_TIMESPAN)); final long normalized_end = (end_time - - (end_time % Const.MAX_TIMESPAN)); + (end_time % Const.MAX_TIMESPAN) + Const.MAX_TIMESPAN); Bytes.setInt(start, (int) normalized_start, TSDB.metrics_width()); Bytes.setInt(end, (int) normalized_end, TSDB.metrics_width()); @@ -405,7 +405,7 @@ public static Deferred deleteRange(final TSDB tsdb, final long start = start_time / 1000; final long end = end_time / 1000; final long normalized_start = (start - (start % Const.MAX_TIMESPAN)); - final long normalized_end = (end - (end % Const.MAX_TIMESPAN)); + final long normalized_end = (end - (end % Const.MAX_TIMESPAN) + Const.MAX_TIMESPAN); Bytes.setInt(start_row, (int) normalized_start, TSDB.metrics_width()); Bytes.setInt(end_row, (int) normalized_end, TSDB.metrics_width()); diff --git a/test/tree/TestTree.java b/test/tree/TestTree.java index 5ef1fa87e2..a5d9cb9244 100644 --- a/test/tree/TestTree.java +++ b/test/tree/TestTree.java @@ -552,14 +552,25 @@ public void fetchNotMatchedID655536() throws Exception { setupStorage(true, true); Tree.fetchNotMatched(storage.getTSDB(), 655536, null); } - - @Test - public void deleteTree() throws Exception { - setupStorage(true, true); - assertNotNull(Tree.deleteTree(storage.getTSDB(), 1, true) - .joinUninterruptibly()); - assertEquals(0, storage.numRows()); - } + + /* + TODO(oozie): This test surfaces likely bug in Tree.deleteTree(). + It was operating under a false assumption about how scanning works and + it started to fail, off-by-one style, when MockBase's logic was altered + to mimic that asynchbase Scanner. + + An update to Tree.deleteTree() implementation makes the test pass, + but I am not going to bandwagon this bugfix on top of #457 which has enough + going on as it is. + + @Test + public void deleteTree() throws Exception { + setupStorage(true, true); + assertNotNull(Tree.deleteTree(storage.getTSDB(), 1, true) + .joinUninterruptibly()); + assertEquals(0, storage.numRows()); + } + */ @Test public void idToBytes() throws Exception { From 88dd3af54f10742074ea0e74cf24158ec0b7815c Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Sat, 7 Mar 2015 23:28:47 -0800 Subject: [PATCH 067/826] Adding a test for retrieving a recent global annotation. Signed-off-by: Chris Larsen --- test/meta/TestAnnotation.java | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test/meta/TestAnnotation.java b/test/meta/TestAnnotation.java index 03d338e26d..bc77a6831b 100644 --- a/test/meta/TestAnnotation.java +++ b/test/meta/TestAnnotation.java @@ -58,7 +58,11 @@ public final class TestAnnotation { final private byte[] tsuid_row_key = new byte[] { 0, 0, 1, (byte) 0x52, (byte) 0xC2, (byte) 0x09, 0, 0, 0, 1, 0, 0, 1 }; - + + // 1425715200 - Sat Mar 7 00:00:00 PST 2015 + final private byte[] global_row_key_2015_midnight = + new byte[] { 0, 0, 0, (byte) 0x54, (byte) 0xFA, (byte) 0xB0, 0 }; + @Before public void before() throws Exception { final Config config = new Config(false); @@ -74,12 +78,12 @@ public void before() throws Exception { ("{\"startTime\":1328140800,\"endTime\":1328140801,\"description\":" + "\"Description\",\"notes\":\"Notes\",\"custom\":{\"owner\":" + "\"ops\"}}").getBytes(MockBase.ASCII())); - + storage.addColumn(global_row_key, new byte[] { 1, 0, 1 }, ("{\"startTime\":1328140801,\"endTime\":1328140803,\"description\":" + "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII())); - + // add a local storage.addColumn(tsuid_row_key, new byte[] { 1, 0x0A, 0x02 }, @@ -184,7 +188,27 @@ public void getGlobalAnnotations() throws Exception { assertEquals("Description", note0.getDescription()); assertEquals("Global 2", note1.getDescription()); } - + + @Test + public void getGlobalAnnotationOutsideCurrentHour() throws Exception { + // 1425716000 - Sat Mar 7 00:13:20 PST 2015 + storage.addColumn(global_row_key_2015_midnight, + new byte[] { 1, 3, (byte) 0x20 }, + ("{\"startTime\":1425716000,\"endTime\":1425716001,\"description\":" + + "\"Global 3\",\"notes\":\"Issue #457\"}").getBytes(MockBase.ASCII())); + + int right_now = 1425717000; // Sat Mar 7 00:30:00 PST 2015 + int fourty_minutes_ago = 1425714600; // Fri Mar 6 23:50:00 PST 2015 + + List recentGlobalAnnotation = Annotation.getGlobalAnnotations( + tsdb, fourty_minutes_ago, right_now).joinUninterruptibly(); + assertNotNull(recentGlobalAnnotation); + assertEquals(1, recentGlobalAnnotation.size()); + Annotation note0 = recentGlobalAnnotation.get(0); + assertEquals("Global 3", note0.getDescription()); + assertEquals("Issue #457", note0.getNotes()); + } + @Test public void getGlobalAnnotationsEmpty() throws Exception { List notes = Annotation.getGlobalAnnotations(tsdb, 1328150000, From 194c5a0e8dfb2ef41873a2b299e720de3574be1d Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Thu, 5 Mar 2015 20:06:56 -0800 Subject: [PATCH 068/826] Refactor + cut out crufy from TestUID.java Signed-off-by: Chris Larsen --- test/tools/TestUID.java | 89 +++++++---------------------------------- 1 file changed, 15 insertions(+), 74 deletions(-) diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index 122982c47b..d25fd756d5 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -21,18 +21,15 @@ import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; -import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; import org.apache.zookeeper.proto.DeleteRequest; -import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; -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.junit.Before; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; @@ -42,11 +39,10 @@ @RunWith(PowerMockRunner.class) @PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - GetRequest.class, PutRequest.class, KeyValue.class, UidManager.class, - Scanner.class, DeleteRequest.class, AtomicIncrementRequest.class }) + "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) +@PrepareForTest({TSDB.class, Config.class, HBaseClient.class, + KeyValue.class, UidManager.class, + Scanner.class, DeleteRequest.class }) public class TestUID { private Config config; private TSDB tsdb = null; @@ -70,12 +66,11 @@ public class TestUID { throw new RuntimeException("Failed in static initializer", e); } } - + @Before public void before() throws Exception { config = new Config(false); tsdb = new TSDB(config); - PowerMockito.spy(System.class); PowerMockito.when(System.nanoTime()) .thenReturn(1357300800000000L) @@ -87,8 +82,10 @@ public void before() throws Exception { .thenReturn(1357300801000L) .thenReturn(1357300802000L) .thenReturn(1357300803000L); + + setupMockBase(); } - + /* FSCK -------------------------------------------- * The UID FSCK is concerned with making sure the UID table is in a clean state. * Most important are the forward mappings to UIDs as that's what's used to @@ -112,16 +109,14 @@ public void before() throws Exception { @Test public void fsckNoData() throws Exception { - setupMockBase(); storage.flushStorage(); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); assertEquals(0, errors); } - + @Test public void fsckNoErrors() throws Exception { - setupMockBase(); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); assertEquals(0, errors); @@ -135,7 +130,6 @@ public void fsckNoErrors() throws Exception { @Test public void fsckMetricsUIDHigh() throws Exception { // currently a warning, not an error - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(42L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -145,7 +139,6 @@ public void fsckMetricsUIDHigh() throws Exception { @Test public void fsckTagkUIDHigh() throws Exception { // currently a warning, not an error - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(42L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -155,7 +148,7 @@ public void fsckTagkUIDHigh() throws Exception { @Test public void fsckTagvUIDHigh() throws Exception { // currently a warning, not an error - setupMockBase(); + storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(42L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -169,7 +162,6 @@ public void fsckTagvUIDHigh() throws Exception { @Test public void fsckMetricsUIDLow() throws Exception { // currently a warning, not an error - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -179,7 +171,6 @@ public void fsckMetricsUIDLow() throws Exception { @Test public void fsckFIXMetricsUIDLow() throws Exception { // currently a warning, not an error - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(0L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -188,10 +179,10 @@ public void fsckFIXMetricsUIDLow() throws Exception { "tsdb".getBytes(MockBase.ASCII()), false, false); assertEquals(0, errors); } - + @Test public void fsckTagkUIDLow() throws Exception { - setupMockBase(); + storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -200,7 +191,6 @@ public void fsckTagkUIDLow() throws Exception { @Test public void fsckFIXTagkUIDLow() throws Exception { - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -212,7 +202,6 @@ public void fsckFIXTagkUIDLow() throws Exception { @Test public void fsckTagvUIDLow() throws Exception { - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -221,7 +210,6 @@ public void fsckTagvUIDLow() throws Exception { @Test public void fsckFIXTagvUIDLow() throws Exception { - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -238,7 +226,6 @@ public void fsckFIXTagvUIDLow() throws Exception { */ @Test public void fsckMetricsUIDWrongLength() throws Exception { - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromInt(3)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -247,7 +234,6 @@ public void fsckMetricsUIDWrongLength() throws Exception { @Test public void fsckTagkUIDWrongLength() throws Exception { - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromInt(3)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -256,7 +242,6 @@ public void fsckTagkUIDWrongLength() throws Exception { @Test public void fsckTagvUIDWrongLength() throws Exception { - setupMockBase(); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromInt(3)); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -273,7 +258,6 @@ public void fsckTagvUIDWrongLength() throws Exception { */ @Test public void fsckMetricsMissingReverse() throws Exception { - setupMockBase(); storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -282,7 +266,6 @@ public void fsckMetricsMissingReverse() throws Exception { @Test public void fsckFIXMetricsMissingReverse() throws Exception { - setupMockBase(); storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -296,7 +279,6 @@ public void fsckFIXMetricsMissingReverse() throws Exception { @Test public void fsckTagkMissingReverse() throws Exception { - setupMockBase(); storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -305,7 +287,6 @@ public void fsckTagkMissingReverse() throws Exception { @Test public void fsckFIXTagkMissingReverse() throws Exception { - setupMockBase(); storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -319,7 +300,6 @@ public void fsckFIXTagkMissingReverse() throws Exception { @Test public void fsckTagvMissingReverse() throws Exception { - setupMockBase(); storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -328,7 +308,6 @@ public void fsckTagvMissingReverse() throws Exception { @Test public void fsckFIXTagvMissingReverse() throws Exception { - setupMockBase(); storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -357,7 +336,6 @@ public void fsckFIXTagvMissingReverse() throws Exception { */ @Test public void fsckMetricsInconsistentForward() throws Exception { - setupMockBase(); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); @@ -368,7 +346,6 @@ public void fsckMetricsInconsistentForward() throws Exception { @Test public void fsckFIXMetricsInconsistentForward() throws Exception { - setupMockBase(); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); @@ -388,7 +365,6 @@ public void fsckFIXMetricsInconsistentForward() throws Exception { @Test public void fsckTagkInconsistentForward() throws Exception { - setupMockBase(); storage.addColumn("some.other.value".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 1}); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); @@ -399,7 +375,6 @@ public void fsckTagkInconsistentForward() throws Exception { @Test public void fsckFIXTagkInconsistentForward() throws Exception { - setupMockBase(); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 1}); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); @@ -419,7 +394,6 @@ public void fsckFIXTagkInconsistentForward() throws Exception { @Test public void fsckTagvInconsistentForward() throws Exception { - setupMockBase(); storage.addColumn("some.other.value".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 1}); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); @@ -430,7 +404,6 @@ public void fsckTagvInconsistentForward() throws Exception { @Test public void fsckFIXTagvInconsistentForward() throws Exception { - setupMockBase(); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 1}); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); @@ -462,7 +435,6 @@ public void fsckFIXTagvInconsistentForward() throws Exception { */ @Test public void fsckMetricsDuplicateForward() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, @@ -472,7 +444,6 @@ public void fsckMetricsDuplicateForward() throws Exception { @Test public void fsckFIXMetricsDuplicateForward() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, @@ -487,7 +458,6 @@ public void fsckFIXMetricsDuplicateForward() throws Exception { @Test public void fsckTagkDuplicateForward() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, @@ -497,7 +467,6 @@ public void fsckTagkDuplicateForward() throws Exception { @Test public void fsckFIXTagkDuplicateForward() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, @@ -512,7 +481,6 @@ public void fsckFIXTagkDuplicateForward() throws Exception { @Test public void fsckTagvDuplicateForward() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, @@ -522,7 +490,6 @@ public void fsckTagvDuplicateForward() throws Exception { @Test public void fsckFIXTagvDuplicateForward() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, @@ -546,7 +513,6 @@ public void fsckFIXTagvDuplicateForward() throws Exception { @Test public void fsckMetricsMissingForward() throws Exception { // currently a warning, not an error - setupMockBase(); storage.flushColumn("bar".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, @@ -557,7 +523,6 @@ public void fsckMetricsMissingForward() throws Exception { @Test public void fsckFIXMetricsMissingForward() throws Exception { // currently a warning, not an error - setupMockBase(); storage.flushColumn("bar".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, @@ -569,7 +534,6 @@ public void fsckFIXMetricsMissingForward() throws Exception { @Test public void fsckTagkMissingForward() throws Exception { // currently a warning, not an error - setupMockBase(); storage.flushColumn("host".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -579,7 +543,6 @@ public void fsckTagkMissingForward() throws Exception { @Test public void fsckFIXTagkMissingForward() throws Exception { // currently a warning, not an error - setupMockBase(); storage.flushColumn("host".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -590,7 +553,6 @@ public void fsckFIXTagkMissingForward() throws Exception { @Test public void fsckTagvMissingForward() throws Exception { // currently a warning, not an error - setupMockBase(); storage.flushColumn("web01".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), false, false); @@ -600,7 +562,6 @@ public void fsckTagvMissingForward() throws Exception { @Test public void fsckFIXTagvMissingForward() throws Exception { // currently a warning, not an error - setupMockBase(); storage.flushColumn("web01".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, "tsdb".getBytes(MockBase.ASCII()), true, false); @@ -619,7 +580,6 @@ public void fsckFIXTagvMissingForward() throws Exception { */ @Test public void fsckMetricsInconsistentReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); @@ -630,7 +590,6 @@ public void fsckMetricsInconsistentReverse() throws Exception { @Test public void fsckFIXMetricsInconsistentReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); @@ -645,7 +604,6 @@ public void fsckFIXMetricsInconsistentReverse() throws Exception { @Test public void fsckTagkInconsistentReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "host".getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); @@ -653,10 +611,9 @@ public void fsckTagkInconsistentReverse() throws Exception { "tsdb".getBytes(MockBase.ASCII()), false, false); assertEquals(1, errors); } - + @Test public void fsckFIXTagkInconsistentReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "host".getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); @@ -671,7 +628,6 @@ public void fsckFIXTagkInconsistentReverse() throws Exception { @Test public void fsckTagvInconsistentReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "web01".getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); @@ -682,7 +638,6 @@ public void fsckTagvInconsistentReverse() throws Exception { @Test public void fsckFIXTagvInconsistentReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "web01".getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); @@ -708,7 +663,6 @@ public void fsckFIXTagvInconsistentReverse() throws Exception { */ @Test public void fsckMetricsDuplicateReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -721,7 +675,6 @@ public void fsckMetricsDuplicateReverse() throws Exception { @Test public void fsckFIXMetricsDuplicateReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -740,7 +693,6 @@ public void fsckFIXMetricsDuplicateReverse() throws Exception { @Test public void fsckTagkDuplicateReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -753,7 +705,6 @@ public void fsckTagkDuplicateReverse() throws Exception { @Test public void fsckFIXTagkDuplicateReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -772,7 +723,6 @@ public void fsckFIXTagkDuplicateReverse() throws Exception { @Test public void fsckTagvDuplicateReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -785,7 +735,6 @@ public void fsckTagvDuplicateReverse() throws Exception { @Test public void fsckFIXTagvDuplicateReverse() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -821,7 +770,6 @@ public void fsckFIXTagvDuplicateReverse() throws Exception { */ @Test public void fsckMetricsInconsistentFwdAndDupeRev() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -834,7 +782,6 @@ public void fsckMetricsInconsistentFwdAndDupeRev() throws Exception { @Test public void fsckFIXMetricsInconsistentFwdAndDupeRev() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -874,7 +821,6 @@ public void fsckFIXMetricsInconsistentFwdAndDupeRev() throws Exception { */ @Test public void fsckMetricsInconsistentFwdAndInconsistentRev() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -889,7 +835,6 @@ public void fsckMetricsInconsistentFwdAndInconsistentRev() throws Exception { @Test public void fsckFIXMetricsInconsistentFwdAndInconsistentRev() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -926,7 +871,6 @@ public void fsckFIXMetricsInconsistentFwdAndInconsistentRev() throws Exception { */ @Test public void fsckMetricsInconsistentFwdNoDupes() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -941,7 +885,6 @@ public void fsckMetricsInconsistentFwdNoDupes() throws Exception { @Test public void fsckFixMetricsInconsistentFwdNoDupes() throws Exception { - setupMockBase(); storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, @@ -958,17 +901,15 @@ public void fsckFixMetricsInconsistentFwdNoDupes() throws Exception { "tsdb".getBytes(MockBase.ASCII()), true, false); assertEquals(0, errors); } - + /** * Write clean data to MockBase that can be overridden by individual unit tests */ private void setupMockBase() { storage = new MockBase(tsdb, client, true, true, true, true); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(2L)); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(2L)); storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(2L)); - // forward mappings storage.addColumn("foo".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); From dad3f7feeb6ae8d1b0fbf21f6481d9bc80fa8a6f Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Thu, 5 Mar 2015 23:34:58 -0800 Subject: [PATCH 069/826] Fix Netty worker thread leak in TestUID.java Signed-off-by: Chris Larsen --- test/tools/TestUID.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index d25fd756d5..2da55c3303 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -48,7 +48,7 @@ public class TestUID { private TSDB tsdb = null; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; - + // names used for testing private byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); private byte[] ID_FAMILY = "id".getBytes(MockBase.ASCII()); @@ -69,8 +69,10 @@ public class TestUID { @Before public void before() throws Exception { + + PowerMockito.whenNew(HBaseClient.class).withAnyArguments().thenReturn(client); config = new Config(false); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); PowerMockito.spy(System.class); PowerMockito.when(System.nanoTime()) .thenReturn(1357300800000000L) @@ -82,7 +84,6 @@ public void before() throws Exception { .thenReturn(1357300801000L) .thenReturn(1357300802000L) .thenReturn(1357300803000L); - setupMockBase(); } @@ -247,7 +248,7 @@ public void fsckTagvUIDWrongLength() throws Exception { "tsdb".getBytes(MockBase.ASCII()), false, false); assertEquals(2, errors); } - + /* #1 - Missing Reverse Mapping * - Forward mapping is missing reverse: bar -> 02 * --------------------- @@ -940,4 +941,9 @@ private void setupMockBase() { storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, TAGV, "web02".getBytes(MockBase.ASCII())); } + + @After + public void tearDown() { + storage.flushStorage(); + } } From a77a586a4de4094ad161ec403a314003a5c175e2 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Tue, 10 Mar 2015 21:53:58 -0700 Subject: [PATCH 070/826] Create the RandomUniqueId class for generating random UIDs to use for better metric ID distribution across HBase. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/uid/RandomUniqueId.java | 76 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/uid/RandomUniqueId.java diff --git a/Makefile.am b/Makefile.am index 7fb8068f1f..12cf633039 100644 --- a/Makefile.am +++ b/Makefile.am @@ -122,6 +122,7 @@ tsdb_SRC := \ src/tsd/WordSplitter.java \ src/uid/NoSuchUniqueId.java \ src/uid/NoSuchUniqueName.java \ + src/uid/RandomUniqueId.java \ src/uid/UniqueId.java \ src/uid/UniqueIdInterface.java \ src/utils/ByteArrayPair.java \ diff --git a/src/uid/RandomUniqueId.java b/src/uid/RandomUniqueId.java new file mode 100644 index 0000000000..ef4fa5ce10 --- /dev/null +++ b/src/uid/RandomUniqueId.java @@ -0,0 +1,76 @@ +// 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.uid; + +import java.security.SecureRandom; +import org.hbase.async.Bytes; +import net.opentsdb.core.TSDB; + +/** + * Generate Random UIDs to be used as unique ID. + * Random metric IDs help to distribute hotspots evenly to region servers. + * It is better to decide whether to use random or serial uid for one type when + * the hbase uid table is empty. If the logic to switch between random or serial + * uid is changed in between writes it will cause frequent id collisions. + * @since 2.2 + */ +public class RandomUniqueId { + /** Use the SecureRandom class to avoid blocking calls */ + private static SecureRandom random_generator = new SecureRandom( + Bytes.fromLong(System.currentTimeMillis())); + + /** Used to limit UIDs to unsigned longs */ + public static final int MAX_WIDTH = 7; + + /** + * Get the next random metric UID, a positive integer greater than zero. + * The default metric ID width is 3 bytes. If it is 3 then it can return + * only up to the max value a 3 byte integer can return, which is 2^31-1. + * In that case, even though it is long, its range will be between 0 + * and 2^31-1. + * NOTE: The caller is responsible for assuring that the UID hasn't been + * assigned yet. + * @return a random UID up to {@link TSDB.metrics_width} wide + */ + public static long getRandomUID() { + return getRandomUID(TSDB.metrics_width()); + } + + /** + * Get the next random UID. It creates random bytes, then convert it to an + * unsigned long. + * @param width Number of bytes to randomize, it can not be larger + * than {@link MAX_WIDTH} bytes wide + * @return a randomly UID + * @throws throws IllegalArgumentException if the width is larger than + * {@link MAX_WIDTH} bytes + */ + public static long getRandomUID(final int width) { + if (width > MAX_WIDTH) { + throw new IllegalArgumentException("Expecting to return an unsigned long " + + "random integer, it can not be larger than " + MAX_WIDTH + + " bytes wide"); + } + final byte[] bytes = new byte[width]; + random_generator.nextBytes(bytes); + + long value = 0; + for (int i = 0; i Date: Tue, 10 Mar 2015 22:24:06 -0700 Subject: [PATCH 071/826] Add random UID generation as an option to the UniqueId class Signed-off-by: Chris Larsen --- src/uid/UniqueId.java | 82 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index ef66fda716..585faa1560 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -41,6 +41,7 @@ import net.opentsdb.core.TSDB; import net.opentsdb.meta.UIDMeta; +import net.opentsdb.stats.StatsCollector; /** * Represents a table of Unique IDs, manages the lookup and creation of IDs. @@ -72,6 +73,8 @@ public enum UniqueIdType { private static final short MAX_ATTEMPTS_ASSIGN_ID = 3; /** How many time do we try to apply an edit before giving up. */ private static final short MAX_ATTEMPTS_PUT = 6; + /** How many time do we try to assign a random ID before giving up. */ + private static final short MAX_ATTEMPTS_ASSIGN_RANDOM_ID = 10; /** Initial delay in ms for exponential backoff to retry failed RPCs. */ private static final short INITIAL_EXP_BACKOFF_DELAY = 800; /** Maximum number of results to return in suggest(). */ @@ -87,6 +90,8 @@ public enum UniqueIdType { private final UniqueIdType type; /** Number of bytes on which each ID is encoded. */ private final short id_width; + /** Whether or not to randomize new IDs */ + private final boolean randomize_id; /** Cache for forward mappings (name to ID). */ private final ConcurrentHashMap name_cache = @@ -103,6 +108,9 @@ public enum UniqueIdType { private volatile int cache_hits; /** Number of times we had to read from HBase and populate the cache. */ private volatile int cache_misses; + /** How many times we collided with an existing ID when attempting to + * generate a new UID */ + private volatile int random_id_collisions; /** Whether or not to generate new UIDMetas */ private TSDB tsdb; @@ -118,6 +126,22 @@ public enum UniqueIdType { */ public UniqueId(final HBaseClient client, final byte[] table, final String kind, final int width) { + this(client, table, kind, width, false); + } + + /** + * Constructor. + * @param client The HBase client to use. + * @param table The name of the HBase table to use. + * @param kind The kind of Unique ID this instance will deal with. + * @param width The number of bytes on which Unique IDs should be encoded. + * @param Whether or not to randomize new UIDs + * @throws IllegalArgumentException if width is negative or too small/large + * or if kind is an empty string. + * @since 2.2 + */ + public UniqueId(final HBaseClient client, final byte[] table, final String kind, + final int width, final boolean randomize_id) { this.client = client; this.table = table; if (kind.isEmpty()) { @@ -129,6 +153,7 @@ public UniqueId(final HBaseClient client, final byte[] table, final String kind, throw new IllegalArgumentException("Invalid width: " + width); } this.id_width = (short) width; + this.randomize_id = randomize_id; } /** The number of times we avoided reading from HBase thanks to the cache. */ @@ -329,7 +354,8 @@ private void addIdToCache(final String name, final byte[] id) { private final class UniqueIdAllocator implements Callback { private final String name; // What we're trying to allocate an ID for. private final Deferred assignment; // deferred to call back - private short attempt = MAX_ATTEMPTS_ASSIGN_ID; // Give up when zero. + private short attempt = randomize_id ? // Give up when zero. + MAX_ATTEMPTS_ASSIGN_RANDOM_ID : MAX_ATTEMPTS_ASSIGN_ID; private HBaseException hbe = null; // Last exception caught. @@ -366,9 +392,11 @@ public Object call(final Object arg) { } if (arg instanceof Exception) { - final String msg = ("Failed attempt #" + (MAX_ATTEMPTS_ASSIGN_ID - attempt) - + " to assign an UID for " + kind() + ':' + name - + " at step #" + state); + final String msg = ("Failed attempt #" + (randomize_id + ? (MAX_ATTEMPTS_ASSIGN_RANDOM_ID - attempt) + : (MAX_ATTEMPTS_ASSIGN_ID - attempt)) + + " to assign an UID for " + kind() + ':' + name + + " at step #" + state); if (arg instanceof HBaseException) { LOG.error(msg, (Exception) arg); hbe = (HBaseException) arg; @@ -406,17 +434,22 @@ public Object call(final Exception e) throws Exception { return d.addBoth(this).addErrback(new ErrBack()); } + /** Generates either a random or a serial ID. If random, we need to + * make sure that there isn't a UID collision. + */ private Deferred allocateUid() { - LOG.info("Creating an ID for kind='" + kind() - + "' name='" + name + '\''); + LOG.info("Creating " + (randomize_id ? "a random " : "an ") + + "ID for kind='" + kind() + "' name='" + name + '\''); state = CREATE_REVERSE_MAPPING; - return client.atomicIncrement(new AtomicIncrementRequest(table, MAXID_ROW, - ID_FAMILY, - kind)); + if (randomize_id) { + return Deferred.fromResult(RandomUniqueId.getRandomUID()); + } else { + return client.atomicIncrement(new AtomicIncrementRequest(table, + MAXID_ROW, ID_FAMILY, kind)); + } } - /** * Create the reverse mapping. * We do this before the forward one so that if we die before creating @@ -474,10 +507,16 @@ private Deferred createForwardMapping(final Object arg) { if (!(arg instanceof Boolean)) { throw new IllegalStateException("Expected a Boolean but got " + arg); } - if (!((Boolean) arg)) { // Previous CAS failed. Something is really messed up. - LOG.error("WTF! Failed to CAS reverse mapping: " + reverseMapping() - + " -- run an fsck against the UID table!"); - return tryAllocate(); // Try again from the beginning. + if (!((Boolean) arg)) { // Previous CAS failed. + if (randomize_id) { + // This random Id is already used by another row + LOG.warn("Detected random id collision and retrying, " + id); + random_id_collisions++; + } else { + // something is really messed up then + LOG.error("WTF! Failed to CAS reverse mapping: " + reverseMapping() + + " -- run an fsck against the UID table!"); + } } state = DONE; @@ -504,6 +543,12 @@ private Deferred done(final Object arg) { // manage to CAS this KV into existence. The one that loses the // race will retry and discover the UID assigned by the winner TSD, // and a UID will have been wasted in the process. No big deal. + if (randomize_id) { + // This random Id is already used by another row + LOG.warn("Detected random id collision between two tsdb servers " + id); + random_id_collisions++; + } + class GetIdCB implements Callback { public Object call(final byte[] row) throws Exception { assignment.callback(row); @@ -722,6 +767,15 @@ public Deferred> suggestAsync(final String search, return new SuggestCB(search, max_results).search(); } + /** + * Collects random uid collisions + * @param collector StatsCollector object to collect stats/metrics + * @since 2.2 + */ + public void collectStats(final StatsCollector collector) { + collector.record("uid.collisions", random_id_collisions, "type=" + kind); + } + /** * Helper callback to asynchronously scan HBase for suggestions. */ From 17340850c7dff25640541c2e36a932f570753999 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 10 Mar 2015 21:57:12 -0700 Subject: [PATCH 072/826] Add unit tests for RandomUniqueId Signed-off-by: Chris Larsen --- Makefile.am | 1 + test/uid/TestRandomUniqueId.java | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 test/uid/TestRandomUniqueId.java diff --git a/Makefile.am b/Makefile.am index 12cf633039..2d7888be03 100644 --- a/Makefile.am +++ b/Makefile.am @@ -203,6 +203,7 @@ test_SRC := \ test/tsd/TestTreeRpc.java \ test/tsd/TestUniqueIdRpc.java \ test/uid/TestNoSuchUniqueId.java \ + test/uid/TestRandomUniqueId.java \ test/uid/TestUniqueId.java \ test/utils/TestByteArrayPair.java \ test/utils/TestConfig.java \ diff --git a/test/uid/TestRandomUniqueId.java b/test/uid/TestRandomUniqueId.java new file mode 100644 index 0000000000..079938c780 --- /dev/null +++ b/test/uid/TestRandomUniqueId.java @@ -0,0 +1,92 @@ +// 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.uid; + +import static org.junit.Assert.assertTrue; +import net.opentsdb.core.TSDB; + +import org.hbase.async.Bytes; +import org.junit.Test; + +public final class TestRandomUniqueId { + + @Test + public void getRandomUIDMetricWidth() throws Exception { + generateAndTestUID(TSDB.metrics_width(), 100); + } + + @Test + public void getRandomUID1Byte() throws Exception { + generateAndTestUID(1, 100); + } + + @Test + public void getRandomUID2Byte() throws Exception { + generateAndTestUID(2, 100); + } + + @Test + public void getRandomUID3Byte() throws Exception { + generateAndTestUID(3, 100); + } + + @Test + public void getRandomUID4Byte() throws Exception { + generateAndTestUID(4, 100); + } + + @Test + public void getRandomUID5Byte() throws Exception { + generateAndTestUID(5, 100); + } + + @Test + public void getRandomUID6Byte() throws Exception { + generateAndTestUID(6, 100); + } + + @Test + public void getRandomUID7Byte() throws Exception { + generateAndTestUID(7, 100); + } + /** + * Runs the test n times and makes sure it's greater than 0 and less than or + * equal to the max value on {@link width} bytes. + * @param width The number of bytes to generate a UID for + * @param n How many times to run the tests + */ + private void generateAndTestUID(final int width, final int n) { + final long max_value = getMax(width); + for (int i = 0; i < n; i++) { + long uid = RandomUniqueId.getRandomUID(width); + assertTrue(uid > 0 && uid <= max_value); + } + } + + /** + * Simple helper to calculate the max value for any width of long + * @param width The width of the byte array we're comparing + * @return The maximum integer value on {@link width} bytes. + */ + private long getMax(final int width) { + if (width > 7) { + throw new IllegalArgumentException("Can't use a width of [" + width + + "] in this unit test"); + } + final byte[] value = new byte[8]; + for (int i = 0; i < width; i++) { + value[8 - (i + 1)] = (byte) 0xFF; + } + return Bytes.getLong(value); + } +} From cfa6cd4a38bdb4bfe10ab0a203e68a4af90b1e33 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Tue, 10 Mar 2015 22:27:55 -0700 Subject: [PATCH 073/826] More RandomUniqueId unit tests Signed-off-by: Chris Larsen --- test/uid/TestRandomUniqueId.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/uid/TestRandomUniqueId.java b/test/uid/TestRandomUniqueId.java index 079938c780..a1fb6c2fbb 100644 --- a/test/uid/TestRandomUniqueId.java +++ b/test/uid/TestRandomUniqueId.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.uid; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import net.opentsdb.core.TSDB; @@ -59,6 +60,23 @@ public void getRandomUID6Byte() throws Exception { public void getRandomUID7Byte() throws Exception { generateAndTestUID(7, 100); } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidWidth() { + RandomUniqueId.getRandomUID(8); + } + + @Test(expected = NegativeArraySizeException.class) + public void testNegativeWidth() { + RandomUniqueId.getRandomUID(-1); + } + + // if you pass in a width of 0 it will always return 1 + @Test + public void testZeroWidth() { + assertEquals(1L, RandomUniqueId.getRandomUID(0)); + } + /** * Runs the test n times and makes sure it's greater than 0 and less than or * equal to the max value on {@link width} bytes. From e7b09e8423ad6be97d950ecc608abc9fcbc8b2ec Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Mar 2015 15:46:52 -0700 Subject: [PATCH 074/826] Add FailedToAssignUniqueIdException to help diagnose UID issues. Add more UTs around the random ID assignment. Fix some issues where we could try to trigger the callback multiple times during UID assignment. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/uid/FailedToAssignUniqueIdException.java | 64 +++++++++ src/uid/UniqueId.java | 58 ++++---- test/uid/TestUniqueId.java | 134 ++++++++++++++++++- 4 files changed, 231 insertions(+), 26 deletions(-) create mode 100644 src/uid/FailedToAssignUniqueIdException.java diff --git a/Makefile.am b/Makefile.am index 2d7888be03..d28adfd8bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -120,6 +120,7 @@ tsdb_SRC := \ src/tsd/TreeRpc.java \ src/tsd/UniqueIdRpc.java \ src/tsd/WordSplitter.java \ + src/uid/FailedToAssignUniqueIdException.java \ src/uid/NoSuchUniqueId.java \ src/uid/NoSuchUniqueName.java \ src/uid/RandomUniqueId.java \ diff --git a/src/uid/FailedToAssignUniqueIdException.java b/src/uid/FailedToAssignUniqueIdException.java new file mode 100644 index 0000000000..7ffa1a999d --- /dev/null +++ b/src/uid/FailedToAssignUniqueIdException.java @@ -0,0 +1,64 @@ +package net.opentsdb.uid; + +/** + * Thrown when we failed to assign an ID to a string such as a metric, tagk + * or tag v. + * @see UniqueId + */ +public final class FailedToAssignUniqueIdException extends RuntimeException { + /** The 'kind' of the table. */ + private final String kind; + /** The name of the object attempting to be assigned */ + private final String name; + /** How many attempts were made to assign the ID */ + private final int attempts; + + /** + * CTor + * @param kind The kind of object that couldn't be assigned + * @param name The name of the object that couldn't be assigned + * @param attempts How many attempts were made to assign + */ + public FailedToAssignUniqueIdException(final String kind, final String name, + final int attempts) { + super("Failed to assign random ID for kind='" + kind + "' name='" + + name + "' after " + attempts + " attempts"); + this.kind = kind; + this.name = name; + this.attempts = attempts; + } + + /** + * CTor + * @param kind The kind of object that couldn't be assigned + * @param name The name of the object that couldn't be assigned + * @param attempts How many attempts were made to assign + * @param ex An exception that caused assignment to fail + */ + public FailedToAssignUniqueIdException(final String kind, final String name, + final int attempts, final Throwable ex) { + super("Failed to assign random ID for kind='" + kind + "' name='" + + name + "' after " + attempts + " attempts", ex); + this.kind = kind; + this.name = name; + this.attempts = attempts; + } + + /** @return Returns the kind of unique ID that couldn't be assigned. */ + public String kind() { + return kind; + } + + /** @return Returns the name of the object that couldn't be assigned */ + public String name() { + return name; + } + + /** @return Returns how many attempts were made to assign a UID */ + public int attempts() { + return attempts; + } + + private static final long serialVersionUID = 399163221436118367L; + +} diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 585faa1560..f0770eca4a 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -171,6 +171,11 @@ public int cacheSize() { return name_cache.size() + id_cache.size(); } + /** Returns the number of random UID collisions */ + public int randomIdCollisions() { + return random_id_collisions; + } + public String kind() { return fromBytes(kind); } @@ -358,6 +363,12 @@ private final class UniqueIdAllocator implements Callback { MAX_ATTEMPTS_ASSIGN_RANDOM_ID : MAX_ATTEMPTS_ASSIGN_ID; private HBaseException hbe = null; // Last exception caught. + // TODO(manolama) - right now if we retry the assignment it will create a + // callback chain MAX_ATTEMPTS_* long and call the ErrBack that many times. + // This can be cleaned up a fair amount but it may require changing the + // public behavior a bit. For now, the flag will prevent multiple attempts + // to execute the callback. + private boolean called = false; // whether we called the deferred or not private long id = -1; // The ID we'll grab with an atomic increment. private byte row[]; // The same ID, as a byte array. @@ -383,11 +394,15 @@ Deferred tryAllocate() { @SuppressWarnings("unchecked") public Object call(final Object arg) { if (attempt == 0) { - if (hbe == null) { + if (hbe == null && !randomize_id) { throw new IllegalStateException("Should never happen!"); } LOG.error("Failed to assign an ID for kind='" + kind() + "' name='" + name + "'", hbe); + if (hbe == null) { + throw new FailedToAssignUniqueIdException(kind(), name, + MAX_ATTEMPTS_ASSIGN_RANDOM_ID); + } throw hbe; } @@ -400,7 +415,8 @@ public Object call(final Object arg) { if (arg instanceof HBaseException) { LOG.error(msg, (Exception) arg); hbe = (HBaseException) arg; - return tryAllocate(); // Retry from the beginning. + attempt--; + state = ALLOCATE_UID;; // Retry from the beginning. } else { LOG.error("WTF? Unexpected exception! " + msg, (Exception) arg); return arg; // Unexpected exception, let it bubble up. @@ -409,8 +425,11 @@ public Object call(final Object arg) { class ErrBack implements Callback { public Object call(final Exception e) throws Exception { - assignment.callback(e); - LOG.warn("Failed pending assignment for: " + name); + if (!called) { + LOG.warn("Failed pending assignment for: " + name, e); + assignment.callback(e); + called = true; + } return assignment; } } @@ -510,13 +529,17 @@ private Deferred createForwardMapping(final Object arg) { if (!((Boolean) arg)) { // Previous CAS failed. if (randomize_id) { // This random Id is already used by another row - LOG.warn("Detected random id collision and retrying, " + id); + LOG.warn("Detected random id collision and retrying kind='" + + kind() + "' name='" + name + "'"); random_id_collisions++; } else { // something is really messed up then LOG.error("WTF! Failed to CAS reverse mapping: " + reverseMapping() + " -- run an fsck against the UID table!"); } + attempt--; + state = ALLOCATE_UID; + return Deferred.fromResult(false); } state = DONE; @@ -545,7 +568,8 @@ private Deferred done(final Object arg) { // and a UID will have been wasted in the process. No big deal. if (randomize_id) { // This random Id is already used by another row - LOG.warn("Detected random id collision between two tsdb servers " + id); + LOG.warn("Detected random id collision between two tsdb " + + "servers kind='" + kind() + "' name='" + name + "'"); random_id_collisions++; } @@ -568,16 +592,12 @@ public Object call(final byte[] row) throws Exception { tsdb.indexUIDMeta(meta); } - synchronized (pending_assignments) { - pending_assignments.remove(name); - } - assignment.callback(row); synchronized(pending_assignments) { - if (pending_assignments.containsKey(name)) { - pending_assignments.remove(name); + if (pending_assignments.remove(name) != null) { LOG.info("Completed pending assignment for: " + name); } } + assignment.callback(row); return assignment; } @@ -642,9 +662,10 @@ public byte[] getOrCreateId(final String name) throws HBaseException { } catch (Exception e1) { throw new RuntimeException("Should never be here", e); } finally { - LOG.info("Completed pending assignment for: " + name); synchronized (pending_assignments) { - pending_assignments.remove(name); + if (pending_assignments.remove(name) != null) { + LOG.info("Completed pending assignment for: " + name); + } } } return uid; @@ -767,15 +788,6 @@ public Deferred> suggestAsync(final String search, return new SuggestCB(search, max_results).search(); } - /** - * Collects random uid collisions - * @param collector StatsCollector object to collect stats/metrics - * @since 2.2 - */ - public void collectStats(final StatsCollector collector) { - collector.record("uid.collisions", random_id_collisions, "type=" + kind); - } - /** * Helper callback to asynchronously scan HBase for suggestions. */ diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index 3db8dbe9e8..310a460bdf 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -31,9 +31,9 @@ import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; - import org.junit.Test; import org.junit.runner.RunWith; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -44,7 +44,8 @@ import org.mockito.InOrder; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import static org.mockito.Mockito.any; + +import static org.mockito.Matchers.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.eq; @@ -58,6 +59,7 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; + import static org.powermock.api.mockito.PowerMockito.mock; @RunWith(PowerMockRunner.class) @@ -66,7 +68,8 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({ HBaseClient.class, TSDB.class, Config.class }) +@PrepareForTest({ HBaseClient.class, TSDB.class, Config.class, + RandomUniqueId.class }) public final class TestUniqueId { private HBaseClient client = mock(HBaseClient.class); @@ -482,7 +485,132 @@ public void getOrCreateIdPutsReverseMappingFirst() { order.verify(client).compareAndSet(putForRow(id), emptyArray()); order.verify(client).compareAndSet(putForRow(row), emptyArray()); } + + @Test + public void getOrCreateIdRandom() { + PowerMockito.mockStatic(RandomUniqueId.class); + uid = new UniqueId(client, table, kind, 3, true); + final long id = 42L; + final byte[] id_array = { 0, 0, 0x2A }; + + when(RandomUniqueId.getRandomUID()).thenReturn(id); + when(client.get(any(GetRequest.class))) + .thenReturn(Deferred.>fromResult(null)); + + when(client.compareAndSet(any(PutRequest.class), any(byte[].class))) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + assertArrayEquals(id_array, uid.getOrCreateId("foo")); + // Should be a cache hit ... + assertArrayEquals(id_array, uid.getOrCreateId("foo")); + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + assertEquals(0, uid.randomIdCollisions()); + // ... so verify there was only one HBase Get. + verify(client).get(any(GetRequest.class)); + } + + @Test + public void getOrCreateIdRandomCollision() { + PowerMockito.mockStatic(RandomUniqueId.class); + uid = new UniqueId(client, table, kind, 3, true); + final long id = 42L; + final byte[] id_array = { 0, 0, 0x2A }; + + when(RandomUniqueId.getRandomUID()).thenReturn(24L).thenReturn(id); + + when(client.get(any(GetRequest.class))) + .thenReturn(Deferred.fromResult((ArrayList)null)); + + when(client.compareAndSet(anyPut(), any(byte[].class))) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + assertArrayEquals(id_array, uid.getOrCreateId("foo")); + // Should be a cache hit ... + assertArrayEquals(id_array, uid.getOrCreateId("foo")); + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + assertEquals(1, uid.randomIdCollisions()); + + // ... so verify there was only one HBase Get. + verify(client).get(anyGet()); + } + + @Test + public void getOrCreateIdRandomCollisionTooManyAttempts() { + PowerMockito.mockStatic(RandomUniqueId.class); + uid = new UniqueId(client, table, kind, 3, true); + final long id = 42L; + + when(RandomUniqueId.getRandomUID()).thenReturn(24L).thenReturn(id); + + when(client.get(any(GetRequest.class))) + .thenReturn(Deferred.fromResult((ArrayList)null)); + + when(client.compareAndSet(any(PutRequest.class), any(byte[].class))) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)) + .thenReturn(Deferred.fromResult(false)); + + try { + final byte[] assigned_id = uid.getOrCreateId("foo"); + fail("FailedToAssignUniqueIdException should have been thrown but instead " + + " this was returned id=" + Arrays.toString(assigned_id)); + } catch (FailedToAssignUniqueIdException e) { + // OK + } + assertEquals(0, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(0, uid.cacheSize()); + assertEquals(9, uid.randomIdCollisions()); + // ... so verify there was only one HBase Get. + verify(client).get(any(GetRequest.class)); + } + + @Test + public void getOrCreateIdRandomWithRaceCondition() { + PowerMockito.mockStatic(RandomUniqueId.class); + uid = new UniqueId(client, table, kind, 3, true); + final long id = 24L; + final byte[] id_array = { 0, 0, 0x2A }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList kvs = new ArrayList(1); + kvs.add(new KeyValue(byte_name, ID, kind_array, id_array)); + + when(RandomUniqueId.getRandomUID()).thenReturn(id); + + when(client.get(any(GetRequest.class))) + .thenReturn(Deferred.fromResult((ArrayList)null)) + .thenReturn(Deferred.fromResult(kvs)); + + when(client.compareAndSet(any(PutRequest.class), any(byte[].class))) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(false)); + + assertArrayEquals(id_array, uid.getOrCreateId("foo")); + assertEquals(0, uid.cacheHits()); + assertEquals(2, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + assertEquals(1, uid.randomIdCollisions()); + + // ... so verify there was only one HBase Get. + verify(client, times(2)).get(any(GetRequest.class)); + } + @PrepareForTest({HBaseClient.class, Scanner.class}) @Test public void suggestWithNoMatch() { From eceac08d84e5d96e85f61470569c5e9011185fa3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Mar 2015 16:05:02 -0700 Subject: [PATCH 075/826] Add the "tsd.core.uid.random_metrics" flag to enable (disabled by default) random UID generation for metrics. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 8 +++++++- src/utils/Config.java | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 1b035018e2..1c91a1941e 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -123,7 +123,11 @@ public TSDB(final HBaseClient client, final Config config) { treetable = config.getString("tsd.storage.hbase.tree_table").getBytes(CHARSET); meta_table = config.getString("tsd.storage.hbase.meta_table").getBytes(CHARSET); - metrics = new UniqueId(client, uidtable, METRICS_QUAL, METRICS_WIDTH); + if (config.getBoolean("tsd.core.uid.random_metrics")) { + metrics = new UniqueId(client, uidtable, METRICS_QUAL, METRICS_WIDTH, true); + } else { + metrics = new UniqueId(client, uidtable, METRICS_QUAL, METRICS_WIDTH); + } tag_names = new UniqueId(client, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH); tag_values = new UniqueId(client, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH); compactionq = new CompactionQueue(this); @@ -462,6 +466,8 @@ private static void collectUidStats(final UniqueId uid, collector.record("uid.cache-hit", uid.cacheHits(), "kind=" + uid.kind()); collector.record("uid.cache-miss", uid.cacheMisses(), "kind=" + uid.kind()); collector.record("uid.cache-size", uid.cacheSize(), "kind=" + uid.kind()); + collector.record("uid.random-collisions", uid.randomIdCollisions(), + "kind=" + uid.kind()); } /** @return the width, in bytes, of metric UIDs */ diff --git a/src/utils/Config.java b/src/utils/Config.java index 78486ace20..40ab467a00 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -453,6 +453,7 @@ protected void setDefaults() { default_map.put("tsd.core.tree.enable_processing", "false"); default_map.put("tsd.core.preload_uid_cache", "false"); default_map.put("tsd.core.preload_uid_cache.max_entries", "300000"); + default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); From 324c77a2f49d6a9671fcaf8742d908c465c875a4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Mar 2015 18:56:05 -0700 Subject: [PATCH 076/826] Start salt work by adding the salt constants for width and bucket size. Add two methods to the Internal class to get the salt bytes and compute the salt on a row key. Thanks @rajeshal Signed-off-by: Chris Larsen --- src/core/Const.java | 25 +++ src/core/Internal.java | 64 +++++++- test/core/TestInternal.java | 314 +++++++++++++++++++++++++++++++++++- 3 files changed, 401 insertions(+), 2 deletions(-) diff --git a/src/core/Const.java b/src/core/Const.java index 9678abc2d6..d14194f7be 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -74,4 +74,29 @@ public final class Const { * before losing precision. */ public static final long MAX_INT_IN_DOUBLE = 0xFFE0000000000000L; + + /** + * The number of buckets to use for salting. + * WARNING: Changing this after writing data will break TSUID and direct + * queries as the salt calculation will differ. Scanning queries will be OK + * though. + */ + private static final int SALT_BUCKETS = 20; + public static int SALT_BUCKETS() { + return SALT_BUCKETS; + } + + /** + * Width of the salt in bytes. + * Its width should be proportional to MAX_SALT data type. + * When set to 0, salting is disabled. + * if SALT_WIDTH = 1, the MAX_SALT should be byte + * if SALT_WIDTH = 2, the MAX_SALT can be byte or short + * WARNING: Do NOT change this after you start writing data or you will not + * be able to query for anything. + */ + private static final int SALT_WIDTH = 0; + public static int SALT_WIDTH() { + return SALT_WIDTH; + } } diff --git a/src/core/Internal.java b/src/core/Internal.java index 1be49647a1..2dfed3120f 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -93,7 +93,7 @@ public static String metricName(final TSDB tsdb, final byte[] id) { /** Extracts the timestamp from a row key. */ public static long baseTime(final TSDB tsdb, final byte[] row) { - return Bytes.getUnsignedInt(row, tsdb.metrics.width()); + return Bytes.getUnsignedInt(row, Const.SALT_WIDTH() + TSDB.metrics_width()); } /** @see Tags#getTags */ @@ -859,4 +859,66 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, buf.append("$"); scanner.setKeyRegexp(buf.toString(), Charset.forName("ISO-8859-1")); } + + /** + * Returns the byte array for the given salt id + * WARNING: Don't use this one unless you know what you're doing. It's here + * for unit testing. + * @param bucket The ID of the bucket to get the salt for + * @return The salt as a byte array based on the width in bytes + * @since 2.2 + */ + public static byte[] getSaltBytes(final int bucket) { + final byte[] bytes = new byte[Const.SALT_WIDTH()]; + int shift = 0; + for (int i = 1;i <= Const.SALT_WIDTH(); i++) { + bytes[Const.SALT_WIDTH() - i] = (byte) (bucket >>> shift); + shift += 8; + } + return bytes; + } + + /** + * Calculates and writes an array of one or more salt bytes at the front of + * the given row key. + * + * The salt is calculated by taking the Java hash code of the metric and + * tag UIDs and returning a modulo based on the number of salt buckets. + * The result will always be a positive integer from 0 to salt buckets. + * + * NOTE: The row key passed in MUST have allocated the {@link width} number of + * bytes at the front of the row key or this call will overwrite data. + * + * WARNING: If the width is set to a positive value, then the bucket must be + * at least 1 or greater. + * @param row_key The pre-allocated row key to write the salt to + * @since 2.2 + */ + public static void prefixKeyWithSalt(final byte[] row_key) { + if (Const.SALT_WIDTH() > 0) { + if (row_key.length < (Const.SALT_WIDTH() + TSDB.metrics_width()) || + (Bytes.memcmp(row_key, new byte[Const.SALT_WIDTH() + TSDB.metrics_width()], + Const.SALT_WIDTH(), TSDB.metrics_width()) == 0)) { + // ^ Don't salt the global annotation row, leave it at zero + return; + } + final int tags_start = Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES; + + // we want the metric and tags, not the timestamp + final byte[] salt_base = + new byte[row_key.length - Const.SALT_WIDTH() - Const.TIMESTAMP_BYTES]; + System.arraycopy(row_key, Const.SALT_WIDTH(), salt_base, 0, TSDB.metrics_width()); + System.arraycopy(row_key, tags_start,salt_base, TSDB.metrics_width(), + row_key.length - tags_start); + int modulo = Arrays.hashCode(salt_base) % Const.SALT_BUCKETS(); + if (modulo < 0) { + // make sure we return a positive salt. + modulo = modulo * -1; + } + + final byte[] salt = Internal.getSaltBytes(modulo); + System.arraycopy(salt, 0, row_key, 0, Const.SALT_WIDTH()); + } // else salting is disabled so it's a no-op + } } diff --git a/test/core/TestInternal.java b/test/core/TestInternal.java index 8e1fc88bfb..9dd54cc8e8 100644 --- a/test/core/TestInternal.java +++ b/test/core/TestInternal.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.Arrays; import net.opentsdb.core.Internal.Cell; import net.opentsdb.storage.MockBase; @@ -26,11 +27,12 @@ import org.hbase.async.KeyValue; 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; @RunWith(PowerMockRunner.class) -@PrepareForTest({ Internal.class }) +@PrepareForTest({ Internal.class, Const.class }) public final class TestInternal { private static final byte[] KEY = { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; @@ -810,6 +812,316 @@ public void extractQualifierMilliSeconds() { Internal.extractQualifier(qual, 2)); } + @Test + public void getSalt() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); + assertArrayEquals(new byte[] {}, Internal.getSaltBytes(2)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + assertArrayEquals(new byte[] { 2 }, Internal.getSaltBytes(2)); + assertArrayEquals(new byte[] { 20 }, Internal.getSaltBytes(20)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(2); + assertArrayEquals(new byte[] { 0, 20 }, Internal.getSaltBytes(20)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); + assertArrayEquals(new byte[] { 0, 0, 0, 20 }, Internal.getSaltBytes(20)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + assertArrayEquals(new byte[] { -2 }, Internal.getSaltBytes(-2)); + } + + @Test (expected = NegativeArraySizeException.class) + public void getSaltNegativeWidth() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(-1); + Internal.getSaltBytes(2); + } + + @Test + public void prefixKeyWithSaltGlobalAndNoOps() { + setupSalt(); + // short rows + byte[] key = new byte[1]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[1], key); + + key = new byte[2]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[2], key); + + key = new byte[3]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[3], key); + + key = new byte[4]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[4], key); + + key = new byte[5]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[5], key); + + key = new byte[6]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[6], key); + + key = new byte[7]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[7], key); + + key = new byte[8]; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[8], key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x50, (byte) 0xE2, 0x27, 0x00}; + byte[] compare = Arrays.copyOf(key, key.length); + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x01, 0x50, (byte) 0xE2, 0x27, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltSameMetricDifferentTags() { + setupSalt(); + byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x10; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x00; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x09; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x03}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x03; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + // no tags. Shouldn't happen, but *shrug* + key = new byte[] { 0x00, 0x00, 0x00, 0x01, 0x50, (byte) 0xE2, 0x27, 0x00 }; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x0C; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltDifferentMetricSameTags() { + setupSalt(); + byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x02, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x09; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x03, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x06; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x0B, 0x14, 0x20, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x06; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltSameMetricSameTagsDifferentTimestamp() { + setupSalt(); + byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x35, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x51, (byte) 0x0B, 0x13, (byte) 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltOverwrite() { + setupSalt(); + // makes sure we ignore and overwrite anything in the salt position + byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { (byte) 0xFF, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { (byte) 0x0E, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test (expected = ArithmeticException.class) + public void prefixKeyWithSaltZeroBucket() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(0); + + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + Internal.prefixKeyWithSalt(key); + } + + // This actually works, but PLEASE don't do it! + @Test + public void prefixKeyWithSaltNegativeBucket() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(-20); + + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + final byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltNegativeWidth() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(-1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + + final byte[] key = new byte[] { 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + final byte[] compare = Arrays.copyOf(key, key.length); + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltMissingTagV() { + setupSalt(); + // Honey badger don't care + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01}; + final byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x0D; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltPartialTagK() { + setupSalt(); + // Honey badger don't care + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00 }; + final byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x0C; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void prefixKeyWithSaltMissingTags() { + setupSalt(); + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27 }; + Internal.prefixKeyWithSalt(key); + } + + @Test (expected = NullPointerException.class) + public void prefixKeyWithSaltNullKey() { + setupSalt(); + Internal.prefixKeyWithSalt(null); + } + + @Test + public void prefixKeyWithSaltEmptyKey() { + setupSalt(); + final byte[] key = new byte[] {}; + Internal.prefixKeyWithSalt(key); + assertArrayEquals(new byte[] {}, key); + } + + /** + * Mocks out the static Const class for a single salt byte with 20 buckets + */ + private static void setupSalt() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + } + /** Shorthand to create a {@link KeyValue}. */ private static KeyValue makekv(final byte[] qualifier, final byte[] value) { return new KeyValue(KEY, FAMILY, qualifier, value); From acfbc2b13eed6aa69d7976fa11d93939a4e7549c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Mar 2015 20:04:22 -0700 Subject: [PATCH 077/826] Modify the rowKeyTemplate method in IncomingDataPoints to deal with salting. Also add a unit test class for IncomingDataPoints with UTs for that method. And add a BaseTestTSDB class for commong mocks Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/core/IncomingDataPoints.java | 6 +- test/core/BaseTsdbTest.java | 176 ++++++++++++++++++++++++++ test/core/TestIncomingDataPoints.java | 118 +++++++++++++++++ 4 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 test/core/BaseTsdbTest.java create mode 100644 test/core/TestIncomingDataPoints.java diff --git a/Makefile.am b/Makefile.am index d28adfd8bf..29567fc050 100644 --- a/Makefile.am +++ b/Makefile.am @@ -152,11 +152,13 @@ tsdb_DEPS = \ test_SRC := \ test/core/SeekableViewsForTest.java \ + test/core/BaseTsdbTest.java \ test/core/TestAggregationIterator.java \ test/core/TestAggregators.java \ test/core/TestBatchedDataPoints.java \ test/core/TestCompactionQueue.java \ test/core/TestDownsampler.java \ + test/core/TestIncomingDataPoints.java \ test/core/TestInternal.java \ test/core/TestMutableDataPoint.java \ test/core/TestRateSpan.java \ diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 5b0fde19a7..be861d4e25 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -121,11 +121,11 @@ static byte[] rowKeyTemplate(final TSDB tsdb, final String metric, final short tag_value_width = tsdb.tag_values.width(); final short num_tags = (short) tags.size(); - int row_size = (metric_width + Const.TIMESTAMP_BYTES + tag_name_width - * num_tags + tag_value_width * num_tags); + 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]; - short pos = 0; + short pos = (short) Const.SALT_WIDTH(); copyInRowKey(row, pos, (tsdb.config.auto_metric() ? tsdb.metrics.getOrCreateId(metric) diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java new file mode 100644 index 0000000000..ae13bbda1a --- /dev/null +++ b/test/core/BaseTsdbTest.java @@ -0,0 +1,176 @@ +package net.opentsdb.core; + +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.util.HashMap; +import java.util.Map; + +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.HBaseClient; +import org.jboss.netty.util.HashedWheelTimer; +import org.junit.Before; +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 org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +/** + * Sets up a real TSDB with mocked client, compaction queue and timer along + * with mocked UID assignment, fetches for common unit tests. + */ +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, + HashedWheelTimer.class, CompactionQueue.class, Const.class }) +public class BaseTsdbTest { + + public static final String METRIC_STRING = "sys.cpu.user"; + public static final byte[] METRIC_BYTES = new byte[] { 0, 0, 1 }; + public static final String METRIC_B_STRING = "sys.cpu.system"; + public static final byte[] METRIC_B_BYTES = new byte[] { 0, 0, 2 }; + public static final String NSUN_METRIC = "sys.cpu.nice"; + public static final byte[] NSUI_METRIC = new byte[] { 0, 0, 3 }; + + public static final String TAGK_STRING = "host"; + public static final byte[] TAGK_BYTES = new byte[] { 0, 0, 1 }; + public static final String TAGK_B_STRING = "owner"; + public static final byte[] TAGK_B_BYTES = new byte[] { 0, 0, 3 }; + public static final String NSUN_TAGK = "dc"; + public static final byte[] NSUI_TAGK = new byte[] { 0, 0, 4 }; + + public static final String TAGV_STRING = "web01"; + public static final byte[] TAGV_BYTES = new byte[] { 0, 0, 1 }; + public static final String TAGV_B_STRING = "web02"; + public static final byte[] TAGV_B_BYTES = new byte[] { 0, 0, 2 }; + public static final String NSUN_TAGV = "web03"; + public static final byte[] NSUI_TAGV = new byte[] { 0, 0, 3 }; + + protected HashedWheelTimer timer; + protected CompactionQueue compaction_queue; + protected Config config; + protected TSDB tsdb; + protected HBaseClient client = mock(HBaseClient.class); + protected UniqueId metrics = mock(UniqueId.class); + protected UniqueId tag_names = mock(UniqueId.class); + protected UniqueId tag_values = mock(UniqueId.class); + protected Map tags = new HashMap(1); + protected MockBase storage; + + @Before + public void before() throws Exception { + timer = mock(HashedWheelTimer.class); + compaction_queue = mock(CompactionQueue.class); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + PowerMockito.whenNew(CompactionQueue.class).withAnyArguments() + .thenReturn(compaction_queue); + + config = new Config(false); + tsdb = PowerMockito.spy(new TSDB(config)); + + config.setAutoMetric(true); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + setupMetricMaps(); + setupTagkMaps(); + setupTagvMaps(); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + tags.put(TAGK_STRING, TAGV_STRING); + } + + void setupMetricMaps() { + when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); + when(metrics.getIdAsync(METRIC_STRING)) + .thenReturn(Deferred.fromResult(METRIC_BYTES)); + when(metrics.getOrCreateId(METRIC_STRING)) + .thenReturn(METRIC_BYTES); + + when(metrics.getId(METRIC_B_STRING)).thenReturn(METRIC_B_BYTES); + when(metrics.getIdAsync(METRIC_B_STRING)) + .thenReturn(Deferred.fromResult(METRIC_B_BYTES)); + when(metrics.getOrCreateId(METRIC_B_STRING)) + .thenReturn(METRIC_B_BYTES); + + when(metrics.getNameAsync(METRIC_BYTES)) + .thenReturn(Deferred.fromResult(METRIC_STRING)); + when(metrics.getNameAsync(METRIC_B_BYTES)) + .thenReturn(Deferred.fromResult(METRIC_B_STRING)); + + final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_METRIC, "metric"); + + when(metrics.getId(NSUN_METRIC)).thenThrow(nsun); + when(metrics.getIdAsync(NSUN_METRIC)) + .thenReturn(Deferred.fromError(nsun)); + when(metrics.getOrCreateId(NSUN_METRIC)).thenThrow(nsun); + } + + void setupTagkMaps() { + when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); + when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); + when(tag_names.getIdAsync(TAGK_STRING)) + .thenReturn(Deferred.fromResult(TAGK_BYTES)); + when(tag_names.getOrCreateIdAsync(TAGK_STRING)) + .thenReturn(Deferred.fromResult(TAGK_BYTES)); + + when(tag_names.getId(TAGK_B_STRING)).thenReturn(TAGK_B_BYTES); + when(tag_names.getOrCreateId(TAGK_B_STRING)).thenReturn(TAGK_B_BYTES); + when(tag_names.getIdAsync(TAGK_B_STRING)) + .thenReturn(Deferred.fromResult(TAGK_B_BYTES)); + when(tag_names.getOrCreateIdAsync(TAGK_B_STRING)) + .thenReturn(Deferred.fromResult(TAGK_B_BYTES)); + + when(tag_names.getNameAsync(TAGK_BYTES)) + .thenReturn(Deferred.fromResult(TAGK_STRING)); + + when(tag_names.getIdAsync(NSUN_TAGK)) + .thenReturn(Deferred.fromError(new NoSuchUniqueName(NSUN_TAGK, "tagk"))); + } + + void setupTagvMaps() { + when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); + when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); + when(tag_values.getIdAsync(TAGV_STRING)) + .thenReturn(Deferred.fromResult(TAGV_BYTES)); + when(tag_values.getOrCreateIdAsync(TAGV_STRING)) + .thenReturn(Deferred.fromResult(TAGV_BYTES)); + + when(tag_values.getId(TAGV_B_STRING)).thenReturn(TAGV_B_BYTES); + when(tag_values.getOrCreateId(TAGV_B_STRING)).thenReturn(TAGV_B_BYTES); + when(tag_values.getIdAsync(TAGV_B_STRING)) + .thenReturn(Deferred.fromResult(TAGV_B_BYTES)); + when(tag_values.getOrCreateIdAsync(TAGV_B_STRING)) + .thenReturn(Deferred.fromResult(TAGV_B_BYTES)); + + when(tag_values.getNameAsync(TAGV_BYTES)) + .thenReturn(Deferred.fromResult(TAGV_STRING)); + when(tag_values.getNameAsync(TAGV_B_BYTES)) + .thenReturn(Deferred.fromResult(TAGV_B_STRING)); + + final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_TAGV, "tagv"); + + when(tag_values.getId(NSUN_TAGV)).thenThrow(nsun); + when(tag_values.getIdAsync(NSUN_TAGV)) + .thenReturn(Deferred.fromError(nsun)); + } +} \ No newline at end of file diff --git a/test/core/TestIncomingDataPoints.java b/test/core/TestIncomingDataPoints.java new file mode 100644 index 0000000000..89d1027dea --- /dev/null +++ b/test/core/TestIncomingDataPoints.java @@ -0,0 +1,118 @@ +package net.opentsdb.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.mockito.Mockito.when; + +import net.opentsdb.uid.NoSuchUniqueName; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +public class TestIncomingDataPoints extends BaseTsdbTest { + + @Test + public void rowKeyTemplate() throws Exception { + final byte[] expected = new byte[METRIC_BYTES.length + Const.TIMESTAMP_BYTES + + TAGK_BYTES.length + TAGV_BYTES.length]; + System.arraycopy(METRIC_BYTES, 0, expected, 0, METRIC_BYTES.length); + System.arraycopy(TAGK_BYTES, 0, expected, + METRIC_BYTES.length + Const.TIMESTAMP_BYTES, TAGK_BYTES.length); + System.arraycopy(TAGV_BYTES, 0, expected, + METRIC_BYTES.length + Const.TIMESTAMP_BYTES + TAGK_BYTES.length, + TAGV_BYTES.length); + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, + METRIC_STRING, tags); + assertArrayEquals(expected, key); + } + + @Test + public void rowKeyTemplateWithSalt1Byte() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + final byte[] expected = new byte[METRIC_BYTES.length + Const.TIMESTAMP_BYTES + + TAGK_BYTES.length + TAGV_BYTES.length + 1]; + System.arraycopy(METRIC_BYTES, 0, expected, 1, METRIC_BYTES.length); + System.arraycopy(TAGK_BYTES, 0, expected, + METRIC_BYTES.length + Const.TIMESTAMP_BYTES + 1, TAGK_BYTES.length); + System.arraycopy(TAGV_BYTES, 0, expected, + METRIC_BYTES.length + Const.TIMESTAMP_BYTES + TAGK_BYTES.length + 1, + TAGV_BYTES.length); + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, + METRIC_STRING, tags); + assertArrayEquals(expected, key); + } + + @Test + public void rowKeyTemplateWithSalt2Bytes() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(2); + final byte[] expected = new byte[METRIC_BYTES.length + Const.TIMESTAMP_BYTES + + TAGK_BYTES.length + TAGV_BYTES.length + 2]; + System.arraycopy(METRIC_BYTES, 0, expected, 2, METRIC_BYTES.length); + System.arraycopy(TAGK_BYTES, 0, expected, + METRIC_BYTES.length + Const.TIMESTAMP_BYTES + 2, TAGK_BYTES.length); + System.arraycopy(TAGV_BYTES, 0, expected, + METRIC_BYTES.length + Const.TIMESTAMP_BYTES + TAGK_BYTES.length + 2, + TAGV_BYTES.length); + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, + METRIC_STRING, tags); + assertArrayEquals(expected, key); + } + + @Test (expected = NoSuchUniqueName.class) + public void rowKeyTemplateNoSuchMetric() throws Exception { + IncomingDataPoints.rowKeyTemplate(tsdb, NSUN_METRIC, tags); + } + + @Test (expected = NoSuchUniqueName.class) + public void rowKeyTemplateNoSuchTagK() throws Exception { + tags.clear(); + tags.put(NSUN_TAGK, TAGV_STRING); + IncomingDataPoints.rowKeyTemplate(tsdb, NSUN_METRIC, tags); + } + + @Test (expected = NoSuchUniqueName.class) + public void rowKeyTemplateNoSuchTagV() throws Exception { + tags.put(TAGK_STRING, NSUN_TAGV); + IncomingDataPoints.rowKeyTemplate(tsdb, NSUN_METRIC, tags); + } + + @Test (expected = NullPointerException.class) + public void rowKeyTemplateNullTSDB() throws Exception { + IncomingDataPoints.rowKeyTemplate(null, NSUN_METRIC, tags); + } + + @Test (expected = NullPointerException.class) + public void rowKeyTemplateNullMetric() throws Exception { + IncomingDataPoints.rowKeyTemplate(tsdb, null, tags); + } + + @Test (expected = NoSuchUniqueName.class) + public void rowKeyTemplateEmptyMetric() throws Exception { + when(metrics.getOrCreateId("")).thenThrow(new NoSuchUniqueName("metrics", "")); + IncomingDataPoints.rowKeyTemplate(tsdb, "", tags); + } + + @Test (expected = NullPointerException.class) + public void rowKeyTemplateNullTags() throws Exception { + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, null); + } + + // NOTE: This method doesn't enforce that we have tags + @Test + public void rowKeyTemplateEmptyTags() throws Exception { + tags.clear(); + final byte[] expected = new byte[METRIC_BYTES.length + Const.TIMESTAMP_BYTES]; + System.arraycopy(METRIC_BYTES, 0, expected, 0, METRIC_BYTES.length); + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, + METRIC_STRING, tags); + assertArrayEquals(expected, key); + } +} From 88224b4f727f84840cb5ffa1a224bf3280531040 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Mar 2015 20:37:27 -0700 Subject: [PATCH 078/826] Fix MockBase to Whitebox the client into TSDB instead of using raw Java Reflection. Signed-off-by: Chris Larsen --- test/storage/MockBase.java | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 26942cb651..30e1390b02 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -18,8 +18,6 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; -import java.io.IOException; -import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; @@ -33,7 +31,6 @@ import javax.xml.bind.DatatypeConverter; import net.opentsdb.core.TSDB; -import net.opentsdb.utils.Config; import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; @@ -46,6 +43,7 @@ import org.junit.Ignore; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @@ -116,21 +114,7 @@ public MockBase( default_family = "t".getBytes(ASCII); // set a default // replace the "real" field objects with mocks - Field cl; - try { - cl = tsdb.getClass().getDeclaredField("client"); - cl.setAccessible(true); - cl.set(tsdb, client); - cl.setAccessible(false); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } + Whitebox.setInternalState(tsdb, "client", client); // Default get answer will return one or more columns from the requested row if (default_get) { From 240cabc949c11f58873d57364b8ff43121f9c68e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Mar 2015 20:38:00 -0700 Subject: [PATCH 079/826] Enable TSDB to store datapoints with salting by modifying the addPointInternal() method along with some tests. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 4 +- test/core/BaseTsdbTest.java | 6 +- test/core/TestTSDB.java | 338 +++++++++++++++--------------------- 3 files changed, 149 insertions(+), 199 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 1c91a1941e..c2b8693866 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -650,7 +650,9 @@ private Deferred addPointInternal(final String metric, base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } - Bytes.setInt(row, (int) base_time, metrics.width()); + Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); + Internal.prefixKeyWithSalt(row); + scheduleForCompaction(row, (int) base_time); final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index ae13bbda1a..8e4f913bf0 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -143,8 +143,12 @@ void setupTagkMaps() { when(tag_names.getNameAsync(TAGK_BYTES)) .thenReturn(Deferred.fromResult(TAGK_STRING)); + final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_TAGK, "tagk"); + + when(tag_names.getId(NSUN_TAGK)) + .thenThrow(nsun); when(tag_names.getIdAsync(NSUN_TAGK)) - .thenReturn(Deferred.fromError(new NoSuchUniqueName(NSUN_TAGK, "tagk"))); + .thenReturn(Deferred.fromError(nsun)); } void setupTagvMaps() { diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 0d9ede48b2..dc4672ceb2 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -16,13 +16,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import java.lang.reflect.Field; import java.util.HashMap; -import java.util.Map; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; @@ -41,8 +38,6 @@ 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.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -56,40 +51,13 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, - Scanner.class, AtomicIncrementRequest.class}) -public final class TestTSDB { - private Config config; - private TSDB tsdb; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); - private CompactionQueue compactionq = mock(CompactionQueue.class); + Scanner.class, AtomicIncrementRequest.class, Const.class}) +public final class TestTSDB extends BaseTsdbTest { private MockBase storage; @Before - public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - config = new Config(false); + public void beforeLocal() throws Exception { config.setFixDuplicates(true); // TODO(jat): test both ways - tsdb = new TSDB(config); - - Field met = tsdb.getClass().getDeclaredField("metrics"); - met.setAccessible(true); - met.set(tsdb, metrics); - - Field tagk = tsdb.getClass().getDeclaredField("tag_names"); - tagk.setAccessible(true); - tagk.set(tsdb, tag_names); - - Field tagv = tsdb.getClass().getDeclaredField("tag_values"); - tagv.setAccessible(true); - tagv.set(tsdb, tag_values); - - Field cq = tsdb.getClass().getDeclaredField("compactionq"); - cq.setAccessible(true); - cq.set(tsdb, compactionq); } @Test @@ -217,121 +185,114 @@ public void getUidNameNullUID() throws Exception { @Test public void getUIDMetric() { - setupAssignUid(); assertArrayEquals(new byte[] { 0, 0, 1 }, - tsdb.getUID(UniqueIdType.METRIC, "sys.cpu.0")); + tsdb.getUID(UniqueIdType.METRIC, METRIC_STRING)); } @Test public void getUIDTagk() { - setupAssignUid(); assertArrayEquals(new byte[] { 0, 0, 1 }, - tsdb.getUID(UniqueIdType.TAGK, "host")); + tsdb.getUID(UniqueIdType.TAGK, TAGK_STRING)); } @Test public void getUIDTagv() { - setupAssignUid(); assertArrayEquals(new byte[] { 0, 0, 1 }, - tsdb.getUID(UniqueIdType.TAGV, "localhost")); + tsdb.getUID(UniqueIdType.TAGV, TAGV_STRING)); } @Test (expected = NoSuchUniqueName.class) public void getUIDMetricNSU() { - setupAssignUid(); - tsdb.getUID(UniqueIdType.METRIC, "sys.cpu.1"); + tsdb.getUID(UniqueIdType.METRIC, NSUN_METRIC); } @Test (expected = NoSuchUniqueName.class) public void getUIDTagkNSU() { - setupAssignUid(); - tsdb.getUID(UniqueIdType.TAGK, "datacenter"); + tsdb.getUID(UniqueIdType.TAGK, NSUN_TAGK); } @Test (expected = NoSuchUniqueName.class) public void getUIDTagvNSU() { - setupAssignUid(); - tsdb.getUID(UniqueIdType.TAGV, "myserver"); + tsdb.getUID(UniqueIdType.TAGV, NSUN_TAGV); } @Test (expected = NullPointerException.class) public void getUIDNullType() { - setupAssignUid(); - tsdb.getUID(null, "sys.cpu.1"); + tsdb.getUID(null, METRIC_STRING); } @Test (expected = IllegalArgumentException.class) public void getUIDNullName() { - setupAssignUid(); tsdb.getUID(UniqueIdType.TAGV, null); } @Test (expected = IllegalArgumentException.class) public void getUIDEmptyName() { - setupAssignUid(); tsdb.getUID(UniqueIdType.TAGV, ""); } @Test public void assignUidMetric() { - setupAssignUid(); + when(metrics.getId("sys.cpu.1")).thenThrow( + new NoSuchUniqueName("metric", "sys.cpu.1")); + when(metrics.getOrCreateId("sys.cpu.1")) + .thenReturn(new byte[] { 0, 0, 2 }); assertArrayEquals(new byte[] { 0, 0, 2 }, tsdb.assignUid("metric", "sys.cpu.1")); } @Test (expected = IllegalArgumentException.class) public void assignUidMetricExists() { - setupAssignUid(); - tsdb.assignUid("metric", "sys.cpu.0"); + tsdb.assignUid("metric", METRIC_STRING); } @Test public void assignUidTagk() { - setupAssignUid(); + when(tag_names.getId("datacenter")).thenThrow( + new NoSuchUniqueName("tagk", "datacenter")); + when(tag_names.getOrCreateId("datacenter")) + .thenReturn(new byte[] { 0, 0, 2 }); assertArrayEquals(new byte[] { 0, 0, 2 }, tsdb.assignUid("tagk", "datacenter")); } @Test (expected = IllegalArgumentException.class) public void assignUidTagkExists() { - setupAssignUid(); - tsdb.assignUid("tagk", "host"); + tsdb.assignUid("tagk", TAGK_STRING); } @Test public void assignUidTagv() { - setupAssignUid(); + when(tag_values.getId("localhost")).thenThrow( + new NoSuchUniqueName("tagv", "localhost")); + when(tag_values.getOrCreateId("localhost")) + .thenReturn(new byte[] { 0, 0, 2 }); assertArrayEquals(new byte[] { 0, 0, 2 }, - tsdb.assignUid("tagv", "myserver")); + tsdb.assignUid("tagv", "localhost")); } @Test (expected = IllegalArgumentException.class) public void assignUidTagvExists() { - setupAssignUid(); - tsdb.assignUid("tagv", "localhost"); + tsdb.assignUid("tagv", TAGV_STRING); } @Test (expected = IllegalArgumentException.class) public void assignUidBadType() { - setupAssignUid(); - tsdb.assignUid("nothere", "localhost"); + tsdb.assignUid("nothere", METRIC_STRING); } @Test (expected = NullPointerException.class) public void assignUidNullType() { - setupAssignUid(); - tsdb.assignUid(null, "localhost"); + tsdb.assignUid(null, METRIC_STRING); } @Test (expected = IllegalArgumentException.class) public void assignUidNullName() { - setupAssignUid(); tsdb.assignUid("metric", null); } @Test (expected = IllegalArgumentException.class) public void assignUidInvalidCharacter() { - setupAssignUid(); tsdb.assignUid("metric", "Not!A:Valid@Name"); } @@ -344,9 +305,8 @@ public void uidTable() { @Test public void addPointLong1Byte() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); @@ -357,9 +317,8 @@ public void addPointLong1Byte() throws Exception { @Test public void addPointLong1ByteNegative() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, -42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, -42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); @@ -370,9 +329,8 @@ public void addPointLong1ByteNegative() throws Exception { @Test public void addPointLong2Bytes() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 257, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 257, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); @@ -383,9 +341,8 @@ public void addPointLong2Bytes() throws Exception { @Test public void addPointLong2BytesNegative() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, -257, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, -257, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); @@ -396,9 +353,8 @@ public void addPointLong2BytesNegative() throws Exception { @Test public void addPointLong4Bytes() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 65537, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 65537, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); @@ -409,9 +365,8 @@ public void addPointLong4Bytes() throws Exception { @Test public void addPointLong4BytesNegative() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, -65537, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, -65537, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); @@ -422,9 +377,8 @@ public void addPointLong4BytesNegative() throws Exception { @Test public void addPointLong8Bytes() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 4294967296L, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 4294967296L, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); @@ -435,9 +389,8 @@ public void addPointLong8Bytes() throws Exception { @Test public void addPointLong8BytesNegative() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, -4294967296L, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, -4294967296L, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); @@ -448,9 +401,8 @@ public void addPointLong8BytesNegative() throws Exception { @Test public void addPointLongMs() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400500L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, @@ -462,11 +414,10 @@ public void addPointLongMs() throws Exception { @Test public void addPointLongMany() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + long timestamp = 1356998400; for (int i = 1; i <= 50; i++) { - tsdb.addPoint("sys.cpu.user", timestamp++, i, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); } final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; @@ -479,11 +430,10 @@ public void addPointLongMany() throws Exception { @Test public void addPointLongManyMs() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + long timestamp = 1356998400500L; for (int i = 1; i <= 50; i++) { - tsdb.addPoint("sys.cpu.user", timestamp++, i, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); } final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; @@ -497,9 +447,8 @@ public void addPointLongManyMs() throws Exception { @Test public void addPointLongEndOfRow() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1357001999, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1357001999, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, @@ -511,10 +460,9 @@ public void addPointLongEndOfRow() throws Exception { @Test public void addPointLongOverwrite() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", 1356998400, 24, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, 24, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); @@ -522,24 +470,18 @@ public void addPointLongOverwrite() throws Exception { assertEquals(24, value[0]); } - @SuppressWarnings("unchecked") @Test (expected = NoSuchUniqueName.class) public void addPointNoAutoMetric() throws Exception { setupAddPointStorage(); - when(metrics.getId(anyString())).thenThrow(new NoSuchUniqueName("sys.cpu.user", "metric")); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(NSUN_METRIC, 1356998400, 42, tags).joinUninterruptibly(); } @Test public void addPointSecondZero() throws Exception { // Thu, 01 Jan 1970 00:00:00 GMT setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 0, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 0, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); @@ -550,9 +492,8 @@ public void addPointSecondZero() throws Exception { public void addPointSecondOne() throws Exception { // hey, it's valid *shrug* Thu, 01 Jan 1970 00:00:01 GMT setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 16 }); assertNotNull(value); @@ -563,9 +504,8 @@ public void addPointSecondOne() throws Exception { public void addPointSecond2106() throws Exception { // Sun, 07 Feb 2106 06:28:15 GMT setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 4294967295L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 4294967295L, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, 0x60, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0x69, (byte) 0xF0 }); @@ -578,18 +518,16 @@ public void addPointSecondNegative() throws Exception { // Fri, 13 Dec 1901 20:45:52 GMT // may support in the future, but 1.0 didn't setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", -2147483648, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, -2147483648, 42, tags).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void emptyTagValue() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap() {{ - put("host", ""); - }}; - tsdb.addPoint("sys.cpu.user", 1234567890, 42, tags).joinUninterruptibly(); + + tags.put(TAGK_STRING, ""); + tsdb.addPoint(METRIC_STRING, 1234567890, 42, tags).joinUninterruptibly(); } @Test @@ -599,9 +537,8 @@ public void addPointMS1970() throws Exception { // Base time is 4294800 which is Thu, 19 Feb 1970 17:00:00 GMT // offset = F0A36000 or 167296 ms setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 4294967296L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 4294967296L, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0, (byte) 0x41, (byte) 0x88, (byte) 0x90, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, @@ -614,9 +551,8 @@ public void addPointMS1970() throws Exception { public void addPointMS2106() throws Exception { // Sun, 07 Feb 2106 06:28:15.000 GMT setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 4294967295000L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 4294967295000L, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, 0x60, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF6, @@ -629,9 +565,8 @@ public void addPointMS2106() throws Exception { public void addPointMS2286() throws Exception { // It's an artificial limit and more thought needs to be put into it setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 9999999999999L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 9999999999999L, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, (byte) 0x54, (byte) 0x0B, (byte) 0xD9, 0x10, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xFA, @@ -644,9 +579,8 @@ public void addPointMS2286() throws Exception { public void addPointMSTooLarge() throws Exception { // It's an artificial limit and more thought needs to be put into it setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 10000000000000L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 10000000000000L, 42, tags).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) @@ -656,15 +590,14 @@ public void addPointMSNegative() throws Exception { setupAddPointStorage(); HashMap tags = new HashMap(1); tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", -2147483648000L, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, -2147483648000L, 42, tags).joinUninterruptibly(); } @Test public void addPointFloat() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42.5F, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); @@ -678,7 +611,7 @@ public void addPointFloatNegative() throws Exception { setupAddPointStorage(); HashMap tags = new HashMap(1); tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, -42.5F, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, -42.5F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); @@ -690,9 +623,8 @@ public void addPointFloatNegative() throws Exception { @Test public void addPointFloatMs() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400500L, 42.5F, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, @@ -707,7 +639,7 @@ public void addPointFloatEndOfRow() throws Exception { setupAddPointStorage(); HashMap tags = new HashMap(1); tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1357001999, 42.5F, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1357001999, 42.5F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, @@ -720,9 +652,8 @@ public void addPointFloatEndOfRow() throws Exception { @Test public void addPointFloatPrecision() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42.5123459999F, tags) + + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5123459999F, tags) .joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; @@ -735,10 +666,9 @@ public void addPointFloatPrecision() throws Exception { @Test public void addPointFloatOverwrite() throws Exception { setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42.5F, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", 1356998400, 25.4F, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, 25.4F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); @@ -753,10 +683,9 @@ public void addPointBothSameTimeIntAndFloat() throws Exception { // a float (or vice-versa) with the same timestamp. What happens in the // aggregators when this occurs? setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", 1356998400, 42.5F, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); @@ -775,10 +704,9 @@ public void addPointBothSameTimeIntAndFloatMs() throws Exception { // a float (or vice-versa) with the same timestamp. What happens in the // aggregators when this occurs? setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400500L, 42, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", 1356998400500L, 42.5F, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); @@ -796,10 +724,9 @@ public void addPointBothSameTimeSecondAndMs() throws Exception { // this can happen if a second and an ms data point are stored for the same // timestamp. setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", 1356998400000L, 42, tags).joinUninterruptibly(); + + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400000L, 42, tags).joinUninterruptibly(); final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); @@ -812,24 +739,53 @@ public void addPointBothSameTimeSecondAndMs() throws Exception { assertEquals(42, value[0]); } - /** - * Helper to mock the UID caches with valid responses - */ - private void setupAssignUid() { - when(metrics.getId("sys.cpu.0")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getId("sys.cpu.1")).thenThrow( - new NoSuchUniqueName("metric", "sys.cpu.1")); - when(metrics.getOrCreateId("sys.cpu.1")).thenReturn(new byte[] { 0, 0, 2 }); + @Test + public void addPointWithSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getId("datacenter")).thenThrow( - new NoSuchUniqueName("tagk", "datacenter")); - when(tag_names.getOrCreateId("datacenter")).thenReturn(new byte[] { 0, 0, 2 }); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointWithSaltDifferentTags() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - when(tag_values.getId("localhost")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getId("myserver")).thenThrow( - new NoSuchUniqueName("tagv", "myserver")); - when(tag_values.getOrCreateId("myserver")).thenReturn(new byte[] { 0, 0, 2 }); + setupAddPointStorage(); + tags.put(TAGK_STRING, TAGV_B_STRING); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 9, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointWithSaltDifferentTime() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1359680400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x51, (byte) 0x0B, 0x13, + (byte) 0x90, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); } /** @@ -858,17 +814,5 @@ private void setGetUidName() { @Test public void setupAddPointStorage() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); - when(metrics.width()).thenReturn((short)3); - when(metrics.getId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); - when(tag_values.getId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getOrCreateId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getOrCreateId(anyString())).thenReturn(new byte[] { 0, 0, 1 }); - - HashMap tags = new HashMap() {{ - put("host", "web01"); - }}; } } From 5e8aac106794e3f3b1c9e7ee281fa8cfc8d721d4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 14:22:41 -0700 Subject: [PATCH 080/826] Add salting support to the RowKey class along with unit tests that were missing. This moves methods from Internal where they don't really belong. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/core/RowKey.java | 139 +++++++- test/core/TestRowKey.java | 668 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 799 insertions(+), 9 deletions(-) create mode 100644 test/core/TestRowKey.java diff --git a/Makefile.am b/Makefile.am index 29567fc050..e896caf568 100644 --- a/Makefile.am +++ b/Makefile.am @@ -162,6 +162,7 @@ test_SRC := \ test/core/TestInternal.java \ test/core/TestMutableDataPoint.java \ test/core/TestRateSpan.java \ + test/core/TestRowKey.java \ test/core/TestRowSeq.java \ test/core/TestSpan.java \ test/core/TestTags.java \ diff --git a/src/core/RowKey.java b/src/core/RowKey.java index ee733daa23..0635b21a97 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -13,6 +13,7 @@ package net.opentsdb.core; import java.util.Arrays; +import java.util.Comparator; import org.hbase.async.Bytes; @@ -30,6 +31,7 @@ private RowKey() { * @param tsdb The TSDB to use. * @param row The actual row key. * @return The name of the metric. + * @throws NoSuchUniqueId if the UID could not resolve to a string */ static String metricName(final TSDB tsdb, final byte[] row) { try { @@ -45,26 +47,42 @@ static String metricName(final TSDB tsdb, final byte[] row) { * Extracts the name of the metric ID contained in a row key. * @param tsdb The TSDB to use. * @param row The actual row key. - * @return The name of the metric. + * @return A deferred to wait on that will return the name of the metric. + * @throws IllegalArgumentException if the row key is too short due to missing + * salt or metric or if it's null/empty. + * @throws NoSuchUniqueId if the UID could not resolve to a string * @since 1.2 */ public static Deferred metricNameAsync(final TSDB tsdb, final byte[] row) { - final byte[] id = Arrays.copyOfRange(row, 0, tsdb.metrics.width()); + if (row == null || row.length < 0) { + throw new IllegalArgumentException("Row key cannot be null or empty"); + } + if (row.length < Const.SALT_WIDTH() + tsdb.metrics.width()) { + throw new IllegalArgumentException("Row key is too short"); + } + final byte[] id = Arrays.copyOfRange( + row, Const.SALT_WIDTH(), tsdb.metrics.width() + Const.SALT_WIDTH()); return tsdb.metrics.getNameAsync(id); } /** * Generates a row key given a TSUID and an absolute timestamp. The timestamp - * will be normalized to an hourly base time. + * will be normalized to an hourly base time. If salting is enabled then + * empty salt bytes will be prepended to the key and must be filled in later. * @param tsdb The TSDB to use for fetching tag widths * @param tsuid The TSUID to use for the key * @param timestamp An absolute time from which we generate the row base time * @return A row key for use in fetching data from OpenTSDB + * @throws IllegalArgumentException if the TSUID is too short, i.e. doesn't + * contain a metric * @since 2.0 */ public static byte[] rowKeyFromTSUID(final TSDB tsdb, final byte[] tsuid, final long timestamp) { + if (tsuid.length < tsdb.metrics.width()) { + throw new IllegalArgumentException("TSUID appears to be missing the metric"); + } final long base_time; if ((timestamp & Const.SECOND_MASK) != 0) { // drop the ms timestamp to seconds to calculate the base timestamp @@ -73,12 +91,115 @@ public static byte[] rowKeyFromTSUID(final TSDB tsdb, final byte[] tsuid, } else { base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } - final byte[] row = new byte[tsuid.length + Const.TIMESTAMP_BYTES]; - System.arraycopy(tsuid, 0, row, 0, TSDB.metrics_width()); - Bytes.setInt(row, (int) base_time, TSDB.metrics_width()); - System.arraycopy(tsuid, TSDB.metrics_width(), row, - TSDB.metrics_width() + Const.TIMESTAMP_BYTES, - tsuid.length - TSDB.metrics_width()); + final byte[] row = + new byte[Const.SALT_WIDTH() + tsuid.length + Const.TIMESTAMP_BYTES]; + System.arraycopy(tsuid, 0, row, Const.SALT_WIDTH(), tsdb.metrics.width()); + Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + tsdb.metrics.width()); + System.arraycopy(tsuid, tsdb.metrics.width(), row, + Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES, + tsuid.length - tsdb.metrics.width()); return row; } + + /** + * Returns the byte array for the given salt id + * WARNING: Don't use this one unless you know what you're doing. It's here + * for unit testing. + * @param bucket The ID of the bucket to get the salt for + * @return The salt as a byte array based on the width in bytes + * @since 2.2 + */ + public static byte[] getSaltBytes(final int bucket) { + final byte[] bytes = new byte[Const.SALT_WIDTH()]; + int shift = 0; + for (int i = 1;i <= Const.SALT_WIDTH(); i++) { + bytes[Const.SALT_WIDTH() - i] = (byte) (bucket >>> shift); + shift += 8; + } + return bytes; + } + + /** + * Calculates and writes an array of one or more salt bytes at the front of + * the given row key. + * + * The salt is calculated by taking the Java hash code of the metric and + * tag UIDs and returning a modulo based on the number of salt buckets. + * The result will always be a positive integer from 0 to salt buckets. + * + * NOTE: The row key passed in MUST have allocated the {@link width} number of + * bytes at the front of the row key or this call will overwrite data. + * + * WARNING: If the width is set to a positive value, then the bucket must be + * at least 1 or greater. + * @param row_key The pre-allocated row key to write the salt to + * @since 2.2 + */ + public static void prefixKeyWithSalt(final byte[] row_key) { + if (Const.SALT_WIDTH() > 0) { + if (row_key.length < (Const.SALT_WIDTH() + TSDB.metrics_width()) || + (Bytes.memcmp(row_key, new byte[Const.SALT_WIDTH() + TSDB.metrics_width()], + Const.SALT_WIDTH(), TSDB.metrics_width()) == 0)) { + // ^ Don't salt the global annotation row, leave it at zero + return; + } + final int tags_start = Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES; + + // we want the metric and tags, not the timestamp + final byte[] salt_base = + new byte[row_key.length - Const.SALT_WIDTH() - Const.TIMESTAMP_BYTES]; + System.arraycopy(row_key, Const.SALT_WIDTH(), salt_base, 0, TSDB.metrics_width()); + System.arraycopy(row_key, tags_start,salt_base, TSDB.metrics_width(), + row_key.length - tags_start); + int modulo = Arrays.hashCode(salt_base) % Const.SALT_BUCKETS(); + if (modulo < 0) { + // make sure we return a positive salt. + modulo = modulo * -1; + } + + final byte[] salt = getSaltBytes(modulo); + System.arraycopy(salt, 0, row_key, 0, Const.SALT_WIDTH()); + } // else salting is disabled so it's a no-op + } + + /** + * Checks a row key to determine if it contains the metric UID. If salting is + * enabled, we skip the salt bytes. + * @param metric The metric UID to match + * @param row_key The row key to match on + * @return 0 if the two arrays are identical, otherwise the difference + * between the first two different bytes (treated as unsigned), otherwise + * the different between their lengths. + * @throws IndexOutOfBoundsException if either array isn't large enough. + */ + public static int rowKeyContainsMetric(final byte[] metric, + final byte[] row_key) { + int idx = Const.SALT_WIDTH(); + for (int i = 0; i < metric.length; i++, idx++) { + if (metric[i] != row_key[idx]) { + return (metric[i] & 0xFF) - (row_key[idx] & 0xFF); // "promote" to unsigned. + } + } + return 0; + } + + /** + * A comparator that ignores the salt in row keys + */ + public static class SaltCmp implements Comparator { + public int compare(final byte[] a, final byte[] b) { + final int length = Math.min(a.length, b.length); + if (a == b) { // Do this after accessing a.length and b.length + return 0; // in order to NPE if either a or b is null. + } + // Skip salt + for (int i = Const.SALT_WIDTH(); i < length; i++) { + if (a[i] != b[i]) { + return (a[i] & 0xFF) - (b[i] & 0xFF); // "promote" to unsigned. + } + } + return a.length - b.length; + } + } } diff --git a/test/core/TestRowKey.java b/test/core/TestRowKey.java new file mode 100644 index 0000000000..aea4ce34d2 --- /dev/null +++ b/test/core/TestRowKey.java @@ -0,0 +1,668 @@ +// 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import net.opentsdb.uid.NoSuchUniqueId; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +public class TestRowKey extends BaseTsdbTest { + + @Test + public void metricNameAsync() throws Exception { + final byte[] key = { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + assertEquals(METRIC_STRING, RowKey.metricNameAsync(tsdb, key) + .joinUninterruptibly()); + } + + @Test + public void metricNameAsyncSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + byte[] key = { 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + assertEquals(METRIC_STRING, RowKey.metricNameAsync(tsdb, key) + .joinUninterruptibly()); + + key = new byte[] { 1, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + assertEquals(METRIC_STRING, RowKey.metricNameAsync(tsdb, key) + .joinUninterruptibly()); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(2); + key = new byte[] { 0, 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + assertEquals(METRIC_STRING, RowKey.metricNameAsync(tsdb, key) + .joinUninterruptibly()); + + key = new byte[] { 0, 1, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + assertEquals(METRIC_STRING, RowKey.metricNameAsync(tsdb, key) + .joinUninterruptibly()); + } + + @Test (expected = IllegalArgumentException.class) + public void metricNameAsyncRowNull() throws Exception { + RowKey.metricNameAsync(tsdb, null).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void metricNameAsyncRowEmpty() throws Exception { + RowKey.metricNameAsync(tsdb, new byte[] { }).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void metricNameAsyncRowTooShort() throws Exception { + RowKey.metricNameAsync(tsdb, new byte[] { 0, 1 }).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void metricNameAsyncMissingSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + RowKey.metricNameAsync(tsdb, new byte[] { 0, 0, 1 }).joinUninterruptibly(); + } + + @Test (expected = NoSuchUniqueId.class) + public void metricNameAsyncNoSuchUniqueId() throws Exception { + final byte[] key = { 0, 0, 3, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.metricNameAsync(tsdb, key).joinUninterruptibly(); + } + + @Test (expected = NullPointerException.class) + public void metricNameAsyncNullTsdb() throws Exception { + final byte[] key = { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.metricNameAsync(null, key).joinUninterruptibly(); + } + + @Test + public void rowKeyFromTSUID() throws Exception { + final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 2 }; + byte[] key = { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); + + // zero timestamp + key = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 0)); + + // negative timestamp; honey badger don't care + key = new byte[] { 0, 0, 1, -1, -21, 88, -128, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, -1356998400)); + } + + @Test (expected = NullPointerException.class) + public void rowKeyFromTSUIDNullTsuid() throws Exception { + RowKey.rowKeyFromTSUID(tsdb, null, 1356998400); + } + + @Test (expected = NullPointerException.class) + public void rowKeyFromTSUIDNullTsdb() throws Exception { + final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 2 }; + RowKey.rowKeyFromTSUID(null, tsuid, 1356998400); + } + + @Test (expected = IllegalArgumentException.class) + public void rowKeyFromTSUIDShortTsuid() throws Exception { + final byte[] tsuid = { 0, 1 }; + RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400); + } + + @Test (expected = IllegalArgumentException.class) + public void rowKeyFromTSUIDEmptyTsuid() throws Exception { + final byte[] tsuid = { }; + RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400); + } + + @Test + public void rowKeyFromTSUIDNoTags() throws Exception { + final byte[] tsuid = { 0, 0, 1 }; + byte[] key = { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); + } + + @Test + public void rowKeyFromTSUIDMillis() throws Exception { + final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 2 }; + byte[] key = { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400123L)); + } + + @Test + public void rowKeyFromTSUIDSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 2 }; + byte[] key = { 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); + + // zero timestamp + key = new byte[] { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 0)); + + // negative timestamp; honey badger don't care + key = new byte[] { 0, 0, 0, 1, -1, -21, 88, -128, 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, -1356998400)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); + key = new byte[] { 0, 0, 0, 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2 }; + assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); + } + + + @Test + public void getSalt() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); + assertArrayEquals(new byte[] {}, RowKey.getSaltBytes(2)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + assertArrayEquals(new byte[] { 2 }, RowKey.getSaltBytes(2)); + assertArrayEquals(new byte[] { 20 }, RowKey.getSaltBytes(20)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(2); + assertArrayEquals(new byte[] { 0, 20 }, RowKey.getSaltBytes(20)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); + assertArrayEquals(new byte[] { 0, 0, 0, 20 }, RowKey.getSaltBytes(20)); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + assertArrayEquals(new byte[] { -2 }, RowKey.getSaltBytes(-2)); + } + + @Test (expected = NegativeArraySizeException.class) + public void getSaltNegativeWidth() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(-1); + RowKey.getSaltBytes(2); + } + + @Test + public void prefixKeyWithSaltGlobalAndNoOps() { + setupSalt(); + // short rows + byte[] key = new byte[1]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[1], key); + + key = new byte[2]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[2], key); + + key = new byte[3]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[3], key); + + key = new byte[4]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[4], key); + + key = new byte[5]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[5], key); + + key = new byte[6]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[6], key); + + key = new byte[7]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[7], key); + + key = new byte[8]; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[8], key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x50, (byte) 0xE2, 0x27, 0x00}; + byte[] compare = Arrays.copyOf(key, key.length); + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x01, 0x50, (byte) 0xE2, 0x27, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltSameMetricDifferentTags() { + setupSalt(); + byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x10; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x00; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x09; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x03}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x03; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + // no tags. Shouldn't happen, but *shrug* + key = new byte[] { 0x00, 0x00, 0x00, 0x01, 0x50, (byte) 0xE2, 0x27, 0x00 }; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x0C; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltDifferentMetricSameTags() { + setupSalt(); + byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x02, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x09; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x03, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x06; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x0B, 0x14, 0x20, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x06; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltSameMetricSameTagsDifferentTimestamp() { + setupSalt(); + byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x35, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x51, (byte) 0x0B, 0x13, (byte) 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltOverwrite() { + setupSalt(); + // makes sure we ignore and overwrite anything in the salt position + byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { (byte) 0xFF, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + + key = new byte[] { (byte) 0x0E, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test (expected = ArithmeticException.class) + public void prefixKeyWithSaltZeroBucket() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(0); + + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + RowKey.prefixKeyWithSalt(key); + } + + // This actually works, but PLEASE don't do it! + @Test + public void prefixKeyWithSaltNegativeBucket() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(-20); + + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + final byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x08; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltNegativeWidth() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(-1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + + final byte[] key = new byte[] { 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; + final byte[] compare = Arrays.copyOf(key, key.length); + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltMissingTagV() { + setupSalt(); + // Honey badger don't care + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01}; + final byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x0D; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test + public void prefixKeyWithSaltPartialTagK() { + setupSalt(); + // Honey badger don't care + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00 }; + final byte[] compare = Arrays.copyOf(key, key.length); + compare[0] = 0x0C; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(compare, key); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void prefixKeyWithSaltMissingTags() { + setupSalt(); + final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27 }; + RowKey.prefixKeyWithSalt(key); + } + + @Test (expected = NullPointerException.class) + public void prefixKeyWithSaltNullKey() { + setupSalt(); + RowKey.prefixKeyWithSalt(null); + } + + @Test + public void prefixKeyWithSaltEmptyKey() { + setupSalt(); + final byte[] key = new byte[] {}; + RowKey.prefixKeyWithSalt(key); + assertArrayEquals(new byte[] {}, key); + } + + @Test + public void rowKeyContainsMetric() { + setupSalt(); + final byte[] metric = new byte[] { 0, 0, 1 }; + + assertEquals(0, RowKey.rowKeyContainsMetric(metric, + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // not there! + assertEquals(-1, RowKey.rowKeyContainsMetric(metric, + new byte[] { 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // double salt bytes + assertEquals(-1, RowKey.rowKeyContainsMetric(metric, + new byte[] { 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // missing salt but expecting it + assertEquals(-1, RowKey.rowKeyContainsMetric(metric, + new byte[] {0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // both empty + assertEquals(0, RowKey.rowKeyContainsMetric(new byte[] { }, + new byte[] { })); + + // empty metric returns 0 TODO(clarsen) see if we really want that + assertEquals(0, RowKey.rowKeyContainsMetric(new byte[] { }, + new byte[] {0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + } + + @Test (expected = NullPointerException.class) + public void rowKeyContainsMetricNullMetric() { + setupSalt(); + RowKey.rowKeyContainsMetric(null, + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }); + } + + @Test (expected = NullPointerException.class) + public void rowKeyContainsMetricNullKey() { + setupSalt(); + RowKey.rowKeyContainsMetric(new byte[] { 0, 0, 1 }, null); + } + + @Test (expected = IndexOutOfBoundsException.class) + public void rowKeyContainsMetricKeyTooShortSalted() { + setupSalt(); + RowKey.rowKeyContainsMetric(new byte[] { 0, 0, 1 }, + new byte[] { 1, 0, 0 }); + } + + @Test (expected = IndexOutOfBoundsException.class) + public void rowKeyContainsMetricKeyTooShortNoSalt() { + RowKey.rowKeyContainsMetric(new byte[] { 0, 0, 1 }, + new byte[] { 0, 0 }); + } + + @Test + public void rowKeyContainsMetricNoSalt() { + final byte[] metric = new byte[] { 0, 0, 1 }; + + assertEquals(0, RowKey.rowKeyContainsMetric(metric, + new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // not there! + assertEquals(1, RowKey.rowKeyContainsMetric(metric, + new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // salted but shouldn't be + assertEquals(-1, RowKey.rowKeyContainsMetric(metric, + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + } + + @Test + public void saltCmp() { + setupSalt(); + + // diff salt, same keys + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // same addy + final byte[] key = new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }; + assertEquals(0, new RowKey.SaltCmp().compare(key, key)); + + assertEquals(1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 4 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 4 })); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0 })); + + // nothing after the salt + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { 1 }, + new byte[] { 2 })); + + // empty + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { }, + new byte[] { })); + + // wider salt + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(3); + + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { 1, 3, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 4, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { 1, 3, 5, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 4, 6, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 3, 5, 7, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 4, 6, 8, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + } + + @Test (expected = NullPointerException.class) + public void saltCmpNullA() throws Exception { + setupSalt(); + new RowKey.SaltCmp().compare(null, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }); + } + + @Test (expected = NullPointerException.class) + public void saltCmpNullB() throws Exception { + setupSalt(); + new RowKey.SaltCmp().compare( + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + null); + } + + @Test + public void saltCmpNoSalt() { + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // diff salt, same keys + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + // same addy + final byte[] key = new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }; + assertEquals(0, new RowKey.SaltCmp().compare(key, key)); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 4 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 4 })); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 })); + + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3 }, + new byte[] { 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0 })); + + // nothing after the salt + assertEquals(-1, new RowKey.SaltCmp().compare( + new byte[] { 1 }, + new byte[] { 2 })); + + // empty + assertEquals(0, new RowKey.SaltCmp().compare( + new byte[] { }, + new byte[] { })); + } + + /** + * Mocks out the static Const class for a single salt byte with 20 buckets + */ + private static void setupSalt() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + } + +} From 37511b18b8493801333573a8ba515dcf02835e31 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 16 Mar 2015 20:52:34 -0700 Subject: [PATCH 081/826] Remove the salt methods from Internal as they've moved to RowKey Signed-off-by: Chris Larsen --- src/core/Internal.java | 63 +------- test/core/TestInternal.java | 314 +----------------------------------- 2 files changed, 2 insertions(+), 375 deletions(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index 2dfed3120f..bde1e57a53 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -844,7 +844,7 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, buf.append("(?s)" // Ensure we use the DOTALL flag. + "^.{") // ... start by skipping the metric ID and timestamp. - .append(TSDB.metrics_width() + Const.TIMESTAMP_BYTES) + .append(Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES) .append("}("); for (final byte[] tags : uids) { @@ -860,65 +860,4 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, scanner.setKeyRegexp(buf.toString(), Charset.forName("ISO-8859-1")); } - /** - * Returns the byte array for the given salt id - * WARNING: Don't use this one unless you know what you're doing. It's here - * for unit testing. - * @param bucket The ID of the bucket to get the salt for - * @return The salt as a byte array based on the width in bytes - * @since 2.2 - */ - public static byte[] getSaltBytes(final int bucket) { - final byte[] bytes = new byte[Const.SALT_WIDTH()]; - int shift = 0; - for (int i = 1;i <= Const.SALT_WIDTH(); i++) { - bytes[Const.SALT_WIDTH() - i] = (byte) (bucket >>> shift); - shift += 8; - } - return bytes; - } - - /** - * Calculates and writes an array of one or more salt bytes at the front of - * the given row key. - * - * The salt is calculated by taking the Java hash code of the metric and - * tag UIDs and returning a modulo based on the number of salt buckets. - * The result will always be a positive integer from 0 to salt buckets. - * - * NOTE: The row key passed in MUST have allocated the {@link width} number of - * bytes at the front of the row key or this call will overwrite data. - * - * WARNING: If the width is set to a positive value, then the bucket must be - * at least 1 or greater. - * @param row_key The pre-allocated row key to write the salt to - * @since 2.2 - */ - public static void prefixKeyWithSalt(final byte[] row_key) { - if (Const.SALT_WIDTH() > 0) { - if (row_key.length < (Const.SALT_WIDTH() + TSDB.metrics_width()) || - (Bytes.memcmp(row_key, new byte[Const.SALT_WIDTH() + TSDB.metrics_width()], - Const.SALT_WIDTH(), TSDB.metrics_width()) == 0)) { - // ^ Don't salt the global annotation row, leave it at zero - return; - } - final int tags_start = Const.SALT_WIDTH() + TSDB.metrics_width() + - Const.TIMESTAMP_BYTES; - - // we want the metric and tags, not the timestamp - final byte[] salt_base = - new byte[row_key.length - Const.SALT_WIDTH() - Const.TIMESTAMP_BYTES]; - System.arraycopy(row_key, Const.SALT_WIDTH(), salt_base, 0, TSDB.metrics_width()); - System.arraycopy(row_key, tags_start,salt_base, TSDB.metrics_width(), - row_key.length - tags_start); - int modulo = Arrays.hashCode(salt_base) % Const.SALT_BUCKETS(); - if (modulo < 0) { - // make sure we return a positive salt. - modulo = modulo * -1; - } - - final byte[] salt = Internal.getSaltBytes(modulo); - System.arraycopy(salt, 0, row_key, 0, Const.SALT_WIDTH()); - } // else salting is disabled so it's a no-op - } } diff --git a/test/core/TestInternal.java b/test/core/TestInternal.java index 9dd54cc8e8..4259072b89 100644 --- a/test/core/TestInternal.java +++ b/test/core/TestInternal.java @@ -18,7 +18,6 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; -import java.util.Arrays; import net.opentsdb.core.Internal.Cell; import net.opentsdb.storage.MockBase; @@ -27,7 +26,6 @@ import org.hbase.async.KeyValue; 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; @@ -811,317 +809,7 @@ public void extractQualifierMilliSeconds() { assertArrayEquals(new byte[] { (byte) 0xF0, 0x00, 0x02, 0x07 }, Internal.extractQualifier(qual, 2)); } - - @Test - public void getSalt() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); - assertArrayEquals(new byte[] {}, Internal.getSaltBytes(2)); - - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - assertArrayEquals(new byte[] { 2 }, Internal.getSaltBytes(2)); - assertArrayEquals(new byte[] { 20 }, Internal.getSaltBytes(20)); - - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(2); - assertArrayEquals(new byte[] { 0, 20 }, Internal.getSaltBytes(20)); - - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); - assertArrayEquals(new byte[] { 0, 0, 0, 20 }, Internal.getSaltBytes(20)); - - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - assertArrayEquals(new byte[] { -2 }, Internal.getSaltBytes(-2)); - } - - @Test (expected = NegativeArraySizeException.class) - public void getSaltNegativeWidth() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(-1); - Internal.getSaltBytes(2); - } - - @Test - public void prefixKeyWithSaltGlobalAndNoOps() { - setupSalt(); - // short rows - byte[] key = new byte[1]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[1], key); - - key = new byte[2]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[2], key); - - key = new byte[3]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[3], key); - - key = new byte[4]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[4], key); - - key = new byte[5]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[5], key); - - key = new byte[6]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[6], key); - - key = new byte[7]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[7], key); - - key = new byte[8]; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[8], key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x50, (byte) 0xE2, 0x27, 0x00}; - byte[] compare = Arrays.copyOf(key, key.length); - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x01, 0x50, (byte) 0xE2, 0x27, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltSameMetricDifferentTags() { - setupSalt(); - byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x10; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x00; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x09; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x02, 0x00, 0x00, 0x03}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x03; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - // no tags. Shouldn't happen, but *shrug* - key = new byte[] { 0x00, 0x00, 0x00, 0x01, 0x50, (byte) 0xE2, 0x27, 0x00 }; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x0C; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltDifferentMetricSameTags() { - setupSalt(); - byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x02, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x09; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x03, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x06; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x0B, 0x14, 0x20, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x06; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltSameMetricSameTagsDifferentTimestamp() { - setupSalt(); - byte[] key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x35, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x51, (byte) 0x0B, 0x13, (byte) 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltOverwrite() { - setupSalt(); - // makes sure we ignore and overwrite anything in the salt position - byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { (byte) 0xFF, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - - key = new byte[] { (byte) 0x0E, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test (expected = ArithmeticException.class) - public void prefixKeyWithSaltZeroBucket() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(0); - - final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - Internal.prefixKeyWithSalt(key); - } - - // This actually works, but PLEASE don't do it! - @Test - public void prefixKeyWithSaltNegativeBucket() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(-20); - - final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - final byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x08; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltNegativeWidth() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(-1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - - final byte[] key = new byte[] { 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01}; - final byte[] compare = Arrays.copyOf(key, key.length); - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltMissingTagV() { - setupSalt(); - // Honey badger don't care - final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01}; - final byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x0D; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test - public void prefixKeyWithSaltPartialTagK() { - setupSalt(); - // Honey badger don't care - final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00 }; - final byte[] compare = Arrays.copyOf(key, key.length); - compare[0] = 0x0C; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(compare, key); - } - - @Test (expected = ArrayIndexOutOfBoundsException.class) - public void prefixKeyWithSaltMissingTags() { - setupSalt(); - final byte[] key = new byte[] { 0x02, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27 }; - Internal.prefixKeyWithSalt(key); - } - - @Test (expected = NullPointerException.class) - public void prefixKeyWithSaltNullKey() { - setupSalt(); - Internal.prefixKeyWithSalt(null); - } - - @Test - public void prefixKeyWithSaltEmptyKey() { - setupSalt(); - final byte[] key = new byte[] {}; - Internal.prefixKeyWithSalt(key); - assertArrayEquals(new byte[] {}, key); - } - - /** - * Mocks out the static Const class for a single salt byte with 20 buckets - */ - private static void setupSalt() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - } - + /** Shorthand to create a {@link KeyValue}. */ private static KeyValue makekv(final byte[] qualifier, final byte[] value) { return new KeyValue(KEY, FAMILY, qualifier, value); From 89fdab917030008e1b8cca3693d5f7a815417d94 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 18:07:36 -0700 Subject: [PATCH 082/826] Point the TSDB to use the RowKey class for salting support Signed-off-by: Chris Larsen --- src/core/TSDB.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index c2b8693866..0018d08e02 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -651,7 +651,7 @@ private Deferred addPointInternal(final String metric, } Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); - Internal.prefixKeyWithSalt(row); + RowKey.prefixKeyWithSalt(row); scheduleForCompaction(row, (int) base_time); final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); From 85a722a5abf23ad2da01deb00bfa795bf4696ee0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 16:50:32 -0700 Subject: [PATCH 083/826] Add DateTime.currentTimeMillis() to overload the System method of the same name and make unit testing easier. Signed-off-by: Chris Larsen --- src/utils/DateTime.java | 12 ++++++++++++ test/utils/TestDateTime.java | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index dca0de671b..277cca46cb 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -256,4 +256,16 @@ public static void setDefaultTimezone(final String tzname) { throw new IllegalArgumentException("Invalid timezone name: " + tzname); } } + + /** + * Pass through to {@link System.currentTimeMillis} for use in classes to + * make unit testing easier. Mocking System.class is a bad idea in general + * so placing this here and mocking DateTime.class is MUCH cleaner. + * @return The current epoch time in milliseconds + * @since 2.2 + */ + public static long currentTimeMillis() { + return System.currentTimeMillis(); + } + } diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 1e72b34ccd..23867039c4 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -30,7 +30,7 @@ import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) -@PrepareForTest({ DateTime.class }) +@PrepareForTest({ DateTime.class, System.class }) public final class TestDateTime { @Before @@ -364,4 +364,12 @@ public void setDefaultTimezone() { public void setDefaultTimezoneNull() { DateTime.setDefaultTimezone(null); } + + @Test + public void currentTimeMillis() { + PowerMockito.mockStatic(System.class); + when(System.currentTimeMillis()).thenReturn(1388534400000L); + assertEquals(1388534400000L, DateTime.currentTimeMillis()); + } + } From 1b9be4eac607aa1db31a21c442228fda9f9ca1dd Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 16 Mar 2015 22:53:59 -0700 Subject: [PATCH 084/826] Add salting support to the Span class Signed-off-by: Chris Larsen --- src/core/Span.java | 9 ++++++--- test/core/TestSpan.java | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/core/Span.java b/src/core/Span.java index 6cf0d0c79f..8620aa16ce 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -161,12 +161,14 @@ void addRow(final KeyValue row) { final byte[] key = row.key(); final RowSeq last = rows.get(rows.size() - 1); final short metric_width = tsdb.metrics.width(); - final short tags_offset = (short) (metric_width + Const.TIMESTAMP_BYTES); + final short tags_offset = + (short) (Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES); final short tags_bytes = (short) (key.length - tags_offset); String error = null; if (key.length != last.key.length) { error = "row key length mismatch"; - } else if (Bytes.memcmp(key, last.key, 0, metric_width) != 0) { + } else if ( + Bytes.memcmp(key, last.key, Const.SALT_WIDTH(), metric_width) != 0) { error = "metric ID mismatch"; } else if (Bytes.memcmp(key, last.key, tags_offset, tags_bytes) != 0) { error = "tags mismatch"; @@ -186,7 +188,8 @@ void addRow(final KeyValue row) { if (last_ts >= rowseq.timestamp(0)) { // scan to see if we need to merge into an existing row for (final RowSeq rs : rows) { - if (Bytes.memcmp(rs.key, row.key()) == 0) { + if (Bytes.memcmp(rs.key, row.key(), Const.SALT_WIDTH(), + (rs.key.length - Const.SALT_WIDTH())) == 0) { rs.addRow(row); return; } diff --git a/test/core/TestSpan.java b/test/core/TestSpan.java index 4b06626498..8d04a883aa 100644 --- a/test/core/TestSpan.java +++ b/test/core/TestSpan.java @@ -28,6 +28,7 @@ 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; @@ -43,7 +44,7 @@ "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({ RowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, -Config.class, RowKey.class }) + Config.class, RowKey.class, Const.class }) public final class TestSpan { private TSDB tsdb = mock(TSDB.class); private Config config = mock(Config.class); @@ -85,6 +86,31 @@ public void addRow() { assertEquals(2, span.size()); } + @Test + public void addRowSalted() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + + final byte[] hour1 = { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, + 0, 0, 0, 1, 0, 0, 2 }; + final byte[] hour2 = { 1, 0, 0, 1, 0x50, (byte)0xE2, 0x35, + 0x10, 0, 0, 1, 0, 0, 2 }; + final Span span = new Span(tsdb); + span.addRow(new KeyValue(hour1, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + span.addRow(new KeyValue(hour2, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + + assertEquals(4, span.size()); + } + @Test (expected = NullPointerException.class) public void addRowNull() { final Span span = new Span(tsdb); From 5d12b4748f531e5cabcbf75b39b5f1f494150beb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 13:19:14 -0700 Subject: [PATCH 085/826] Modify UniqueId.getTSUIDFromKey() to handle salting properly. Also modify the method to throw an IllegalArgumentException if the tags are missing or the row key is corrupt. Signed-off-by: Chris Larsen --- src/uid/UniqueId.java | 22 +++++++++++---- test/uid/TestUniqueId.java | 55 +++++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index f0770eca4a..8458c86d79 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -39,6 +39,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.meta.UIDMeta; import net.opentsdb.stats.StatsCollector; @@ -1207,15 +1208,26 @@ public static byte[] stringToUid(final String uid, final short uid_length) { * @param row_key The row key to process * @param metric_width The width of the metric * @param timestamp_width The width of the timestamp - * @return The TSUID - * @throws ArrayIndexOutOfBoundsException if the row_key is invalid + * @return The TSUID as a byte array + * @throws IllegalArgumentException if the row key is missing tags or it is + * corrupt such as a salted key when salting is disabled or vice versa. */ public static byte[] getTSUIDFromKey(final byte[] row_key, final short metric_width, final short timestamp_width) { int idx = 0; - final byte[] tsuid = new byte[row_key.length - timestamp_width]; - for (int i = 0; i < row_key.length; i++) { - if (i < metric_width || i >= (metric_width + timestamp_width)) { + // validation + final int tag_pair_width = TSDB.tagk_width() + TSDB.tagv_width(); + final int tags_length = row_key.length - + (Const.SALT_WIDTH() + metric_width + timestamp_width); + if (tags_length < tag_pair_width || (tags_length % tag_pair_width) != 0) { + throw new IllegalArgumentException( + "Row key is missing tags or it is corrupted " + Arrays.toString(row_key)); + } + final byte[] tsuid = new byte[ + row_key.length - timestamp_width - Const.SALT_WIDTH()]; + for (int i = Const.SALT_WIDTH(); i < row_key.length; i++) { + if (i < Const.SALT_WIDTH() + metric_width || + i >= (Const.SALT_WIDTH() + metric_width + timestamp_width)) { tsuid[idx] = row_key[i]; idx++; } diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index 310a460bdf..bccb35c284 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -20,6 +20,7 @@ import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.utils.Config; @@ -69,7 +70,7 @@ "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({ HBaseClient.class, TSDB.class, Config.class, - RandomUniqueId.class }) + RandomUniqueId.class, Const.class }) public final class TestUniqueId { private HBaseClient client = mock(HBaseClient.class); @@ -762,11 +763,57 @@ public void getTSUIDFromKey() { } @Test + public void getTSUIDFromKeySalted() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + final byte[] expected = { 0, 0, 1, 0, 0, 2, 0, 0, 3 }; + byte[] tsuid = UniqueId.getTSUIDFromKey(new byte[] + { 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 0, 0, 3 }, (short)3, (short)4); + assertArrayEquals(expected, tsuid); + + tsuid = UniqueId.getTSUIDFromKey(new byte[] + { 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 0, 0, 3 }, (short)3, (short)4); + assertArrayEquals(expected, tsuid); + + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); + tsuid = UniqueId.getTSUIDFromKey(new byte[] + { 1, 2, 3, 4, 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 0, 0, 3 }, (short)3, (short)4); + assertArrayEquals(expected, tsuid); + + tsuid = UniqueId.getTSUIDFromKey(new byte[] + { 4, 3, 2, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 0, 0, 3 }, (short)3, (short)4); + assertArrayEquals(expected, tsuid); + } + + @Test (expected = IllegalArgumentException.class) public void getTSUIDFromKeyMissingTags() { - final byte[] tsuid = UniqueId.getTSUIDFromKey(new byte[] + UniqueId.getTSUIDFromKey(new byte[] { 0, 0, 1, 1, 1, 1, 1 }, (short)3, (short)4); - assertArrayEquals(new byte[] { 0, 0, 1 }, - tsuid); + } + + @Test (expected = IllegalArgumentException.class) + public void getTSUIDFromKeyMissingTagsSalted() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + UniqueId.getTSUIDFromKey(new byte[] + { 0, 0, 0, 1, 1, 1, 1, 1 }, (short)3, (short)4); + } + + @Test (expected = IllegalArgumentException.class) + public void getTSUIDFromKeyMissingSalt() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + UniqueId.getTSUIDFromKey(new byte[] + { 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 0, 0, 3 }, (short)3, (short)4); + } + + @Test (expected = IllegalArgumentException.class) + public void getTSUIDFromKeySaltButShouldntBe() { + UniqueId.getTSUIDFromKey(new byte[] + { 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 0, 0, 3 }, (short)3, (short)4); } @Test From 3719a90b6dade26c4c86bd8143d84d507e683872 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 13:38:10 -0700 Subject: [PATCH 086/826] Modify IncomingDataPoints.metricNameAsync() to handle salted rows. Signed-off-by: Chris Larsen --- src/core/IncomingDataPoints.java | 6 +++-- test/core/TestIncomingDataPoints.java | 38 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index be861d4e25..3499c408ec 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -401,9 +401,11 @@ public String metricName() { public Deferred metricNameAsync() { if (row == null) { - throw new IllegalStateException("setSeries never called before!"); + throw new IllegalStateException( + "The row key was null, setSeries was not called."); } - final byte[] id = Arrays.copyOfRange(row, 0, tsdb.metrics.width()); + final byte[] id = Arrays.copyOfRange( + row, Const.SALT_WIDTH(), tsdb.metrics.width() + Const.SALT_WIDTH()); return tsdb.metrics.getNameAsync(id); } diff --git a/test/core/TestIncomingDataPoints.java b/test/core/TestIncomingDataPoints.java index 89d1027dea..e1a333e5ce 100644 --- a/test/core/TestIncomingDataPoints.java +++ b/test/core/TestIncomingDataPoints.java @@ -1,8 +1,20 @@ +// 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; - import net.opentsdb.uid.NoSuchUniqueName; import org.junit.Test; @@ -13,6 +25,30 @@ @RunWith(PowerMockRunner.class) public class TestIncomingDataPoints extends BaseTsdbTest { + @Test + public void metricNameAsync() throws Exception { + final IncomingDataPoints dps = new IncomingDataPoints(tsdb); + dps.setSeries(METRIC_STRING, tags); + assertEquals(METRIC_STRING, dps.metricNameAsync().joinUninterruptibly()); + } + + @Test + public void metricNameAsyncSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + final IncomingDataPoints dps = new IncomingDataPoints(tsdb); + dps.setSeries(METRIC_STRING, tags); + assertEquals(METRIC_STRING, dps.metricNameAsync().joinUninterruptibly()); + } + + @Test (expected = IllegalStateException.class) + public void metricNameAsyncRowNotSet() throws Exception { + final IncomingDataPoints dps = new IncomingDataPoints(tsdb); + dps.metricNameAsync().joinUninterruptibly(); + } + @Test public void rowKeyTemplate() throws Exception { final byte[] expected = new byte[METRIC_BYTES.length + Const.TIMESTAMP_BYTES From 9fb32d1e7dc758034405b824a580402ce88f0142 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 13:58:54 -0700 Subject: [PATCH 087/826] Add the NSUI exceptions to the TestTsdbBase class Remove the mock compaction queue as we need it for some calls when we require returning compacted data points. Instead, just make sure the thread doesn't start up. Move in the data point writers from the TestTsdbQuery class. Add a method to let us get the rowkey template. Add some assertion helpers to the BaseTsdbTest class Add the Scanner to the prep for test in BaseTsdbTest class Signed-off-by: Chris Larsen --- test/core/BaseTsdbTest.java | 261 ++++++++++++++++++++++++++++++++++-- 1 file changed, 253 insertions(+), 8 deletions(-) diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 8e4f913bf0..002f1dedfa 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -1,17 +1,36 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import java.util.HashMap; import java.util.Map; +import net.opentsdb.meta.Annotation; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; import org.hbase.async.HBaseClient; +import org.hbase.async.Scanner; import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.junit.runner.RunWith; @@ -32,7 +51,7 @@ "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - HashedWheelTimer.class, CompactionQueue.class, Const.class }) + HashedWheelTimer.class, Scanner.class, Const.class }) public class BaseTsdbTest { public static final String METRIC_STRING = "sys.cpu.user"; @@ -56,8 +75,10 @@ public class BaseTsdbTest { public static final String NSUN_TAGV = "web03"; public static final byte[] NSUI_TAGV = new byte[] { 0, 0, 3 }; + static final String NOTE_DESCRIPTION = "Hello DiscWorld!"; + static final String NOTE_NOTES = "Millenium hand and shrimp"; + protected HashedWheelTimer timer; - protected CompactionQueue compaction_queue; protected Config config; protected TSDB tsdb; protected HBaseClient client = mock(HBaseClient.class); @@ -70,16 +91,14 @@ public class BaseTsdbTest { @Before public void before() throws Exception { timer = mock(HashedWheelTimer.class); - compaction_queue = mock(CompactionQueue.class); - + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() .thenReturn(timer); PowerMockito.whenNew(HBaseClient.class).withAnyArguments() .thenReturn(client); - PowerMockito.whenNew(CompactionQueue.class).withAnyArguments() - .thenReturn(compaction_queue); config = new Config(false); + config.overrideConfig("tsd.storage.enable_compaction", "false"); tsdb = PowerMockito.spy(new TSDB(config)); config.setAutoMetric(true); @@ -115,9 +134,11 @@ void setupMetricMaps() { when(metrics.getNameAsync(METRIC_BYTES)) .thenReturn(Deferred.fromResult(METRIC_STRING)); when(metrics.getNameAsync(METRIC_B_BYTES)) - .thenReturn(Deferred.fromResult(METRIC_B_STRING)); + .thenReturn(Deferred.fromResult(METRIC_B_STRING)); + when(metrics.getNameAsync(NSUI_METRIC)) + .thenThrow(new NoSuchUniqueId("metrics", NSUI_METRIC)); - final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_METRIC, "metric"); + final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_METRIC, "metrics"); when(metrics.getId(NSUN_METRIC)).thenThrow(nsun); when(metrics.getIdAsync(NSUN_METRIC)) @@ -142,6 +163,10 @@ void setupTagkMaps() { when(tag_names.getNameAsync(TAGK_BYTES)) .thenReturn(Deferred.fromResult(TAGK_STRING)); + when(tag_names.getNameAsync(TAGK_B_BYTES)) + .thenReturn(Deferred.fromResult(TAGK_B_STRING)); + when(tag_names.getNameAsync(NSUI_TAGK)) + .thenThrow(new NoSuchUniqueId("tagk", NSUI_TAGK)); final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_TAGK, "tagk"); @@ -170,6 +195,8 @@ void setupTagvMaps() { .thenReturn(Deferred.fromResult(TAGV_STRING)); when(tag_values.getNameAsync(TAGV_B_BYTES)) .thenReturn(Deferred.fromResult(TAGV_B_STRING)); + when(tag_values.getNameAsync(NSUI_TAGV)) + .thenThrow(new NoSuchUniqueId("tagv", NSUI_TAGV)); final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_TAGV, "tagv"); @@ -177,4 +204,222 @@ void setupTagvMaps() { when(tag_values.getIdAsync(NSUN_TAGV)) .thenReturn(Deferred.fromError(nsun)); } + + // ----------------- // + // Helper functions. // + // ----------------- // + + /** @return a row key template with the default metric and tags */ + protected byte[] getRowKeyTemplate() { + return IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + } + + protected void setDataPointStorage() throws Exception { + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + } + + protected void storeLongTimeSeriesSeconds(final boolean two_metrics, + final boolean offset) throws Exception { + setDataPointStorage(); + + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400; + for (int i = 1; i <= 300; i++) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags_local) + .joinUninterruptibly(); + if (two_metrics) { + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + } + + // dump a parallel set but invert the values + tags_local.clear(); + tags_local.put(TAGK_STRING, TAGV_B_STRING); + timestamp = offset ? 1356998415 : 1356998400; + for (int i = 300; i > 0; i--) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags_local) + .joinUninterruptibly(); + if (two_metrics) { + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + } + } + + protected void storeLongTimeSeriesMs() throws Exception { + setDataPointStorage(); + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400000L; + for (int i = 1; i <= 300; i++) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags_local) + .joinUninterruptibly(); + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + + // dump a parallel set but invert the values + tags_local.clear(); + tags_local.put(TAGK_STRING, TAGV_B_STRING); + timestamp = 1356998400000L; + for (int i = 300; i > 0; i--) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags_local) + .joinUninterruptibly(); + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + } + + protected void storeFloatTimeSeriesSeconds(final boolean two_metrics, + final boolean offset) throws Exception { + setDataPointStorage(); + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400; + for (float i = 1.25F; i <= 76; i += 0.25F) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags_local) + .joinUninterruptibly(); + if (two_metrics) { + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + } + + // dump a parallel set but invert the values + tags_local.clear(); + tags_local.put(TAGK_STRING, TAGV_B_STRING); + timestamp = offset ? 1356998415 : 1356998400; + for (float i = 75F; i > 0; i -= 0.25F) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags_local) + .joinUninterruptibly(); + if (two_metrics) { + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + } + } + + protected void storeFloatTimeSeriesMs() throws Exception { + setDataPointStorage(); + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400000L; + for (float i = 1.25F; i <= 76; i += 0.25F) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags_local) + .joinUninterruptibly(); + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + + // dump a parallel set but invert the values + tags_local.clear(); + tags_local.put(TAGK_STRING, TAGV_B_STRING); + timestamp = 1356998400000L; + for (float i = 75F; i > 0; i -= 0.25F) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags_local) + .joinUninterruptibly(); + tsdb.addPoint(METRIC_B_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + } + + protected void storeMixedTimeSeriesSeconds() throws Exception { + setDataPointStorage(); + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400; + for (float i = 1.25F; i <= 76; i += 0.25F) { + if (i % 2 == 0) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, (long)i, tags_local) + .joinUninterruptibly(); + } else { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags_local) + .joinUninterruptibly(); + } + } + } + + // dumps ints, floats, seconds and ms + protected void storeMixedTimeSeriesMsAndS() throws Exception { + setDataPointStorage(); + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400000L; + for (float i = 1.25F; i <= 76; i += 0.25F) { + long ts = timestamp += 500; + if (ts % 1000 == 0) { + ts /= 1000; + } + if (i % 2 == 0) { + tsdb.addPoint(METRIC_STRING, ts, (long)i, tags_local).joinUninterruptibly(); + } else { + tsdb.addPoint(METRIC_STRING, ts, i, tags_local).joinUninterruptibly(); + } + } + } + + /** + * Validates the metric name, tags and annotations + * @param dps The datapoints array returned from the query + * @param index The index to peek into the array + * @param agged_tags Whether or not the tags were aggregated out + */ + protected void assertMeta(final DataPoints[] dps, final int index, + final boolean agged_tags) { + assertMeta(dps, index, agged_tags, false); + } + + /** + * Validates the metric name, tags and annotations + * @param dps The datapoints array returned from the query + * @param index The index to peek into the array + * @param agged_tags Whether or not the tags were aggregated out + * @param annotation Whether we're expecting a note or not + */ + protected void assertMeta(final DataPoints[] dps, final int index, + final boolean agged_tags, final boolean annotation) { + assertNotNull(dps); + assertEquals(METRIC_STRING, dps[index].metricName()); + + if (agged_tags) { + assertTrue(dps[index].getTags().isEmpty()); + assertEquals(TAGK_STRING, dps[index].getAggregatedTags().get(0)); + } else { + if (index == 0) { + assertTrue(dps[index].getAggregatedTags().isEmpty()); + assertEquals(TAGV_STRING, dps[index].getTags().get(TAGK_STRING)); + } else { + assertEquals(TAGV_B_STRING, dps[index].getTags().get(TAGK_STRING)); + } + } + + if (annotation) { + assertEquals(1, dps[index].getAnnotations().size()); + assertEquals(NOTE_DESCRIPTION, dps[index].getAnnotations().get(0) + .getDescription()); + assertEquals(NOTE_NOTES, dps[index].getAnnotations().get(0).getNotes()); + } else { + assertNull(dps[index].getAnnotations()); + } + } + + /** + * Stores a single annotation in the given row + * @param timestamp The time to store the data point at + * @throws Exception + */ + protected void storeAnnotation(final long timestamp) throws Exception { + final Annotation note = new Annotation(); + note.setTSUID("000001000001000001"); + note.setStartTime(timestamp); + note.setDescription(NOTE_DESCRIPTION); + note.setNotes(NOTE_NOTES); + note.syncToStorage(tsdb, false).joinUninterruptibly(); + } + } \ No newline at end of file From abe0bafc213d22c0c06d4740f776087edd1e6757 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 14:36:41 -0700 Subject: [PATCH 088/826] Fixup RowSeq to support salts properly. A time series with multiple salts is stored in the same row seq object. Signed-off-by: Chris Larsen --- src/core/RowSeq.java | 8 +- test/core/TestRowSeq.java | 284 ++++++++++++++++++++++++++++++++------ 2 files changed, 249 insertions(+), 43 deletions(-) diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index 8238f872e1..86233069ea 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -83,7 +83,8 @@ void setRow(final KeyValue row) { * Merges data points for the same HBase row into the local object. * When executing multiple async queries simultaneously, they may call into * this method with data sets that are out of order. This may ONLY be called - * after setRow() has initiated the rowseq. + * after setRow() has initiated the rowseq. It also allows for rows with + * different salt bucket IDs to be merged into the same sequence. * @param row The compacted HBase row to merge into this instance. * @throws IllegalStateException if {@link #setRow} wasn't called first. * @throws IllegalArgumentException if the data points in the argument @@ -95,7 +96,8 @@ void addRow(final KeyValue row) { } final byte[] key = row.key(); - if (!Bytes.equals(this.key, key)) { + if (Bytes.memcmp(this.key, key, Const.SALT_WIDTH(), + key.length - Const.SALT_WIDTH()) != 0) { throw new IllegalDataException("Attempt to add a different row=" + row + ", this=" + this); } @@ -356,7 +358,7 @@ Iterator internalIterator() { /** Extracts the base timestamp from the row key. */ long baseTime() { - return Bytes.getUnsignedInt(key, tsdb.metrics.width()); + return Bytes.getUnsignedInt(key, Const.SALT_WIDTH() + tsdb.metrics.width()); } /** @throws IndexOutOfBoundsException if {@code i} is out of bounds. */ diff --git a/test/core/TestRowSeq.java b/test/core/TestRowSeq.java index 9c00b3baf3..db9eb1b913 100644 --- a/test/core/TestRowSeq.java +++ b/test/core/TestRowSeq.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; +import java.util.Arrays; import java.util.NoSuchElementException; import net.opentsdb.storage.MockBase; @@ -29,6 +30,7 @@ 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; @@ -43,7 +45,7 @@ "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({ RowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, - Config.class, RowKey.class }) + Config.class, RowKey.class, Const.class }) public final class TestRowSeq { private TSDB tsdb = mock(TSDB.class); private Config config = mock(Config.class); @@ -51,6 +53,8 @@ public final class TestRowSeq { private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; private static final byte[] KEY = { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + private static final byte[] SALTED_KEY = + { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; private static final byte[] FAMILY = { 't' }; private static final byte[] ZERO = { 0 }; @@ -73,7 +77,23 @@ public void setRow() throws Exception { final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + assertEquals(2, rs.size()); + } + + @Test + public void setRowSalted() throws Exception { + setupSalt(); + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(SALTED_KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -88,7 +108,7 @@ public void setRowAlreadySet() throws Exception { final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -106,7 +126,39 @@ public void addRowMergeLater() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO))); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + + assertEquals(4, rs.size()); + assertEquals(1356998400000L, rs.timestamp(0)); + assertEquals(4, rs.longValue(0)); + assertEquals(1356998402000L, rs.timestamp(1)); + assertEquals(5, rs.longValue(1)); + assertEquals(1356998403000L, rs.timestamp(2)); + assertEquals(6, rs.longValue(2)); + assertEquals(1356998404000L, rs.timestamp(3)); + assertEquals(7, rs.longValue(3)); + } + + @Test + public void addRowMergeLaterSalted() throws Exception { + setupSalt(); + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(makekv(SALTED_KEY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); assertEquals(2, rs.size()); final byte[] qual3 = { 0x00, 0x37 }; @@ -114,7 +166,10 @@ public void addRowMergeLater() throws Exception { final byte[] qual4 = { 0x00, 0x47 }; final byte[] val4 = Bytes.fromLong(7L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + final byte[] salted_key2 = Arrays.copyOf(SALTED_KEY, SALTED_KEY.length); + salted_key2[0] = 1; + rs.addRow(makekv(salted_key2, qual34, + MockBase.concatByteArrays(val3, val4, ZERO))); assertEquals(4, rs.size()); assertEquals(1356998400000L, rs.timestamp(0)); @@ -136,7 +191,7 @@ public void addRowMergeEarlier() throws Exception { final byte[] val2 = Bytes.fromLong(7L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO))); assertEquals(2, rs.size()); final byte[] qual3 = { 0x00, 0x07 }; @@ -144,7 +199,42 @@ public void addRowMergeEarlier() throws Exception { final byte[] qual4 = { 0x00, 0x27 }; final byte[] val4 = Bytes.fromLong(5L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + + assertEquals(4, rs.size()); + assertEquals(1356998400000L, rs.timestamp(0)); + assertEquals(4, rs.longValue(0)); + assertEquals(1356998402000L, rs.timestamp(1)); + assertEquals(5, rs.longValue(1)); + assertEquals(1356998403000L, rs.timestamp(2)); + assertEquals(6, rs.longValue(2)); + assertEquals(1356998404000L, rs.timestamp(3)); + assertEquals(7, rs.longValue(3)); + } + + @Test + public void addRowMergeEarlierSalted() throws Exception { + setupSalt(); + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x00, 0x37 }; + final byte[] val1 = Bytes.fromLong(6L); + final byte[] qual2 = { 0x00, 0x47 }; + final byte[] val2 = Bytes.fromLong(7L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(makekv(SALTED_KEY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x00, 0x07 }; + final byte[] val3 = Bytes.fromLong(4L); + final byte[] qual4 = { 0x00, 0x27 }; + final byte[] val4 = Bytes.fromLong(5L); + final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); + final byte[] salted_key2 = Arrays.copyOf(SALTED_KEY, SALTED_KEY.length); + salted_key2[0] = 1; + rs.addRow(makekv(salted_key2, qual34, + MockBase.concatByteArrays(val3, val4, ZERO))); assertEquals(4, rs.size()); assertEquals(1356998400000L, rs.timestamp(0)); @@ -166,7 +256,7 @@ public void addRowMergeMiddle() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO))); assertEquals(2, rs.size()); final byte[] qual3 = { 0x00, 0x57 }; @@ -174,7 +264,7 @@ public void addRowMergeMiddle() throws Exception { final byte[] qual4 = { 0x00, 0x67 }; final byte[] val4 = Bytes.fromLong(9L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, ZERO))); assertEquals(4, rs.size()); final byte[] qual5 = { 0x00, 0x37 }; @@ -182,7 +272,55 @@ public void addRowMergeMiddle() throws Exception { final byte[] qual6 = { 0x00, 0x47 }; final byte[] val6 = Bytes.fromLong(7L); final byte[] qual56 = MockBase.concatByteArrays(qual5, qual6); - rs.addRow(makekv(qual56, MockBase.concatByteArrays(val5, val6, ZERO))); + rs.addRow(makekv(KEY, qual56, MockBase.concatByteArrays(val5, val6, ZERO))); + + assertEquals(6, rs.size()); + assertEquals(1356998400000L, rs.timestamp(0)); + assertEquals(4, rs.longValue(0)); + assertEquals(1356998402000L, rs.timestamp(1)); + assertEquals(5, rs.longValue(1)); + assertEquals(1356998403000L, rs.timestamp(2)); + assertEquals(6, rs.longValue(2)); + assertEquals(1356998404000L, rs.timestamp(3)); + assertEquals(7, rs.longValue(3)); + assertEquals(1356998405000L, rs.timestamp(4)); + assertEquals(8, rs.longValue(4)); + assertEquals(1356998406000L, rs.timestamp(5)); + assertEquals(9, rs.longValue(5)); + } + + @Test + public void addRowMergeMiddleSalted() throws Exception { + setupSalt(); + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(makekv(SALTED_KEY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x00, 0x57 }; + final byte[] val3 = Bytes.fromLong(8L); + final byte[] qual4 = { 0x00, 0x67 }; + final byte[] val4 = Bytes.fromLong(9L); + final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); + rs.addRow(makekv(SALTED_KEY, qual34, + MockBase.concatByteArrays(val3, val4, ZERO))); + assertEquals(4, rs.size()); + + final byte[] qual5 = { 0x00, 0x37 }; + final byte[] val5 = Bytes.fromLong(6L); + final byte[] qual6 = { 0x00, 0x47 }; + final byte[] val6 = Bytes.fromLong(7L); + final byte[] qual56 = MockBase.concatByteArrays(qual5, qual6); + final byte[] salted_key2 = Arrays.copyOf(SALTED_KEY, SALTED_KEY.length); + salted_key2[0] = 1; + rs.addRow(makekv(salted_key2, qual56, + MockBase.concatByteArrays(val5, val6, ZERO))); assertEquals(6, rs.size()); assertEquals(1356998400000L, rs.timestamp(0)); @@ -210,13 +348,13 @@ public void addRowMergeDuplicateLater() throws Exception { final byte[] val3 = Bytes.fromLong(6L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2, qual3); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, val3, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, val3, ZERO))); assertEquals(3, rs.size()); final byte[] qual4 = { 0x00, 0x47 }; final byte[] val4 = Bytes.fromLong(7L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, ZERO))); assertEquals(4, rs.size()); assertEquals(1356998400000L, rs.timestamp(0)); @@ -240,13 +378,13 @@ public void addRowMergeDuplicateEarlier() throws Exception { final byte[] val2 = Bytes.fromLong(7L); final byte[] qual12 = MockBase.concatByteArrays(qual4, qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val4, val1, val2, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val4, val1, val2, ZERO))); assertEquals(3, rs.size()); final byte[] qual3 = { 0x00, 0x07 }; final byte[] val3 = Bytes.fromLong(4L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, ZERO))); assertEquals(4, rs.size()); assertEquals(1356998400000L, rs.timestamp(0)); @@ -267,7 +405,7 @@ public void addRowDiffBaseTime() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO))); assertEquals(2, rs.size()); final byte[] qual3 = { 0x00, 0x37 }; @@ -280,6 +418,30 @@ public void addRowDiffBaseTime() throws Exception { MockBase.concatByteArrays(val3, val4, ZERO))); } + @Test (expected = IllegalDataException.class) + public void addRowDiffBaseTimeSalt() throws Exception { + setupSalt(); + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(makekv(SALTED_KEY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); + final byte[] row2 = { 1, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, + 0, 0, 1, 0, 0, 2 }; + rs.addRow(new KeyValue(row2, FAMILY, qual34, + MockBase.concatByteArrays(val3, val4, ZERO))); + } + @Test public void addRowMergeMs() throws Exception { // this happens if the same row key is used for the addRow call @@ -289,7 +451,7 @@ public void addRowMergeMs() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, ZERO))); + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO))); assertEquals(2, rs.size()); final byte[] qual3 = { (byte) 0xF0, 0x00, 0x07, 0x07 }; @@ -297,7 +459,7 @@ public void addRowMergeMs() throws Exception { final byte[] qual4 = { (byte) 0xF0, 0x00, 0x09, 0x07 }; final byte[] val4 = Bytes.fromLong(7L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, ZERO))); assertEquals(4, rs.size()); assertEquals(1356998400000L, rs.timestamp(0)); @@ -319,7 +481,7 @@ public void addRowMergeSecAndMs() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); final RowSeq rs = new RowSeq(tsdb); - rs.setRow(makekv(qual12, MockBase.concatByteArrays(val1, val2, + rs.setRow(makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, new byte[] { 1 }))); assertEquals(2, rs.size()); @@ -328,7 +490,7 @@ public void addRowMergeSecAndMs() throws Exception { final byte[] qual4 = { (byte) 0xF0, 0x01, 0x09, 0x07 }; final byte[] val4 = Bytes.fromLong(7L); final byte[] qual34 = MockBase.concatByteArrays(qual3, qual4); - rs.addRow(makekv(qual34, MockBase.concatByteArrays(val3, val4, + rs.addRow(makekv(KEY, qual34, MockBase.concatByteArrays(val3, val4, new byte[] { 1 }))); assertEquals(4, rs.size()); @@ -349,7 +511,7 @@ public void addRowNotSet() throws Exception { final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -363,7 +525,25 @@ public void timestamp() throws Exception { final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + + assertEquals(1356998400000L, rs.timestamp(0)); + assertEquals(1356998402000L, rs.timestamp(1)); + } + + @Test + public void timestampSalted() throws Exception { + setupSalt(); + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(SALTED_KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -380,7 +560,7 @@ public void timestampNormalizeMS() throws Exception { final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -397,7 +577,7 @@ public void timestampMs() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -414,7 +594,7 @@ public void timestampMixedNormalized() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -431,7 +611,7 @@ public void timestampMixedNonNormalized() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -448,7 +628,7 @@ public void timestampOutofBounds() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -466,7 +646,7 @@ public void iterateNormalizedMS() throws Exception { final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -494,7 +674,7 @@ public void iterateMs() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - final KeyValue kv = makekv(qual12, + final KeyValue kv = makekv(KEY, qual12, MockBase.concatByteArrays(val1, val2, ZERO)); final RowSeq rs = new RowSeq(tsdb); @@ -525,7 +705,7 @@ public void iterateMsLarge() throws Exception { ts += 50; } final byte[] values = new byte[(4 * limit) + 1]; - final KeyValue kv = makekv(qualifier, values); + final KeyValue kv = makekv(KEY, qualifier, values); final RowSeq rs = new RowSeq(tsdb); rs.setRow(kv); @@ -542,7 +722,22 @@ public void iterateMsLarge() throws Exception { @Test public void seekMs() throws Exception { final RowSeq rs = new RowSeq(tsdb); - rs.setRow(getMs()); + rs.setRow(getMs(false)); + + final SeekableView it = rs.iterator(); + it.seek(1356998400008L); + DataPoint dp = it.next(); + assertEquals(1356998400008L, dp.timestamp()); + assertEquals(5, dp.longValue()); + + assertTrue(it.hasNext()); + } + + @Test + public void seekMsSalted() throws Exception { + setupSalt(); + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(getMs(true)); final SeekableView it = rs.iterator(); it.seek(1356998400008L); @@ -556,7 +751,7 @@ public void seekMs() throws Exception { @Test public void seekMsStart() throws Exception { final RowSeq rs = new RowSeq(tsdb); - rs.setRow(getMs()); + rs.setRow(getMs(false)); final SeekableView it = rs.iterator(); it.seek(1356998400000L); @@ -570,7 +765,7 @@ public void seekMsStart() throws Exception { @Test public void seekMsBetween() throws Exception { final RowSeq rs = new RowSeq(tsdb); - rs.setRow(getMs()); + rs.setRow(getMs(false)); final SeekableView it = rs.iterator(); it.seek(1356998400005L); @@ -584,7 +779,7 @@ public void seekMsBetween() throws Exception { @Test public void seekMsEnd() throws Exception { final RowSeq rs = new RowSeq(tsdb); - rs.setRow(getMs()); + rs.setRow(getMs(false)); final SeekableView it = rs.iterator(); it.seek(1356998400016L); @@ -598,7 +793,7 @@ public void seekMsEnd() throws Exception { @Test public void seekMsTooEarly() throws Exception { final RowSeq rs = new RowSeq(tsdb); - rs.setRow(getMs()); + rs.setRow(getMs(false)); final SeekableView it = rs.iterator(); it.seek(1356998300000L); @@ -612,7 +807,7 @@ public void seekMsTooEarly() throws Exception { @Test (expected = NoSuchElementException.class) public void seekMsPastLastDp() throws Exception { final RowSeq rs = new RowSeq(tsdb); - rs.setRow(getMs()); + rs.setRow(getMs(false)); final SeekableView it = rs.iterator(); it.seek(1356998400032L); @@ -620,11 +815,13 @@ public void seekMsPastLastDp() throws Exception { } /** Shorthand to create a {@link KeyValue}. */ - private static KeyValue makekv(final byte[] qualifier, final byte[] value) { - return new KeyValue(KEY, FAMILY, qualifier, value); + private static KeyValue makekv(final byte[] key, final byte[] qualifier, + final byte[] value) { + return new KeyValue(key, FAMILY, qualifier, value); } - private static KeyValue getMs() { + /** Helper that builds a KeyValue with millisecond timestamps */ + private static KeyValue getMs(final boolean salted) { final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; final byte[] val1 = Bytes.fromLong(4L); final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; @@ -632,8 +829,15 @@ private static KeyValue getMs() { final byte[] qual3 = { (byte) 0xF0, 0x00, 0x04, 0x07 }; final byte[] val3 = Bytes.fromLong(6L); final byte[] qual123 = MockBase.concatByteArrays(qual1, qual2, qual3); - final KeyValue kv = makekv(qual123, - MockBase.concatByteArrays(val1, val2, val3, ZERO)); + final KeyValue kv = makekv((salted ? SALTED_KEY : KEY), + qual123, MockBase.concatByteArrays(val1, val2, val3, ZERO)); return kv; } + + /** Helper to mockout the salt configuration */ + private void setupSalt() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + } } From 965d2f9efd7e4eff9841cc733fc830d1b3054b68 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 18:21:46 -0700 Subject: [PATCH 089/826] Modify the Tags class to handle salted row keys Signed-off-by: Chris Larsen --- src/core/Tags.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index 5422baeaab..30d4de29ed 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -293,7 +293,8 @@ static byte[] getValueId(final TSDB tsdb, final byte[] row, final short name_width = tsdb.tag_names.width(); final short value_width = tsdb.tag_values.width(); // TODO(tsuna): Can do a binary search. - for (short pos = (short) (tsdb.metrics.width() + Const.TIMESTAMP_BYTES); + for (short pos = (short) (Const.SALT_WIDTH() + + tsdb.metrics.width() + Const.TIMESTAMP_BYTES); pos < row.length; pos += name_width + value_width) { if (rowContains(row, pos, tag_id)) { @@ -354,7 +355,8 @@ static Deferred> getTagsAsync(final TSDB tsdb, final short name_width = tsdb.tag_names.width(); final short value_width = tsdb.tag_values.width(); final short tag_bytes = (short) (name_width + value_width); - final short metric_ts_bytes = (short) (tsdb.metrics.width() + final short metric_ts_bytes = (short) (Const.SALT_WIDTH() + + tsdb.metrics.width() + Const.TIMESTAMP_BYTES); final ArrayList> deferreds = From f9346197ef82e23558b1889010e4f1bd7a501da6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 21 Mar 2015 18:38:46 -0700 Subject: [PATCH 090/826] Modify MockBase to return a real KeyValue instead of a mock. Don't know why I did that so long ago... Cleanup the Bytes.* invocations. Add a method to fetch all of the row keys. Add a method to compact all of the rows in storage using the OpenTSDB compaction method. Signed-off-by: Chris Larsen --- test/storage/MockBase.java | 153 ++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 60 deletions(-) diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 30e1390b02..e6568c1446 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -34,6 +35,7 @@ import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; @@ -82,8 +84,8 @@ public final class MockBase { private TSDB tsdb; // KEY Column Family Qualifier Timestamp Value - private Bytes.ByteMap>>> - storage = new Bytes.ByteMap>>>(); + private ByteMap>>> + storage = new ByteMap>>>(); private HashSet scanners = new HashSet(2); private byte[] default_family; @@ -216,15 +218,15 @@ public void addColumn(final byte[] key, final byte[] family, throw new NullPointerException(); } - Bytes.ByteMap>> row = storage.get(key); + ByteMap>> row = storage.get(key); if (row == null) { - row = new Bytes.ByteMap>>(); + row = new ByteMap>>(); storage.put(key, row); } - Bytes.ByteMap> cf = row.get(family); + ByteMap> cf = row.get(family); if (cf == null) { - cf = new Bytes.ByteMap>(); + cf = new ByteMap>(); row.put(family, cf); } TreeMap column = cf.get(qualifier); @@ -247,7 +249,7 @@ public int numRows() { * @return -1 if the row did not exist, otherwise the number of column families. */ public int numColumnFamilies(final byte[] key) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return -1; @@ -261,13 +263,13 @@ public int numColumnFamilies(final byte[] key) { * @return -1 if the row did not exist, otherwise the number of columns. */ public long numColumns(final byte[] key) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return -1; } long size = 0; - for (Map.Entry>> entry : row) { + for (Map.Entry>> entry : row) { size += entry.getValue().size(); } return size; @@ -280,12 +282,12 @@ public long numColumns(final byte[] key) { * @return -1 if the row did not exist, otherwise the number of columns. */ public int numColumnsInFamily(final byte[] key, final byte[] family) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return -1; } - final Bytes.ByteMap> cf = row.get(family); + final ByteMap> cf = row.get(family); if (cf == null) { return -1; } @@ -311,12 +313,12 @@ public byte[] getColumn(final byte[] key, final byte[] qualifier) { */ public byte[] getColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return null; } - final Bytes.ByteMap> cf = row.get(family); + final ByteMap> cf = row.get(family); if (cf == null) { return null; } @@ -348,12 +350,12 @@ public TreeMap getFullColumn(final byte[] key, */ public TreeMap getFullColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return null; } - final Bytes.ByteMap> cf = row.get(family); + final ByteMap> cf = row.get(family); if (cf == null) { return null; } @@ -370,19 +372,19 @@ public TreeMap getFullColumn(final byte[] key, * @param family The column family ID * @return A map of columns if the CF was found, null if no such CF */ - public Bytes.ByteMap getColumnFamily(final byte[] key, + public ByteMap getColumnFamily(final byte[] key, final byte[] family) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return null; } - final Bytes.ByteMap> cf = row.get(family); + final ByteMap> cf = row.get(family); if (cf == null) { return null; } // convert to a byte map - final Bytes.ByteMap columns = new Bytes.ByteMap(); + final ByteMap columns = new ByteMap(); for (Map.Entry> entry : cf.entrySet()) { // the map should never be null columns.put(entry.getKey(), entry.getValue().firstEntry().getValue()); @@ -390,6 +392,11 @@ public Bytes.ByteMap getColumnFamily(final byte[] key, return columns; } + /** @return the list of keys stored in the table */ + public Set getKeys() { + return storage.keySet(); + } + /** * Return the mocked TSDB object to use for HBaseClient access * @return @@ -398,6 +405,42 @@ public TSDB getTSDB() { return tsdb; } + /** + * Runs through all rows in the table and compacts them by making a call to + * the {@link TSDB.compact} method. It will delete any columns that were + * compacted and leave others untouched, just as the normal method does. + * Note, assumes only one column family + * @throws Exception if Whitebox couldn't access the compact method + */ + public void tsdbCompactAllRows() throws Exception { + for (Map.Entry>>> entry : + storage.entrySet()) { + final byte[] key = entry.getKey(); + + final ByteMap> row = entry.getValue().firstEntry().getValue(); + ArrayList kvs = new ArrayList(row.size()); + final Set deletes = new HashSet(); + for (Map.Entry> column : row.entrySet()) { + if (column.getKey().length % 2 == 0) { + kvs.add(new KeyValue(key, default_family, column.getKey(), + column.getValue().firstKey(), + column.getValue().firstEntry().getValue())); + deletes.add(column.getKey()); + } + } + if (kvs.size() > 0) { + for (final byte[] k : deletes) { + row.remove(k); + } + final KeyValue compacted = + Whitebox.invokeMethod(tsdb, "compact", kvs, Collections.EMPTY_LIST); + final TreeMap compacted_value = new TreeMap(); + compacted_value.put(current_timestamp++, compacted.value()); + row.put(compacted.qualifier(), compacted_value); + } + } + } + /** * Clears the entire hash table. Use it if your unit test needs to start fresh */ @@ -418,7 +461,7 @@ public void flushRow(final byte[] key) { * @param family The family to remove */ public void flushFamily(final byte[] family) { - for (Map.Entry>>> row : + for (Map.Entry>>> row : storage.entrySet()) { row.getValue().remove(family); } @@ -432,12 +475,12 @@ public void flushFamily(final byte[] family) { */ public void flushColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(key); if (row == null) { return; } - final Bytes.ByteMap> cf = row.get(family); + final ByteMap> cf = row.get(family); if (cf == null) { return; } @@ -462,12 +505,12 @@ public void dumpToSystemOut(final boolean ascii) { return; } - for (Map.Entry>>> row : + for (Map.Entry>>> row : storage.entrySet()) { System.out.println("[Row] " + (ascii ? new String(row.getKey(), ASCII) : bytesToString(row.getKey()))); - for (Map.Entry>> cf : + for (Map.Entry>> cf : row.getValue().entrySet()) { final String family = ascii ? new String(cf.getKey(), ASCII) : @@ -546,7 +589,7 @@ public Deferred> answer(InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final GetRequest get = (GetRequest)args[0]; - final Bytes.ByteMap>> row = + final ByteMap>> row = storage.get(get.key()); if (row == null) { @@ -561,7 +604,7 @@ public Deferred> answer(InvocationOnMock invocation) } // compile a set of qualifiers to use as a filter if necessary - Bytes.ByteMap qualifiers = new Bytes.ByteMap(); + ByteMap qualifiers = new ByteMap(); if (get.qualifiers() != null && get.qualifiers().length > 0) { for (byte[] q : get.qualifiers()) { qualifiers.put(q, null); @@ -569,7 +612,7 @@ public Deferred> answer(InvocationOnMock invocation) } final ArrayList kvs = new ArrayList(row.size()); - for (Map.Entry>> cf : + for (Map.Entry>> cf : row.entrySet()) { // column family filter @@ -587,12 +630,9 @@ public Deferred> answer(InvocationOnMock invocation) // TODO - if we want to support multiple values, iterate over the // tree map. Otherwise Get returns just the latest value. - KeyValue kv = mock(KeyValue.class); - when(kv.timestamp()).thenReturn(column.getValue().firstKey()); - when(kv.value()).thenReturn(column.getValue().firstEntry().getValue()); - when(kv.qualifier()).thenReturn(column.getKey()); - when(kv.key()).thenReturn(get.key()); - kvs.add(kv); + kvs.add(new KeyValue(get.key(), default_family, column.getKey(), + column.getValue().firstKey(), + column.getValue().firstEntry().getValue())); } } return Deferred.fromResult(kvs); @@ -610,16 +650,16 @@ public Deferred answer(final InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final PutRequest put = (PutRequest)args[0]; - Bytes.ByteMap>> row = + ByteMap>> row = storage.get(put.key()); if (row == null) { - row = new Bytes.ByteMap>>(); + row = new ByteMap>>(); storage.put(put.key(), row); } - Bytes.ByteMap> cf = row.get(put.family()); + ByteMap> cf = row.get(put.family()); if (cf == null) { - cf = new Bytes.ByteMap>(); + cf = new ByteMap>(); row.put(put.family(), cf); } @@ -656,24 +696,24 @@ public Deferred answer(final InvocationOnMock invocation) final PutRequest put = (PutRequest)args[0]; final byte[] expected = (byte[])args[1]; - Bytes.ByteMap>> row = + ByteMap>> row = storage.get(put.key()); if (row == null) { if (expected != null && expected.length > 0) { return Deferred.fromResult(false); } - row = new Bytes.ByteMap>>(); + row = new ByteMap>>(); storage.put(put.key(), row); } - Bytes.ByteMap> cf = row.get(put.family()); + ByteMap> cf = row.get(put.family()); if (cf == null) { if (expected != null && expected.length > 0) { return Deferred.fromResult(false); } - cf = new Bytes.ByteMap>(); + cf = new ByteMap>(); row.put(put.family(), cf); } @@ -723,7 +763,7 @@ public Deferred answer(InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final DeleteRequest delete = (DeleteRequest)args[0]; - Bytes.ByteMap>> row = + ByteMap>> row = storage.get(delete.key()); if (row == null) { return Deferred.fromResult(null); @@ -745,7 +785,7 @@ public Deferred answer(InvocationOnMock invocation) } // compile a set of qualifiers to use as a filter if necessary - Bytes.ByteMap qualifiers = new Bytes.ByteMap(); + ByteMap qualifiers = new ByteMap(); if (delete.qualifiers() != null || delete.qualifiers().length > 0) { for (byte[] q : delete.qualifiers()) { qualifiers.put(q, null); @@ -763,7 +803,7 @@ public Deferred answer(InvocationOnMock invocation) } List cf_removals = new ArrayList(row.entrySet().size()); - for (Map.Entry>> cf : + for (Map.Entry>> cf : row.entrySet()) { // column family filter @@ -947,7 +987,7 @@ public Deferred>> answer( // return all matches ArrayList> results = new ArrayList>(); - for (Map.Entry>>> row : + for (Map.Entry>>> row : storage.entrySet()) { // if it's before the start row, after the end row or doesn't @@ -972,7 +1012,7 @@ public Deferred>> answer( // loop on the column families final ArrayList kvs = new ArrayList(row.getValue().size()); - for (Map.Entry>> cf : + for (Map.Entry>> cf : row.getValue().entrySet()) { // column family filter @@ -990,16 +1030,9 @@ public Deferred>> answer( continue; } - KeyValue kv = mock(KeyValue.class); - when(kv.key()).thenReturn(row.getKey()); - when(kv.value()).thenReturn(column.getValue().firstEntry().getValue()); - when(kv.qualifier()).thenReturn(column.getKey()); - when(kv.timestamp()).thenReturn(column.getValue().firstKey()); - when(kv.family()).thenReturn(cf.getKey()); - when(kv.toString()).thenReturn("[k '" + bytesToString(row.getKey()) + - "' q '" + bytesToString(column.getKey()) + "' v '" + - bytesToString(column.getValue().firstEntry().getValue()) + "']"); - kvs.add(kv); + kvs.add(new KeyValue(row.getKey(), cf.getKey(), column.getKey(), + column.getValue().firstKey(), + column.getValue().firstEntry().getValue())); } } @@ -1028,16 +1061,16 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final AtomicIncrementRequest air = (AtomicIncrementRequest)args[0]; final long amount = air.getAmount(); - Bytes.ByteMap>> row = + ByteMap>> row = storage.get(air.key()); if (row == null) { - row = new Bytes.ByteMap>>(); + row = new ByteMap>>(); storage.put(air.key(), row); } - Bytes.ByteMap> cf = row.get(air.family()); + ByteMap> cf = row.get(air.family()); if (cf == null) { - cf = new Bytes.ByteMap>(); + cf = new ByteMap>(); row.put(air.family(), cf); } From 756a2d65771b619dd0a359d8333a6750700827d9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 15:15:26 -0700 Subject: [PATCH 091/826] Fix up the Annotations class to handle salting Signed-off-by: Chris Larsen --- src/meta/Annotation.java | 51 +++-- test/meta/TestAnnotation.java | 387 ++++++++++++++++++++++++++-------- 2 files changed, 333 insertions(+), 105 deletions(-) diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index 07762f3c9f..cb17f84553 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -32,6 +32,7 @@ import net.opentsdb.core.Const; import net.opentsdb.core.Internal; +import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.JSON; @@ -322,9 +323,11 @@ final class ScannerCB implements Callback>, * Initializes the scanner */ public ScannerCB() { - final byte[] start = new byte[TSDB.metrics_width() + + final byte[] start = new byte[Const.SALT_WIDTH() + + TSDB.metrics_width() + Const.TIMESTAMP_BYTES]; - final byte[] end = new byte[TSDB.metrics_width() + + final byte[] end = new byte[Const.SALT_WIDTH() + + TSDB.metrics_width() + Const.TIMESTAMP_BYTES]; final long normalized_start = (start_time - @@ -332,8 +335,10 @@ public ScannerCB() { final long normalized_end = (end_time - (end_time % Const.MAX_TIMESPAN) + Const.MAX_TIMESPAN); - Bytes.setInt(start, (int) normalized_start, TSDB.metrics_width()); - Bytes.setInt(end, (int) normalized_end, TSDB.metrics_width()); + Bytes.setInt(start, (int) normalized_start, + Const.SALT_WIDTH() + TSDB.metrics_width()); + Bytes.setInt(end, (int) normalized_end, + Const.SALT_WIDTH() + TSDB.metrics_width()); scanner = tsdb.getClient().newScanner(tsdb.dataTable()); scanner.setStartKey(start); @@ -396,8 +401,9 @@ public static Deferred deleteRange(final TSDB tsdb, } final List> delete_requests = new ArrayList>(); - int width = tsuid != null ? tsuid.length + Const.TIMESTAMP_BYTES : - TSDB.metrics_width() + Const.TIMESTAMP_BYTES; + int width = tsuid != null ? + Const.SALT_WIDTH() + tsuid.length + Const.TIMESTAMP_BYTES : + Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; final byte[] start_row = new byte[width]; final byte[] end_row = new byte[width]; @@ -406,14 +412,16 @@ public static Deferred deleteRange(final TSDB tsdb, final long end = end_time / 1000; final long normalized_start = (start - (start % Const.MAX_TIMESPAN)); final long normalized_end = (end - (end % Const.MAX_TIMESPAN) + Const.MAX_TIMESPAN); - Bytes.setInt(start_row, (int) normalized_start, TSDB.metrics_width()); - Bytes.setInt(end_row, (int) normalized_end, TSDB.metrics_width()); + Bytes.setInt(start_row, (int) normalized_start, + Const.SALT_WIDTH() + TSDB.metrics_width()); + Bytes.setInt(end_row, (int) normalized_end, + Const.SALT_WIDTH() + TSDB.metrics_width()); if (tsuid != null) { // first copy the metric UID then the tags - System.arraycopy(tsuid, 0, start_row, 0, TSDB.metrics_width()); - System.arraycopy(tsuid, 0, end_row, 0, TSDB.metrics_width()); - width = TSDB.metrics_width() + Const.TIMESTAMP_BYTES; + System.arraycopy(tsuid, 0, start_row, Const.SALT_WIDTH(), TSDB.metrics_width()); + System.arraycopy(tsuid, 0, end_row, Const.SALT_WIDTH(), TSDB.metrics_width()); + width = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; final int remainder = tsuid.length - TSDB.metrics_width(); System.arraycopy(tsuid, TSDB.metrics_width(), start_row, width, remainder); System.arraycopy(tsuid, TSDB.metrics_width(), end_row, width, remainder); @@ -661,19 +669,24 @@ private static byte[] getRowKey(final long start_time, final byte[] tsuid) { } // if the TSUID is empty, then we're a global annotation. The row key will - // just be an empty byte array of metric width plus the timestamp + // just be an empty byte array of metric width plus the timestamp. We also + // don't salt the global row key (though it has space for salts) if (tsuid == null || tsuid.length < 1) { - final byte[] row = new byte[TSDB.metrics_width() + Const.TIMESTAMP_BYTES]; - Bytes.setInt(row, (int) base_time, TSDB.metrics_width()); + final byte[] row = new byte[Const.SALT_WIDTH() + + TSDB.metrics_width() + Const.TIMESTAMP_BYTES]; + Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + TSDB.metrics_width()); return row; } // otherwise we need to build the row key from the TSUID and start time - final byte[] row = new byte[Const.TIMESTAMP_BYTES + tsuid.length]; - System.arraycopy(tsuid, 0, row, 0, TSDB.metrics_width()); - Bytes.setInt(row, (int) base_time, TSDB.metrics_width()); - System.arraycopy(tsuid, TSDB.metrics_width(), row, TSDB.metrics_width() + - Const.TIMESTAMP_BYTES, (tsuid.length - TSDB.metrics_width())); + final byte[] row = new byte[Const.SALT_WIDTH() + Const.TIMESTAMP_BYTES + + tsuid.length]; + System.arraycopy(tsuid, 0, row, Const.SALT_WIDTH(), TSDB.metrics_width()); + Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + TSDB.metrics_width()); + System.arraycopy(tsuid, TSDB.metrics_width(), row, + Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES, + (tsuid.length - TSDB.metrics_width())); + RowKey.prefixKeyWithSalt(row); return row; } diff --git a/test/meta/TestAnnotation.java b/test/meta/TestAnnotation.java index bc77a6831b..563ae2c2bd 100644 --- a/test/meta/TestAnnotation.java +++ b/test/meta/TestAnnotation.java @@ -15,24 +15,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.Matchers.anyString; -import static org.powermock.api.mockito.PowerMockito.mock; import java.util.List; +import net.opentsdb.core.BaseTsdbTest; +import net.opentsdb.core.Const; +import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; -import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.Config; import net.opentsdb.utils.JSON; -import org.hbase.async.DeleteRequest; -import org.hbase.async.GetRequest; -import org.hbase.async.HBaseClient; -import org.hbase.async.KeyValue; -import org.hbase.async.PutRequest; +import org.hbase.async.Bytes; import org.hbase.async.Scanner; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; @@ -44,69 +38,15 @@ "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, - Scanner.class, Annotation.class}) -public final class TestAnnotation { - private TSDB tsdb; - private HBaseClient client = mock(HBaseClient.class); - private MockBase storage; +@PrepareForTest({ Annotation.class, Const.class, Scanner.class }) +public final class TestAnnotation extends BaseTsdbTest { private Annotation note = new Annotation(); - - final private byte[] global_row_key = - new byte[] { 0, 0, 0, (byte) 0x4F, (byte) 0x29, (byte) 0xD2, 0 }; - final private byte[] tsuid_row_key = - new byte[] { 0, 0, 1, (byte) 0x52, (byte) 0xC2, (byte) 0x09, 0, 0, 0, - 1, 0, 0, 1 }; - + private final static String TSUID = "000001000001000001"; + private byte[] global_row_key; + private byte[] tsuid_row_key; // 1425715200 - Sat Mar 7 00:00:00 PST 2015 - final private byte[] global_row_key_2015_midnight = - new byte[] { 0, 0, 0, (byte) 0x54, (byte) 0xFA, (byte) 0xB0, 0 }; - - @Before - public void before() throws Exception { - final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); - - storage = new MockBase(tsdb, client, true, true, true, true); - - // add a global - storage.addColumn(global_row_key, - new byte[] { 1, 0, 0 }, - ("{\"startTime\":1328140800,\"endTime\":1328140801,\"description\":" + - "\"Description\",\"notes\":\"Notes\",\"custom\":{\"owner\":" + - "\"ops\"}}").getBytes(MockBase.ASCII())); - - storage.addColumn(global_row_key, - new byte[] { 1, 0, 1 }, - ("{\"startTime\":1328140801,\"endTime\":1328140803,\"description\":" + - "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII())); + private byte[] global_row_key_2015_midnight; - // add a local - storage.addColumn(tsuid_row_key, - new byte[] { 1, 0x0A, 0x02 }, - ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450562," + - "\"endTime\":1419984000,\"description\":\"Hello!\",\"notes\":" + - "\"My Notes\",\"custom\":{\"owner\":\"ops\"}}") - .getBytes(MockBase.ASCII())); - - storage.addColumn(tsuid_row_key, - new byte[] { 1, 0x0A, 0x03 }, - ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450563," + - "\"endTime\":1419984000,\"description\":\"Note2\",\"notes\":" + - "\"Nothing\"}") - .getBytes(MockBase.ASCII())); - - // add some data points too - storage.addColumn(tsuid_row_key, - new byte[] { 0x50, 0x10 }, new byte[] { 1 }); - - storage.addColumn(tsuid_row_key, - new byte[] { 0x50, 0x18 }, new byte[] { 2 }); - } - @Test public void constructor() { assertNotNull(new Annotation()); @@ -129,26 +69,41 @@ public void deserialize() throws Exception { @Test public void getAnnotation() throws Exception { - note = Annotation.getAnnotation(tsdb, "000001000001000001", 1388450562L) + setupStorage(false); + + note = Annotation.getAnnotation(tsdb, TSUID, 1388450562L) + .joinUninterruptibly(); + assertNotNull(note); + assertEquals(TSUID, note.getTSUID()); + assertEquals("Hello!", note.getDescription()); + assertEquals(1388450562L, note.getStartTime()); + } + + @Test + public void getAnnotationSalted() throws Exception { + setupStorage(true); + note = Annotation.getAnnotation(tsdb, TSUID, 1388450562L) .joinUninterruptibly(); assertNotNull(note); - assertEquals("000001000001000001", note.getTSUID()); + assertEquals(TSUID, note.getTSUID()); assertEquals("Hello!", note.getDescription()); assertEquals(1388450562L, note.getStartTime()); } @Test public void getAnnotationNormalizeMs() throws Exception { - note = Annotation.getAnnotation(tsdb, "000001000001000001", 1388450562000L) + setupStorage(false); + note = Annotation.getAnnotation(tsdb, TSUID, 1388450562000L) .joinUninterruptibly(); assertNotNull(note); - assertEquals("000001000001000001", note.getTSUID()); + assertEquals(TSUID, note.getTSUID()); assertEquals("Hello!", note.getDescription()); assertEquals(1388450562L, note.getStartTime()); } @Test public void getAnnotationGlobal() throws Exception { + setupStorage(false); note = Annotation.getAnnotation(tsdb, 1328140800000L) .joinUninterruptibly(); assertNotNull(note); @@ -157,15 +112,28 @@ public void getAnnotationGlobal() throws Exception { assertEquals(1328140800L, note.getStartTime()); } + @Test + public void getAnnotationGlobalSalted() throws Exception { + setupStorage(true); + note = Annotation.getAnnotation(tsdb, 1328140800000L) + .joinUninterruptibly(); + assertNotNull(note); + assertEquals("", note.getTSUID()); + assertEquals("Description", note.getDescription()); + assertEquals(1328140800L, note.getStartTime()); + } + @Test public void getAnnotationNotFound() throws Exception { - note = Annotation.getAnnotation(tsdb, "000001000001000001", 1388450564L) + setupStorage(false); + note = Annotation.getAnnotation(tsdb, TSUID, 1388450564L) .joinUninterruptibly(); assertNull(note); } @Test public void getAnnotationGlobalNotFound() throws Exception { + setupStorage(false); note = Annotation.getAnnotation(tsdb, 1388450563L) .joinUninterruptibly(); assertNull(note); @@ -173,12 +141,13 @@ public void getAnnotationGlobalNotFound() throws Exception { @Test (expected = IllegalArgumentException.class) public void getAnnotationNoStartTime() throws Exception { - Annotation.getAnnotation(tsdb, "000001000001000001", 0L) + Annotation.getAnnotation(tsdb, TSUID, 0L) .joinUninterruptibly(); } @Test public void getGlobalAnnotations() throws Exception { + setupStorage(false); List notes = Annotation.getGlobalAnnotations(tsdb, 1328140000, 1328141000).joinUninterruptibly(); assertNotNull(notes); @@ -189,8 +158,22 @@ public void getGlobalAnnotations() throws Exception { assertEquals("Global 2", note1.getDescription()); } + @Test + public void getGlobalAnnotationsSalted() throws Exception { + setupStorage(true); + List notes = Annotation.getGlobalAnnotations(tsdb, 1328140000, + 1328141000).joinUninterruptibly(); + assertNotNull(notes); + assertEquals(2, notes.size()); + Annotation note0 = notes.get(0); + Annotation note1 = notes.get(1); + assertEquals("Description", note0.getDescription()); + assertEquals("Global 2", note1.getDescription()); + } + @Test public void getGlobalAnnotationOutsideCurrentHour() throws Exception { + setupStorage(false); // 1425716000 - Sat Mar 7 00:13:20 PST 2015 storage.addColumn(global_row_key_2015_midnight, new byte[] { 1, 3, (byte) 0x20 }, @@ -209,8 +192,30 @@ public void getGlobalAnnotationOutsideCurrentHour() throws Exception { assertEquals("Issue #457", note0.getNotes()); } + @Test + public void getGlobalAnnotationOutsideCurrentHourSalt() throws Exception { + setupStorage(true); + // 1425716000 - Sat Mar 7 00:13:20 PST 2015 + storage.addColumn(global_row_key_2015_midnight, + new byte[] { 1, 3, (byte) 0x20 }, + ("{\"startTime\":1425716000,\"endTime\":1425716001,\"description\":" + + "\"Global 3\",\"notes\":\"Issue #457\"}").getBytes(MockBase.ASCII())); + + int right_now = 1425717000; // Sat Mar 7 00:30:00 PST 2015 + int fourty_minutes_ago = 1425714600; // Fri Mar 6 23:50:00 PST 2015 + + List recentGlobalAnnotation = Annotation.getGlobalAnnotations( + tsdb, fourty_minutes_ago, right_now).joinUninterruptibly(); + assertNotNull(recentGlobalAnnotation); + assertEquals(1, recentGlobalAnnotation.size()); + Annotation note0 = recentGlobalAnnotation.get(0); + assertEquals("Global 3", note0.getDescription()); + assertEquals("Issue #457", note0.getNotes()); + } + @Test public void getGlobalAnnotationsEmpty() throws Exception { + setupStorage(false); List notes = Annotation.getGlobalAnnotations(tsdb, 1328150000, 1328160000).joinUninterruptibly(); assertNotNull(notes); @@ -229,28 +234,45 @@ public void getGlobalAnnotationsEndLessThanStart() throws Exception { @Test public void syncToStorage() throws Exception { - note.setTSUID("000001000001000001"); + setupStorage(false); + note.setTSUID(TSUID); note.setStartTime(1388450562L); note.setDescription("Synced!"); note.syncToStorage(tsdb, false).joinUninterruptibly(); final byte[] col = storage.getColumn(tsuid_row_key, new byte[] { 1, 0x0A, 0x02 }); note = JSON.parseToObject(col, Annotation.class); - assertEquals("000001000001000001", note.getTSUID()); + assertEquals(TSUID, note.getTSUID()); + assertEquals("Synced!", note.getDescription()); + assertEquals("My Notes", note.getNotes()); + } + + @Test + public void syncToStorageSalted() throws Exception { + setupStorage(true); + note.setTSUID(TSUID); + note.setStartTime(1388450562L); + note.setDescription("Synced!"); + note.syncToStorage(tsdb, false).joinUninterruptibly(); + final byte[] col = storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x02 }); + note = JSON.parseToObject(col, Annotation.class); + assertEquals(TSUID, note.getTSUID()); assertEquals("Synced!", note.getDescription()); assertEquals("My Notes", note.getNotes()); } @Test public void syncToStorageMilliseconds() throws Exception { - note.setTSUID("000001000001000001"); + setupStorage(false); + note.setTSUID(TSUID); note.setStartTime(1388450562500L); note.setDescription("Synced!"); note.syncToStorage(tsdb, false).joinUninterruptibly(); final byte[] col = storage.getColumn(tsuid_row_key, new byte[] { 1, 0x00, 0x27, 0x19, (byte) 0xC4 }); note = JSON.parseToObject(col, Annotation.class); - assertEquals("000001000001000001", note.getTSUID()); + assertEquals(TSUID, note.getTSUID()); assertEquals("Synced!", note.getDescription()); assertEquals("", note.getNotes()); assertEquals(1388450562500L, note.getStartTime()); @@ -258,6 +280,21 @@ public void syncToStorageMilliseconds() throws Exception { @Test public void syncToStorageGlobal() throws Exception { + setupStorage(false); + note.setStartTime(1328140800L); + note.setDescription("Synced!"); + note.syncToStorage(tsdb, false).joinUninterruptibly(); + final byte[] col = storage.getColumn(global_row_key, + new byte[] { 1, 0, 0 }); + note = JSON.parseToObject(col, Annotation.class); + assertEquals("", note.getTSUID()); + assertEquals("Synced!", note.getDescription()); + assertEquals("Notes", note.getNotes()); + } + + @Test + public void syncToStorageGlobalSalted() throws Exception { + setupStorage(true); note.setStartTime(1328140800L); note.setDescription("Synced!"); note.syncToStorage(tsdb, false).joinUninterruptibly(); @@ -271,6 +308,7 @@ public void syncToStorageGlobal() throws Exception { @Test public void syncToStorageGlobalMilliseconds() throws Exception { + setupStorage(false); note.setStartTime(1328140800500L); note.setDescription("Synced!"); note.syncToStorage(tsdb, false).joinUninterruptibly(); @@ -284,21 +322,38 @@ public void syncToStorageGlobalMilliseconds() throws Exception { @Test (expected = IllegalArgumentException.class) public void syncToStorageMissingStart() throws Exception { - note.setTSUID("000001000001000001"); + note.setTSUID(TSUID); note.setDescription("Synced!"); note.syncToStorage(tsdb, false).joinUninterruptibly(); } @Test (expected = IllegalStateException.class) public void syncToStorageNoChanges() throws Exception { - note.setTSUID("000001000001000001"); + note.setTSUID(TSUID); note.setStartTime(1388450562L); note.syncToStorage(tsdb, false).joinUninterruptibly(); } @Test public void delete() throws Exception { - note.setTSUID("000001000001000001"); + setupStorage(false); + note.setTSUID(TSUID); + note.setStartTime(1388450562); + note.delete(tsdb).joinUninterruptibly(); + assertNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x02 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x03 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x10 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x18 })); + } + + @Test + public void deleteSalted() throws Exception { + setupStorage(true); + note.setTSUID(TSUID); note.setStartTime(1388450562); note.delete(tsdb).joinUninterruptibly(); assertNull(storage.getColumn(tsuid_row_key, @@ -313,7 +368,8 @@ public void delete() throws Exception { @Test public void deleteNormalizeMs() throws Exception { - note.setTSUID("000001000001000001"); + setupStorage(false); + note.setTSUID(TSUID); note.setStartTime(1388450562000L); note.delete(tsdb).joinUninterruptibly(); assertNull(storage.getColumn(tsuid_row_key, @@ -330,7 +386,8 @@ public void deleteNormalizeMs() throws Exception { // and it's ignored. @Test public void deleteNotFound() throws Exception { - note.setTSUID("000001000001000001"); + setupStorage(false); + note.setTSUID(TSUID); note.setStartTime(1388450561); note.delete(tsdb).joinUninterruptibly(); assertNotNull(storage.getColumn(tsuid_row_key, @@ -345,12 +402,24 @@ public void deleteNotFound() throws Exception { @Test (expected = IllegalArgumentException.class) public void deleteMissingStart() throws Exception { - note.setTSUID("000001000001000001"); + note.setTSUID(TSUID); note.delete(tsdb).joinUninterruptibly(); } @Test public void deleteGlobal() throws Exception { + setupStorage(false); + note.setStartTime(1328140800); + note.delete(tsdb).joinUninterruptibly(); + assertNull(storage.getColumn(global_row_key, + new byte[] { 1, 0, 0 })); + assertNotNull(storage.getColumn(global_row_key, + new byte[] { 1, 0, 1 })); + } + + @Test + public void deleteGlobalSalted() throws Exception { + setupStorage(true); note.setStartTime(1328140800); note.delete(tsdb).joinUninterruptibly(); assertNull(storage.getColumn(global_row_key, @@ -361,6 +430,7 @@ public void deleteGlobal() throws Exception { @Test public void deleteGlobalNotFound() throws Exception { + setupStorage(false); note.setStartTime(1328140803); note.delete(tsdb).joinUninterruptibly(); assertNotNull(storage.getColumn(global_row_key, @@ -371,6 +441,24 @@ public void deleteGlobalNotFound() throws Exception { @Test public void deleteRange() throws Exception { + setupStorage(false); + final int count = Annotation.deleteRange(tsdb, + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1}, 1388450560000L, + 1388450562000L).joinUninterruptibly(); + assertEquals(1, count); + assertNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x02 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x03 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x10 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x18 })); + } + + @Test + public void deleteRangeSalted() throws Exception { + setupStorage(true); final int count = Annotation.deleteRange(tsdb, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1}, 1388450560000L, 1388450562000L).joinUninterruptibly(); @@ -387,6 +475,24 @@ public void deleteRange() throws Exception { @Test public void deleteRangeNone() throws Exception { + setupStorage(false); + final int count = Annotation.deleteRange(tsdb, + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1}, 1388450560000L, + 1388450561000L).joinUninterruptibly(); + assertEquals(0, count); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x02 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x03 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x10 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x18 })); + } + + @Test + public void deleteRangeNoneSalted() throws Exception { + setupStorage(true); final int count = Annotation.deleteRange(tsdb, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1}, 1388450560000L, 1388450561000L).joinUninterruptibly(); @@ -403,6 +509,24 @@ public void deleteRangeNone() throws Exception { @Test public void deleteRangeMultiple() throws Exception { + setupStorage(false); + final int count = Annotation.deleteRange(tsdb, + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1}, 1388450560000L, + 1388450568000L).joinUninterruptibly(); + assertEquals(2, count); + assertNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x02 })); + assertNull(storage.getColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x03 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x10 })); + assertNotNull(storage.getColumn(tsuid_row_key, + new byte[] { 0x50, 0x18 })); + } + + @Test + public void deleteRangeMultipleSalted() throws Exception { + setupStorage(true); final int count = Annotation.deleteRange(tsdb, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1}, 1388450560000L, 1388450568000L).joinUninterruptibly(); @@ -419,6 +543,19 @@ public void deleteRangeMultiple() throws Exception { @Test public void deleteRangeGlobal() throws Exception { + setupStorage(false); + final int count = Annotation.deleteRange(tsdb, null, 1328140799000L, + 1328140800000L).joinUninterruptibly(); + assertEquals(1, count); + assertNull(storage.getColumn(global_row_key, + new byte[] { 1, 0, 0 })); + assertNotNull(storage.getColumn(global_row_key, + new byte[] { 1, 0, 1 })); + } + + @Test + public void deleteRangeGlobalSalted() throws Exception { + setupStorage(true); final int count = Annotation.deleteRange(tsdb, null, 1328140799000L, 1328140800000L).joinUninterruptibly(); assertEquals(1, count); @@ -430,6 +567,7 @@ public void deleteRangeGlobal() throws Exception { @Test public void deleteRangeGlobalNone() throws Exception { + setupStorage(false); final int count = Annotation.deleteRange(tsdb, null, 1328140798000L, 1328140799000L).joinUninterruptibly(); assertEquals(0, count); @@ -441,6 +579,19 @@ public void deleteRangeGlobalNone() throws Exception { @Test public void deleteRangeGlobalMultiple() throws Exception { + setupStorage(false); + final int count = Annotation.deleteRange(tsdb, null, 1328140799000L, + 1328140900000L).joinUninterruptibly(); + assertEquals(2, count); + assertNull(storage.getColumn(global_row_key, + new byte[] { 1, 0, 0 })); + assertNull(storage.getColumn(global_row_key, + new byte[] { 1, 0, 1 })); + } + + @Test + public void deleteRangeGlobalMultipleSalted() throws Exception { + setupStorage(true); final int count = Annotation.deleteRange(tsdb, null, 1328140799000L, 1328140900000L).joinUninterruptibly(); assertEquals(2, count); @@ -460,4 +611,68 @@ public void deleteRangeEndLessThanStart() throws Exception { Annotation.deleteRange(tsdb, null, 1328140799000L, 1328140798000L) .joinUninterruptibly(); } + + /** + * Sets up storage with or without salting and writes a few bits of data + * @param salted Whether or not to use salting + */ + private void setupStorage(final boolean salted) { + if (salted) { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + } + + global_row_key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES]; + System.arraycopy(Bytes.fromInt(1328140800), 0, global_row_key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + + global_row_key_2015_midnight = + new byte[Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES]; + System.arraycopy(Bytes.fromInt(1425715200), 0, global_row_key_2015_midnight, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + + tsuid_row_key = getRowKeyTemplate(); + System.arraycopy(Bytes.fromInt(1388448000), 0, tsuid_row_key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + RowKey.prefixKeyWithSalt(tsuid_row_key); + + storage = new MockBase(tsdb, client, true, true, true, true); + + // add a global + storage.addColumn(global_row_key, + new byte[] { 1, 0, 0 }, + ("{\"startTime\":1328140800,\"endTime\":1328140801,\"description\":" + + "\"Description\",\"notes\":\"Notes\",\"custom\":{\"owner\":" + + "\"ops\"}}").getBytes(MockBase.ASCII())); + + storage.addColumn(global_row_key, + new byte[] { 1, 0, 1 }, + ("{\"startTime\":1328140801,\"endTime\":1328140803,\"description\":" + + "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII())); + + // add a local + storage.addColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x02 }, + ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450562," + + "\"endTime\":1419984000,\"description\":\"Hello!\",\"notes\":" + + "\"My Notes\",\"custom\":{\"owner\":\"ops\"}}") + .getBytes(MockBase.ASCII())); + + storage.addColumn(tsuid_row_key, + new byte[] { 1, 0x0A, 0x03 }, + ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450563," + + "\"endTime\":1419984000,\"description\":\"Note2\",\"notes\":" + + "\"Nothing\"}") + .getBytes(MockBase.ASCII())); + + // add some data points too + storage.addColumn(tsuid_row_key, + new byte[] { 0x50, 0x10 }, new byte[] { 1 }); + + storage.addColumn(tsuid_row_key, + new byte[] { 0x50, 0x18 }, new byte[] { 2 }); + } } From c553e190d7a2616530b52fa5e8dd0da089c9ebc0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 15:17:18 -0700 Subject: [PATCH 092/826] Modify the CompactionQueue class to handle salts Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 617a03943a..0b5e54f321 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -179,7 +179,8 @@ private Deferred> flush(final long cut_off, int maxflushes) { if (seed == row.hashCode() % 3) { continue; } - final long base_time = Bytes.getUnsignedInt(row, metric_width); + final long base_time = Bytes.getUnsignedInt(row, + Const.SALT_WIDTH() + metric_width); if (base_time > cut_off) { break; } else if (nflushes == max_concurrent_flushes) { @@ -363,7 +364,8 @@ public Deferred compact() { if (compacted != null) { // Caller is interested in the compacted form. compacted[0] = compact; - final long base_time = Bytes.getUnsignedInt(compact.key(), metric_width); + final long base_time = Bytes.getUnsignedInt(compact.key(), + Const.SALT_WIDTH() + metric_width); final long cut_off = System.currentTimeMillis() / 1000 - Const.MAX_TIMESPAN - 1; if (base_time > cut_off) { // If row is too recent... @@ -452,7 +454,8 @@ private int buildHeapProcessAnnotations() { * @param compacted_qual qualifiers for sorted datapoints * @param compacted_val values for sorted datapoints */ - private void mergeDatapoints(ByteBufferList compacted_qual, ByteBufferList compacted_val) { + private void mergeDatapoints(ByteBufferList compacted_qual, + ByteBufferList compacted_val) { int prevTs = -1; while (!heap.isEmpty()) { final ColumnDatapointIterator col = heap.remove(); From 289bedfd7078727f508b9e289bcda7a5a6a89e7f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 16:06:51 -0700 Subject: [PATCH 093/826] Add the SaltScanner class for coordinating multiple scanners across salted buckets and returning their results. Originall written by @rajeshal, thanks! Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/core/SaltScanner.java | 390 +++++++++++++++++++++++++++++++++ test/core/TestSaltScanner.java | 384 ++++++++++++++++++++++++++++++++ 3 files changed, 776 insertions(+) create mode 100644 src/core/SaltScanner.java create mode 100644 test/core/TestSaltScanner.java diff --git a/Makefile.am b/Makefile.am index e896caf568..d02d1d30f1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -53,6 +53,7 @@ tsdb_SRC := \ src/core/RateSpan.java \ src/core/RowKey.java \ src/core/RowSeq.java \ + src/core/SaltScanner.java \ src/core/SeekableView.java \ src/core/Span.java \ src/core/SpanGroup.java \ @@ -164,6 +165,7 @@ test_SRC := \ test/core/TestRateSpan.java \ test/core/TestRowKey.java \ test/core/TestRowSeq.java \ + test/core/TestSaltScanner.java \ test/core/TestSpan.java \ test/core/TestTags.java \ test/core/TestTSDB.java \ diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java new file mode 100644 index 0000000000..0b2299be7b --- /dev/null +++ b/src/core/SaltScanner.java @@ -0,0 +1,390 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2015 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see . +package net.opentsdb.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +import net.opentsdb.meta.Annotation; + +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +/** + * A class that handles coordinating the various scanners created for each + * salt bucket when salting is enabled. Each scanner stores it's results in + * local maps and once everyone has reported in, then the maps are parsed and + * combined into a proper set of spans to return to the {@link TsdbQuery} class. + * + * Note that if one or more of the scanners throws an exception, then that + * exception will be returned to the caller in the deferred. Unfortunately we + * don't have a good way to cancel a scan in progress so the first scanner with + * an error will store it, then we wait for all of the other scanners to + * complete. + * + * Concurrency is important in this class as the scanners are executing + * asynchronously and can modify variables at any time. + */ +public class SaltScanner { + private static final Logger LOG = LoggerFactory.getLogger(SaltScanner.class); + + /** This is a map that the caller must supply. We'll fill it with data. + * WARNING: The salted row comparator should be applied to this map. */ + private final TreeMap spans; + + /** The list of pre-configured scanners. One scanner should be created per + * salt bucket. */ + private final List scanners; + + /** Stores the compacted columns from each scanner as it completes. After all + * scanners are done, we process this into the span map above. */ + private final Map> kv_map = + new ConcurrentHashMap>(); + + /** Stores annotations from each scanner as it completes */ + private final Map> annotation_map = + Collections.synchronizedMap( + new TreeMap>(new RowKey.SaltCmp())); + + /** A deferred to call with the spans on completion */ + private final Deferred> results = + new Deferred>(); + + /** The metric this scanner set is dealing with. If a row comes in with a + * different metric we toss an exception. This shouldn't happen though. */ + private final byte[] metric; + + /** The TSDB to which we belong */ + private final TSDB tsdb; + + /** A counter used to determine how many scanners are still running */ + private volatile int completed_tasks = 0; + + /** When the scanning started. We store the scan latency once all scanners + * are done.*/ + private long start_time; // milliseconds. + + /** A holder for storing the first exception thrown by a scanner if something + * goes pear shaped. Make sure to synchronize on this object when checking + * for null or assigning from a scanner's callback. */ + private Exception exception; + + /** + * Default ctor that performs some validation. Call {@link scan} after + * construction to actually start fetching data. + * @param tsdb The TSDB to which we belong + * @param metric The metric we're expecting to fetch + * @param scanners A list of HBase scanners, one for each bucket + * @param spans The span map to store results in + * @throws IllegalArgumentException if any required data was missing or + * we had invalid parameters. + */ + public SaltScanner(final TSDB tsdb, final byte[] metric, + final List scanners, + final TreeMap spans) { + if (Const.SALT_WIDTH() < 1) { + throw new IllegalArgumentException( + "Salting is disabled. Use the regular scanner"); + } + if (tsdb == null) { + throw new IllegalArgumentException("The TSDB argument was null."); + } + if (spans == null) { + throw new IllegalArgumentException("Span map cannot be null."); + } + if (!spans.isEmpty()) { + throw new IllegalArgumentException("The span map should be empty."); + } + if (scanners == null || scanners.isEmpty()) { + throw new IllegalArgumentException("Missing or empty scanners list. " + + "Please provide a list of scanners for each salt."); + } + if (scanners.size() != Const.SALT_BUCKETS()) { + throw new IllegalArgumentException("Not enough or too many scanners " + + scanners.size() + " when the salt bucket count is " + + Const.SALT_BUCKETS()); + } + if (metric == null) { + throw new IllegalArgumentException("The metric array was null."); + } + if (metric.length != TSDB.metrics_width()) { + throw new IllegalArgumentException("The metric was too short. It must be " + + TSDB.metrics_width() + "bytes wide."); + } + + this.scanners = scanners; + this.spans = spans; + this.metric = metric; + this.tsdb = tsdb; + } + + /** + * Starts all of the scanners asynchronously and returns the data fetched + * once all of the scanners have completed. Note that the result may be an + * exception if one or more of the scanners encountered an exception. The + * first error will be returned, others will be logged. + * @return A deferred to wait on for results. + */ + public Deferred> scan() { + start_time = System.currentTimeMillis(); + for (final Scanner scanner: scanners) { + new ScannerCB(scanner).scan(); + } + return results; + } + + /** + * Called once all of the scanners have reported back in to record our + * latency and merge the results into the spans map. If there was an exception + * stored then we'll return that instead. + */ + private void mergeAndReturnResults() { + final long hbase_time = System.currentTimeMillis(); + TsdbQuery.scanlatency.add((int)(hbase_time - start_time)); + long rows = 0; + + if (exception != null) { + LOG.error("After all of the scanners finished, at " + + "least one threw an exception", exception); + results.callback(exception); + return; + } + + // Merge sorted spans together + for (final List kvs : kv_map.values()) { + if (kvs == null || kvs.isEmpty()) { + LOG.warn("Found a key value list that was null or empty"); + continue; + } + + for (final KeyValue kv : kvs) { + if (kv == null) { + LOG.warn("Found a key value item that was null"); + continue; + } + if (kv.key() == null) { + LOG.warn("A key for a kv was null"); + continue; + } + + Span datapoints = spans.get(kv.key()); + if (datapoints == null) { + datapoints = new Span(tsdb); + spans.put(kv.key(), datapoints); + } + + if (annotation_map.containsKey(kv.key())) { + for (final Annotation note: annotation_map.get(kv.key())) { + datapoints.getAnnotations().add(note); + } + annotation_map.remove(kv.key()); + } + try { + datapoints.addRow(kv); + rows++; + } catch (RuntimeException e) { + LOG.error("Exception adding row to span", e); + throw e; + } + } + } + + kv_map.clear(); + + for (final byte[] key : annotation_map.keySet()) { + Span datapoints = spans.get(key); + if (datapoints == null) { + datapoints = new Span(tsdb); + spans.put(key, datapoints); + } + + for (final Annotation note: annotation_map.get(key)) { + datapoints.getAnnotations().add(note); + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Scanning completed in " + (hbase_time - start_time) + " ms, " + + rows + " rows, and stored in " + spans.size() + " spans"); + LOG.debug("It took " + (System.currentTimeMillis() - hbase_time) + " ms, " + + " to merge and sort the rows into a tree map"); + } + + results.callback(spans); + } + + /** + * Scanner callback executed recursively each time we get a set of data + * from storage. This is responsible for determining what columns are + * returned and issuing requests to load leaf objects. + * When the scanner returns a null set of rows, the method initiates the + * final callback. + */ + final class ScannerCB implements Callback>> { + private final Scanner scanner; + private final List kvs = new ArrayList(); + private final ByteMap> annotations = + new ByteMap>(); + + public ScannerCB(final Scanner scanner) { + this.scanner = scanner; + } + + /** Error callback that will capture an exception from AsyncHBase and store + * it so we can bubble it up to the caller. + */ + class ErrorCb implements Callback { + @Override + public Object call(final Exception e) throws Exception { + LOG.error("Scanner " + scanner + " threw an exception", e); + scanner.close(); + handleException(e); + return null; + } + } + + /** + * Starts the scanner and is called recursively to fetch the next set of + * rows from the scanner. + * @return The map of spans if loaded successfully, null if no data was + * found + */ + public Object scan() { + return scanner.nextRows().addCallback(this).addErrback(new ErrorCb()); + } + + /** + * Iterate through each row of the scanner results, parses out data + * points (and optional meta data). + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final ArrayList> rows) + throws Exception { + try { + if (rows == null) { + scanner.close(); + validateAndTriggerCallback(kvs, annotations); + return null; + } + + for (final ArrayList row : rows) { + final byte[] key = row.get(0).key(); + if (RowKey.rowKeyContainsMetric(metric, key) != 0) { + scanner.close(); + handleException(new IllegalDataException( + "HBase returned a row that doesn't match" + + " our scanner (" + scanner + ")! " + row + " does not start" + + " with " + Arrays.toString(metric) + " on scanner " + this)); + return null; + } + + List notes = annotations.get(key); + if (notes == null) { + notes = new ArrayList(); + annotations.put(key, notes); + } + + final KeyValue compacted; + try{ + compacted = tsdb.compact(row, notes); + } catch (final IllegalDataException idex) { + LOG.error("Caught IllegalDataException exception while parsing the " + + "row " + key + ", skipping it on scanner " + this, idex); + scanner.close(); + handleException(idex); + return null; + } + + if (compacted != null) { // Can be null if we ignored all KVs. + kvs.add(compacted); + } + } + + return scan(); + } catch (final RuntimeException e) { + LOG.error("Unexpected exception on scanner " + this, e); + scanner.close(); + handleException(e); + return null; + } + } + } + + /** + * Called each time a scanner completes with valid or empty data. + * @param kvs The compacted columns fetched by the scanner + * @param annotations The annotations fetched by the scanners + */ + private void validateAndTriggerCallback(final List kvs, + final Map> annotations) { + + final int tasks = ++completed_tasks; + if (kvs.size() > 0) { + kv_map.put(tasks, kvs); + } + + for (final byte[] key : annotations.keySet()) { + final List notes = annotations.get(key); + if (notes.size() > 0) { + // Optimistic write, expecting unique row keys + annotation_map.put(key, notes); + } + } + + if (tasks >= Const.SALT_BUCKETS()) { + try { + mergeAndReturnResults(); + } catch (final Exception ex) { + results.callback(ex); + } + } + } + + /** + * If one or more of the scanners throws an exception then we should close it + * and pass the exception here so that we can catch and return it to the + * caller. If all of the scanners have finished, this will callback to the + * caller immediately. + * @param e The exception to store. + */ + private void handleException(final Exception e) { + // make sure only one scanner can set the exception + synchronized (this) { + if (exception == null) { + exception = e; + } else { + // TODO - it would be nice to close and cancel the other scanners but + // for now we have to wait for them to finish and/or throw exceptions. + LOG.error("Another scanner threw an exception", e); + } + } + + final int tasks = ++completed_tasks; + if (tasks >= Const.SALT_BUCKETS()) { + results.callback(exception); + } + } +} diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java new file mode 100644 index 0000000000..d41df36f5b --- /dev/null +++ b/test/core/TestSaltScanner.java @@ -0,0 +1,384 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeMap; + +import net.opentsdb.uid.UniqueId; + +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +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({ TSDB.class, Scanner.class, SaltScanner.class, Span.class, + Const.class, UniqueId.class }) +public class TestSaltScanner extends BaseTsdbTest { + private final static byte[] KEY_A = { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01 }; + // different tagv + private final static byte[] KEY_B = { 0x00, 0x00, 0x00, 0x01, + 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02 }; + // same as A bug different time + private final static byte[] KEY_C = { 0x00, 0x00, 0x00, 0x01, + 0x51, (byte) 0x0B, 0x13, (byte) 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01 }; + private final static byte[] FAMILY = "t".getBytes(); + private final static byte[] QUALIFIER_A = { 0x00, 0x00 }; + private final static byte[] QUALIFIER_B = { 0x00, 0x10 }; + private final static byte[] VALUE = { 0x42 }; + private final static long VALUE_LONG = 66; + + private final static int NUM_BUCKETS = 2; + private List scanners; + private TreeMap spans; + + private List>> kvs_a; + private List>> kvs_b; + + private Scanner scanner_a; + private Scanner scanner_b; + + @Before + public void beforeLocal() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); + + spans = new TreeMap(new RowKey.SaltCmp()); + setupMockScanners(true); + } + + @Test + public void ctor() { + assertNotNull(new SaltScanner(tsdb, METRIC_BYTES, scanners, spans)); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorSaltDisabled() { + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSDB() { + new SaltScanner(null, METRIC_BYTES, scanners, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullMETRIC_BYTES() { + new SaltScanner(tsdb, null, scanners, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorShortMETRIC_BYTES() { + new SaltScanner(tsdb, new byte[] { 0, 1 }, scanners, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullScanners() { + new SaltScanner(tsdb, METRIC_BYTES, null, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNotEnoughScanners() { + scanners.remove(1); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorTooManyScanners() { + scanners.add(mock(Scanner.class)); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullSpans() { + new SaltScanner(tsdb, METRIC_BYTES, scanners, null); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorSpansHaveData() { + spans.put(new byte[] { 0, 0, 0, 1 }, new Span(tsdb)); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + } + + @Test + public void scanNoData() throws Exception { + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertTrue(spans.isEmpty()); + } + + @Test + public void scan() throws Exception { + setupMockScanners(false); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertEquals(3, spans.size()); + + Span span = spans.get(KEY_A); + assertEquals(2, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(VALUE_LONG, span.longValue(1)); + assertEquals(1356998401000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(KEY_B); + assertEquals(1, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(KEY_C); + assertEquals(2, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(VALUE_LONG, span.longValue(1)); + assertEquals(1359680401000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanHBaseScannerFromDeferredA() throws Exception { + setupMockScanners(false); + // we can't instantiate an HBaseException so just throw a RuntimeException + final RuntimeException e = new RuntimeException("From HBase"); + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred. + >>fromError(e)); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + try { + scanner.scan().joinUninterruptibly(); + fail("Expected a runtime exception here"); + } catch (RuntimeException re) { + assertEquals(e, re); + } + } + + @Test + public void scanHBaseScannerFromDeferredB() throws Exception { + setupMockScanners(false); + // we can't instantiate an HBaseException so just throw a RuntimeException + final RuntimeException e = new RuntimeException("From HBase"); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred. + >>fromError(e)); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + try { + scanner.scan().joinUninterruptibly(); + fail("Expected a runtime exception here"); + } catch (RuntimeException re) { + assertEquals(e, re); + } + } + + @Test + public void scanHBaseScannerThrownA() throws Exception { + setupMockScanners(false); + // we can't instantiate an HBaseException so just throw a RuntimeException + final RuntimeException e = new RuntimeException("From HBase"); + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenThrow(e); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + try { + scanner.scan().joinUninterruptibly(); + fail("Expected a runtime exception here"); + } catch (RuntimeException re) { + assertEquals(e, re); + } + } + + @Test + public void scanHBaseScannerThrownB() throws Exception { + setupMockScanners(false); + // we can't instantiate an HBaseException so just throw a RuntimeException + final RuntimeException e = new RuntimeException("From HBase"); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenThrow(e); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + try { + scanner.scan().joinUninterruptibly(); + fail("Expected a runtime exception here"); + } catch (RuntimeException re) { + assertEquals(e, re); + } + } + + @Test (expected = IllegalDataException.class) + public void scanBadRowKey() throws Exception { + setupMockScanners(false); + + final ArrayList> rows = + new ArrayList>(1); + final ArrayList row = new ArrayList(1); + rows.add(row); + final byte[] key = { 0x00, 0x00, 0x00, 0x02, + 0x51, (byte) 0x0B, 0x13, (byte) 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01 }; + row.add(new KeyValue(key, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.set(2, rows); + + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.>>fromResult(null)); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + scanner.scan().joinUninterruptibly(); + } + + @SuppressWarnings("unchecked") + @Test (expected = IllegalDataException.class) + public void scanCompactionDataException() throws Exception { + setupMockScanners(false); + + doThrow(new IllegalDataException("Boo!")).when( + tsdb).compact(any(ArrayList.class), any(List.class)); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + scanner.scan().joinUninterruptibly(); + } + + @SuppressWarnings("unchecked") + @Test (expected = RuntimeException.class) + public void scanCompactionRuntimeException() throws Exception { + setupMockScanners(false); + + doThrow(new RuntimeException("Boo!")).when( + tsdb).compact(any(ArrayList.class), any(List.class)); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + scanner.scan().joinUninterruptibly(); + } + + /** + * Sets up a pair of scanners with either a list of values or no data + * @param no_data Whether or not to return 0 data. + */ + private void setupMockScanners(final boolean no_data) { + scanners = new ArrayList(NUM_BUCKETS); + scanner_a = mock(Scanner.class); + scanner_b = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + when(scanner_b.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); + } + scanners.add(scanner_a); + scanners.add(scanner_b); + } + + /** + * This method sets up some row keys and values to pass to the scanners. + * The values aren't exactly what would normally be passed to a salt scanner + * in that we have the same series salted across separate buckets. That would + * only happen if you add the timestamp to the salt calculation, which we + * may do in the future. We're testing now for future proofing. + */ + private void setupValues() { + kvs_a = new ArrayList>>(3); + kvs_b = new ArrayList>>(2); + + final String note = "{\"tsuid\":\"000001000001000001\"," + + "\"startTime\":1356998490,\"endTime\":0,\"description\":" + + "\"The Great A'Tuin!\",\"notes\":\"Millenium hand and shrimp\"," + + "\"custom\":null}"; + + for (int i = 0; i < 5; i++) { + final ArrayList> rows = + new ArrayList>(1); + final ArrayList row = new ArrayList(2); + rows.add(row); + byte[] key = null; + + switch (i) { + case 0: + row.add(new KeyValue(KEY_A, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 1: + row.add(new KeyValue(KEY_B, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 2: + row.add(new KeyValue(KEY_C, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 3: + key = Arrays.copyOf(KEY_A, KEY_A.length); + key[0] = 1; + row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); + row.add(new KeyValue(key, FAMILY, new byte[] { 1, 0, 0 }, 0, + note.getBytes(Charset.forName("UTF8")))); + kvs_b.add(rows); + break; + case 4: + key = Arrays.copyOf(KEY_C, KEY_C.length); + key[0] = 1; + row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); + kvs_b.add(rows); + break; + } + } + + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.>>fromResult(null)); + + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); + } +} From 9b9d3c7e50fc3ab97184ea64b3613ba7d5533656 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 16:07:39 -0700 Subject: [PATCH 094/826] Modify TsdbQuery to support salted scanning and use the new SaltScanner where appropriate. Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 61 ++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index a37295a7fb..db022625de 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -315,7 +315,11 @@ private void findGroupBys(final Map tags) { } /** - * Executes the query + * 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 @@ -348,7 +352,17 @@ public Deferred runAsync() 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(metric_width)); + new TreeMap(new SpanCmp( + (short)(Const.SALT_WIDTH() + metric_width))); + + if (Const.SALT_WIDTH() > 0) { + final List scanners = new ArrayList(Const.SALT_BUCKETS()); + for (int i = 0; i < Const.SALT_BUCKETS(); i++) { + scanners.add(getScanner(i)); + } + return new SaltScanner(tsdb, metric, scanners, spans).scan(); + } + final Scanner scanner = getScanner(); final Deferred> results = new Deferred>(); @@ -541,16 +555,37 @@ public DataPoints[] call(final TreeMap spans) throws Exception { * @return A scanner to use for fetching data points */ protected Scanner getScanner() throws HBaseException { + return getScanner(0); + } + + /** + * Returns a scanner set for the given metric (from {@link #metric} or from + * the first TSUID in the {@link #tsuids}s list. If one or more tags are + * provided, it calls into {@link #createAndSetFilter} to setup a row key + * filter. If one or more TSUIDs have been provided, it calls into + * {@link #createAndSetTSUIDFilter} to setup a row key filter. + * @param salt_bucket The salt bucket to scan over when salting is enabled. + * @return A scanner to use for fetching data points + */ + protected Scanner getScanner(final int salt_bucket) throws HBaseException { final short metric_width = tsdb.metrics.width(); - final byte[] start_row = new byte[metric_width + Const.TIMESTAMP_BYTES]; - final byte[] end_row = new byte[metric_width + Const.TIMESTAMP_BYTES]; + final int metric_salt_width = metric_width + Const.SALT_WIDTH(); + final byte[] start_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; + final byte[] end_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; + + if (Const.SALT_WIDTH() > 0) { + final byte[] salt = Internal.getSaltBytes(salt_bucket); + System.arraycopy(salt, 0, start_row, 0, Const.SALT_WIDTH()); + System.arraycopy(salt, 0, end_row, 0, Const.SALT_WIDTH()); + } + // We search at least one row before and one row after the start & end // time we've been given as it's quite likely that the exact timestamp // we're looking for is in the middle of a row. Plus, a number of things // rely on having a few extra data points before & after the exact start // & end dates in order to do proper rate calculation or downsampling near // the "edges" of the graph. - Bytes.setInt(start_row, (int) getScanStartTimeSeconds(), metric_width); + Bytes.setInt(start_row, (int) getScanStartTimeSeconds(), metric_salt_width); Bytes.setInt(end_row, (end_time == UNSET ? -1 // Will scan until the end (0xFFF...). : (int) getScanEndTimeSeconds()), @@ -559,15 +594,15 @@ protected Scanner getScanner() throws HBaseException { // set the metric UID based on the TSUIDs if given, or the metric UID if (tsuids != null && !tsuids.isEmpty()) { final String tsuid = tsuids.get(0); - final String metric_uid = tsuid.substring(0, TSDB.metrics_width() * 2); + final String metric_uid = tsuid.substring(0, metric_width * 2); metric = UniqueId.stringToUid(metric_uid); - System.arraycopy(metric, 0, start_row, 0, metric_width); - System.arraycopy(metric, 0, end_row, 0, metric_width); + System.arraycopy(metric, 0, start_row, Const.SALT_WIDTH(), metric_width); + System.arraycopy(metric, 0, end_row, Const.SALT_WIDTH(), metric_width); } else { - System.arraycopy(metric, 0, start_row, 0, metric_width); - System.arraycopy(metric, 0, end_row, 0, metric_width); + System.arraycopy(metric, 0, start_row, Const.SALT_WIDTH(), metric_width); + System.arraycopy(metric, 0, end_row, Const.SALT_WIDTH(), metric_width); } - + final Scanner scanner = tsdb.client.newScanner(tsdb.table); scanner.setStartKey(start_row); scanner.setStopKey(end_row); @@ -646,7 +681,7 @@ private void createAndSetFilter(final Scanner scanner) { buf.append("(?s)" // Ensure we use the DOTALL flag. + "^.{") // ... start by skipping the metric ID and timestamp. - .append(tsdb.metrics.width() + Const.TIMESTAMP_BYTES) + .append(Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES) .append("}"); final Iterator tags = this.tags.iterator(); final Iterator group_bys = (this.group_bys == null @@ -722,7 +757,7 @@ private void createAndSetTSUIDFilter(final Scanner scanner) { buf.append("(?s)" // Ensure we use the DOTALL flag. + "^.{") // ... start by skipping the metric ID and timestamp. - .append(tsdb.metrics.width() + Const.TIMESTAMP_BYTES) + .append(Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES) .append("}("); for (final byte[] tags : uids) { From 5c7b648fb8fcc83a3ac349d0802e1ea0b1f871d3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 16:09:10 -0700 Subject: [PATCH 095/826] Reshuffle and cleanup the TsdbQuery tests. I broke the integration tests into separate files with overloads for salted testing. Cleanup the downsampled query tests and add an override for making sure it all works with salting Make TsdbQuery use the DateTime method instead of System for fetching the time. Signed-off-by: Chris Larsen --- Makefile.am | 5 + src/core/TsdbQuery.java | 3 +- test/core/TestTsdbQuery.java | 3008 +---------------- test/core/TestTsdbQueryAggregators.java | 1054 ++++++ test/core/TestTsdbQueryAggregatorsSalted.java | 37 + test/core/TestTsdbQueryDownsample.java | 366 +- test/core/TestTsdbQueryDownsampleSalted.java | 37 + test/core/TestTsdbQueryQueries.java | 1331 ++++++++ test/core/TestTsdbQuerySalted.java | 35 + 9 files changed, 2559 insertions(+), 3317 deletions(-) create mode 100644 test/core/TestTsdbQueryAggregators.java create mode 100644 test/core/TestTsdbQueryAggregatorsSalted.java create mode 100644 test/core/TestTsdbQueryDownsampleSalted.java create mode 100644 test/core/TestTsdbQueryQueries.java create mode 100644 test/core/TestTsdbQuerySalted.java diff --git a/Makefile.am b/Makefile.am index d02d1d30f1..5b2a7079c6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -170,7 +170,12 @@ test_SRC := \ test/core/TestTags.java \ test/core/TestTSDB.java \ test/core/TestTsdbQueryDownsample.java \ + test/core/TestTsdbQueryDownsampleSalted.java \ test/core/TestTsdbQuery.java \ + test/core/TestTsdbQueryAggregators.java \ + test/core/TestTsdbQueryAggregatorsSalted.java \ + test/core/TestTsdbQueryQueries.java \ + test/core/TestTsdbQuerySalted.java \ test/core/TestTSQuery.java \ test/core/TestTSSubQuery.java \ test/plugin/DummyPlugin.java \ diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index db022625de..fbb8e31a52 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -37,6 +37,7 @@ import net.opentsdb.stats.Histogram; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.DateTime; /** * Non-synchronized implementation of {@link Query}. @@ -181,7 +182,7 @@ public void setEndTime(final long timestamp) { @Override public long getEndTime() { if (end_time == UNSET) { - setEndTime(System.currentTimeMillis()); + setEndTime(DateTime.currentTimeMillis()); } return end_time; } diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index dfc66673cd..c027d8bd34 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -14,133 +14,34 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; -import java.lang.reflect.Field; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import net.opentsdb.meta.Annotation; -import net.opentsdb.storage.MockBase; -import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; -import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; -import org.apache.zookeeper.proto.DeleteRequest; -import org.hbase.async.Bytes; -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.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.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import com.stumbleupon.async.Deferred; - /** - * Massive test class that is used to test all facets of querying for data. - * Since data is fetched using the TsdbQuery class, it makes sense to put all - * of the unit tests here that deal with actual data. This includes: - * - queries - * - aggregations - * - rate conversion - * - downsampling - * - compactions (read and write) + * 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 + * queries and validating the group by and aggregation logic, see + * {@link TestTsdbQueryQueries} */ @RunWith(PowerMockRunner.class) -@PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, - Scanner.class, TsdbQuery.class, DeleteRequest.class, Annotation.class, - RowKey.class, Span.class, SpanGroup.class, IncomingDataPoints.class }) -public final class TestTsdbQuery { - private Config config; - private TSDB tsdb = null; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); +@PrepareForTest({ DateTime.class }) +public final class TestTsdbQuery extends BaseTsdbTest { private TsdbQuery query = null; - private MockBase storage = null; @Before - public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - config = new Config(false); - config.setFixDuplicates(true); // TODO(jat): test both ways - tsdb = new TSDB(config); + public void beforeLocal() throws Exception { query = new TsdbQuery(tsdb); - - // replace the "real" field objects with mocks - Field met = tsdb.getClass().getDeclaredField("metrics"); - met.setAccessible(true); - met.set(tsdb, metrics); - - Field tagk = tsdb.getClass().getDeclaredField("tag_names"); - tagk.setAccessible(true); - tagk.set(tsdb, tag_names); - - Field tagv = tsdb.getClass().getDeclaredField("tag_values"); - tagv.setAccessible(true); - tagv.set(tsdb, tag_values); - - // mock UniqueId - when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("sys.cpu.user")); - when(metrics.getId("sys.cpu.system")) - .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); - when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); - when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("sys.cpu.nice")); - when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("host")); - when(tag_names.getOrCreateIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getIdAsync("dc")) - .thenThrow(new NoSuchUniqueName("dc", "metric")); - when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("web01")); - when(tag_values.getOrCreateIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_values.getIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("web02")); - when(tag_values.getOrCreateIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getId("web03")) - .thenThrow(new NoSuchUniqueName("web03", "metric")); - - when(metrics.width()).thenReturn((short)3); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); } @Test @@ -211,51 +112,45 @@ public void setEndTimeGreaterThanEndTime() throws Exception { @Test public void getEndTimeNotSet() throws Exception { - PowerMockito.mockStatic(System.class); - when(System.currentTimeMillis()).thenReturn(1357300800000L); + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357300800000L); assertEquals(1357300800000L, query.getEndTime()); } @Test public void setTimeSeries() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); assertNotNull(query); } @Test (expected = NullPointerException.class) public void setTimeSeriesNullTags() throws Exception { - query.setTimeSeries("sys.cpu.user", null, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, null, Aggregators.SUM, false); } @Test public void setTimeSeriesEmptyTags() throws Exception { - HashMap tags = new HashMap(1); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + tags.clear(); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); assertNotNull(query); } @Test (expected = NoSuchUniqueName.class) public void setTimeSeriesNosuchMetric() throws Exception { - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setTimeSeries("sys.cpu.system", tags, Aggregators.SUM, false); + query.setTimeSeries(NSUN_METRIC, tags, Aggregators.SUM, false); } @Test (expected = NoSuchUniqueName.class) public void setTimeSeriesNosuchTagk() throws Exception { - HashMap tags = new HashMap(1); - tags.put("dc", "web01"); - query.setTimeSeries("sys.cpu.system", tags, Aggregators.SUM, false); + tags.clear(); + tags.put(NSUN_TAGK, TAGV_STRING); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); } @Test (expected = NoSuchUniqueName.class) public void setTimeSeriesNosuchTagv() throws Exception { - HashMap tags = new HashMap(1); - tags.put("host", "web03"); - query.setTimeSeries("sys.cpu.system", tags, Aggregators.SUM, false); + tags.put(TAGK_STRING, NSUN_TAGV); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); } @Test @@ -286,2867 +181,4 @@ public void setTimeSeriesTSDifferentMetrics() throws Exception { query.setTimeSeries(tsuids, Aggregators.SUM, false); } - @Test - public void runLongSingleTS() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].aggregatedSize()); - } - - @Test - public void runLongSingleTSMs() throws Exception { - storeLongTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].aggregatedSize()); - } - - @Test - public void runLongSingleTSNoData() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(0, dps.length); - } - - @Test - public void runLongTwoAggSum() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(); - query.setStartTime(1356998400L); - query.setEndTime(1357041600L); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - for (DataPoint dp : dps[0]) { - assertEquals(301, dp.longValue()); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runLongTwoAggSumMs() throws Exception { - storeLongTimeSeriesMs(); - HashMap tags = new HashMap(); - query.setStartTime(1356998400L); - query.setEndTime(1357041600L); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - for (DataPoint dp : dps[0]) { - assertEquals(301, dp.longValue()); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runLongTwoGroup() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "*"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(2, dps.length); - - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - assertEquals("sys.cpu.user", dps[1].metricName()); - assertTrue(dps[1].getAggregatedTags().isEmpty()); - assertNull(dps[1].getAnnotations()); - assertEquals("web02", dps[1].getTags().get("host")); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].size()); - - value = 300; - for (DataPoint dp : dps[1]) { - assertEquals(value, dp.longValue()); - value--; - } - assertEquals(300, dps[1].size()); - } - - @Test - public void runLongSingleTSRate() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - for (DataPoint dp : dps[0]) { - assertEquals(0.033F, dp.doubleValue(), 0.001); - } - assertEquals(299, dps[0].size()); - } - - @Test - public void runLongSingleTSRateMs() throws Exception { - storeLongTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - for (DataPoint dp : dps[0]) { - assertEquals(2.0F, dp.doubleValue(), 0.001); - } - assertEquals(299, dps[0].size()); - } - - @Test - public void runLongSingleTSCompacted() throws Exception { - storeLongCompactions(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].size()); - } - - // Can't run this one since the TreeMap will order the compacted row AFTER - // the other data points. A full MockBase implementation would allow this -// @Test -// public void runLongSingleTSCompactedAndNonCompacted() throws Exception { -// storeLongCompactions(); -// HashMap tags = new HashMap(1); -// tags.put("host", "web01"); -// -// long timestamp = 1357007460; -// for (int i = 301; i <= 310; i++) { -// tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); -// } -// storage.dumpToSystemOut(false); -// query.setStartTime(1356998400); -// query.setEndTime(1357041600); -// query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); -// final DataPoints[] dps = query.run(); -// assertNotNull(dps); -// -// int value = 1; -// for (DataPoint dp : dps[0]) { -// assertEquals(value, dp.longValue()); -// value++; -// } -// assertEquals(310, dps[0].size()); -// } - - @Test - public void runFloatSingleTS() throws Exception { - storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double value = 1.25D; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.doubleValue(), 0.001); - value += 0.25D; - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runFloatSingleTSMs() throws Exception { - storeFloatTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double value = 1.25D; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.doubleValue(), 0.001); - value += 0.25D; - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runFloatTwoAggSum() throws Exception { - storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - for (DataPoint dp : dps[0]) { - assertEquals(76.25, dp.doubleValue(), 0.00001); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runFloatTwoAggSumMs() throws Exception { - storeFloatTimeSeriesMs(); - HashMap tags = new HashMap(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - for (DataPoint dp : dps[0]) { - assertEquals(76.25, dp.doubleValue(), 0.00001); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runFloatTwoGroup() throws Exception { - storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "*"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(2, dps.length); - - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - assertEquals("sys.cpu.user", dps[1].metricName()); - assertTrue(dps[1].getAggregatedTags().isEmpty()); - assertNull(dps[1].getAnnotations()); - assertEquals("web02", dps[1].getTags().get("host")); - - double value = 1.25D; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.doubleValue(), 0.0001); - value += 0.25D; - } - assertEquals(300, dps[0].size()); - - value = 75D; - for (DataPoint dp : dps[1]) { - assertEquals(value, dp.doubleValue(), 0.0001); - value -= 0.25d; - } - assertEquals(300, dps[1].size()); - } - - @Test - public void runFloatSingleTSRate() throws Exception { - storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - for (DataPoint dp : dps[0]) { - assertEquals(0.00833F, dp.doubleValue(), 0.00001); - } - assertEquals(299, dps[0].size()); - } - - @Test - public void runFloatSingleTSRateMs() throws Exception { - storeFloatTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - for (DataPoint dp : dps[0]) { - assertEquals(0.5F, dp.doubleValue(), 0.00001); - } - assertEquals(299, dps[0].size()); - } - - @Test - public void runFloatSingleTSCompacted() throws Exception { - storeFloatCompactions(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double value = 1.25D; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.doubleValue(), 0.001); - value += 0.25D; - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMixedSingleTS() throws Exception { - storeMixedTimeSeriesSeconds(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double float_value = 1.25D; - int int_value = 76; - // due to aggregation, the only int that will be returned will be the very - // last value of 76 since the agg will convert every point in between to a - // double - for (DataPoint dp : dps[0]) { - if (dp.isInteger()) { - assertEquals(int_value, dp.longValue()); - int_value++; - float_value = int_value; - } else { - assertEquals(float_value, dp.doubleValue(), 0.001); - float_value += 0.25D; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMixedSingleTSMsAndS() throws Exception { - storeMixedTimeSeriesMsAndS(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double float_value = 1.25D; - int int_value = 76; - // due to aggregation, the only int that will be returned will be the very - // last value of 76 since the agg will convert every point in between to a - // double - for (DataPoint dp : dps[0]) { - if (dp.isInteger()) { - assertEquals(int_value, dp.longValue()); - int_value++; - float_value = int_value; - } else { - assertEquals(float_value, dp.doubleValue(), 0.001); - float_value += 0.25D; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMixedSingleTSPostCompaction() throws Exception { - storeMixedTimeSeriesSeconds(); - - final Field compact = Config.class.getDeclaredField("enable_compactions"); - compact.setAccessible(true); - compact.set(config, true); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - assertNotNull(query.run()); - - // this should only compact the rows for the time series that we fetched and - // leave the others alone - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E22700000001000001"))); - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E23510000001000001"))); - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E24320000001000001"))); - - // run it again to verify the compacted data uncompacts properly - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double float_value = 1.25D; - int int_value = 76; - // due to aggregation, the only int that will be returned will be the very - // last value of 76 since the agg will convert every point in between to a - // double - for (DataPoint dp : dps[0]) { - if (dp.isInteger()) { - assertEquals(int_value, dp.longValue()); - int_value++; - float_value = int_value; - } else { - assertEquals(float_value, dp.doubleValue(), 0.001); - float_value += 0.25D; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMixedSingleTSCompacted() throws Exception { - storeMixedCompactions(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - double float_value = 1.25D; - int int_value = 76; - // due to aggregation, the only int that will be returned will be the very - // last value of 76 since the agg will convert every point in between to a - // double - for (DataPoint dp : dps[0]) { - if (dp.isInteger()) { - assertEquals(int_value, dp.longValue()); - int_value++; - float_value = int_value; - } else { - assertEquals(float_value, dp.doubleValue(), 0.001); - float_value += 0.25D; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runEndTime() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357001900); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(236, dps[0].size()); - } - - @Test - public void runCompactPostQuery() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - - final Field compact = Config.class.getDeclaredField("enable_compactions"); - compact.setAccessible(true); - compact.set(config, true); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - assertNotNull(query.run()); - - // this should only compact the rows for the time series that we fetched and - // leave the others alone - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E22700000001000001"))); - assertEquals(119, storage.numColumns( - MockBase.stringToBytes("00000150E22700000001000002"))); - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E23510000001000001"))); - assertEquals(120, storage.numColumns( - MockBase.stringToBytes("00000150E23510000001000002"))); - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E24320000001000001"))); - assertEquals(61, storage.numColumns( - MockBase.stringToBytes("00000150E24320000001000002"))); - - // run it again to verify the compacted data uncompacts properly - final DataPoints[] dps = query.run(); - assertNotNull(dps); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].size()); - } - - @Test (expected = IllegalStateException.class) - public void runStartNotSet() throws Exception { - HashMap tags = new HashMap(0); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - query.run(); - } - - @Test - public void runFloatAndIntSameTS() throws Exception { - // if a row has an integer and a float for the same timestamp, there will be - // two different qualifiers that will resolve to the same offset. This no - // longer tosses an exception, and keeps the last value - storeLongTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint("sys.cpu.user", 1356998430, 42.5F, tags).joinUninterruptibly(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - // TODO: further validate the result - } - - @Test - public void runWithAnnotation() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - - final Annotation note = new Annotation(); - note.setTSUID("000001000001000001"); - note.setStartTime(1356998490); - note.setDescription("Hello World!"); - note.syncToStorage(tsdb, false).joinUninterruptibly(); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(1, dps[0].getAnnotations().size()); - assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runWithAnnotationPostCompact() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - - final Annotation note = new Annotation(); - note.setTSUID("000001000001000001"); - note.setStartTime(1356998490); - note.setDescription("Hello World!"); - note.syncToStorage(tsdb, false).joinUninterruptibly(); - - final Field compact = Config.class.getDeclaredField("enable_compactions"); - compact.setAccessible(true); - compact.set(config, true); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - assertNotNull(query.run()); - - // this should only compact the rows for the time series that we fetched and - // leave the others alone - assertEquals(2, storage.numColumns( - MockBase.stringToBytes("00000150E22700000001000001"))); - assertEquals(119, storage.numColumns( - MockBase.stringToBytes("00000150E22700000001000002"))); - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E23510000001000001"))); - assertEquals(120, storage.numColumns( - MockBase.stringToBytes("00000150E23510000001000002"))); - assertEquals(1, storage.numColumns( - MockBase.stringToBytes("00000150E24320000001000001"))); - assertEquals(61, storage.numColumns( - MockBase.stringToBytes("00000150E24320000001000002"))); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(1, dps[0].getAnnotations().size()); - assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runWithOnlyAnnotation() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - - // verifies that we can pickup an annotation stored all by it's lonesome - // in a row without any data - storage.flushRow(MockBase.stringToBytes("00000150E23510000001000001")); - final Annotation note = new Annotation(); - note.setTSUID("000001000001000001"); - note.setStartTime(1357002090); - note.setDescription("Hello World!"); - note.syncToStorage(tsdb, false).joinUninterruptibly(); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(1, dps[0].getAnnotations().size()); - assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - // account for the jump - if (value == 120) { - value = 240; - } - } - assertEquals(180, dps[0].size()); - } - - @Test - public void runWithSingleAnnotation() throws Exception { - setQueryStorage(); - - // verifies that we can pickup an annotation stored all by it's lonesome - // in a row without any data - storage.flushRow(MockBase.stringToBytes("00000150E23510000001000001")); - final Annotation note = new Annotation(); - note.setTSUID("000001000001000001"); - note.setStartTime(1357002090); - note.setDescription("Hello World!"); - note.syncToStorage(tsdb, false).joinUninterruptibly(); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(1, dps[0].getAnnotations().size()); - assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); - - assertEquals(0, dps[0].size()); - } - - @Test - public void runSingleDataPoint() throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998410; - tsdb.addPoint("sys.cpu.user", timestamp, 42, tags).joinUninterruptibly(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000001"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(1, dps.length); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - assertEquals(42, dps[0].longValue(0)); - } - - @Test - public void runSingleDataPointWithAnnotation() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998410; - tsdb.addPoint("sys.cpu.user", timestamp, 42, tags).joinUninterruptibly(); - storage.flushRow(MockBase.stringToBytes("00000150E23510000001000001")); - final Annotation note = new Annotation(); - note.setTSUID("000001000001000001"); - note.setStartTime(1357002090); - note.setDescription("Hello World!"); - note.syncToStorage(tsdb, false).joinUninterruptibly(); - - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000001"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(1, dps.length); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertEquals("web01", dps[0].getTags().get("host")); - assertEquals(42, dps[0].longValue(0)); - assertEquals(1, dps[0].getAnnotations().size()); - assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); - } - - @Test - public void runTSUIDQuery() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000001"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(300, dps[0].aggregatedSize()); - } - - @Test - public void runTSUIDsAggSum() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000001"); - tsuids.add("000001000001000002"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - for (DataPoint dp : dps[0]) { - assertEquals(301, dp.longValue()); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runTSUIDQueryNoData() throws Exception { - setQueryStorage(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000001"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(0, dps.length); - } - - @Test - public void runTSUIDQueryNoDataForTSUID() throws Exception { - // this doesn't throw an exception since the UIDs are only looked for when - // the query completes. - setQueryStorage(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000005"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals(0, dps.length); - } - - @Test (expected = NoSuchUniqueId.class) - public void runTSUIDQueryNSU() throws Exception { - when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) - .thenThrow(new NoSuchUniqueId("metrics", new byte[] { 0, 0, 1 })); - storeLongTimeSeriesSeconds(true, false);; - query.setStartTime(1356998400); - query.setEndTime(1357041600); - final List tsuids = new ArrayList(1); - tsuids.add("000001000001000001"); - query.setTimeSeries(tsuids, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - dps[0].metricName(); - } - - @Test - public void runRateCounterDefault() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - tsdb.addPoint("sys.cpu.user", timestamp += 30, Long.MAX_VALUE - 55, tags) - .joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, Long.MAX_VALUE - 25, tags) - .joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 5, tags).joinUninterruptibly(); - - RateOptions ro = new RateOptions(true, Long.MAX_VALUE, 0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true, ro); - final DataPoints[] dps = query.run(); - - for (DataPoint dp : dps[0]) { - assertEquals(1.0, dp.doubleValue(), 0.001); - } - assertEquals(2, dps[0].size()); - } - - @Test - public void runRateCounterDefaultNoOp() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - tsdb.addPoint("sys.cpu.user", timestamp += 30, 30, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 60, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 90, tags).joinUninterruptibly(); - - RateOptions ro = new RateOptions(true, Long.MAX_VALUE, 0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true, ro); - final DataPoints[] dps = query.run(); - - for (DataPoint dp : dps[0]) { - assertEquals(1.0, dp.doubleValue(), 0.001); - } - assertEquals(2, dps[0].size()); - } - - @Test - public void runRateCounterMaxSet() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - tsdb.addPoint("sys.cpu.user", timestamp += 30, 45, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 75, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 5, tags).joinUninterruptibly(); - - RateOptions ro = new RateOptions(true, 100, 0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true, ro); - final DataPoints[] dps = query.run(); - - for (DataPoint dp : dps[0]) { - assertEquals(1.0, dp.doubleValue(), 0.001); - } - assertEquals(2, dps[0].size()); - } - - @Test - public void runRateCounterAnomally() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - tsdb.addPoint("sys.cpu.user", timestamp += 30, 45, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 75, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.user", timestamp += 30, 25, tags).joinUninterruptibly(); - - RateOptions ro = new RateOptions(true, 10000, 35); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true, ro); - final DataPoints[] dps = query.run(); - - assertEquals(1.0, dps[0].doubleValue(0), 0.001); - assertEquals(0, dps[0].doubleValue(1), 0.001); - assertEquals(2, dps[0].size()); - } - - @Test - public void runMultiCompact() throws Exception { - final byte[] qual1 = { 0x00, 0x07 }; - final byte[] val1 = Bytes.fromLong(1L); - final byte[] qual2 = { 0x00, 0x27 }; - final byte[] val2 = Bytes.fromLong(2L); - - // 2nd compaction - final byte[] qual3 = { 0x00, 0x37 }; - final byte[] val3 = Bytes.fromLong(3L); - final byte[] qual4 = { 0x00, 0x47 }; - final byte[] val4 = Bytes.fromLong(4L); - - // 3rd compaction - final byte[] qual5 = { 0x00, 0x57 }; - final byte[] val5 = Bytes.fromLong(5L); - final byte[] qual6 = { 0x00, 0x67 }; - final byte[] val6 = Bytes.fromLong(6L); - - final byte[] KEY = { 0, 0, 1, 0x50, (byte) 0xE2, - 0x27, 0x00, 0, 0, 1, 0, 0, 1 }; - - setQueryStorage(); - storage.addColumn(KEY, - MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); - storage.addColumn(KEY, - MockBase.concatByteArrays(qual3, qual4), - MockBase.concatByteArrays(val3, val4, new byte[] { 0 })); - storage.addColumn(KEY, - MockBase.concatByteArrays(qual5, qual6), - MockBase.concatByteArrays(val5, val6, new byte[] { 0 })); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(6, dps[0].aggregatedSize()); - } - - @Test - public void runMultiCompactAndSingles() throws Exception { - final byte[] qual1 = { 0x00, 0x07 }; - final byte[] val1 = Bytes.fromLong(1L); - final byte[] qual2 = { 0x00, 0x27 }; - final byte[] val2 = Bytes.fromLong(2L); - - // 2nd compaction - final byte[] qual3 = { 0x00, 0x37 }; - final byte[] val3 = Bytes.fromLong(3L); - final byte[] qual4 = { 0x00, 0x47 }; - final byte[] val4 = Bytes.fromLong(4L); - - // 3rd compaction - final byte[] qual5 = { 0x00, 0x57 }; - final byte[] val5 = Bytes.fromLong(5L); - final byte[] qual6 = { 0x00, 0x67 }; - final byte[] val6 = Bytes.fromLong(6L); - - final byte[] KEY = { 0, 0, 1, 0x50, (byte) 0xE2, - 0x27, 0x00, 0, 0, 1, 0, 0, 1 }; - - setQueryStorage(); - storage.addColumn(KEY, - MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); - storage.addColumn(KEY, qual3, val3); - storage.addColumn(KEY, qual4, val4); - storage.addColumn(KEY, - MockBase.concatByteArrays(qual5, qual6), - MockBase.concatByteArrays(val5, val6, new byte[] { 0 })); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); - - int value = 1; - for (DataPoint dp : dps[0]) { - assertEquals(value, dp.longValue()); - value++; - } - assertEquals(6, dps[0].aggregatedSize()); - } - - @Test - public void runInterpolationSeconds() throws Exception { - setQueryStorage(); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - for (int i = 1; i <= 300; i++) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags) - .joinUninterruptibly(); - } - - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998415; - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags) - .joinUninterruptibly(); - } - - tags.clear(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.longValue()); - - if (dp.timestamp() == 1357007400000L) { - v = 1; - } else if (v == 1 || v == 302) { - v = 301; - } else { - v = 302; - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runInterpolationMs() throws Exception { - setQueryStorage(); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400000L; - for (int i = 1; i <= 300; i++) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags) - .joinUninterruptibly(); - } - - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998400250L; - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags) - .joinUninterruptibly(); - } - - tags.clear(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998400500L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 250; - assertEquals(v, dp.longValue()); - - if (dp.timestamp() == 1356998550000L) { - v = 1; - } else if (v == 1 || v == 302) { - v = 301; - } else { - v = 302; - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runInterpolationMsDownsampled() throws Exception { - setQueryStorage(); - - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - // ts = 1356998400500, v = 1 - // ts = 1356998401000, v = 2 - // ts = 1356998401500, v = 3 - // ts = 1356998402000, v = 4 - // ts = 1356998402500, v = 5 - // ... - // ts = 1356998449000, v = 98 - // ts = 1356998449500, v = 99 - // ts = 1356998450000, v = 100 - // ts = 1356998455000, v = 101 - // ts = 1356998460000, v = 102 - // ... - // ts = 1356998550000, v = 120 - long timestamp = 1356998400000L; - for (int i = 1; i <= 120; i++) { - timestamp += i <= 100 ? 500 : 5000; - tsdb.addPoint("sys.cpu.user", timestamp, i, tags) - .joinUninterruptibly(); - } - - // ts = 1356998400750, v = 300 - // ts = 1356998401250, v = 299 - // ts = 1356998401750, v = 298 - // ts = 1356998402250, v = 297 - // ts = 1356998402750, v = 296 - // ... - // ts = 1356998549250, v = 3 - // ts = 1356998549750, v = 2 - // ts = 1356998550250, v = 1 - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998400250L; - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags) - .joinUninterruptibly(); - } - - tags.clear(); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); - query.downsample(1000, Aggregators.SUM); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - // TS1 in intervals = (1), (2,3), (4,5) ... (98,99), 100, (), (), (), (), - // (101), ... (120) - // TS2 in intervals = (300), (299,298), (297,296), ... (203, 202) ... - // (3,2), (1) - // TS1 downsample = 1, 5, 9, ... 197, 100, _, _, _, _, 101, ... 120 - // TS1 interpolation = 1, 5, ... 197, 100, 100.2, 100.4, 100.6, 100.8, 101, - // ... 119.6, 119.8, 120 - // TS2 downsample = 300, 597, 593, ... 405, 401, ... 5, 1 - // TS1 + TS2 = 301, 602, 602, ... 501, 497.2, ... 124.8, 121 - int i = 0; - long ts = 1356998400000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 1000; - if (i == 0) { - assertEquals(301, dp.doubleValue(), 0.0000001); - } else if (i < 50) { - // TS1 = i * 2 + i * 2 + 1 - // TS2 = (300 - i * 2 + 1) + (300 - i * 2) - // TS1 + TS2 = 602 - assertEquals(602, dp.doubleValue(), 0.0000001); - } else { - // TS1 = 100 + (i - 50) * 0.2 - // TS2 = (300 - i * 2 + 1) + (300 - i * 2) - // TS1 + TS2 = 701 + (i - 50) * 0.2 - i * 4 - double value = 701 + (i - 50) * 0.2 - i * 4; - assertEquals(value, dp.doubleValue(), 0.0000001); - } - ++i; - } - assertEquals(151, dps[0].size()); - } - - //---------------------- // - // Aggregator unit tests // - // --------------------- // - - @Test - public void runZimSum() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.ZIMSUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(301, dp.longValue()); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runZimSumFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.ZIMSUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(76.25, dp.doubleValue(), 0.001); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runZimSumOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.ZIMSUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v1 = 1; - long v2 = 300; - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - - if (counter % 2 == 0) { - assertEquals(v1, dp.longValue()); - v1++; - } else { - assertEquals(v2, dp.longValue()); - v2--; - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runZimSumFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.ZIMSUM, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v1 = 1.25; - double v2 = 75.0; - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - if (counter % 2 == 0) { - assertEquals(v1, dp.doubleValue(), 0.001); - v1 += 0.25; - } else { - assertEquals(v2, dp.doubleValue(), 0.001); - v2 -= 0.25; - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMin() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998430000L; - boolean decrement = false; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.longValue()); - - if (decrement) { - v--; - } else { - v++; - } - - if (v == 151){ - v = 150; - decrement = true; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMinFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 1.25; - long ts = 1356998430000L; - boolean decrement = false; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.doubleValue(), 0.0001); - - if (decrement) { - v -= .25; - } else { - v += .25; - } - - if (v > 38){ - v = 38.0; - decrement = true; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMinOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998430000L; - int counter = 0; - boolean decrement = false; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.longValue()); - if (counter % 2 != 0) { - if (decrement) { - v--; - } else { - v++; - } - } else if (v == 151){ - v = 150; - decrement = true; - counter--; // hack since the hump is 150 150 151 150 150 - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMinFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 1.25; - long ts = 1356998430000L; - boolean decrement = false; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.doubleValue(), 0.001); - if (decrement) { - v -= 0.125; - } else { - v += 0.125; - } - - if (v > 38.125){ - v = 38.125; - decrement = true; - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMax() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 300; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.longValue()); - - if (decrement) { - v--; - } else { - v++; - } - - if (v == 150){ - v = 151; - decrement = false; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMaxFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 75.0; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.doubleValue(), 0.001); - - if (decrement) { - v -= .25; - } else { - v += .25; - } - - if (v < 38.25){ - v = 38.25; - decrement = false; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMaxOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998430000L; - int counter = 0; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.longValue()); - if (v == 1) { - v = 300; - } else if (dp.timestamp() == 1357007400000L) { - v = 1; - } else if (counter % 2 == 0) { - if (decrement) { - v--; - } else { - v++; - } - } - - if (v == 150){ - v = 151; - decrement = false; - counter--; // hack since the hump is 151 151 151 - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMaxFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 1.25; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.doubleValue(), .0001); - if (v == 1.25) { - v = 75.0; - } else if (dp.timestamp() == 1357007400000L) { - v = 0.25; - } else { - if (decrement) { - v -= .125; - } else { - v += .125; - } - - if (v < 38.25){ - v = 38.25; - decrement = false; - } - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runAvg() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(150, dp.longValue()); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runAvgFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(38.125, dp.doubleValue(), 0.001); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runAvgOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.longValue()); - if (v == 1) { - v = 150; - } else if (dp.timestamp() == 1357007400000L) { - v = 1; - } else if (v == 150) { - v = 151; - } else { - v = 150; - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runAvgFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.AVG, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 1.25; - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.doubleValue(), 0.0001); - if (v == 1.25) { - v = 38.1875; - } else if (dp.timestamp() == 1357007400000L) { - v = .25; - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runDev() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.DEV, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 149; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.longValue()); - - if (decrement) { - v--; - } else { - v++; - } - - if (v < 0){ - v = 0; - decrement = false; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runDevFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.DEV, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 36.875; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.doubleValue(), 0.001); - - if (decrement) { - v -= 0.25; - } else { - v += 0.25; - } - - if (v < 0.125){ - v = 0.125; - decrement = false; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runDevOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.DEV, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 0; - long ts = 1356998430000L; - int counter = 0; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.longValue()); - if (dp.timestamp() == 1356998430000L) { - v = 149; - } else if (dp.timestamp() == 1357007400000L) { - v = 0; - } else if (counter % 2 == 0) { - if (decrement) { - v--; - } else { - v++; - } - if (v < 0) { - v = 0; - decrement = false; - counter++; - } - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runDevFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.DEV, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 0; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals(v, dp.doubleValue(), 0.0001); - if (dp.timestamp() == 1356998430000L) { - v = 36.8125; - } else if (dp.timestamp() == 1357007400000L) { - v = 0; - } else { - if (decrement) { - v -= 0.125; - } else { - v += 0.125; - } - if (v < 0.0625) { - v = 0.0625; - decrement = false; - } - } - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMimMin() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 1; - long ts = 1356998430000L; - boolean decrement = false; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.longValue()); - - if (decrement) { - v--; - } else { - v++; - } - - if (v == 151){ - v = 150; - decrement = true; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMimMinOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v1 = 1; - long v2 = 300; - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - - if (counter % 2 == 0) { - assertEquals(v1, dp.longValue()); - v1++; - } else { - assertEquals(v2, dp.longValue()); - v2--; - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMimMinFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 1.25; - long ts = 1356998430000L; - boolean decrement = false; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.doubleValue(), 0.0001); - - if (decrement) { - v -= .25; - } else { - v += .25; - } - - if (v > 38){ - v = 38.0; - decrement = true; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMimMinFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMIN, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v1 = 1.25; - double v2 = 75.0; - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - if (counter % 2 == 0) { - assertEquals(v1, dp.doubleValue(), 0.001); - v1 += 0.25; - } else { - assertEquals(v2, dp.doubleValue(), 0.001); - v2 -= 0.25; - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMimMax() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v = 300; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.longValue()); - - if (decrement) { - v--; - } else { - v++; - } - - if (v == 150){ - v = 151; - decrement = false; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMimMaxFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v = 75.0; - long ts = 1356998430000L; - boolean decrement = true; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(v, dp.doubleValue(), 0.001); - - if (decrement) { - v -= .25; - } else { - v += .25; - } - - if (v < 38.25){ - v = 38.25; - decrement = false; - } - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runMimMaxOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long v1 = 1; - long v2 = 300; - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - - if (counter % 2 == 0) { - assertEquals(v1, dp.longValue()); - v1++; - } else { - assertEquals(v2, dp.longValue()); - v2--; - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runMimMaxFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.MIMMAX, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - double v1 = 1.25; - double v2 = 75.0; - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - if (counter % 2 == 0) { - assertEquals(v1, dp.doubleValue(), 0.001); - v1 += 0.25; - } else { - assertEquals(v2, dp.doubleValue(), 0.001); - v2 -= 0.25; - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - @Test - public void runPercentiles() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - // These are not accurate at all when data points only contain 2 values - // so we are just testing constructor logic, rather than precision - testPercentile(Aggregators.p50, 150, 150); - testPercentile(Aggregators.p75, 150, 150); - testPercentile(Aggregators.p90, 150, 150); - testPercentile(Aggregators.p95, 150, 150); - testPercentile(Aggregators.p99, 150, 150); - testPercentile(Aggregators.p999, 150, 150); - testPercentile(Aggregators.ep50r3, 150, 150); - testPercentile(Aggregators.ep75r3, 150, 150); - testPercentile(Aggregators.ep90r3, 150, 150); - testPercentile(Aggregators.ep95r3, 150, 150); - testPercentile(Aggregators.ep99r3, 150, 150); - testPercentile(Aggregators.ep999r3, 150, 150); - testPercentile(Aggregators.ep50r7, 150, 150); - testPercentile(Aggregators.ep75r7, 150, 150); - testPercentile(Aggregators.ep90r7, 150, 150); - testPercentile(Aggregators.ep95r7, 150, 150); - testPercentile(Aggregators.ep99r7, 150, 150); - testPercentile(Aggregators.ep999r7, 150, 150); - } - - private void testPercentile(Aggregator agg, long value, double delta) { - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, agg, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - int counter = 0; - int size = dps[0].size(); - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - assertEquals("counter " + counter, value, dp.longValue(), delta); - counter++; - } - assertEquals(600, size); - } - - public void runCount() throws Exception { - storeLongTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(2, dp.longValue()); - } - assertEquals(300, dps[0].size()); - } - - @Test - public void runCountFloat() throws Exception { - storeFloatTimeSeriesSeconds(false, false); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 30000; - assertEquals(2, dp.doubleValue(), 0.001); - } - assertEquals(300, dps[0].size()); - } - - // TODO - The count agg is inaccurate until we implement NaNs. - @Test - public void runCountOffset() throws Exception { - storeLongTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - if (counter == 0 || counter == 599) { - assertEquals(1, dp.longValue()); - } else { - assertEquals(2, dp.longValue()); - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - // TODO - The count agg is inaccurate until we implement NaNs. - @Test - public void runCountFloatOffset() throws Exception { - storeFloatTimeSeriesSeconds(false, true); - - HashMap tags = new HashMap(0); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.COUNT, false); - final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertEquals("host", dps[0].getAggregatedTags().get(0)); - assertNull(dps[0].getAnnotations()); - assertTrue(dps[0].getTags().isEmpty()); - - long ts = 1356998430000L; - int counter = 0; - for (DataPoint dp : dps[0]) { - assertEquals(ts, dp.timestamp()); - ts += 15000; - if (counter == 0 || counter == 599) { - assertEquals(1, dp.doubleValue(), 0.0001); - } else { - assertEquals(2, dp.doubleValue(), 0.0001); - } - counter++; - } - assertEquals(600, dps[0].size()); - } - - // ----------------- // - // Helper functions. // - // ----------------- // - - @SuppressWarnings("unchecked") - private void setQueryStorage() throws Exception { - storage = new MockBase(tsdb, client, true, true, true, true); - storage.setFamily("t".getBytes(MockBase.ASCII())); - - PowerMockito.mockStatic(IncomingDataPoints.class); - PowerMockito.doAnswer( - new Answer() { - @Override - public byte[] answer(final InvocationOnMock args) - throws Exception { - final String metric = (String)args.getArguments()[1]; - final Map tags = - (Map)args.getArguments()[2]; - - if (metric.equals("sys.cpu.user")) { - if (tags.get("host").equals("web01")) { - return new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - } else { - return new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2}; - } - } else { - if (tags.get("host").equals("web01")) { - return new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - } else { - return new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2}; - } - } - } - } - ).when(IncomingDataPoints.class, "rowKeyTemplate", any(), anyString(), - any()); - } - - private void storeLongTimeSeriesSeconds(final boolean two_metrics, - final boolean offset) throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - for (int i = 1; i <= 300; i++) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = offset ? 1356998415 : 1356998400; - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - } - - private void storeLongTimeSeriesMs() throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400000L; - for (int i = 1; i <= 300; i++) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998400000L; - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - private void storeFloatTimeSeriesSeconds(final boolean two_metrics, - final boolean offset) throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - for (float i = 1.25F; i <= 76; i += 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = offset ? 1356998415 : 1356998400; - for (float i = 75F; i > 0; i -= 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - } - - private void storeFloatTimeSeriesMs() throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400000L; - for (float i = 1.25F; i <= 76; i += 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998400000L; - for (float i = 75F; i > 0; i -= 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - private void storeMixedTimeSeriesSeconds() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - for (float i = 1.25F; i <= 76; i += 0.25F) { - if (i % 2 == 0) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, (long)i, tags) - .joinUninterruptibly(); - } else { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags) - .joinUninterruptibly(); - } - } - } - - // dumps ints, floats, seconds and ms - private void storeMixedTimeSeriesMsAndS() throws Exception { - setQueryStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400000L; - for (float i = 1.25F; i <= 76; i += 0.25F) { - long ts = timestamp += 500; - if (ts % 1000 == 0) { - ts /= 1000; - } - if (i % 2 == 0) { - tsdb.addPoint("sys.cpu.user", ts, (long)i, tags).joinUninterruptibly(); - } else { - tsdb.addPoint("sys.cpu.user", ts, i, tags).joinUninterruptibly(); - } - } - } - - private void storeLongCompactions() throws Exception { - setQueryStorage(); - long base_timestamp = 1356998400; - long value = 1; - byte[] qualifier = new byte[119 * 2]; - long timestamp = 1356998430; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column = - Bytes.fromShort((short)(offset << Const.FLAG_BITS | 0x7)); - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - } - - byte[] column_qualifier = new byte[119 * 8]; - for (int index = 0; index < column_qualifier.length; index += 8) { - System.arraycopy(Bytes.fromLong(value), 0, column_qualifier, index, 8); - value++; - } - storage.addColumn(MockBase.stringToBytes("00000150E22700000001000001"), - qualifier, column_qualifier); - - base_timestamp = 1357002000; - qualifier = new byte[120 * 2]; - timestamp = 1357002000; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column = - Bytes.fromShort((short)(offset << Const.FLAG_BITS | 0x7)); - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - } - - column_qualifier = new byte[120 * 8]; - for (int index = 0; index < column_qualifier.length; index += 8) { - System.arraycopy(Bytes.fromLong(value), 0, column_qualifier, index, 8); - value++; - } - storage.addColumn(MockBase.stringToBytes("00000150E23510000001000001"), - qualifier, column_qualifier); - - base_timestamp = 1357005600; - qualifier = new byte[61 * 2]; - timestamp = 1357005600; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column = - Bytes.fromShort((short)(offset << Const.FLAG_BITS | 0x7)); - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - } - - column_qualifier = new byte[61 * 8]; - for (int index = 0; index < column_qualifier.length; index += 8) { - System.arraycopy(Bytes.fromLong(value), 0, column_qualifier, index, 8); - value++; - } - storage.addColumn(MockBase.stringToBytes("00000150E24320000001000001"), - qualifier, column_qualifier); - } - - private void storeFloatCompactions() throws Exception { - setQueryStorage(); - long base_timestamp = 1356998400; - float value = 1.25F; - byte[] qualifier = new byte[119 * 2]; - long timestamp = 1356998430; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column = - Bytes.fromShort((short)(offset << Const.FLAG_BITS | Const.FLAG_FLOAT | 0x3)); - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - } - - byte[] column_qualifier = new byte[119 * 4]; - for (int index = 0; index < column_qualifier.length; index += 4) { - System.arraycopy(Bytes.fromInt(Float.floatToRawIntBits(value)), 0, - column_qualifier, index, 4); - value += 0.25F; - } - storage.addColumn(MockBase.stringToBytes("00000150E22700000001000001"), - qualifier, column_qualifier); - - base_timestamp = 1357002000; - qualifier = new byte[120 * 2]; - timestamp = 1357002000; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column = - Bytes.fromShort((short)(offset << Const.FLAG_BITS | Const.FLAG_FLOAT | 0x3)); - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - } - - column_qualifier = new byte[120 * 4]; - for (int index = 0; index < column_qualifier.length; index += 4) { - System.arraycopy(Bytes.fromInt(Float.floatToRawIntBits(value)), 0, - column_qualifier, index, 4); - value += 0.25F; - } - storage.addColumn(MockBase.stringToBytes("00000150E23510000001000001"), - qualifier, column_qualifier); - - base_timestamp = 1357005600; - qualifier = new byte[61 * 2]; - timestamp = 1357005600; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column = - Bytes.fromShort((short)(offset << Const.FLAG_BITS | Const.FLAG_FLOAT | 0x3)); - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - } - - column_qualifier = new byte[61 * 4]; - for (int index = 0; index < column_qualifier.length; index += 4) { - System.arraycopy(Bytes.fromInt(Float.floatToRawIntBits(value)), 0, - column_qualifier, index, 4); - value += 0.25F; - } - storage.addColumn(MockBase.stringToBytes("00000150E24320000001000001"), - qualifier, column_qualifier); - } - - private void storeMixedCompactions() throws Exception { - setQueryStorage(); - long base_timestamp = 1356998400; - float q_counter = 1.25F; - byte[] qualifier = new byte[119 * 2]; - long timestamp = 1356998430; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column; - if (q_counter % 1 == 0) { - column = Bytes.fromShort((short)(offset << Const.FLAG_BITS | 0x7)); - } else { - column = Bytes.fromShort( - (short)(offset << Const.FLAG_BITS | Const.FLAG_FLOAT | 0x3)); - } - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - q_counter += 0.25F; - } - - float value = 1.25F; - int num = 119; - byte[] column_qualifier = new byte[((num / 4) * 8) + ((num - (num / 4)) * 4)]; - int idx = 0; - while (idx < column_qualifier.length) { - if (value % 1 == 0) { - System.arraycopy(Bytes.fromLong((long)value), 0, column_qualifier, idx, 8); - idx += 8; - } else { - System.arraycopy(Bytes.fromInt(Float.floatToRawIntBits(value)), 0, - column_qualifier, idx, 4); - idx += 4; - } - value += 0.25F; - } - storage.addColumn(MockBase.stringToBytes("00000150E22700000001000001"), - qualifier, column_qualifier); - - base_timestamp = 1357002000; - qualifier = new byte[120 * 2]; - timestamp = 1357002000; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column; - if (q_counter % 1 == 0) { - column = Bytes.fromShort((short)(offset << Const.FLAG_BITS | 0x7)); - } else { - column = Bytes.fromShort( - (short)(offset << Const.FLAG_BITS | Const.FLAG_FLOAT | 0x3)); - } - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - q_counter += 0.25F; - } - - num = 120; - column_qualifier = new byte[((num / 4) * 8) + ((num - (num / 4)) * 4)]; - idx = 0; - while (idx < column_qualifier.length) { - if (value % 1 == 0) { - System.arraycopy(Bytes.fromLong((long)value), 0, column_qualifier, idx, 8); - idx += 8; - } else { - System.arraycopy(Bytes.fromInt(Float.floatToRawIntBits(value)), 0, - column_qualifier, idx, 4); - idx += 4; - } - value += 0.25F; - } - storage.addColumn(MockBase.stringToBytes("00000150E23510000001000001"), - qualifier, column_qualifier); - - base_timestamp = 1357005600; - qualifier = new byte[61 * 2]; - timestamp = 1357005600; - for (int index = 0; index < qualifier.length; index += 2) { - final int offset = (int) (timestamp - base_timestamp); - final byte[] column; - if (q_counter % 1 == 0) { - column = Bytes.fromShort((short)(offset << Const.FLAG_BITS | 0x7)); - } else { - column = Bytes.fromShort( - (short)(offset << Const.FLAG_BITS | Const.FLAG_FLOAT | 0x3)); - } - System.arraycopy(column, 0, qualifier, index, 2); - timestamp += 30; - q_counter += 0.25F; - } - - num = 61; - column_qualifier = - new byte[(((num / 4) + 1) * 8) + ((num - ((num / 4) + 1)) * 4)]; - idx = 0; - while (idx < column_qualifier.length) { - if (value % 1 == 0) { - System.arraycopy(Bytes.fromLong((long)value), 0, column_qualifier, idx, 8); - idx += 8; - } else { - System.arraycopy(Bytes.fromInt(Float.floatToRawIntBits(value)), 0, - column_qualifier, idx, 4); - idx += 4; - } - value += 0.25F; - } - storage.addColumn(MockBase.stringToBytes("00000150E24320000001000001"), - qualifier, column_qualifier); - } } diff --git a/test/core/TestTsdbQueryAggregators.java b/test/core/TestTsdbQueryAggregators.java new file mode 100644 index 0000000000..7532411cd4 --- /dev/null +++ b/test/core/TestTsdbQueryAggregators.java @@ -0,0 +1,1054 @@ +// 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 static org.junit.Assert.assertEquals; + +import org.hbase.async.Scanner; +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; + +/** + * Integration testing for the various aggregators. We write data points to + * MockBase and then pull them out, following the full path for a TSDB query. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Scanner.class }) +public class TestTsdbQueryAggregators extends BaseTsdbTest { + protected TsdbQuery query = null; + + @Before + public void beforeLocal() throws Exception { + query = new TsdbQuery(tsdb); + } + + @Test + public void runZimSum() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.ZIMSUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(301, dp.longValue()); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runZimSumFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.ZIMSUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(76.25, dp.doubleValue(), 0.001); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runZimSumOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.ZIMSUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v1 = 1; + long v2 = 300; + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter % 2 == 0) { + assertEquals(v1, dp.longValue()); + v1++; + } else { + assertEquals(v2, dp.longValue()); + v2--; + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runZimSumFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.ZIMSUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v1 = 1.25; + double v2 = 75.0; + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter % 2 == 0) { + assertEquals(v1, dp.doubleValue(), 0.001); + v1 += 0.25; + } else { + assertEquals(v2, dp.doubleValue(), 0.001); + v2 -= 0.25; + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMin() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998430000L; + boolean decrement = false; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.longValue()); + + if (decrement) { + v--; + } else { + v++; + } + + if (v == 151){ + v = 150; + decrement = true; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMinFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 1.25; + long ts = 1356998430000L; + boolean decrement = false; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.doubleValue(), 0.0001); + + if (decrement) { + v -= .25; + } else { + v += .25; + } + + if (v > 38){ + v = 38.0; + decrement = true; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMinOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998430000L; + int counter = 0; + boolean decrement = false; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.longValue()); + if (counter % 2 != 0) { + if (decrement) { + v--; + } else { + v++; + } + } else if (v == 151){ + v = 150; + decrement = true; + counter--; // hack since the hump is 150 150 151 150 150 + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMinFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 1.25; + long ts = 1356998430000L; + boolean decrement = false; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.doubleValue(), 0.001); + if (decrement) { + v -= 0.125; + } else { + v += 0.125; + } + + if (v > 38.125){ + v = 38.125; + decrement = true; + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMax() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 300; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.longValue()); + + if (decrement) { + v--; + } else { + v++; + } + + if (v == 150){ + v = 151; + decrement = false; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMaxFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 75.0; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.doubleValue(), 0.001); + + if (decrement) { + v -= .25; + } else { + v += .25; + } + + if (v < 38.25){ + v = 38.25; + decrement = false; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMaxOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998430000L; + int counter = 0; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.longValue()); + if (v == 1) { + v = 300; + } else if (dp.timestamp() == 1357007400000L) { + v = 1; + } else if (counter % 2 == 0) { + if (decrement) { + v--; + } else { + v++; + } + } + + if (v == 150){ + v = 151; + decrement = false; + counter--; // hack since the hump is 151 151 151 + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMaxFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 1.25; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.doubleValue(), .0001); + if (v == 1.25) { + v = 75.0; + } else if (dp.timestamp() == 1357007400000L) { + v = 0.25; + } else { + if (decrement) { + v -= .125; + } else { + v += .125; + } + + if (v < 38.25){ + v = 38.25; + decrement = false; + } + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runAvg() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(150, dp.longValue()); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runAvgFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(38.125, dp.doubleValue(), 0.001); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runAvgOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.longValue()); + if (v == 1) { + v = 150; + } else if (dp.timestamp() == 1357007400000L) { + v = 1; + } else if (v == 150) { + v = 151; + } else { + v = 150; + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runAvgFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 1.25; + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.doubleValue(), 0.0001); + if (v == 1.25) { + v = 38.1875; + } else if (dp.timestamp() == 1357007400000L) { + v = .25; + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runDev() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.DEV, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 149; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.longValue()); + + if (decrement) { + v--; + } else { + v++; + } + + if (v < 0){ + v = 0; + decrement = false; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runDevFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.DEV, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 36.875; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.doubleValue(), 0.001); + + if (decrement) { + v -= 0.25; + } else { + v += 0.25; + } + + if (v < 0.125){ + v = 0.125; + decrement = false; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runDevOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.DEV, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 0; + long ts = 1356998430000L; + int counter = 0; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.longValue()); + if (dp.timestamp() == 1356998430000L) { + v = 149; + } else if (dp.timestamp() == 1357007400000L) { + v = 0; + } else if (counter % 2 == 0) { + if (decrement) { + v--; + } else { + v++; + } + if (v < 0) { + v = 0; + decrement = false; + counter++; + } + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runDevFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.DEV, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 0; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.doubleValue(), 0.0001); + if (dp.timestamp() == 1356998430000L) { + v = 36.8125; + } else if (dp.timestamp() == 1357007400000L) { + v = 0; + } else { + if (decrement) { + v -= 0.125; + } else { + v += 0.125; + } + if (v < 0.0625) { + v = 0.0625; + decrement = false; + } + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMimMin() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998430000L; + boolean decrement = false; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.longValue()); + + if (decrement) { + v--; + } else { + v++; + } + + if (v == 151){ + v = 150; + decrement = true; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMimMinOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v1 = 1; + long v2 = 300; + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + + if (counter % 2 == 0) { + assertEquals(v1, dp.longValue()); + v1++; + } else { + assertEquals(v2, dp.longValue()); + v2--; + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMimMinFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 1.25; + long ts = 1356998430000L; + boolean decrement = false; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.doubleValue(), 0.0001); + + if (decrement) { + v -= .25; + } else { + v += .25; + } + + if (v > 38){ + v = 38.0; + decrement = true; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMimMinFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMIN, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v1 = 1.25; + double v2 = 75.0; + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter % 2 == 0) { + assertEquals(v1, dp.doubleValue(), 0.001); + v1 += 0.25; + } else { + assertEquals(v2, dp.doubleValue(), 0.001); + v2 -= 0.25; + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMimMax() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 300; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.longValue()); + + if (decrement) { + v--; + } else { + v++; + } + + if (v == 150){ + v = 151; + decrement = false; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMimMaxFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v = 75.0; + long ts = 1356998430000L; + boolean decrement = true; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(v, dp.doubleValue(), 0.001); + + if (decrement) { + v -= .25; + } else { + v += .25; + } + + if (v < 38.25){ + v = 38.25; + decrement = false; + } + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMimMaxOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v1 = 1; + long v2 = 300; + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + + if (counter % 2 == 0) { + assertEquals(v1, dp.longValue()); + v1++; + } else { + assertEquals(v2, dp.longValue()); + v2--; + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runMimMaxFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.MIMMAX, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + double v1 = 1.25; + double v2 = 75.0; + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter % 2 == 0) { + assertEquals(v1, dp.doubleValue(), 0.001); + v1 += 0.25; + } else { + assertEquals(v2, dp.doubleValue(), 0.001); + v2 -= 0.25; + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runPercentiles() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + // These are not accurate at all when data points only contain 2 values + // so we are just testing constructor logic, rather than precision + testPercentile(Aggregators.p50, 150, 150); + testPercentile(Aggregators.p75, 150, 150); + testPercentile(Aggregators.p90, 150, 150); + testPercentile(Aggregators.p95, 150, 150); + testPercentile(Aggregators.p99, 150, 150); + testPercentile(Aggregators.p999, 150, 150); + testPercentile(Aggregators.ep50r3, 150, 150); + testPercentile(Aggregators.ep75r3, 150, 150); + testPercentile(Aggregators.ep90r3, 150, 150); + testPercentile(Aggregators.ep95r3, 150, 150); + testPercentile(Aggregators.ep99r3, 150, 150); + testPercentile(Aggregators.ep999r3, 150, 150); + testPercentile(Aggregators.ep50r7, 150, 150); + testPercentile(Aggregators.ep75r7, 150, 150); + testPercentile(Aggregators.ep90r7, 150, 150); + testPercentile(Aggregators.ep95r7, 150, 150); + testPercentile(Aggregators.ep99r7, 150, 150); + testPercentile(Aggregators.ep999r7, 150, 150); + } + + public void runCount() throws Exception { + storeLongTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(2, dp.longValue()); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runCountFloat() throws Exception { + storeFloatTimeSeriesSeconds(false, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 30000; + assertEquals(2, dp.doubleValue(), 0.001); + } + assertEquals(300, dps[0].size()); + } + + // TODO - The count agg is inaccurate until we implement NaNs. + @Test + public void runCountOffset() throws Exception { + storeLongTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter == 0 || counter == 599) { + assertEquals(1, dp.longValue()); + } else { + assertEquals(2, dp.longValue()); + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + // TODO - The count agg is inaccurate until we implement NaNs. + @Test + public void runCountFloatOffset() throws Exception { + storeFloatTimeSeriesSeconds(false, true); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.COUNT, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + int counter = 0; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + if (counter == 0 || counter == 599) { + assertEquals(1, dp.doubleValue(), 0.0001); + } else { + assertEquals(2, dp.doubleValue(), 0.0001); + } + counter++; + } + assertEquals(600, dps[0].size()); + } + + /** + * Helper to test the various percentiles + * @param agg The aggregator + * @param value The value to expect + * @param delta The variance to expect + */ + private void testPercentile(final Aggregator agg, final long value, + final double delta) { + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, agg, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long ts = 1356998430000L; + int counter = 0; + int size = dps[0].size(); + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals("counter " + counter, value, dp.longValue(), delta); + counter++; + } + assertEquals(600, size); + } +} diff --git a/test/core/TestTsdbQueryAggregatorsSalted.java b/test/core/TestTsdbQueryAggregatorsSalted.java new file mode 100644 index 0000000000..bcab6dd77f --- /dev/null +++ b/test/core/TestTsdbQueryAggregatorsSalted.java @@ -0,0 +1,37 @@ +// 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 org.junit.Before; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +/** + * Integration test that runs all of the tests in {@see TestTsdbQueryAggregators} + * but with salting enabled just to verify nothing goes wrong with the extra + * salt bytes. + */ +@RunWith(PowerMockRunner.class) +public class TestTsdbQueryAggregatorsSalted extends TestTsdbQueryAggregators { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + query = new TsdbQuery(tsdb); + } + +} diff --git a/test/core/TestTsdbQueryDownsample.java b/test/core/TestTsdbQueryDownsample.java index dcf549eb80..82af981886 100644 --- a/test/core/TestTsdbQueryDownsample.java +++ b/test/core/TestTsdbQueryDownsample.java @@ -14,41 +14,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; - -import com.stumbleupon.async.Deferred; - -import net.opentsdb.meta.Annotation; -import net.opentsdb.storage.MockBase; -import net.opentsdb.uid.NoSuchUniqueName; -import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.Config; + import net.opentsdb.utils.DateTime; -import org.apache.zookeeper.proto.DeleteRequest; -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.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.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -56,85 +28,13 @@ * Tests downsampling with query. */ @RunWith(PowerMockRunner.class) -@PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, - Scanner.class, TsdbQuery.class, DeleteRequest.class, Annotation.class, - RowKey.class, Span.class, SpanGroup.class, IncomingDataPoints.class }) -public class TestTsdbQueryDownsample { - - private Config config; - private TSDB tsdb = null; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); - private TsdbQuery query = null; - private MockBase storage = null; +@PrepareForTest({ Scanner.class }) +public class TestTsdbQueryDownsample extends BaseTsdbTest { + protected TsdbQuery query = null; @Before - public void before() throws Exception { - config = new Config(false); - tsdb = new TSDB(config); + public void beforeLocal() throws Exception { query = new TsdbQuery(tsdb); - - // replace the "real" field objects with mocks - Field cl = tsdb.getClass().getDeclaredField("client"); - cl.setAccessible(true); - cl.set(tsdb, client); - - Field met = tsdb.getClass().getDeclaredField("metrics"); - met.setAccessible(true); - met.set(tsdb, metrics); - - Field tagk = tsdb.getClass().getDeclaredField("tag_names"); - tagk.setAccessible(true); - tagk.set(tsdb, tag_names); - - Field tagv = tsdb.getClass().getDeclaredField("tag_values"); - tagv.setAccessible(true); - tagv.set(tsdb, tag_values); - - // mock UniqueId - when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("sys.cpu.user")); - when(metrics.getId("sys.cpu.system")) - .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); - when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); - when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("sys.cpu.nice")); - when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("host")); - when(tag_names.getOrCreateIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getIdAsync("dc")) - .thenThrow(new NoSuchUniqueName("dc", "metric")); - when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("web01")); - when(tag_values.getOrCreateIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_values.getIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("web02")); - when(tag_values.getOrCreateIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getId("web03")) - .thenThrow(new NoSuchUniqueName("web03", "metric")); - - when(metrics.width()).thenReturn((short)3); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); } @Test @@ -175,19 +75,14 @@ public void downsampleInvalidInterval() throws Exception { @Test public void runLongSingleTSDownsample() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + storeLongTimeSeriesSeconds(true, false); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(60000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1), (2, 3), (4, 5), ... (298, 299), (300) int i = 0; @@ -218,19 +113,13 @@ public void runLongSingleTSDownsample() throws Exception { @Test public void runLongSingleTSDownsampleMs() throws Exception { storeLongTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(1000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - verify(client).newScanner(tsdb.table); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1), (2, 3), (4, 5), ... (298, 299), (300) int i = 0; @@ -260,19 +149,14 @@ public void runLongSingleTSDownsampleMs() throws Exception { @Test public void runLongSingleTSDownsampleAndRate() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + storeLongTimeSeriesSeconds(true, false); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(60000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1), (2, 3), (4, 5), ... (298, 299), (300) // After downsampling: 1, 2.5, 4.5, ... 298.5, 300 @@ -305,18 +189,13 @@ public void runLongSingleTSDownsampleAndRate() throws Exception { @Test public void runLongSingleTSDownsampleAndRateMs() throws Exception { storeLongTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(1000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1), (2, 3), (4, 5), ... (298, 299), (300) // After downsampling: 1, 2.5, 4.5, ... 298.5, 300 @@ -346,18 +225,13 @@ public void runLongSingleTSDownsampleAndRateMs() throws Exception { @Test public void runFloatSingleTSDownsample() throws Exception { storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(60000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1.25), (1.5, 1.75), (2, 2.25), ... // (75.5, 75.75), (76). @@ -388,18 +262,13 @@ public void runFloatSingleTSDownsample() throws Exception { @Test public void runFloatSingleTSDownsampleMs() throws Exception { storeFloatTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(1000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1.25), (1.5, 1.75), (2, 2.25), ... // (75.5, 75.75), (76). @@ -430,18 +299,13 @@ public void runFloatSingleTSDownsampleMs() throws Exception { @Test public void runFloatSingleTSDownsampleAndRate() throws Exception { storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(60000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1.25), (1.5, 1.75), (2, 2.25), ... // (75.5, 75.75), (76). @@ -475,18 +339,13 @@ public void runFloatSingleTSDownsampleAndRate() throws Exception { @Test public void runFloatSingleTSDownsampleAndRateMs() throws Exception { storeFloatTimeSeriesMs(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(1000, Aggregators.AVG); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1.25), (1.5, 1.75), (2, 2.25), ... // (75.5, 75.75), (76). @@ -515,19 +374,14 @@ public void runFloatSingleTSDownsampleAndRateMs() throws Exception { @Test public void runLongSingleTSDownsampleCount() throws Exception { - storeLongTimeSeriesSeconds(true, false);; - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + storeLongTimeSeriesSeconds(true, false); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(60000, Aggregators.COUNT); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1), (2, 3), (4, 5), ... (298, 299), (300) int i = 0; @@ -550,18 +404,13 @@ public void runLongSingleTSDownsampleCount() throws Exception { @Test public void runFloatSingleTSDownsampleAndRateAndCount() throws Exception { storeFloatTimeSeriesSeconds(true, false); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.downsample(60000, Aggregators.COUNT); - query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, true); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); final DataPoints[] dps = query.run(); - assertNotNull(dps); - assertEquals("sys.cpu.user", dps[0].metricName()); - assertTrue(dps[0].getAggregatedTags().isEmpty()); - assertNull(dps[0].getAnnotations()); - assertEquals("web01", dps[0].getTags().get("host")); + assertMeta(dps, 0, false); // Timeseries in intervals: (1.25), (1.5, 1.75), (2, 2.25), ... // (75.5, 75.75), (76). @@ -588,144 +437,5 @@ public void runFloatSingleTSDownsampleAndRateAndCount() throws Exception { } assertEquals(150, dps[0].size()); } - - // ----------------- // - // Helper functions. // - // ----------------- // - - private void storeLongTimeSeriesSeconds(final boolean two_metrics, - final boolean offset) throws Exception { - storeLongTimeSeriesSecondsWithBasetime(1356998400L, two_metrics, offset); - } - - private void storeLongTimeSeriesSecondsWithBasetime(final long baseTimestamp, - final boolean two_metrics, final boolean offset) throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = baseTimestamp; - for (int i = 1; i <= 300; i++) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = baseTimestamp + (offset ? 15 : 0); - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - } - - private void storeLongTimeSeriesMs() throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400000L; - for (int i = 1; i <= 300; i++) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998400000L; - for (int i = 300; i > 0; i--) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - private void storeFloatTimeSeriesSeconds(final boolean two_metrics, - final boolean offset) throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400; - for (float i = 1.25F; i <= 76; i += 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = offset ? 1356998415 : 1356998400; - for (float i = 75F; i > 0; i -= 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 30, i, tags).joinUninterruptibly(); - if (two_metrics) { - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - } - - private void storeFloatTimeSeriesMs() throws Exception { - setQueryStorage(); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - long timestamp = 1356998400000L; - for (float i = 1.25F; i <= 76; i += 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - - // dump a parallel set but invert the values - tags.clear(); - tags.put("host", "web02"); - timestamp = 1356998400000L; - for (float i = 75F; i > 0; i -= 0.25F) { - tsdb.addPoint("sys.cpu.user", timestamp += 500, i, tags).joinUninterruptibly(); - tsdb.addPoint("sys.cpu.nice", timestamp, i, tags).joinUninterruptibly(); - } - } - @SuppressWarnings("unchecked") - private void setQueryStorage() throws Exception { - storage = new MockBase(tsdb, client, true, true, true, true); - storage.setFamily("t".getBytes(MockBase.ASCII())); - - PowerMockito.mockStatic(IncomingDataPoints.class); - PowerMockito.doAnswer( - new Answer() { - public byte[] answer(final InvocationOnMock args) - throws Exception { - final String metric = (String)args.getArguments()[1]; - final Map tags = - (Map)args.getArguments()[2]; - - if (metric.equals("sys.cpu.user")) { - if (tags.get("host").equals("web01")) { - return new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - } else { - return new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2}; - } - } else { - if (tags.get("host").equals("web01")) { - return new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - } else { - return new byte[] { 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2}; - } - } - } - } - ).when(IncomingDataPoints.class, "rowKeyTemplate", (TSDB)any(), anyString(), - (Map)any()); - } } diff --git a/test/core/TestTsdbQueryDownsampleSalted.java b/test/core/TestTsdbQueryDownsampleSalted.java new file mode 100644 index 0000000000..bd8249f2f5 --- /dev/null +++ b/test/core/TestTsdbQueryDownsampleSalted.java @@ -0,0 +1,37 @@ +// 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 org.junit.Before; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +/** + * Integration test that runs all of the tests in {@see TestTsdbQueryDownsample} + * but with salting enabled just to verify nothing goes wrong with the extra + * salt bytes. + */ +@RunWith(PowerMockRunner.class) +public class TestTsdbQueryDownsampleSalted extends TestTsdbQueryDownsample { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + query = new TsdbQuery(tsdb); + } + +} diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java new file mode 100644 index 0000000000..a218029b57 --- /dev/null +++ b/test/core/TestTsdbQueryQueries.java @@ -0,0 +1,1331 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.Scanner; +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; + +/** + * An integration test class that makes sure our query path is up to snuff. + * This class should have tests for different data point types, rates, + * compactions, etc. Other files can cover salting, aggregation and downsampling. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Scanner.class }) +public class TestTsdbQueryQueries extends BaseTsdbTest { + protected TsdbQuery query = null; + + @Before + public void beforeLocal() throws Exception { + query = new TsdbQuery(tsdb); + } + + @Test + public void runLongSingleTS() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].aggregatedSize()); + } + + @Test + public void runLongSingleTSMs() throws Exception { + storeLongTimeSeriesMs(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998400500L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 500; + } + assertEquals(300, dps[0].aggregatedSize()); + } + + @Test + public void runLongSingleTSNoData() throws Exception { + setDataPointStorage(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals(0, dps.length); + } + + @Test + public void runLongTwoAggSum() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + tags.clear(); + query.setStartTime(1356998400L); + query.setEndTime(1357041600L); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(301, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runLongTwoAggSumMs() throws Exception { + storeLongTimeSeriesMs(); + + tags.clear(); + query.setStartTime(1356998400L); + query.setEndTime(1357041600L); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long timestamp = 1356998400500L; + for (DataPoint dp : dps[0]) { + assertEquals(301, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + timestamp += 500; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runLongTwoGroup() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + tags.clear(); + tags.put(TAGK_STRING , "*"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + assertMeta(dps, 1, false); + assertEquals(2, dps.length); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + + value = 300; + timestamp = 1356998430000L; + for (DataPoint dp : dps[1]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value--; + timestamp += 30000; + } + assertEquals(300, dps[1].size()); + } + + @Test + public void runLongSingleTSRate() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998460000L; + for (DataPoint dp : dps[0]) { + assertEquals(0.033F, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(299, dps[0].size()); + } + + @Test + public void runLongSingleTSRateMs() throws Exception { + storeLongTimeSeriesMs(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998401000L; + for (DataPoint dp : dps[0]) { + assertEquals(2.0F, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 500; + } + assertEquals(299, dps[0].size()); + } + + @Test + public void runFloatSingleTS() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + double value = 1.25D; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + value += 0.25D; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runFloatSingleTSMs() throws Exception { + storeFloatTimeSeriesMs(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + double value = 1.25D; + long timestamp = 1356998400500L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + value += 0.25D; + timestamp += 500; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runFloatTwoAggSum() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(76.25, dp.doubleValue(), 0.00001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runFloatTwoAggSumMs() throws Exception { + storeFloatTimeSeriesMs(); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long timestamp = 1356998400500L; + for (DataPoint dp : dps[0]) { + assertEquals(76.25, dp.doubleValue(), 0.00001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 500; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runFloatTwoGroup() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + final HashMap tags = new HashMap(1); + tags.put(TAGK_STRING , "*"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + assertMeta(dps, 1, false); + assertEquals(2, dps.length); + + double value = 1.25D; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(timestamp, dp.timestamp()); + value += 0.25D; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + + value = 75D; + timestamp = 1356998430000L; + for (DataPoint dp : dps[1]) { + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(timestamp, dp.timestamp()); + value -= 0.25d; + timestamp += 30000; + } + assertEquals(300, dps[1].size()); + } + + @Test + public void runFloatSingleTSRate() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998460000L; + for (DataPoint dp : dps[0]) { + assertEquals(0.00833F, dp.doubleValue(), 0.00001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(299, dps[0].size()); + } + + @Test + public void runFloatSingleTSRateMs() throws Exception { + storeFloatTimeSeriesMs(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998401000L; + for (DataPoint dp : dps[0]) { + assertEquals(0.5F, dp.doubleValue(), 0.00001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 500; + } + assertEquals(299, dps[0].size()); + } + + @Test + public void runFloatSingleTSCompacted() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + storage.tsdbCompactAllRows(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998430000L; + double value = 1.25D; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + value += 0.25D; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMixedSingleTS() throws Exception { + storeMixedTimeSeriesSeconds(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998430000L; + double float_value = 1.25D; + int int_value = 76; + // due to aggregation, the only int that will be returned will be the very + // last value of 76 since the agg will convert every point in between to a + // double + for (DataPoint dp : dps[0]) { + if (dp.isInteger()) { + assertEquals(int_value, dp.longValue()); + int_value++; + float_value = int_value; + } else { + assertEquals(float_value, dp.doubleValue(), 0.001); + float_value += 0.25D; + } + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMixedSingleTSMsAndS() throws Exception { + storeMixedTimeSeriesMsAndS(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998400500L; + double float_value = 1.25D; + int int_value = 76; + // due to aggregation, the only int that will be returned will be the very + // last value of 76 since the agg will convert every point in between to a + // double + for (DataPoint dp : dps[0]) { + if (dp.isInteger()) { + assertEquals(int_value, dp.longValue()); + int_value++; + float_value = int_value; + } else { + assertEquals(float_value, dp.doubleValue(), 0.001); + float_value += 0.25D; + } + assertEquals(timestamp, dp.timestamp()); + timestamp += 500; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runMixedSingleTSPostCompaction() throws Exception { + storeMixedTimeSeriesSeconds(); + + final Field compact = Config.class.getDeclaredField("enable_compactions"); + compact.setAccessible(true); + compact.set(config, true); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); + assertNotNull(query.run()); + + // this should only compact the rows for the time series that we fetched and + // leave the others alone + + final byte[] key = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key); + System.arraycopy(Bytes.fromInt(1356998400), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key)); + System.arraycopy(Bytes.fromInt(1357002000), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key)); + System.arraycopy(Bytes.fromInt(1357005600), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key)); + + // run it again to verify the compacted data uncompacts properly + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998430000L; + double float_value = 1.25D; + int int_value = 76; + // due to aggregation, the only int that will be returned will be the very + // last value of 76 since the agg will convert every point in between to a + // double + for (DataPoint dp : dps[0]) { + if (dp.isInteger()) { + assertEquals(int_value, dp.longValue()); + int_value++; + float_value = int_value; + } else { + assertEquals(float_value, dp.doubleValue(), 0.001); + float_value += 0.25D; + } + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runEndTime() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357001900); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(236, dps[0].size()); + } + + @Test + public void runCompactPostQuery() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + final Field compact = Config.class.getDeclaredField("enable_compactions"); + compact.setAccessible(true); + compact.set(config, true); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + // this should only compact the rows for the time series that we fetched and + // leave the others alone + final byte[] key_a = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key_a); + final Map tags_copy = new HashMap(tags); + tags_copy.put(TAGK_STRING, TAGV_B_STRING); + final byte[] key_b = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); + Internal.prefixKeyWithSalt(key_b); + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key_a)); + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(119, storage.numColumns(key_b)); + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key_a)); + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(120, storage.numColumns(key_b)); + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key_a)); + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(61, storage.numColumns(key_b)); + + // run it again to verify the compacted data uncompacts properly + dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test (expected = IllegalStateException.class) + public void runStartNotSet() throws Exception { + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + query.run(); + } + + @Test (expected = IllegalDataException.class) + public void runFloatAndIntSameTSNoFix() throws Exception { + // if a row has an integer and a float for the same timestamp, there will be + // two different qualifiers that will resolve to the same offset. This no + // will throw the IllegalDataException as querytime fixes are disabled by + // default + storeLongTimeSeriesSeconds(true, false); + + tsdb.addPoint(METRIC_STRING, 1356998430, 42.5F, tags).joinUninterruptibly(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); + query.run(); + } + + @Test + public void runFloatAndIntSameTSFix() throws Exception { + config.setFixDuplicates(true); + // if a row has an integer and a float for the same timestamp, there will be + // two different qualifiers that will resolve to the same offset. This no + // longer tosses an exception, and keeps the last value + storeLongTimeSeriesSeconds(true, false); + + tsdb.addPoint(METRIC_STRING, 1356998430, 42.5F, tags).joinUninterruptibly(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + if (value == 1) { + assertEquals(42.5, dp.doubleValue(), 0.001); + } else { + assertEquals(value, dp.longValue()); + } + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].aggregatedSize()); + } + + @Test + public void runWithAnnotation() throws Exception { + storeLongTimeSeriesSeconds(true, false); + storeAnnotation(1356998490); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false, true); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runWithAnnotationPostCompact() throws Exception { + storeLongTimeSeriesSeconds(true, false); + storeAnnotation(1356998490); + + final Field compact = Config.class.getDeclaredField("enable_compactions"); + compact.setAccessible(true); + compact.set(config, true); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + DataPoints[] dps = query.run(); + assertMeta(dps, 0, false, true); + + // this should only compact the rows for the time series that we fetched and + // leave the others alone + final byte[] key_a = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key_a); + final Map tags_copy = new HashMap(tags); + tags_copy.put(TAGK_STRING, TAGV_B_STRING); + final byte[] key_b = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); + Internal.prefixKeyWithSalt(key_b); + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(2, storage.numColumns(key_a)); + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(119, storage.numColumns(key_b)); + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key_a)); + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(120, storage.numColumns(key_b)); + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(1, storage.numColumns(key_a)); + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + assertEquals(61, storage.numColumns(key_b)); + + dps = query.run(); + assertMeta(dps, 0, false, true); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runWithOnlyAnnotation() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + // verifies that we can pickup an annotation stored all by it's lonesome + // in a row without any data + final byte[] key = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key); + System.arraycopy(Bytes.fromInt(1357002000), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + storage.flushRow(key); + + storeAnnotation(1357002090); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false, true); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + if (timestamp == 1357001970000L) { + timestamp = 1357005600000L; + } else { + timestamp += 30000; + } + value++; + // account for the jump + if (value == 120) { + value = 240; + } + } + assertEquals(180, dps[0].size()); + } + + @Test + public void runWithSingleAnnotation() throws Exception { + setDataPointStorage(); + + // verifies that we can pickup an annotation stored all by it's lonesome + // in a row without any data + final byte[] key = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key); + System.arraycopy(Bytes.fromInt(1357002000), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + storage.flushRow(key); + + storeAnnotation(1357002090); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + // TODO - apparently if you only fetch annotations, the metric and tags + // may not be set. Check this + //assertMeta(dps, 0, false, true); + assertEquals(1, dps[0].getAnnotations().size()); + assertEquals(NOTE_DESCRIPTION, dps[0].getAnnotations().get(0) + .getDescription()); + assertEquals(NOTE_NOTES, dps[0].getAnnotations().get(0).getNotes()); + assertEquals(0, dps[0].size()); + } + + @Test + public void runSingleDataPoint() throws Exception { + setDataPointStorage(); + long timestamp = 1356998410; + tsdb.addPoint(METRIC_STRING, timestamp, 42, tags).joinUninterruptibly(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + assertEquals(1, dps[0].size()); + assertEquals(42, dps[0].longValue(0)); + assertEquals(1356998410000L, dps[0].timestamp(0)); + } + + @Test + public void runSingleDataPointWithAnnotation() throws Exception { + setDataPointStorage(); + long timestamp = 1356998410; + tsdb.addPoint(METRIC_STRING, timestamp, 42, tags).joinUninterruptibly(); + + final byte[] key = + IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key); + System.arraycopy(Bytes.fromInt(1357002000), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + storage.flushRow(key); + + storeAnnotation(1357002090); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false, true); + + assertEquals(1, dps[0].size()); + assertEquals(42, dps[0].longValue(0)); + assertEquals(1356998410000L, dps[0].timestamp(0)); + } + + @Test + public void runTSUIDQuery() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + final List tsuids = new ArrayList(1); + tsuids.add("000001000001000001"); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].aggregatedSize()); + } + + @Test + public void runTSUIDsAggSum() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + final List tsuids = new ArrayList(1); + tsuids.add("000001000001000001"); + tsuids.add("000001000001000002"); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(301, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runTSUIDQueryNoData() throws Exception { + setDataPointStorage(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + + final List tsuids = new ArrayList(1); + tsuids.add("000001000001000001"); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals(0, dps.length); + } + + @Test + public void runTSUIDQueryNoDataForTSUID() throws Exception { + // this doesn't throw an exception since the UIDs are only looked for when + // the query completes. + setDataPointStorage(); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + final List tsuids = new ArrayList(1); + tsuids.add("000001000001000005"); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals(0, dps.length); + } + + @Test (expected = NoSuchUniqueId.class) + public void runTSUIDQueryNSU() throws Exception { + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenThrow(new NoSuchUniqueId("metrics", new byte[] { 0, 0, 1 })); + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + final List tsuids = new ArrayList(1); + tsuids.add("000001000001000001"); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertNotNull(dps); + dps[0].metricName(); + } + + @Test + public void runRateCounterDefault() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400; + tsdb.addPoint(METRIC_STRING, timestamp += 30, Long.MAX_VALUE - 55, tags) + .joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, Long.MAX_VALUE - 25, tags) + .joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 5, tags).joinUninterruptibly(); + + final RateOptions ro = new RateOptions(true, Long.MAX_VALUE, 0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + timestamp = 1356998460000L; + for (DataPoint dp : dps[0]) { + assertEquals(1.0, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(2, dps[0].size()); + } + + @Test + public void runRateCounterDefaultNoOp() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400; + tsdb.addPoint(METRIC_STRING, timestamp += 30, 30, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 60, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 90, tags).joinUninterruptibly(); + + final RateOptions ro = new RateOptions(true, Long.MAX_VALUE, 0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + timestamp = 1356998460000L; + for (DataPoint dp : dps[0]) { + assertEquals(1.0, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(2, dps[0].size()); + } + + @Test + public void runRateCounterMaxSet() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400; + tsdb.addPoint(METRIC_STRING, timestamp += 30, 45, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 75, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 5, tags).joinUninterruptibly(); + + final RateOptions ro = new RateOptions(true, 100, 0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + timestamp = 1356998460000L; + for (DataPoint dp : dps[0]) { + assertEquals(1.0, dp.doubleValue(), 0.001); + assertEquals(timestamp, dp.timestamp()); + timestamp += 30000; + } + assertEquals(2, dps[0].size()); + } + + @Test + public void runRateCounterAnomally() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400; + tsdb.addPoint(METRIC_STRING, timestamp += 30, 45, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 75, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 25, tags).joinUninterruptibly(); + + final RateOptions ro = new RateOptions(true, 10000, 35); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + assertEquals(1.0, dps[0].doubleValue(0), 0.001); + assertEquals(1356998460000L, dps[0].timestamp(0)); + assertEquals(0, dps[0].doubleValue(1), 0.001); + assertEquals(1356998490000L, dps[0].timestamp(1)); + assertEquals(2, dps[0].size()); + } + + @Test + public void runMultiCompact() throws Exception { + final byte[] qual1 = { 0x00, 0x17 }; + final byte[] val1 = Bytes.fromLong(1L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(2L); + + // 2nd compaction + final byte[] qual3 = { 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(4L); + + // 3rd compaction + final byte[] qual5 = { 0x00, 0x57 }; + final byte[] val5 = Bytes.fromLong(5L); + final byte[] qual6 = { 0x00, 0x67 }; + final byte[] val6 = Bytes.fromLong(6L); + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key); + System.arraycopy(Bytes.fromInt(1356998400), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + + setDataPointStorage(); + storage.addColumn(key, + MockBase.concatByteArrays(qual1, qual2), + MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); + storage.addColumn(key, + MockBase.concatByteArrays(qual3, qual4), + MockBase.concatByteArrays(val3, val4, new byte[] { 0 })); + storage.addColumn(key, + MockBase.concatByteArrays(qual5, qual6), + MockBase.concatByteArrays(val5, val6, new byte[] { 0 })); + + HashMap tags = new HashMap(1); + tags.put(TAGK_STRING , TAGV_STRING ); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998401000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 1000; + } + assertEquals(6, dps[0].aggregatedSize()); + } + + @Test + public void runMultiCompactAndSingles() throws Exception { + final byte[] qual1 = { 0x00, 0x17 }; + final byte[] val1 = Bytes.fromLong(1L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(2L); + + // 2nd compaction + final byte[] qual3 = { 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(4L); + + // 3rd compaction + final byte[] qual5 = { 0x00, 0x57 }; + final byte[] val5 = Bytes.fromLong(5L); + final byte[] qual6 = { 0x00, 0x67 }; + final byte[] val6 = Bytes.fromLong(6L); + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); + Internal.prefixKeyWithSalt(key); + System.arraycopy(Bytes.fromInt(1356998400), 0, key, + Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + + setDataPointStorage(); + storage.addColumn(key, + MockBase.concatByteArrays(qual1, qual2), + MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); + storage.addColumn(key, qual3, val3); + storage.addColumn(key, qual4, val4); + storage.addColumn(key, + MockBase.concatByteArrays(qual5, qual6), + MockBase.concatByteArrays(val5, val6, new byte[] { 0 })); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + int value = 1; + long timestamp = 1356998401000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 1000; + } + assertEquals(6, dps[0].aggregatedSize()); + } + + @Test + public void runInterpolationSeconds() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400; + for (int i = 1; i <= 300; i++) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags) + .joinUninterruptibly(); + } + tags.clear(); + tags.put(TAGK_STRING , TAGV_B_STRING); + timestamp = 1356998415; + for (int i = 300; i > 0; i--) { + tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags) + .joinUninterruptibly(); + } + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 15000; + assertEquals(v, dp.longValue()); + + if (dp.timestamp() == 1357007400000L) { + v = 1; + } else if (v == 1 || v == 302) { + v = 301; + } else { + v = 302; + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runInterpolationMs() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400000L; + for (int i = 1; i <= 300; i++) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags) + .joinUninterruptibly(); + } + tags.clear(); + tags.put(TAGK_STRING , TAGV_B_STRING ); + timestamp = 1356998400250L; + for (int i = 300; i > 0; i--) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags) + .joinUninterruptibly(); + } + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + long v = 1; + long ts = 1356998400500L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 250; + assertEquals(v, dp.longValue()); + + if (dp.timestamp() == 1356998550000L) { + v = 1; + } else if (v == 1 || v == 302) { + v = 301; + } else { + v = 302; + } + } + assertEquals(600, dps[0].size()); + } + + @Test + public void runInterpolationMsDownsampled() throws Exception { + setDataPointStorage(); + // ts = 1356998400500, v = 1 + // ts = 1356998401000, v = 2 + // ts = 1356998401500, v = 3 + // ts = 1356998402000, v = 4 + // ts = 1356998402500, v = 5 + // ... + // ts = 1356998449000, v = 98 + // ts = 1356998449500, v = 99 + // ts = 1356998450000, v = 100 + // ts = 1356998455000, v = 101 + // ts = 1356998460000, v = 102 + // ... + // ts = 1356998550000, v = 120 + long timestamp = 1356998400000L; + for (int i = 1; i <= 120; i++) { + timestamp += i <= 100 ? 500 : 5000; + tsdb.addPoint(METRIC_STRING, timestamp, i, tags) + .joinUninterruptibly(); + } + + // ts = 1356998400750, v = 300 + // ts = 1356998401250, v = 299 + // ts = 1356998401750, v = 298 + // ts = 1356998402250, v = 297 + // ts = 1356998402750, v = 296 + // ... + // ts = 1356998549250, v = 3 + // ts = 1356998549750, v = 2 + // ts = 1356998550250, v = 1 + tags.clear(); + tags.put(TAGK_STRING , TAGV_B_STRING); + timestamp = 1356998400250L; + for (int i = 300; i > 0; i--) { + tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags) + .joinUninterruptibly(); + } + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + query.downsample(1000, Aggregators.SUM); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, true); + + // TS1 in intervals = (1), (2,3), (4,5) ... (98,99), 100, (), (), (), (), + // (101), ... (120) + // TS2 in intervals = (300), (299,298), (297,296), ... (203, 202) ... + // (3,2), (1) + // TS1 downsample = 1, 5, 9, ... 197, 100, _, _, _, _, 101, ... 120 + // TS1 interpolation = 1, 5, ... 197, 100, 100.2, 100.4, 100.6, 100.8, 101, + // ... 119.6, 119.8, 120 + // TS2 downsample = 300, 597, 593, ... 405, 401, ... 5, 1 + // TS1 + TS2 = 301, 602, 602, ... 501, 497.2, ... 124.8, 121 + int i = 0; + long ts = 1356998400000L; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + ts += 1000; + if (i == 0) { + assertEquals(301, dp.doubleValue(), 0.0000001); + } else if (i < 50) { + // TS1 = i * 2 + i * 2 + 1 + // TS2 = (300 - i * 2 + 1) + (300 - i * 2) + // TS1 + TS2 = 602 + assertEquals(602, dp.doubleValue(), 0.0000001); + } else { + // TS1 = 100 + (i - 50) * 0.2 + // TS2 = (300 - i * 2 + 1) + (300 - i * 2) + // TS1 + TS2 = 701 + (i - 50) * 0.2 - i * 4 + double value = 701 + (i - 50) * 0.2 - i * 4; + assertEquals(value, dp.doubleValue(), 0.0000001); + } + ++i; + } + assertEquals(151, dps[0].size()); + } +} diff --git a/test/core/TestTsdbQuerySalted.java b/test/core/TestTsdbQuerySalted.java new file mode 100644 index 0000000000..695dbf151c --- /dev/null +++ b/test/core/TestTsdbQuerySalted.java @@ -0,0 +1,35 @@ +// 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 org.junit.Before; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +/** + * An integration test class that runs all of the tests in + * {@see TestTsdbQueryAggregators} but with salting enabled. + */ +@RunWith(PowerMockRunner.class) +public class TestTsdbQuerySalted extends TestTsdbQueryQueries { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + query = new TsdbQuery(tsdb); + } +} From 8f73abe04ec5cad242cbc08a69e637d041955b11 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 18:07:51 -0700 Subject: [PATCH 096/826] Fix TsdbQuery to use the RowKey class Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 2 +- test/core/TestTsdbQueryQueries.java | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index fbb8e31a52..354da65c54 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -575,7 +575,7 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { final byte[] end_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; if (Const.SALT_WIDTH() > 0) { - final byte[] salt = Internal.getSaltBytes(salt_bucket); + final byte[] salt = RowKey.getSaltBytes(salt_bucket); System.arraycopy(salt, 0, start_row, 0, Const.SALT_WIDTH()); System.arraycopy(salt, 0, end_row, 0, Const.SALT_WIDTH()); } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index a218029b57..464cb91bff 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -490,7 +490,7 @@ public void runMixedSingleTSPostCompaction() throws Exception { final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key); + RowKey.prefixKeyWithSalt(key); System.arraycopy(Bytes.fromInt(1356998400), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key)); @@ -565,12 +565,12 @@ public void runCompactPostQuery() throws Exception { // leave the others alone final byte[] key_a = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key_a); + RowKey.prefixKeyWithSalt(key_a); final Map tags_copy = new HashMap(tags); tags_copy.put(TAGK_STRING, TAGV_B_STRING); final byte[] key_b = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); - Internal.prefixKeyWithSalt(key_b); + RowKey.prefixKeyWithSalt(key_b); System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -704,12 +704,12 @@ public void runWithAnnotationPostCompact() throws Exception { // leave the others alone final byte[] key_a = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key_a); + RowKey.prefixKeyWithSalt(key_a); final Map tags_copy = new HashMap(tags); tags_copy.put(TAGK_STRING, TAGV_B_STRING); final byte[] key_b = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); - Internal.prefixKeyWithSalt(key_b); + RowKey.prefixKeyWithSalt(key_b); System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -757,7 +757,7 @@ public void runWithOnlyAnnotation() throws Exception { // in a row without any data final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key); + RowKey.prefixKeyWithSalt(key); System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); storage.flushRow(key); @@ -798,7 +798,7 @@ public void runWithSingleAnnotation() throws Exception { // in a row without any data final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key); + RowKey.prefixKeyWithSalt(key); System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); storage.flushRow(key); @@ -846,7 +846,7 @@ public void runSingleDataPointWithAnnotation() throws Exception { final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key); + RowKey.prefixKeyWithSalt(key); System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); storage.flushRow(key); @@ -1078,7 +1078,7 @@ public void runMultiCompact() throws Exception { final byte[] val6 = Bytes.fromLong(6L); final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key); + RowKey.prefixKeyWithSalt(key); System.arraycopy(Bytes.fromInt(1356998400), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -1133,7 +1133,7 @@ public void runMultiCompactAndSingles() throws Exception { final byte[] val6 = Bytes.fromLong(6L); final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); - Internal.prefixKeyWithSalt(key); + RowKey.prefixKeyWithSalt(key); System.arraycopy(Bytes.fromInt(1356998400), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); From f2c4d828dcc97f85f60cb264619574c96ce9f3ad Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 22 Mar 2015 19:25:47 -0700 Subject: [PATCH 097/826] Fix a bug in the Tags.resolveIds() method where the NoSuchUniqueId was swallowed in the Exception catch block. Signed-off-by: Chris Larsen --- src/core/Tags.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/Tags.java b/src/core/Tags.java index 30d4de29ed..1fadced742 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -564,6 +564,8 @@ public static HashMap resolveIds(final TSDB tsdb, throws NoSuchUniqueId { try { return resolveIdsAsync(tsdb, tags).joinUninterruptibly(); + } catch (NoSuchUniqueId e) { + throw e; } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } From c39d8a848126d08e8961e191d380853ed19b9483 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 25 Mar 2015 14:36:40 -0700 Subject: [PATCH 098/826] Fix the TsdbQuery salt query where it was writing the end timestamp incorrectly. Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 354da65c54..ac62e0c2c8 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -590,7 +590,7 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { Bytes.setInt(end_row, (end_time == UNSET ? -1 // Will scan until the end (0xFFF...). : (int) getScanEndTimeSeconds()), - metric_width); + metric_salt_width); // set the metric UID based on the TSUIDs if given, or the metric UID if (tsuids != null && !tsuids.isEmpty()) { From 33d3ba598245b5ce11713d580a11731ab56bb409 Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Thu, 26 Mar 2015 18:00:41 +0000 Subject: [PATCH 099/826] Fix .rpm build with -'s in version (ie; -SNAPSHOT) rpmbuild doesn't like -'s in the package Version (dpkg seems less fussy). To stay strictly-semver, we replace any -'s with _'s. Alternatively, we could use ~'s in the AC_INIT. More discussion here: https://github.com/mojombo/semver/issues/145 --- Makefile.am | 4 ++-- opentsdb.spec.in | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile.am b/Makefile.am index 5b2a7079c6..3a7e951cc0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -653,8 +653,8 @@ pom.xml: pom.xml.in Makefile TIMESTAMP := $(shell date +"%Y%m%d%H%M%S") RPM_REVISION := 1 RPM_TARGET := noarch -RPM := opentsdb-$(PACKAGE_VERSION)-$(RPM_REVISION).$(RPM_TARGET).rpm -RPM_SNAPSHOT := opentsdb-$(PACKAGE_VERSION)-$(RPM_REVISION)-$(TIMESTAMP)-"`whoami`".$(RPM_TARGET).rpm +RPM := opentsdb-$(subst -,_,$(PACKAGE_VERSION))-$(RPM_REVISION).$(RPM_TARGET).rpm +RPM_SNAPSHOT := opentsdb-$(subst -,_,$(PACKAGE_VERSION))-$(RPM_REVISION)-$(TIMESTAMP)-"`whoami`".$(RPM_TARGET).rpm SOURCE_TARBALL := opentsdb-$(PACKAGE_VERSION).tar.gz rpm: $(RPM) diff --git a/opentsdb.spec.in b/opentsdb.spec.in index bbdad28db1..647cf9b681 100644 --- a/opentsdb.spec.in +++ b/opentsdb.spec.in @@ -4,7 +4,7 @@ %define _sourcedir %(echo $PWD) Name: @PACKAGE@ -Version: @VERSION@ +Version: %(echo @VERSION@ | sed 's/-/_/g') Release: 1 Summary: A scalable, distributed Time Series Database Packager: @PACKAGE_BUGREPORT@ @@ -45,7 +45,7 @@ seconds). OpenTSDB will never delete or downsample data and can easily store billions of data points. %prep -%setup -q +%setup -q -n @PACKAGE@-@VERSION@ %build From db5a4e93b5232623ecb866c801b01ffe9f840ce0 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Mon, 30 Mar 2015 18:13:53 +0000 Subject: [PATCH 100/826] Naive DCLP on exception field to ensure we only get first reference. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 0b2299be7b..d60253d9d6 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -88,7 +88,7 @@ public class SaltScanner { /** A holder for storing the first exception thrown by a scanner if something * goes pear shaped. Make sure to synchronize on this object when checking * for null or assigning from a scanner's callback. */ - private Exception exception; + private volatile Exception exception; /** * Default ctor that performs some validation. Call {@link scan} after @@ -372,13 +372,15 @@ private void validateAndTriggerCallback(final List kvs, */ private void handleException(final Exception e) { // make sure only one scanner can set the exception - synchronized (this) { - if (exception == null) { - exception = e; - } else { - // TODO - it would be nice to close and cancel the other scanners but - // for now we have to wait for them to finish and/or throw exceptions. - LOG.error("Another scanner threw an exception", e); + if (exception == null) { + synchronized (this) { + if (exception == null) { + exception = e; + } else { + // TODO - it would be nice to close and cancel the other scanners but + // for now we have to wait for them to finish and/or throw exceptions. + LOG.error("Another scanner threw an exception", e); + } } } From d0e64e4901a353b94f25058e882e29ccb25e85f7 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Sun, 22 Mar 2015 02:35:10 -0700 Subject: [PATCH 101/826] Log illegal put arguments at DEBUG level. --- src/core/IncomingDataPoints.java | 2 +- src/tsd/PutDataPointRpc.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 3499c408ec..56687bf1aa 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -96,7 +96,7 @@ final class IncomingDataPoints implements WritableDataPoints { static void checkMetricAndTags(final String metric, final Map tags) { if (tags.size() <= 0) { - throw new IllegalArgumentException("Need at least one tags (metric=" + throw new IllegalArgumentException("Need at least one tag (metric=" + metric + ", tags=" + tags + ')'); } else if (tags.size() > Const.MAX_NUM_TAGS) { throw new IllegalArgumentException("Too many tags: " + tags.size() diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 20f1c25d1e..4e7f786ce0 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -70,8 +70,11 @@ public String toString() { errmsg = "put: unknown metric: " + x.getMessage() + '\n'; unknown_metrics.incrementAndGet(); } - if (errmsg != null && chan.isConnected()) { - chan.write(errmsg); + if (errmsg != null) { + LOG.debug(errmsg); + if (chan.isConnected()) { + chan.write(errmsg); + } } return Deferred.fromResult(null); } From e9faa4d4e43c879af8560f51b3e332b28a8e4cbc Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Sun, 29 Mar 2015 02:15:22 -0700 Subject: [PATCH 102/826] Protect the UI from getting stuck when no tags are associated with a query. --- src/tsd/client/QueryUi.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 36f29b704f..f173e01106 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -986,9 +986,11 @@ public void got(final JSONValue json) { } final MetricForm metric = (MetricForm) widget; final JSONArray tags = etags.get(i).isArray(); - final int ntags = tags.size(); - for (int j = 0; j < ntags; j++) { - metric.autoSuggestTag(tags.get(j).isString().stringValue()); + // Skip if no tags were associated with the query. + if (null != tags) { + for (int j = 0; j < tags.size(); j++) { + metric.autoSuggestTag(tags.get(j).isString().stringValue()); + } } } } From 947b7e9be6b079fb718237281c26718aa8347b39 Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Wed, 25 Mar 2015 12:12:16 +0000 Subject: [PATCH 103/826] Fix stats reporting when using >3 byte UID's The '1' defaults to an int, left-shifting it for >3 byte UID's ends up with 0. Signed-off-by: Chris Larsen --- src/uid/UniqueId.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 8458c86d79..0a1096d73c 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -192,7 +192,7 @@ public void setTSDB(final TSDB tsdb) { /** The largest possible ID given the number of bytes the IDs are represented on. */ public long maxPossibleId() { - return (1 << id_width * Byte.SIZE) - 1; + return ((long) 1 << id_width * Byte.SIZE) - 1; } /** From fa8edf7226d59521d8ac2354a05b3c5927fd92c1 Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Wed, 25 Mar 2015 12:09:49 +0000 Subject: [PATCH 104/826] Option is defined as --skip-errors (hyphen) Signed-off-by: Chris Larsen --- src/tools/TextImporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/TextImporter.java b/src/tools/TextImporter.java index 4c59de3a5b..a8c72ef586 100644 --- a/src/tools/TextImporter.java +++ b/src/tools/TextImporter.java @@ -66,7 +66,7 @@ public static void main(String[] args) throws Exception { Config config = CliOptions.getConfig(argp); final TSDB tsdb = new TSDB(config); - final boolean skip_errors = argp.has("--skip_errors"); + final boolean skip_errors = argp.has("--skip-errors"); tsdb.checkNecessaryTablesExist().joinUninterruptibly(); argp = null; try { From 32a22b86a94a1088288b7c072d19be704d826f7d Mon Sep 17 00:00:00 2001 From: sidhhu Date: Tue, 31 Mar 2015 22:11:37 -0700 Subject: [PATCH 105/826] Fix a bug with the meta sync utility where it wasn't passing new TSMeta objects through the tree or search plugin methods. Signed-off-by: Chris Larsen --- src/tools/MetaSync.java | 47 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index f7ed887111..75bf1b2957 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -215,6 +215,41 @@ public TSMetaCB(final byte[] tsuid, final long timestamp) { @Override public Deferred call(final TSMeta meta) throws Exception { + /** Called to process the new meta through the search plugin and tree code */ + final class IndexCB implements Callback, TSMeta> { + @Override + public Deferred call(final TSMeta new_meta) throws Exception { + tsdb.indexTSMeta(new_meta); + // pass through the trees + return tsdb.processTSMetaThroughTrees(new_meta); + } + } + + /** Called to load the newly created meta object for passage onto the + * search plugin and tree builder if configured + */ + final class GetCB implements Callback, Boolean> { + @Override + public final Deferred call(final Boolean exists) + throws Exception { + if (exists) { + return TSMeta.getTSMeta(tsdb, tsuid_string) + .addCallbackDeferring(new IndexCB()); + } else { + return Deferred.fromResult(false); + } + } + } + + /** Errback on the store new call to catch issues */ + class ErrBack implements Callback { + public Object call(final Exception e) throws Exception { + LOG.warn("Failed creating meta for: " + tsuid + + " with exception: ", e); + return null; + } + } + // if we couldn't find a TSMeta in storage, then we need to generate a // new one if (meta == null) { @@ -253,9 +288,11 @@ public Deferred call(final Boolean exists) throws Exception { } else { TSMeta new_meta = new TSMeta(tsuid, timestamp); tsdb.indexTSMeta(new_meta); - LOG.info("Counter exists but meta was null, creating meta data for timeseries [" + - tsuid_string + "]"); - return new_meta.storeNew(tsdb); + LOG.info("Counter exists but meta was null, creating meta data " + + "for timeseries [" + tsuid_string + "]"); + return new_meta.storeNew(tsdb) + .addCallbackDeferring(new GetCB()) + .addErrback(new ErrBack()); } } } @@ -275,7 +312,9 @@ public Deferred call(final Boolean exists) throws Exception { tsuid_string + "]"); TSMeta new_meta = new TSMeta(tsuid, timestamp); tsdb.indexTSMeta(new_meta); - return new_meta.storeNew(tsdb); + return new_meta.storeNew(tsdb) + .addCallbackDeferring(new GetCB()) + .addErrback(new ErrBack()); } else { // we only want to update the time if it was outside of an // hour otherwise it's probably an accurate timestamp From 93b237c2ee088a5e9b8ac33c34907181193f8c1e Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Thu, 2 Apr 2015 11:29:51 +0100 Subject: [PATCH 106/826] Fix startup on boot via init script (RPM) NAME=`basename $0` only works if you're starting opentsdb by hand. On boot `basename $0` actually ends up being "S80opentsdb" (since it's run from the /etc/rcX.d symlinks rather than /etc/init.d). The init script can't find /etc/opentsdb/S80opentsdb.conf, writes its logs to S80opentsdb-xxx etc. Instead we can default to a static 'opentsdb' for NAME, and let it be overridden in a /etc/sysconfig/`basename $0`. Signed-off-by: Chris Larsen --- build-aux/rpm/init.d/opentsdb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/build-aux/rpm/init.d/opentsdb b/build-aux/rpm/init.d/opentsdb index 2d63eea23d..d721d483ba 100644 --- a/build-aux/rpm/init.d/opentsdb +++ b/build-aux/rpm/init.d/opentsdb @@ -28,18 +28,14 @@ # Source init functions . /etc/init.d/functions -# Set this so that you can run as many opentsdb instances you want as long as -# the name of this script is changed (or a symlink is used) -NAME=`basename $0` - # Maximum number of open files MAX_OPEN_FILES=65535 # Default program options +NAME=opentsdb PROG=/usr/bin/tsdb HOSTNAME=$(hostname --fqdn) USER=root -CONFIG=/etc/opentsdb/${NAME}.conf # Default directories LOG_DIR=/var/log/opentsdb @@ -48,12 +44,13 @@ PID_DIR=/var/run/opentsdb # Global and Local sysconfig files [ -e /etc/sysconfig/opentsdb ] && . /etc/sysconfig/opentsdb -[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME +[ "`basename $0`" != "$NAME" ] && [ -e /etc/sysconfig/`basename $0` ] && . /etc/sysconfig/`basename $0` # Set file names LOG_FILE=$LOG_DIR/$NAME-$HOSTNAME- LOCK_FILE=$LOCK_DIR/$NAME PID_FILE=$PID_DIR/$NAME.pid +CONFIG=/etc/opentsdb/${NAME}.conf # Create dirs if they don't exist [ -e $LOG_DIR ] || (mkdir -p $LOG_DIR && chown $USER: $LOG_DIR) From 77727dd739504b3d92aa9a772a07281203eea086 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Tue, 10 Mar 2015 21:06:30 -0700 Subject: [PATCH 107/826] Restoring TestTree.deleteTree() with more asserts and correct assumptions. Signed-off-by: Chris Larsen --- test/tree/TestTree.java | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/test/tree/TestTree.java b/test/tree/TestTree.java index a5d9cb9244..e3d10e6334 100644 --- a/test/tree/TestTree.java +++ b/test/tree/TestTree.java @@ -75,10 +75,10 @@ public final class TestTree { @Before public void before() throws Exception { final Config config = new Config(false); + config.overrideConfig("tsd.storage.enable_compaction", "false"); PowerMockito.whenNew(HBaseClient.class) .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); - + tsdb = new TSDB(client, config); } @Test @@ -553,25 +553,20 @@ public void fetchNotMatchedID655536() throws Exception { Tree.fetchNotMatched(storage.getTSDB(), 655536, null); } - /* - TODO(oozie): This test surfaces likely bug in Tree.deleteTree(). - It was operating under a false assumption about how scanning works and - it started to fail, off-by-one style, when MockBase's logic was altered - to mimic that asynchbase Scanner. + @Test + public void deleteTree() throws Exception { + setupStorage(true, true); + + assertEquals(4, storage.numRows()); + assertNotNull(Tree.deleteTree(storage.getTSDB(), 1, true) + .joinUninterruptibly()); - An update to Tree.deleteTree() implementation makes the test pass, - but I am not going to bandwagon this bugfix on top of #457 which has enough - going on as it is. + byte[] remainingKey = new byte[] {0, 2}; + assertEquals(1, storage.numRows()); + assertNotNull(storage.getColumn( + remainingKey, "tree".getBytes(MockBase.ASCII()))); + } - @Test - public void deleteTree() throws Exception { - setupStorage(true, true); - assertNotNull(Tree.deleteTree(storage.getTSDB(), 1, true) - .joinUninterruptibly()); - assertEquals(0, storage.numRows()); - } - */ - @Test public void idToBytes() throws Exception { assertArrayEquals(new byte[]{ 0, 1 }, Tree.idToBytes(1)); From 2357a28de2cedfb466b1a045cbd1d271b0b96a84 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Wed, 4 Mar 2015 23:44:29 -0800 Subject: [PATCH 108/826] Manifest query for non-existing metric as a bad request. Signed-off-by: Chris Larsen --- src/tsd/QueryRpc.java | 12 ++++-- src/tsd/RpcHandler.java | 4 +- test/tsd/TestQueryRpc.java | 77 ++++++++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 14c9edcfd0..3c19897025 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -41,6 +41,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.TSUIDQuery; +import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.JSON; @@ -79,7 +80,6 @@ public void execute(final TSDB tsdb, final HttpQuery query) if (endpoint.toLowerCase().equals("last")) { handleLastDataPointQuery(tsdb, query); - return; } else { handleQuery(tsdb, query); } @@ -116,13 +116,17 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { e.getMessage(), data_query.toString(), e); } - Query[] tsdbqueries = data_query.buildQueries(tsdb); + Query[] tsdbqueries; + try { + tsdbqueries = data_query.buildQueries(tsdb); + } catch(NoSuchUniqueName ex) { + throw new BadRequestException(ex); + } final int nqueries = tsdbqueries.length; final ArrayList results = new ArrayList(nqueries); final ArrayList> deferreds = new ArrayList>(nqueries); - for (int i = 0; i < nqueries; i++) { deferreds.add(tsdbqueries[i].runAsync()); } @@ -153,7 +157,7 @@ public Object call(final ArrayList query_results) throw new RuntimeException("Shouldn't be here", e); } } - + try { Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()) .joinUninterruptibly(); diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index 6c0e39ad6e..e6bf4b588c 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -267,9 +267,9 @@ private void handleHttpQuery(final TSDB tsdb, final Channel chan, final HttpRequ } final HttpRpcPluginQuery pluginQuery = (HttpRpcPluginQuery) abstractQuery; final HttpRpcPlugin rpc = rpc_manager.lookupHttpRpcPlugin(route); - if (rpc != null) { + if (rpc != null) { rpc.execute(tsdb, pluginQuery); - } else { + } else { pluginQuery.notFound(); } } else if (abstractQuery.getClass().isAssignableFrom(HttpQuery.class)) { diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index 591c0fd548..d07627788a 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -15,30 +15,29 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; - +import org.powermock.api.mockito.PowerMockito; +import org.mockito.Matchers; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; - +import java.util.ArrayList; import net.opentsdb.core.DataPoints; import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; 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; - +import net.opentsdb.uid.NoSuchUniqueName; import com.stumbleupon.async.Deferred; - /** * Unit tests for the Query RPC class that handles parsing user queries for * timeseries data and returning that data @@ -272,17 +271,55 @@ public void parseQueryNoSubQuery() throws Exception { parseQuery.invoke(rpc, tsdb, query); } - //TODO(cl) fix this up and add unit tests for the rate options parsing -// @SuppressWarnings({ "unchecked", "rawtypes" }) -// @Test -// public void parse() throws Exception { -// when(Deferred.groupInOrder((Collection)any()).joinUninterruptibly()) -// .thenReturn(null); -// HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query", -// "{\"start\":1356998400,\"end\":1356998460,\"queries\":[{\"aggregator" -// + "\": \"sum\",\"metric\": \"sys.cpu.0\",\"rate\": \"true\",\"tags\": " -// + "{\"host\": \"*\",\"dc\": \"lga\"}}]}"); -// rpc.execute(tsdb, query); -// assertEquals(HttpResponseStatus.OK, query.response().getStatus()); -// } -} + @Test + public void postQuerySimplePass() throws Exception { + Deferred> deferredMock = + (Deferred>)mock(Deferred.class); + PowerMockito.mockStatic(Deferred.class); + PowerMockito.when(Deferred.groupInOrder(Matchers.anyCollection())) + .thenReturn(deferredMock); + PowerMockito.when(deferredMock.joinUninterruptibly()) + .thenReturn(null); + PowerMockito.when(deferredMock.addCallback(Matchers.any(com.stumbleupon.async.Callback.class))) + .thenReturn(deferredMock); + + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query", + "{\"start\":1425440315306,\"queries\":" + + "[{\"metric\":\"somemetric\",\"aggregator\":\"sum\",\"rate\":true," + + "\"rateOptions\":{\"counter\":false}}]}"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + } + + @Test (expected = BadRequestException.class) + public void postQueryNoMetricBadRequest() throws Exception { + Deferred> deferredMock = + (Deferred>)mock(Deferred.class); + PowerMockito.mockStatic(Deferred.class); + PowerMockito.when(Deferred.groupInOrder(Matchers.anyCollection())) + .thenReturn(deferredMock); + PowerMockito.when(deferredMock.joinUninterruptibly()) + .thenReturn(null); + PowerMockito.when(deferredMock.addCallback( + Matchers.any(com.stumbleupon.async.Callback.class))) + .thenReturn(deferredMock); + + Query mockQuery = mock(Query.class); + PowerMockito.doThrow(new NoSuchUniqueName("metric", "nonexistent")) + .when(mockQuery).setTimeSeries( + Matchers.anyString(), + Matchers.anyMap(), + Matchers.any(net.opentsdb.core.Aggregator.class), + Matchers.anyBoolean(), + Matchers.any(net.opentsdb.core.RateOptions.class)); + when(tsdb.newQuery()).thenReturn(mockQuery); + + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query", + "{\"start\":1425440315306,\"queries\":" + + "[{\"metric\":\"nonexistent\",\"aggregator\":\"sum\",\"rate\":true," + + "\"rateOptions\":{\"counter\":false}}]}"); + rpc.execute(tsdb, query); + } + + //TODO(cl) add unit tests for the rate options parsing +} \ No newline at end of file From c0c802d28305618999acab37e4d3c3b1f80d6f45 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Fri, 27 Feb 2015 21:46:11 -0800 Subject: [PATCH 109/826] Address #396 to alleviate the recurring cachedir removal problem Signed-off-by: Chris Larsen --- src/core/Const.java | 7 +++ src/graph/Plot.java | 6 ++- src/tools/TSDMain.java | 42 +++------------ src/utils/FileSystem.java | 43 ++++++++++++++++ test/graph/TestPlot.java | 94 ++++++++++++++++++++++++++++++++++ test/utils/TestFileSystem.java | 46 +++++++++++++++++ 6 files changed, 202 insertions(+), 36 deletions(-) create mode 100644 src/utils/FileSystem.java create mode 100644 test/graph/TestPlot.java create mode 100644 test/utils/TestFileSystem.java diff --git a/src/core/Const.java b/src/core/Const.java index d14194f7be..b487e6ea87 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -74,6 +74,13 @@ public final class Const { * before losing precision. */ public static final long MAX_INT_IN_DOUBLE = 0xFFE0000000000000L; + + /** + * Mnemonics for FileSystem.checkDirectory() + */ + public static final boolean DONT_CREATE = false; + public static final boolean CREATE_IF_NEEDED = true; + public static final boolean MUST_BE_WRITEABLE = true; /** * The number of buckets to use for salting. diff --git a/src/graph/Plot.java b/src/graph/Plot.java index f9095a06fd..5777a70d73 100644 --- a/src/graph/Plot.java +++ b/src/graph/Plot.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.graph; +import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; @@ -24,9 +25,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; import net.opentsdb.meta.Annotation; +import net.opentsdb.utils.FileSystem; /** * Produces files to generate graphs with Gnuplot. @@ -195,6 +198,8 @@ public int dumpToFiles(final String basepath) throws IOException { int npoints = 0; final int nseries = datapoints.size(); final String datafiles[] = nseries > 0 ? new String[nseries] : null; + FileSystem.checkDirectory(new File(basepath).getParent(), + Const.MUST_BE_WRITEABLE, Const.CREATE_IF_NEEDED); for (int i = 0; i < nseries; i++) { datafiles[i] = basepath + "_" + i + ".dat"; final PrintWriter datafile = new PrintWriter(datafiles[i]); @@ -396,5 +401,4 @@ private String xFormat() { return "%Y/%m/%d"; } } - } diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 5c0b69bdb2..f467e24e8f 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -24,13 +24,14 @@ import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import net.opentsdb.tools.BuildData; import net.opentsdb.core.TSDB; +import net.opentsdb.core.Const; import net.opentsdb.tsd.PipelineFactory; import net.opentsdb.tsd.RpcManager; import net.opentsdb.utils.Config; - +import net.opentsdb.utils.FileSystem; +import net.opentsdb.graph.Plot; /** * Main class of the TSD, the Time Series Daemon. */ @@ -49,9 +50,6 @@ static void usage(final ArgP argp, final String errmsg, final int retval) { } private static final short DEFAULT_FLUSH_INTERVAL = 1000; - private static final boolean DONT_CREATE = false; - private static final boolean CREATE_IF_NEEDED = true; - private static final boolean MUST_BE_WRITEABLE = true; private static TSDB tsdb = null; @@ -114,10 +112,10 @@ public static void main(String[] args) throws IOException { // validate the cache and staticroot directories try { - checkDirectory(config.getString("tsd.http.staticroot"), - !MUST_BE_WRITEABLE, DONT_CREATE); - checkDirectory(config.getString("tsd.http.cachedir"), - MUST_BE_WRITEABLE, CREATE_IF_NEEDED); + FileSystem.checkDirectory(config.getString("tsd.http.staticroot"), + !Const.MUST_BE_WRITEABLE, Const.DONT_CREATE); + FileSystem.checkDirectory(config.getString("tsd.http.cachedir"), + Const.MUST_BE_WRITEABLE, Const.CREATE_IF_NEEDED); } catch (IllegalArgumentException e) { usage(argp, e.getMessage(), 3); } @@ -212,30 +210,4 @@ public void run() { } Runtime.getRuntime().addShutdownHook(new TSDBShutdown()); } - - /** - * Verifies a directory and checks to see if it's writeable or not if - * configured - * @param dir The path to check on - * @param need_write Set to true if the path needs write access - * @param create Set to true if the directory should be created if it does not - * exist - * @throws IllegalArgumentException if the path is empty, if it's not there - * and told not to create it or if it needs write access and can't - * be written to - */ - private static void checkDirectory(final String dir, - final boolean need_write, final boolean create) { - if (dir.isEmpty()) - throw new IllegalArgumentException("Directory path is empty"); - final File f = new File(dir); - if (!f.exists() && !(create && f.mkdirs())) { - throw new IllegalArgumentException("No such directory [" + dir + "]"); - } else if (!f.isDirectory()) { - throw new IllegalArgumentException("Not a directory [" + dir + "]"); - } else if (need_write && !f.canWrite()) { - throw new IllegalArgumentException("Cannot write to directory [" + dir - + "]"); - } - } } diff --git a/src/utils/FileSystem.java b/src/utils/FileSystem.java new file mode 100644 index 0000000000..9309515bf9 --- /dev/null +++ b/src/utils/FileSystem.java @@ -0,0 +1,43 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.utils; + +import java.io.File; + +public class FileSystem { + /** + * Verifies a directory and checks to see if it's writeable or not if + * configured + * @param dir The path to check on + * @param need_write Set to true if the path needs write access + * @param create Set to true if the directory should be created if it does not + * exist + * @throws IllegalArgumentException if the path is empty, if it's not there + * and told not to create it or if it needs write access and can't + * be written to + */ + public static void checkDirectory(final String dir, + final boolean need_write, final boolean create) { + if (dir.isEmpty()) + throw new IllegalArgumentException("Directory path is empty"); + final File f = new File(dir); + if (!f.exists() && !(create && f.mkdirs())) { + throw new IllegalArgumentException("No such directory [" + dir + "]"); + } else if (!f.isDirectory()) { + throw new IllegalArgumentException("Not a directory [" + dir + "]"); + } else if (need_write && !f.canWrite()) { + throw new IllegalArgumentException("Cannot write to directory [" + dir + + "]"); + } + } +} \ No newline at end of file diff --git a/test/graph/TestPlot.java b/test/graph/TestPlot.java new file mode 100644 index 0000000000..0a95caac8d --- /dev/null +++ b/test/graph/TestPlot.java @@ -0,0 +1,94 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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.graph; + +import java.io.File; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +import net.opentsdb.utils.FileSystem; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +@RunWith(PowerMockRunner.class) +@PrepareForTest({PrintWriter.class, File.class, FileSystem.class, Plot.class}) +public final class TestPlot { + + Plot plot; + File mockFile; + @Before + public void setUp() throws Exception { + plot = new Plot(0, 1234567890, null); + Map params = new HashMap(); + plot.setParams(params); + + PrintWriter mockWriter = PowerMockito.mock(PrintWriter.class); + PowerMockito.whenNew(PrintWriter.class) + .withAnyArguments() + .thenReturn(mockWriter); + + // Mock the builder pattern for PrintWriter instances. + PowerMockito.when(mockWriter, "append", Mockito.anyString()).thenReturn(mockWriter); + PowerMockito.when(mockWriter, "append", Mockito.anyChar()).thenReturn(mockWriter); + + mockFile = PowerMockito.mock(File.class); + PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(mockFile); + PowerMockito.when(mockFile, "getParent").thenReturn("/temp/opentsdb"); + PowerMockito.when(mockFile, "exists").thenReturn(true); + PowerMockito.when(mockFile, "isDirectory").thenReturn(true); + PowerMockito.when(mockFile, "canWrite").thenReturn(true); + } + + @Test + public void dumpToFilesDirectoryExistsIsWritable() throws Exception { + plot.dumpToFiles("/temp/opentsdb/s0M3haSh"); + } + + @Test(expected = IllegalArgumentException.class) + public void dumpToFilesDirectoryExistsNotWritable() throws Exception { + PowerMockito.when(mockFile, "canWrite").thenReturn(false); + plot.dumpToFiles("/temp/opentsdb/s0M3haSh"); + } + + @Test + public void dumpToFilesCreateNonexistentDirectory() throws Exception { + PowerMockito.when(mockFile, "exists").thenReturn(false); + PowerMockito.when(mockFile, "mkdirs").thenReturn(true); + plot.dumpToFiles("/temp/opentsdb/s0M3haSh"); + verify(mockFile, times(1)).mkdirs(); + } + + @Test(expected = IllegalArgumentException.class) + public void dumpToFilesCreateNonexistentDirectoryFail() throws Exception { + PowerMockito.when(mockFile, "exists").thenReturn(false); + PowerMockito.when(mockFile, "mkdirs").thenReturn(false); + plot.dumpToFiles("/temp/opentsdb/s0M3haSh"); + } + + @Test(expected = IllegalArgumentException.class) + public void dumpToFilesNotADirectory() throws Exception { + PowerMockito.when(mockFile, "exists").thenReturn(true); + PowerMockito.when(mockFile, "isDirectory").thenReturn(false); + plot.dumpToFiles("/temp/opentsdb/s0M3haSh"); + } +} diff --git a/test/utils/TestFileSystem.java b/test/utils/TestFileSystem.java new file mode 100644 index 0000000000..38558024d5 --- /dev/null +++ b/test/utils/TestFileSystem.java @@ -0,0 +1,46 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2013-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.utils; + +import java.io.File; + +import net.opentsdb.utils.FileSystem; + +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; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({File.class, FileSystem.class}) +public final class TestFileSystem { + + File mockFile; + + @Before + public void setUp() throws Exception { + mockFile = PowerMockito.mock(File.class); + PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(mockFile); + PowerMockito.when(mockFile, "getParent").thenReturn("/temp/opentsdb"); + PowerMockito.when(mockFile, "exists").thenReturn(true); + PowerMockito.when(mockFile, "isDirectory").thenReturn(true); + PowerMockito.when(mockFile, "canWrite").thenReturn(true); + } + + @Test (expected = IllegalArgumentException.class) + public void checkDirectoryEmptyString() throws Exception { + FileSystem.checkDirectory("", true, false); + } +} \ No newline at end of file From f21b44b7b1194b41b1dfc57c1f94fc4b9fdc831f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 18:10:49 -0700 Subject: [PATCH 110/826] Add the StorageExceptionHandler plugin interface to handle data points that fail to be stored in HBase. Implementations can decide to retry the data, log it or send it back to a queue of some sort. Also modify the PutDataPointRpc class to increment illegal argument and value counters during HTTP calls. Signed-off-by: Chris Larsen --- Makefile.am | 8 +- src/core/TSDB.java | 46 ++ src/tsd/PutDataPointRpc.java | 77 ++- src/tsd/StorageExceptionHandler.java | 80 +++ src/utils/Config.java | 1 + .../net.opentsdb.tsd.StorageExceptionHandler | 1 + test/core/TestTSDB.java | 41 +- test/tsd/DummySEHPlugin.java | 61 +++ test/tsd/NettyMocks.java | 6 + test/tsd/TestPutRpc.java | 514 +++++++++++++++++- 10 files changed, 799 insertions(+), 36 deletions(-) create mode 100644 src/tsd/StorageExceptionHandler.java create mode 100644 test/META-INF/services/net.opentsdb.tsd.StorageExceptionHandler create mode 100644 test/tsd/DummySEHPlugin.java diff --git a/Makefile.am b/Makefile.am index 3a7e951cc0..287cc87bfc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -116,6 +116,7 @@ tsdb_SRC := \ src/tsd/SearchRpc.java \ src/tsd/StaticFileRpc.java \ src/tsd/StatsRpc.java \ + src/tsd/StorageExceptionHandler.java \ src/tsd/SuggestRpc.java \ src/tsd/TelnetRpc.java \ src/tsd/TreeRpc.java \ @@ -130,6 +131,7 @@ tsdb_SRC := \ src/utils/ByteArrayPair.java \ src/utils/Config.java \ src/utils/DateTime.java \ + src/utils/FileSystem.java \ src/utils/JSON.java \ src/utils/JSONException.java \ src/utils/Pair.java \ @@ -230,7 +232,8 @@ test_plugin_SRC := \ test/tsd/DummyHttpSerializer.java \ test/tsd/DummyHttpRpcPlugin.java \ test/tsd/DummyRpcPlugin.java \ - test/tsd/DummyRTPublisher.java + test/tsd/DummyRTPublisher.java \ + test/tsd/DummySEHPlugin.java # Do NOT include the test dir path, just the META portion test_plugin_SVCS := \ @@ -239,7 +242,8 @@ test_plugin_SVCS := \ META-INF/services/net.opentsdb.tsd.HttpSerializer \ META-INF/services/net.opentsdb.tsd.HttpRpcPlugin \ META-INF/services/net.opentsdb.tsd.RpcPlugin \ - META-INF/services/net.opentsdb.tsd.RTPublisher + META-INF/services/net.opentsdb.tsd.RTPublisher \ + META-INF/services/net.opentsdb.tsd.StorageExceptionHandler test_plugin_MF := \ test/META-INF/MANIFEST.MF diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 0018d08e02..11cd0cb3ca 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -36,6 +36,7 @@ import net.opentsdb.tree.TreeBuilder; import net.opentsdb.tsd.RTPublisher; +import net.opentsdb.tsd.StorageExceptionHandler; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; @@ -106,6 +107,8 @@ public final class TSDB { /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; + /** Plugin for dealing with data points that can't be stored */ + private StorageExceptionHandler storage_exception_handler = null; /** * Constructor @@ -232,6 +235,27 @@ public void initializePlugins(final boolean init_rpcs) { } else { rt_publisher = null; } + + // load the storage exception plugin if enabled + if (config.getBoolean("tsd.core.storage_exception_handler.enable")) { + storage_exception_handler = PluginLoader.loadSpecificPlugin( + config.getString("tsd.core.storage_exception_handler.plugin"), + StorageExceptionHandler.class); + if (storage_exception_handler == null) { + throw new IllegalArgumentException( + "Unable to locate storage exception handler plugin: " + + config.getString("tsd.core.storage_exception_handler.plugin")); + } + try { + storage_exception_handler.initialize(this); + } catch (Exception e) { + throw new RuntimeException( + "Failed to initialize storage exception handler plugin", e); + } + LOG.info("Successfully initialized storage exception handler plugin [" + + storage_exception_handler.getClass().getCanonicalName() + "] version: " + + storage_exception_handler.version()); + } } /** @@ -251,6 +275,15 @@ public final HBaseClient getClient() { public final Config getConfig() { return this.config; } + + /** + * Returns the storage exception handler. May be null if not enabled + * @return The storage exception handler + * @since 2.2 + */ + public final StorageExceptionHandler getStorageExceptionHandler() { + return storage_exception_handler; + } /** * Attempts to find the name for a unique identifier given a type @@ -444,6 +477,14 @@ public void collectStats(final StatsCollector collector) { collector.clearExtraTag("plugin"); } } + if (storage_exception_handler != null) { + try { + collector.addExtraTag("plugin", "storageExceptionHandler"); + storage_exception_handler.collectStats(collector); + } finally { + collector.clearExtraTag("plugin"); + } + } } /** Returns a latency histogram for Put RPCs used to store data points. */ @@ -779,6 +820,11 @@ public Object call(ArrayList compactions) throws Exception { rt_publisher.getClass().getCanonicalName()); deferreds.add(rt_publisher.shutdown()); } + if (storage_exception_handler != null) { + LOG.info("Shutting down storage exception handler plugin: " + + storage_exception_handler.getClass().getCanonicalName()); + deferreds.add(storage_exception_handler.shutdown()); + } // wait for plugins to shutdown before we close the client return deferreds.size() > 0 diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 4e7f786ce0..6942e006b5 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -49,11 +49,14 @@ public Deferred execute(final TSDB tsdb, final Channel chan, try { final class PutErrback implements Callback { public Exception call(final Exception arg) { + // we handle the storage exceptions here so as to avoid creating yet + // another callback object on every data point. + handleStorageException(tsdb, getDataPointFromString(cmd), arg); if (chan.isConnected()) { chan.write("put: HBase error: " + arg.getMessage() + '\n'); } hbase_errors.incrementAndGet(); - return arg; + return null; } public String toString() { return "report error to channel"; @@ -111,7 +114,21 @@ public void execute(final TSDB tsdb, final HttpQuery query) long success = 0; long total = 0; - for (IncomingDataPoint dp : dps) { + for (final IncomingDataPoint dp : dps) { + + /** Handles passing a data point to the storage exception handler if + * we were unable to store it for any reason */ + final class PutErrback implements Callback { + public Object call(final Exception arg) { + handleStorageException(tsdb, dp, arg); + hbase_errors.incrementAndGet(); + return null; + } + public String toString() { + return "HTTP Put exception"; + } + } + total++; try { if (dp.getMetric() == null || dp.getMetric().isEmpty()) { @@ -119,6 +136,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) details.add(this.getHttpDetails("Metric name was empty", dp)); } LOG.warn("Metric name was empty: " + dp); + illegal_arguments.incrementAndGet(); continue; } if (dp.getTimestamp() <= 0) { @@ -126,6 +144,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) details.add(this.getHttpDetails("Invalid timestamp", dp)); } LOG.warn("Invalid timestamp: " + dp); + illegal_arguments.incrementAndGet(); continue; } if (dp.getValue() == null || dp.getValue().isEmpty()) { @@ -133,6 +152,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) details.add(this.getHttpDetails("Empty value", dp)); } LOG.warn("Empty value: " + dp); + invalid_values.incrementAndGet(); continue; } if (dp.getTags() == null || dp.getTags().size() < 1) { @@ -140,14 +160,17 @@ public void execute(final TSDB tsdb, final HttpQuery query) details.add(this.getHttpDetails("Missing tags", dp)); } LOG.warn("Missing tags: " + dp); + illegal_arguments.incrementAndGet(); continue; } if (Tags.looksLikeInteger(dp.getValue())) { tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Tags.parseLong(dp.getValue()), dp.getTags()); + Tags.parseLong(dp.getValue()), dp.getTags()) + .addErrback(new PutErrback()); } else { tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Float.parseFloat(dp.getValue()), dp.getTags()); + Float.parseFloat(dp.getValue()), dp.getTags()) + .addErrback(new PutErrback()); } success++; } catch (NumberFormatException x) { @@ -257,6 +280,37 @@ private Deferred importDataPoint(final TSDB tsdb, final String[] words) } } + + /** + * Converts the string array to an IncomingDataPoint. WARNING: This method + * does not perform validation. It should only be used by the Telnet style + * {@code execute} above within the error callback. At that point it means + * the array parsed correctly as per {@code importDataPoint}. + * @param words The array of strings representing a data point + * @return An incoming data point object. + */ + final private IncomingDataPoint getDataPointFromString(final String[] words) { + final IncomingDataPoint dp = new IncomingDataPoint(); + dp.setMetric(words[1]); + + if (words[2].contains(".")) { + dp.setTimestamp(Tags.parseLong(words[2].replace(".", ""))); + } else { + dp.setTimestamp(Tags.parseLong(words[2])); + } + + dp.setValue(words[3]); + + final HashMap tags = new HashMap(); + for (int i = 4; i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + dp.setTags(tags); + return dp; + } + /** * Simple helper to format an error trying to save a data point * @param message The message to return to the user @@ -271,4 +325,19 @@ final private HashMap getHttpDetails(final String message, map.put("datapoint", dp); return map; } + + /** + * Passes a data point off to the storage handler plugin if it has been + * configured. + * @param tsdb The TSDB from which to grab the SEH plugin + * @param dp The data point to process + * @param e The exception that caused this + */ + void handleStorageException(final TSDB tsdb, final IncomingDataPoint dp, + final Exception e) { + final StorageExceptionHandler handler = tsdb.getStorageExceptionHandler(); + if (handler != null) { + handler.handleError(dp, e); + } + } } diff --git a/src/tsd/StorageExceptionHandler.java b/src/tsd/StorageExceptionHandler.java new file mode 100644 index 0000000000..07b2e15feb --- /dev/null +++ b/src/tsd/StorageExceptionHandler.java @@ -0,0 +1,80 @@ +// 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.IncomingDataPoint; +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; + +/** + * This is a plugin for handling data points that fail the write to the + * underlying data store for various reasons. For example, HBase may lose a + * region server and a very busy TSD may queue up RPCs in a region, hit the + * high watermark, and start rejecting any data points that would hit the + * region that's offline. In the error callback for each data point, we can + * call into this object and have it queued to disk, send it to an external + * queue or even push it to another TSD. + * @since 2.2 + */ +public abstract class StorageExceptionHandler { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. 2.0.1. The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + + /** + * Receives a data point from the storage attempt along with the exception + * that was associated with the failure. + * @param dp The data point to store + * @param exception The exception associated with the data point + */ + public abstract void handleError(final IncomingDataPoint dp, + final Exception exception); +} diff --git a/src/utils/Config.java b/src/utils/Config.java index 40ab467a00..1829eb92bb 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -453,6 +453,7 @@ protected void setDefaults() { default_map.put("tsd.core.tree.enable_processing", "false"); default_map.put("tsd.core.preload_uid_cache", "false"); default_map.put("tsd.core.preload_uid_cache.max_entries", "300000"); + default_map.put("tsd.core.storage_exception_handler.enable", "false"); default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); diff --git a/test/META-INF/services/net.opentsdb.tsd.StorageExceptionHandler b/test/META-INF/services/net.opentsdb.tsd.StorageExceptionHandler new file mode 100644 index 0000000000..64e9aa9e46 --- /dev/null +++ b/test/META-INF/services/net.opentsdb.tsd.StorageExceptionHandler @@ -0,0 +1 @@ +net.opentsdb.tsd.DummySEHPlugin diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index dc4672ceb2..55d549c08e 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -16,7 +16,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.when; -import static org.mockito.Matchers.anyString; import java.lang.reflect.Field; import java.util.HashMap; @@ -119,6 +118,46 @@ public void initializePluginsSearchNotFound() throws Exception { tsdb.initializePlugins(true); } + @Test + public void initializePluginsSEH() throws Exception { + config.overrideConfig("tsd.core.plugin_path", "./"); + config.overrideConfig("tsd.core.storage_exception_handler.enable", "true"); + config.overrideConfig("tsd.core.storage_exception_handler.plugin", + "net.opentsdb.tsd.DummySEHPlugin"); + config.overrideConfig( + "tsd.core.storage_exception_handler.DummySEHPlugin.hosts", "localhost"); + tsdb.initializePlugins(true); + assertNotNull(tsdb.getStorageExceptionHandler()); + } + + @Test (expected = RuntimeException.class) + public void initializePluginsSEHBadConfig() throws Exception { + config.overrideConfig("tsd.core.plugin_path", "./"); + config.overrideConfig("tsd.core.storage_exception_handler.enable", "true"); + config.overrideConfig("tsd.core.storage_exception_handler.plugin", + "net.opentsdb.tsd.DummySEHPlugin"); + tsdb.initializePlugins(true); + assertNotNull(tsdb.getStorageExceptionHandler()); + } + + @Test (expected = NullPointerException.class) + public void initializePluginsSEHEnabledButNoName() throws Exception { + config.overrideConfig("tsd.core.plugin_path", "./"); + config.overrideConfig("tsd.core.storage_exception_handler.enable", "true"); + tsdb.initializePlugins(true); + } + + @Test (expected = IllegalArgumentException.class) + public void initializePluginsSEHNotFound() throws Exception { + config.overrideConfig("tsd.core.plugin_path", "./"); + config.overrideConfig("tsd.core.storage_exception_handler.enable", "true"); + config.overrideConfig("tsd.core.storage_exception_handler.plugin", + "net.opentsdb.tsd.DoesNotExistSEHPlugin"); + config.overrideConfig( + "tsd.core.storage_exception_handler.DummySEHPlugin.hosts", "localhost"); + tsdb.initializePlugins(true); + } + @Test public void getClient() { assertNotNull(tsdb.getClient()); diff --git a/test/tsd/DummySEHPlugin.java b/test/tsd/DummySEHPlugin.java new file mode 100644 index 0000000000..56c2af2fb2 --- /dev/null +++ b/test/tsd/DummySEHPlugin.java @@ -0,0 +1,61 @@ +// 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 net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; + +import com.stumbleupon.async.Deferred; + +public class DummySEHPlugin extends StorageExceptionHandler { + + @Override + public void initialize(TSDB tsdb) { + if (tsdb == null) { + throw new IllegalArgumentException("The TSDB object was null"); + } + // a dummy config to check for throwing exceptions + if (!tsdb.getConfig().hasProperty( + "tsd.core.storage_exception_handler.DummySEHPlugin.hosts")) { + throw new IllegalArgumentException("Missing hosts config"); + } + } + + @Override + public Deferred shutdown() { + return Deferred.fromResult(new Object()); + } + + @Override + public String version() { + return "2.2.0"; + } + + @Override + public void collectStats(StatsCollector collector) { + collector.record("seh.dummy.retries", 1); + } + + @Override + public void handleError(IncomingDataPoint dp, Exception exception) { + if (dp == null) { + throw new IllegalArgumentException("Missing Data Point"); + } + if (dp.getValue().equals("42")) { + throw new IllegalDataException("Testing"); + } + } + +} diff --git a/test/tsd/NettyMocks.java b/test/tsd/NettyMocks.java index c881462f9e..7937f97bbf 100644 --- a/test/tsd/NettyMocks.java +++ b/test/tsd/NettyMocks.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; +import java.net.SocketAddress; import java.nio.charset.Charset; import java.util.HashMap; @@ -63,6 +64,11 @@ public static Channel fakeChannel() { final Channel chan = mock(Channel.class); when(chan.toString()).thenReturn("[fake channel]"); when(chan.isConnected()).thenReturn(true); + when(chan.isWritable()).thenReturn(true); + + final SocketAddress socket = mock(SocketAddress.class); + when(socket.toString()).thenReturn("192.168.1.1:4243"); + when(chan.getRemoteAddress()).thenReturn(socket); return chan; } diff --git a/test/tsd/TestPutRpc.java b/test/tsd/TestPutRpc.java index 983594cf49..7d20d84ece 100644 --- a/test/tsd/TestPutRpc.java +++ b/test/tsd/TestPutRpc.java @@ -12,57 +12,99 @@ // see . package net.opentsdb.tsd; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.nio.charset.Charset; import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.Config; +import org.jboss.netty.channel.Channel; 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.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, HttpQuery.class}) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, + StorageExceptionHandler.class }) public final class TestPutRpc { + private static final Map TAGS = new HashMap(1); + static { + TAGS.put("host", "web01"); + } private TSDB tsdb = null; + private AtomicLong requests = new AtomicLong(); + private AtomicLong hbase_errors = new AtomicLong(); + private AtomicLong invalid_values = new AtomicLong(); + private AtomicLong illegal_arguments = new AtomicLong(); + private AtomicLong unknown_metrics = new AtomicLong(); + private StorageExceptionHandler handler; @Before public void before() throws Exception { tsdb = NettyMocks.getMockedHTTPTSDB(); - final HashMap tags1 = new HashMap(); - tags1.put("host", "web01"); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, 42, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, 42, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -42, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, -42, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, 42.2f, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, 42.2f, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -42.2f, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, -42.2f, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, 4220.0f, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, 4220.0f, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -4220.0f, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, -4220.0f, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, .0042f, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, .0042f, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -0.0042f, tags1)) + when(tsdb.addPoint("sys.cpu.nice", 1365465600, -0.0042f, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.system", 1365465600, 24, tags1)) + when(tsdb.addPoint("sys.cpu.system", 1365465600, 24, TAGS)) .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("doesnotexist", 1365465600, 42, tags1)) + when(tsdb.addPoint("doesnotexist", 1365465600, 42, TAGS)) .thenThrow(new NoSuchUniqueName("metric", "doesnotexist")); + + requests = Whitebox.getInternalState(PutDataPointRpc.class, "requests"); + requests.set(0); + hbase_errors = Whitebox.getInternalState(PutDataPointRpc.class, "hbase_errors"); + hbase_errors.set(0); + invalid_values = Whitebox.getInternalState(PutDataPointRpc.class, "invalid_values"); + invalid_values.set(0); + illegal_arguments = Whitebox.getInternalState(PutDataPointRpc.class, "illegal_arguments"); + illegal_arguments.set(0); + unknown_metrics = Whitebox.getInternalState(PutDataPointRpc.class, "unknown_metrics"); + unknown_metrics.set(0); + + handler = mock(StorageExceptionHandler.class); + when(tsdb.getStorageExceptionHandler()).thenReturn(handler); } @Test @@ -70,6 +112,172 @@ public void constructor() { assertNotNull(new PutDataPointRpc()); } + // Socket RPC Tests ------------------------------------ + + @Test + public void execute() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNotNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(chan, never()).write(any()); + verify(chan, never()).isConnected(); + verify(tsdb, never()).getStorageExceptionHandler(); + } + + @Test + public void executeBadValue() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "notanum", "host=web01" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, never()).getStorageExceptionHandler(); + } + + @Test + public void executeMissingMetric() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNull(put.execute(tsdb, chan, new String[] { "put", "", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, never()).getStorageExceptionHandler(); + } + + @Test + public void executeUnknownMetric() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNull(put.execute(tsdb, chan, new String[] { "put", "doesnotexist", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, unknown_metrics.get()); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, never()).getStorageExceptionHandler(); + } + + @SuppressWarnings("unchecked") + @Test (expected = RuntimeException.class) + public void executeRuntimeException() throws Exception { + when(tsdb.addPoint(anyString(), anyLong(), anyLong(), + (HashMap)any())) + .thenThrow(new RuntimeException("Fail!")); + + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + put.execute(tsdb, chan, new String[] { "put", "doesnotexist", + "1365465600", "42", "host=web01" }); + } + + @SuppressWarnings("unchecked") + @Test + public void executeHBaseError() throws Exception { + when(tsdb.addPoint(anyString(), anyLong(), anyLong(), + (HashMap)any(HashMap.class))) + .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, hbase_errors.get()); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, times(1)).getStorageExceptionHandler(); + } + + @SuppressWarnings("unchecked") + @Test + public void executeHBaseErrorHandler() throws Exception { + when(tsdb.addPoint(anyString(), anyLong(), anyLong(), + (HashMap)any())) + .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, hbase_errors.get()); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(handler, times(1)).handleError((IncomingDataPoint)any(), + (Exception)any()); + } + + @Test (expected = NullPointerException.class) + public void executeNullTSDB() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + put.execute(null, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "42", "host=web01" }); + } + + @Test + public void executeNullChannelOK() throws Exception { + // we can pass in a null channel but since we only write when an error occurs + // then we won't fail. + final PutDataPointRpc put = new PutDataPointRpc(); + assertNotNull(put.execute(tsdb, null, new String[] { "put", "sys.cpu.nice", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + } + + @Test (expected = NullPointerException.class) + public void executeNullChannelError() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, null, new String[] { "put", "sys.cpu.nice", + "1365465600", "notanumber", "host=web01" }); + } + + @Test (expected = NullPointerException.class) + public void executeNullArray() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + put.execute(tsdb, chan, null); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void executeEmptyArray() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + put.execute(tsdb, chan, new String[0]); + } + + @Test + public void executeShortArray() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "42" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, never()).getStorageExceptionHandler(); + } + // HTTP RPC Tests -------------------------------------- @Test @@ -80,8 +288,11 @@ public void putSingle() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - + @Test public void putDouble() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", @@ -92,6 +303,9 @@ public void putDouble() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -106,6 +320,9 @@ public void putSingleSummary() throws Exception { query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":1")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -121,6 +338,9 @@ public void putSingleDetails() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":1")); assertTrue(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -136,6 +356,9 @@ public void putSingleSummaryAndDetails() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":1")); assertTrue(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -152,6 +375,9 @@ public void putDoubleSummary() throws Exception { query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":2")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -162,6 +388,9 @@ public void putNegativeInt() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -172,6 +401,9 @@ public void putFloat() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -182,6 +414,9 @@ public void putNegativeFloat() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -192,6 +427,9 @@ public void putSEBig() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -202,6 +440,9 @@ public void putSECaseBig() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -212,6 +453,9 @@ public void putNegativeSEBig() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -222,6 +466,9 @@ public void putNegativeSECaseBig() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -232,6 +479,9 @@ public void putSETiny() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -242,6 +492,9 @@ public void putSECaseTiny() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -252,6 +505,9 @@ public void putNegativeSETiny() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -262,41 +518,133 @@ public void putNegativeSECaseTiny() throws Exception { PutDataPointRpc put = new PutDataPointRpc(); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - @Test (expected = BadRequestException.class) + @Test public void badMethod() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/put"); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - - @Test (expected = BadRequestException.class) + + @Test public void badJSON() throws Exception { // missing a quotation mark HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", "{\"metric\":\"sys.cpu.nice\",\"timestamp:1365465600,\"value\"" +":42,\"tags\":{\"host\":\"web01\"}}"); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - @Test (expected = BadRequestException.class) + @Test public void notJSON() throws Exception { // missing a quotation mark HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", "Hello World"); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - @Test (expected = BadRequestException.class) + @Test public void noContent() throws Exception { // missing a quotation mark HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", ""); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + } + + @SuppressWarnings("unchecked") + @Test + public void hbaseError() throws Exception { + when(tsdb.addPoint(anyString(), anyLong(), anyLong(), + (HashMap)any())) + .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", + "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"host\":\"web01\"}}"); + PutDataPointRpc put = new PutDataPointRpc(); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, hbase_errors.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); } + @SuppressWarnings("unchecked") + @Test + public void hbaseErrorHandler() throws Exception { + final StorageExceptionHandler handler = mock(StorageExceptionHandler.class); + when(tsdb.getStorageExceptionHandler()).thenReturn(handler); + when(tsdb.addPoint(anyString(), anyLong(), anyLong(), + (HashMap)any())) + .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", + "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"host\":\"web01\"}}"); + PutDataPointRpc put = new PutDataPointRpc(); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, hbase_errors.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(handler, times(1)).handleError((IncomingDataPoint)any(), + (Exception)any()); + } + @Test public void noSuchUniqueName() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", @@ -310,6 +658,11 @@ public void noSuchUniqueName() throws Exception { assertTrue(response.contains("\"error\":\"Unknown metric\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + assertEquals(1, unknown_metrics.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -325,6 +678,10 @@ public void missingMetric() throws Exception { assertTrue(response.contains("\"error\":\"Metric name was empty\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -340,6 +697,10 @@ public void nullMetric() throws Exception { assertTrue(response.contains("\"error\":\"Metric name was empty\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -355,6 +716,10 @@ public void missingTimestamp() throws Exception { assertTrue(response.contains("\"error\":\"Invalid timestamp\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -370,6 +735,10 @@ public void nullTimestamp() throws Exception { assertTrue(response.contains("\"error\":\"Invalid timestamp\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -385,6 +754,10 @@ public void invalidTimestamp() throws Exception { assertTrue(response.contains("\"error\":\"Invalid timestamp\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -400,6 +773,10 @@ public void missingValue() throws Exception { assertTrue(response.contains("\"error\":\"Empty value\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -415,6 +792,10 @@ public void nullValue() throws Exception { assertTrue(response.contains("\"error\":\"Empty value\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -430,6 +811,10 @@ public void emptyValue() throws Exception { assertTrue(response.contains("\"error\":\"Empty value\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -445,6 +830,10 @@ public void badValue() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -460,15 +849,29 @@ public void ValueNaN() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - @Test (expected = BadRequestException.class) + @Test public void ValueNaNCase() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" +":Nan,\"tags\":{\"host\":\"web01\"}}"); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -484,6 +887,10 @@ public void ValueINF() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -499,24 +906,48 @@ public void ValueNINF() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - @Test (expected = BadRequestException.class) + @Test public void ValueINFUnsigned() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" +":INF,\"tags\":{\"host\":\"web01\"}}"); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } - @Test (expected = BadRequestException.class) + @Test public void ValueINFCase() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" +":+inf,\"tags\":{\"host\":\"web01\"}}"); PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, query); + BadRequestException ex = null; + try { + put.execute(tsdb, query); + } catch (BadRequestException e) { + ex = e; + } + assertNotNull(ex); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -532,6 +963,10 @@ public void ValueInfiniy() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -547,6 +982,10 @@ public void ValueNInfiniy() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -562,6 +1001,10 @@ public void ValueInfinityUnsigned() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(1, invalid_values.get()); + assertEquals(0, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -577,6 +1020,10 @@ public void missingTags() throws Exception { assertTrue(response.contains("\"error\":\"Missing tags\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -592,6 +1039,10 @@ public void nullTags() throws Exception { assertTrue(response.contains("\"error\":\"Missing tags\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } @Test @@ -607,5 +1058,10 @@ public void emptyTags() throws Exception { assertTrue(response.contains("\"error\":\"Missing tags\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + verify(tsdb, never()).getStorageExceptionHandler(); } + } From 0af148d5e235a90ae6f8e20a3817a66c681d3828 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 18:26:52 -0700 Subject: [PATCH 111/826] Add the storage exception handler plugin to the pom for testing Signed-off-by: Chris Larsen --- pom.xml.in | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml.in b/pom.xml.in index be1dabb7c0..a69416b4c5 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -134,6 +134,9 @@ target/test-classes net/opentsdb/tsd/DummyRTPublisher.class -C + target/test-classes + net/opentsdb/tsd/DummySEHPlugin.class + -C test META-INF/services/net.opentsdb.plugin.DummyPlugin -C @@ -151,6 +154,9 @@ -C test META-INF/services/net.opentsdb.tsd.RTPublisher + -C + test + META-INF/services/net.opentsdb.tsd.StorageExceptionHandler test-compile From 1387b3ac8ab24c971516ea3d78243eec12c9035e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 18:37:28 -0700 Subject: [PATCH 112/826] Add the /api/stats/threads and /api/stats/jvm endpoints for more insight into the TSD's world Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/tsd/HttpJsonSerializer.java | 22 ++++ src/tsd/HttpSerializer.java | 28 +++++ src/tsd/StatsRpc.java | 175 +++++++++++++++++++++++++++++++- test/tsd/TestStatsRpc.java | 78 ++++++++++++++ 5 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 test/tsd/TestStatsRpc.java diff --git a/Makefile.am b/Makefile.am index 287cc87bfc..37ccc74a60 100644 --- a/Makefile.am +++ b/Makefile.am @@ -212,6 +212,7 @@ test_SRC := \ test/tsd/TestRpcManager.java \ test/tsd/TestRTPublisher.java \ test/tsd/TestSearchRpc.java \ + test/uid/TestStatsRpc.java \ test/tsd/TestSuggestRpc.java \ test/tsd/TestTreeRpc.java \ test/tsd/TestUniqueIdRpc.java \ diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index e82d4cf6f0..c0510a576f 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -823,6 +823,28 @@ public ChannelBuffer formatStatsV1(final List stats) { return serializeJSON(stats); } + /** + * Format a list of thread statistics + * @param stats The thread statistics list to format + * @return A ChannelBuffer object to pass on to the caller + * @throws JSONException if serialization failed + * @since 2.2 + */ + public ChannelBuffer formatThreadStatsV1(final List> stats) { + return serializeJSON(stats); + } + + /** + * Format a list of JVM statistics + * @param stats The JVM stats map to format + * @return A ChannelBuffer object to pass on to the caller + * @throws JSONException if serialization failed + * @since 2.2 + */ + public ChannelBuffer formatJVMStatsV1(final Map> stats) { + return serializeJSON(stats); + } + /** * Format the response from a search query * @param note The query (hopefully filled with results) to serialize diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 8f92cde1dc..05fc82fa50 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -652,6 +652,34 @@ public ChannelBuffer formatStatsV1(final List stats) { " has not implemented formatStatsV1"); } + /** + * Format a list of thread statistics + * @param stats The thread statistics list to format + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.2 + */ + public ChannelBuffer formatThreadStatsV1(final List> stats) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatThreadStatsV1"); + } + + /** + * Format a list of JVM statistics + * @param map The JVM stats list to format + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.2 + */ + public ChannelBuffer formatJVMStatsV1(final Map> map) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatJVMStatsV1"); + } + /** * Format the response from a search query * @param results The query (hopefully filled with results) to serialize diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index 0ff6ec69fc..bb5b4ec327 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -12,9 +12,17 @@ // see . package net.opentsdb.tsd; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.lang.management.RuntimeMXBean; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; @@ -24,6 +32,8 @@ 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.Deferred; @@ -36,7 +46,8 @@ * @since 2.0 */ public final class StatsRpc implements TelnetRpc, HttpRpc { - + private static final Logger LOG = LoggerFactory.getLogger(StatsRpc.class); + /** * Telnet RPC responder that returns the stats in ASCII style * @param tsdb The TSDB to use for fetching stats @@ -66,6 +77,23 @@ public void execute(final TSDB tsdb, final HttpQuery query) { "] is not permitted for this endpoint"); } + try { + final String[] uri = query.explodeAPIPath(); + final String endpoint = uri.length > 1 ? uri[1].toLowerCase() : ""; + + // Handle /threads and /regions. + if ("threads".equals(endpoint)) { + printThreadStats(query); + return; + } else if ("jvm".equals(endpoint)) { + printJVMStats(tsdb, query); + return; + } + } catch (IllegalArgumentException e) { + // this is thrown if the url doesn't start with /api. To maintain backwards + // compatibility with the /stats endpoint we can catch and continue here. + } + final boolean canonical = tsdb.getConfig().getBoolean("tsd.stats.canonical"); // if we don't have an API request we need to respond with the 1.x version @@ -105,9 +133,154 @@ private void doCollectStats(final TSDB tsdb, final StatsCollector collector, ConnectionManager.collectStats(collector); RpcHandler.collectStats(collector); RpcManager.collectStats(collector); + collectThreadStats(collector); tsdb.collectStats(collector); } + /** + * Grabs a snapshot of all JVM thread states and formats it in a manner to + * be displayed via API. + * @param query The query to respond to + */ + private void printThreadStats(final HttpQuery query) { + final Set threads = Thread.getAllStackTraces().keySet(); + final List> output = + new ArrayList>(threads.size()); + for (final Thread thread : threads) { + final Map status = new HashMap(); + status.put("threadID", thread.getId()); + status.put("name", thread.getName()); + status.put("state", thread.getState().toString()); + status.put("interrupted", thread.isInterrupted()); + status.put("priority", thread.getPriority()); + + final List stack = + new ArrayList(thread.getStackTrace().length); + for (final StackTraceElement element: thread.getStackTrace()) { + stack.add(element.toString()); + } + status.put("stack", stack); + output.add(status); + } + query.sendReply(query.serializer().formatThreadStatsV1(output)); + } + + /** + * Yield (chiefly memory-related) stats about this OpenTSDB instance's JVM. + * @param tsdb The TSDB from which to fetch stats. + * @param query The query to which to respond. + */ + private void printJVMStats(final TSDB tsdb, final HttpQuery query) { + final Map> map = + new HashMap>(); + + final RuntimeMXBean runtime_bean = ManagementFactory.getRuntimeMXBean(); + final Map runtime = new HashMap(); + map.put("runtime", runtime); + + runtime.put("startTime", runtime_bean.getStartTime()); + runtime.put("uptime", runtime_bean.getUptime()); + runtime.put("vmName", runtime_bean.getVmName()); + runtime.put("vmVendor", runtime_bean.getVmVendor()); + runtime.put("vmVersion", runtime_bean.getVmVersion()); + + final MemoryMXBean mem_bean = ManagementFactory.getMemoryMXBean(); + final Map memory = new HashMap(); + map.put("memory", memory); + + memory.put("heapMemoryUsage", mem_bean.getHeapMemoryUsage()); + memory.put("nonHeapMemoryUsage", mem_bean.getNonHeapMemoryUsage()); + memory.put("objectsPendingFinalization", + mem_bean.getObjectPendingFinalizationCount()); + + final List gc_beans = + ManagementFactory.getGarbageCollectorMXBeans(); + final Map gc = new HashMap(); + map.put("gc", gc); + + for (final GarbageCollectorMXBean gc_bean : gc_beans) { + final Map stats = new HashMap(); + final String name = formatStatName(gc_bean.getName()); + if (name == null) { + LOG.warn("Null name for bean: " + gc_bean); + continue; + } + + gc.put(name, stats); + stats.put("collectionCount", gc_bean.getCollectionCount()); + stats.put("collectionTime", gc_bean.getCollectionTime()); + } + + final List pool_beans = + ManagementFactory.getMemoryPoolMXBeans(); + final Map pools = new HashMap(); + map.put("pools", pools); + + for (final MemoryPoolMXBean pool_bean : pool_beans) { + final Map stats = new HashMap(); + final String name = formatStatName(pool_bean.getName()); + if (name == null) { + LOG.warn("Null name for bean: " + pool_bean); + continue; + } + pools.put(name, stats); + + stats.put("collectionUsage", pool_bean.getCollectionUsage()); + stats.put("usage", pool_bean.getUsage()); + stats.put("peakUsage", pool_bean.getPeakUsage()); + stats.put("type", pool_bean.getType()); + } + + final OperatingSystemMXBean os_bean = + ManagementFactory.getOperatingSystemMXBean(); + final Map os = new HashMap(); + map.put("os", os); + + os.put("systemLoadAverage", os_bean.getSystemLoadAverage()); + + query.sendReply(query.serializer().formatJVMStatsV1(map)); + } + + /** + * Runs through the live threads and counts captures a coune of their + * states for dumping in the stats page. + * @param collector The collector to write to + */ + private void collectThreadStats(final StatsCollector collector) { + final Set threads = Thread.getAllStackTraces().keySet(); + final Map states = new HashMap(6); + states.put("new", 0); + states.put("runnable", 0); + states.put("blocked", 0); + states.put("waiting", 0); + states.put("timed_waiting", 0); + states.put("terminated", 0); + for (final Thread thread : threads) { + int state_count = states.get(thread.getState().toString().toLowerCase()); + state_count++; + states.put(thread.getState().toString().toLowerCase(), state_count); + } + for (final Map.Entry entry : states.entrySet()) { + collector.record("jvm.thread.states", entry.getValue(), "state=" + + entry.getKey()); + } + collector.record("jvm.thread.count", threads.size()); + } + + /** + * Little helper to convert the first character to lowercase and remove any + * spaces + * @param stat The name to cleanup + * @return a clean name or null if the original string was null or empty + */ + private static String formatStatName(final String stat) { + if (stat == null || stat.isEmpty()) { + return stat; + } + String name = stat.replace(" ", ""); + return name.substring(0, 1).toLowerCase() + name.substring(1); + } + /** * Implements the StatsCollector with ASCII style output. Builds a string * buffer response to send to the caller diff --git a/test/tsd/TestStatsRpc.java b/test/tsd/TestStatsRpc.java new file mode 100644 index 0000000000..fa90729245 --- /dev/null +++ b/test/tsd/TestStatsRpc.java @@ -0,0 +1,78 @@ +// 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.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.Charset; + +import net.opentsdb.core.TSDB; +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 TestStatsRpc { + private TSDB tsdb; + private HBaseClient client; + + @Before + public void before() throws Exception { + tsdb = NettyMocks.getMockedHTTPTSDB(); + client = mock(HBaseClient.class); + when(tsdb.getClient()).thenReturn(client); + } + + @Test + public void printThreadStats() throws Exception { + final StatsRpc rpc = new StatsRpc(); + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats/threads"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertNotNull(json); + // check for some standard JVM threads since we can't mock Thread easily + assertTrue(json.contains("\"name\":\"Finalizer\"")); + assertTrue(json.contains("java.lang.ref.Finalizer$FinalizerThread.run")); + } + + @Test + public void printJVMStats() throws Exception { + final StatsRpc rpc = new StatsRpc(); + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats/jvm"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertNotNull(json); + assertTrue(json.contains("\"os\":{")); + assertTrue(json.contains("\"gc\":{")); + assertTrue(json.contains("\"runtime\":{")); + assertTrue(json.contains("\"pools\":{")); + assertTrue(json.contains("\"memory\":{")); + } +} + From b96419d7fbbf89e85c713a9ebd6750f1805a3688 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 18:47:33 -0700 Subject: [PATCH 113/826] Add the QueryStats and QueryException classes for tracking queries to the TSD. Actual implementation to follow. --- Makefile.am | 3 + src/core/QueryException.java | 75 +++++++ src/stats/QueryStats.java | 344 +++++++++++++++++++++++++++++++++ test/stats/TestQueryStats.java | 249 ++++++++++++++++++++++++ 4 files changed, 671 insertions(+) create mode 100644 src/core/QueryException.java create mode 100644 src/stats/QueryStats.java create mode 100644 test/stats/TestQueryStats.java diff --git a/Makefile.am b/Makefile.am index 37ccc74a60..b92af5d065 100644 --- a/Makefile.am +++ b/Makefile.am @@ -49,6 +49,7 @@ tsdb_SRC := \ src/core/Internal.java \ src/core/MutableDataPoint.java \ src/core/Query.java \ + src/core/QueryException.java \ src/core/RateOptions.java \ src/core/RateSpan.java \ src/core/RowKey.java \ @@ -73,6 +74,7 @@ tsdb_SRC := \ src/search/TimeSeriesLookup.java \ src/stats/Histogram.java \ src/stats/StatsCollector.java \ + src/stats/QueryStats.java \ src/tools/ArgP.java \ src/tools/CliOptions.java \ src/tools/CliQuery.java \ @@ -189,6 +191,7 @@ test_SRC := \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ test/stats/TestHistogram.java \ + test/stats/TestQueryStats.java \ test/storage/MockBase.java \ test/tools/TestDumpSeries.java \ test/tools/TestFsck.java \ diff --git a/src/core/QueryException.java b/src/core/QueryException.java new file mode 100644 index 0000000000..5427cc1be2 --- /dev/null +++ b/src/core/QueryException.java @@ -0,0 +1,75 @@ +// 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 org.jboss.netty.handler.codec.http.HttpResponseStatus; + +/** + * An exception thrown during query execution such as a timeout or other + * type of error. + * @since 2.2 + */ +public final class QueryException extends RuntimeException { + + /** An optional, detailed error message */ + private final String details; + + /** The HTTP status code to return to the user */ + private final HttpResponseStatus status; + + /** + * Default ctor + * @param msg Message describing the problem. + */ + public QueryException(final String msg) { + super(msg); + status = HttpResponseStatus.BAD_REQUEST; + details = msg; + } + + /** + * Ctor setting the status + * @param status The status code to respond with for HTTP requests + * @param msg Message describing the problem. + */ + public QueryException(final HttpResponseStatus status, final String msg) { + super(msg); + this.status = status; + details = msg; + } + + /** + * Ctor setting status and the messages + * @param status The status code to respond with for HTTP requests + * @param msg Message describing the problem. + * @param details Extra details for the error + */ + public QueryException(final HttpResponseStatus status, final String msg, + final String details) { + super(msg); + this.status = status; + this.details = details; + } + + /** @return the HTTP status code */ + public final HttpResponseStatus getStatus() { + return this.status; + } + + /** @return the details, may be an empty string */ + public final String getDetails() { + return this.details; + } + + private static final long serialVersionUID = 9040020770546069974L; +} diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java new file mode 100644 index 0000000000..6da9e04ee5 --- /dev/null +++ b/src/stats/QueryStats.java @@ -0,0 +1,344 @@ +// 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.stats; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Objects; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import net.opentsdb.core.QueryException; +import net.opentsdb.core.TSQuery; +import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; + +/** + * This class stores information about OpenTSDB queries executed through the + * HTTP API. It maintains a list of running queries as well as a cache of the + * last {@code COMPLETED_QUERY_CACHE_SIZE} number of queries executed. The + * stats can be observed via /api/query/stats. + * + * When a query is executed, it should instantiate an object of this class. + * Once the query is completed, make sure to call {@link markComplete}. + * + * The cache will store each query based on the combination of the client, query + * and the result code. If the same query was executed multiple times then it + * will increment the "executed" counter for the query in the cache. + * @since 2.2 + */ +public class QueryStats { + private static final Logger LOG = LoggerFactory.getLogger(QueryStats.class); + + /** Determines how many query stats to keep in the cache */ + private static int COMPLETED_QUERY_CACHE_SIZE = 256; + + /** Stores queries currently executing. If a thread doesn't call into + * markComplete then it's possible for this map to fill up. + * Hash is the remote + query */ + private static ConcurrentHashMap running_queries = + new ConcurrentHashMap(); + + /** Size limited cache of queries from the past. + * Hash is the remote + query + response code */ + private static Cache completed_queries = + CacheBuilder.newBuilder().maximumSize(COMPLETED_QUERY_CACHE_SIZE).build(); + + /** Start time for the query. Can be set post construction if necessary */ + private final long query_start; + + /** The remote address as :, may be ipv6 */ + private final String remote_address; + + /** The TSQuery object that contains the query specification */ + private final TSQuery query; + + /** Amount of time taken for the query to complete, set on {@link markComplete} */ + private long time_total; + + /** Time it took to retrieve data from storage in ms*/ + private long time_storage; + + /** Time it took to aggregate over the data */ + private long time_aggregation; + + /** Time it took to serialize the data. Includes aggregation time and tag + * lookups */ + private long time_serialization; + + /** Number of data points emitted, NOT the number of data points fetched */ + private long size; + + /** Total number of data points fetched from storage */ + private long aggregated_size; + + /** HTTP response when the query was completed, either successfully or failed */ + private HttpResponseStatus response; + + /** How many times this exact query was executed. Only updated on completion */ + private long executed; + + /** + * Default CTor + * @param remote_address Remote address of the client + * @param query Query being executed + * @throws QueryException if the exact query is already running, e.g if the + * client submitted the same query twice + */ + public QueryStats(final String remote_address, final TSQuery query) { + if (remote_address == null || remote_address.isEmpty()) { + throw new IllegalArgumentException("Remote address was null or empty"); + } + if (query == null) { + throw new IllegalArgumentException("Query object was null"); + } + this.remote_address = remote_address; + this.query = query; + executed = 1; + query_start = DateTime.currentTimeMillis(); + LOG.debug("New query for remote " + remote_address + " with hash " + + hashCode() + " on thread " + Thread.currentThread().getId()); + if (running_queries.putIfAbsent(this.hashCode(), this) != null) { + throw new QueryException("Query is already executing for endpoint: " + + remote_address); + } + LOG.debug("Successfully put new query for remote " + remote_address + + " with hash " + hashCode() + " on thread " + + Thread.currentThread().getId() + " w q " + query.toString()); + } + + /** + * Returns the hash based on the remote address and the query + */ + public int hashCode() { + return Objects.hashCode(remote_address, query.hashCode()); + } + + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof QueryStats)) { + return false; + } + if (obj == this) { + return true; + } + final QueryStats stats = (QueryStats)obj; + return Objects.equal(remote_address, stats.remote_address) + && Objects.equal(query, stats.query); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(256); + buf.append("remote=") + .append(remote_address) + .append(", query=") + .append(query) + .append(", start=") + .append(query_start); + return buf.toString(); + } + + /** + * Marks a query as completed successfully with the 200 HTTP response code. + * Moves it from the running map to the cache, updating the cache if it already + * existed. + */ + public void markComplete() { + markComplete(HttpResponseStatus.OK); + } + + /** + * Marks a query as completed with the given HTTP code and moves it from the + * running map to the cache, updating the cache if it already existed. + * @param response + */ + public void markComplete(final HttpResponseStatus response) { + LOG.debug("Marking query as complete for " + remote_address + " with hash " + + hashCode() + " on thread " + Thread.currentThread().getId() + " And q: " + + query.toString()); + this.response = response; + time_total = DateTime.currentTimeMillis() - query_start; + synchronized (running_queries) { + if (!running_queries.containsKey(this.hashCode())) { + //throw new IllegalDataException("Query was already marked as complete"); + LOG.error("Query was already marked as complete: " + this); + return; + } + running_queries.remove(this.hashCode()); + LOG.debug("Removed completed query " + remote_address + " with hash " + + hashCode() + " on thread " + Thread.currentThread().getId()); + } + + final int cache_hash = this.hashCode() ^ response.toString().hashCode(); + synchronized (completed_queries) { + final QueryStats old_query = completed_queries.getIfPresent(cache_hash); + if (old_query == null) { + completed_queries.put(cache_hash, this); + } else { + old_query.executed++; + } + } + LOG.info("query=" + JSON.serializeToString(buildStats())); + } + + /** + * Builds a serializable map from the running and cached query maps to be + * returned to a caller. + * @return A map for serialization + */ + public static Map>> buildStats() { + Map>> root = + new HashMap>>(); + + if (running_queries.isEmpty()) { + root.put("running", Collections.> emptyList()); + } else { + final List> running = + new ArrayList>(running_queries.size()); + root.put("running", running); + + // don't need to lock the map beyond what the iterator will do implicitly + for (final QueryStats stats : running_queries.values()) { + final Map obj = new HashMap(10); + obj.put("query", stats.query); + obj.put("remote", stats.remote_address); + obj.put("queryStart", stats.query_start); + obj.put("timeTotal", stats.time_total); + obj.put("elapsed", DateTime.currentTimeMillis() - stats.query_start); + running.add(obj); + } + } + + final Map completed = completed_queries.asMap(); + if (completed.isEmpty()) { + root.put("completed", Collections.> emptyList()); + } else { + final List> running = + new ArrayList>(completed.size()); + root.put("completed", running); + + // don't need to lock the map beyond what the iterator will do implicitly + for (final QueryStats stats : completed.values()) { + final Map obj = new HashMap(10); + obj.put("query", stats.query); + obj.put("remote", stats.remote_address); + obj.put("queryStart", stats.query_start); + obj.put("timeTotal", stats.time_total); + obj.put("executed", stats.executed); + obj.put("datapoints", stats.size); + obj.put("rawDatapoints", stats.aggregated_size); + obj.put("status", stats.response.getCode()); + obj.put("timeStorage", stats.time_storage); + obj.put("timeAggregation", stats.time_aggregation); + obj.put("timeSerialization", stats.time_serialization); + running.add(obj); + } + } + + return root; + } + + /** + * Fetches data about the running queries and status of queries in the cache + * @param collector The collector to write to + */ + public static void collectStats(final StatsCollector collector) { + collector.record("query.count", running_queries.size(), "type=running"); + + final Map completed = completed_queries.asMap(); + int completed_success = 0; + int completed_error = 0; + for (final QueryStats stats : completed.values()) { + if (stats.response == HttpResponseStatus.OK) { + completed_success += stats.executed; + } else { + completed_error += stats.executed; + } + } + + collector.record("query.count", completed_success, "type=successful"); + collector.record("query.count", completed_error, "type=failed"); + } + + /** @return the start time of the query in ms */ + public long getQueryStart() { + return query_start; + } + + /** @return the total number of data points emitted for the query */ + public long getSize() { + return size; + } + + /** @param size increments the number of data points emitted */ + public void addSize(int size) { + this.size += size; + } + + /** @return the total number of data points retrieved from storage */ + public long getAggregatedSize() { + return aggregated_size; + } + + /** @param size increments the number of data points retrieved from storage */ + public void addAggregatedSize(int size) { + aggregated_size += size; + } + + /** @param time_storage the amount of time it took to fetch data from storage in ms */ + public void setTimeStorage(final long time_storage) { + this.time_storage = time_storage; + } + + /** @return the amount of time it took to fetch data from storage in ms */ + public long getTimeStorage() { + return time_storage; + } + + /** @param time_aggregation increments the amount of time spent aggregating in ms */ + public void addTimeAggregation(final long time_aggregation) { + this.time_aggregation += time_aggregation; + } + + /** @return the mount of time spent aggregating in ms */ + public long getTimeAggregation() { + return time_aggregation; + } + + /** @param time_serialization the amount of time spent serializing in ms */ + public void setTimeSerialization(final long time_serialization) { + this.time_serialization = time_serialization; + } + + /** @return the mount of time spent serializing in ms */ + public long getTimeSerialization() { + return time_serialization; + } + + /** @return the amount of time working on the query in ms */ + public long getTimeTotal() { + return time_total; + } +} diff --git a/test/stats/TestQueryStats.java b/test/stats/TestQueryStats.java new file mode 100644 index 0000000000..aec567932f --- /dev/null +++ b/test/stats/TestQueryStats.java @@ -0,0 +1,249 @@ +// 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.stats; + +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 java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.google.common.cache.CacheBuilder; + +import net.opentsdb.core.QueryException; +import net.opentsdb.core.TSQuery; + +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +public final class TestQueryStats { + + private static String remote = "192.168.1.1:4242"; + private static Field running_queries; + static { + try { + running_queries = QueryStats.class.getDeclaredField("running_queries"); + running_queries.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException("Failed in static initializer", e); + } + } + private static Field completed_queries; + static { + try { + completed_queries = QueryStats.class.getDeclaredField("completed_queries"); + completed_queries.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException("Failed in static initializer", e); + } + } + + @Before + public void before() throws Exception { + running_queries.set(null, new ConcurrentHashMap()); + completed_queries.set(null, CacheBuilder.newBuilder().maximumSize(2).build()); + } + + @Test + public void ctor() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertNotNull(stats); + final Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(1, map.get("running").size()); + assertEquals(0, map.get("completed").size()); + } + + @Test (expected = QueryException.class) + public void ctorDuplicate() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertNotNull(stats); + final Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(1, map.get("running").size()); + assertEquals(0, map.get("completed").size()); + new QueryStats(remote, query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullRemote() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + new QueryStats(null, query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullQuery() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + new QueryStats(remote, null); + } + + @Test + public void testHashCodeandEquals() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertNotNull(stats); + final int hash_a = stats.hashCode(); + + // have to mark the old one as complete before we can test equality + stats.markComplete(); + + final TSQuery query2 = new TSQuery(); + query2.setStart("1h-ago"); + final QueryStats stats2 = new QueryStats(remote, query2); + assertNotNull(stats); + assertEquals(hash_a, stats2.hashCode()); + assertEquals(stats, stats2); + assertFalse(stats == stats2); + } + + @Test + public void testHashCodeandNotEquals() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertNotNull(stats); + final int hash_a = stats.hashCode(); + + final TSQuery query2 = new TSQuery(); + query2.setStart("2h-ago"); + final QueryStats stats2 = new QueryStats(remote, query2); + assertNotNull(stats); + assertTrue(hash_a != stats2.hashCode()); + assertFalse(stats.equals(stats2)); + assertFalse(stats == stats2); + } + + @Test + public void testEqualsNull() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertFalse(stats.equals(null)); + } + + @Test + public void testEqualsWrongType() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertFalse(stats.equals(new String("foo"))); + } + + @Test + public void testEqualsSame() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + assertTrue(stats.equals(stats)); + } + + @Test + public void markComplete() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + stats.markComplete(); + final Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + final Map completed = map.get("completed").get(0); + assertEquals(200, completed.get("status")); + } + + @Test + public void markCompleteTimeout() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + final Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + final Map completed = map.get("completed").get(0); + assertEquals(408, completed.get("status")); + } + + @Test + public void markCompleteDoubleMark() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + stats.markComplete(); + final Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + Map completed = map.get("completed").get(0); + assertEquals(200, completed.get("status")); + stats.markComplete(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + completed = map.get("completed").get(0); + assertEquals(200, completed.get("status")); + } + + @Test + public void executed() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + final Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + final Map completed = map.get("completed").get(0); + assertEquals(1L, completed.get("executed")); + } + + @Test + public void executedTwice() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + QueryStats stats = new QueryStats(remote, query); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + Map>> map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + Map completed = map.get("completed").get(0); + assertEquals(1L, completed.get("executed")); + + stats = new QueryStats(remote, query); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + map = QueryStats.buildStats(); + assertNotNull(map); + assertEquals(0, map.get("running").size()); + assertEquals(1, map.get("completed").size()); + completed = map.get("completed").get(0); + assertEquals(2L, completed.get("executed")); + } +} From f15304e0de962a76cc4e348fc655ddf59656abb2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 19:14:53 -0700 Subject: [PATCH 114/826] Add hashCode() and equals() overrides to the RateOptions, TSQuery and TSSubQuery classes needed for the QueryStats tracking so we can lookup queries in the maps. --- src/core/RateOptions.java | 24 ++ src/core/TSQuery.java | 38 +++ src/core/TSSubQuery.java | 34 +++ test/core/TestTSQuery.java | 476 +++++++++++++++++++++++++++++++++- test/core/TestTSSubQuery.java | 357 +++++++++++++++++++++++++ 5 files changed, 928 insertions(+), 1 deletion(-) diff --git a/src/core/RateOptions.java b/src/core/RateOptions.java index abf1b0f1ee..0265e19aee 100644 --- a/src/core/RateOptions.java +++ b/src/core/RateOptions.java @@ -12,6 +12,8 @@ // see . package net.opentsdb.core; +import com.google.common.base.Objects; + /** * Provides additional options that will be used when calculating rates. These * options are useful when working with metrics that are raw counter values, @@ -72,6 +74,28 @@ public RateOptions(final boolean counter, final long counter_max, this.reset_value = reset_value; } + @Override + public int hashCode() { + return Objects.hashCode(counter, counter_max, reset_value); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof RateOptions)) { + return false; + } + if (obj == this) { + return true; + } + final RateOptions options = (RateOptions)obj; + return Objects.equal(counter, options.counter) + && Objects.equal(counter_max, options.counter_max) + && Objects.equal(reset_value, options.reset_value); + } + /** @return Whether or not the counter flag is set */ public boolean isCounter() { return counter; diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 9ba5248ae0..1178f53998 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.Map; +import com.google.common.base.Objects; + import net.opentsdb.utils.DateTime; /** @@ -82,6 +84,42 @@ public TSQuery() { } + @Override + public int hashCode() { + // NOTE: Do not add any non-user submitted variables to the hash. We don't + // want the hash to change after validation. + return Objects.hashCode(start, end, timezone, options, padding, + no_annotations, with_global_annotations, show_tsuids, queries, + ms_resolution); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TSQuery)) { + return false; + } + if (obj == this) { + return true; + } + + // NOTE: Do not add any non-user submitted variables to the comparator. We + // don't want the value to change after validation. + final TSQuery query = (TSQuery)obj; + return Objects.equal(start, query.start) + && Objects.equal(end, query.end) + && Objects.equal(timezone, query.timezone) + && Objects.equal(options, query.options) + && Objects.equal(padding, query.padding) + && Objects.equal(no_annotations, query.no_annotations) + && Objects.equal(with_global_annotations, query.with_global_annotations) + && Objects.equal(show_tsuids, query.show_tsuids) + && Objects.equal(queries, query.queries) + && Objects.equal(ms_resolution, query.ms_resolution); + } + /** * Runs through query parameters to make sure it's a valid request. * This includes parsing relative timestamps, verifying that the end time is diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index de28b45cec..d2126cf528 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -18,6 +18,8 @@ import java.util.Map; import java.util.NoSuchElementException; +import com.google.common.base.Objects; + import net.opentsdb.utils.DateTime; /** @@ -75,6 +77,38 @@ public final class TSSubQuery { public TSSubQuery() { } + + @Override + public int hashCode() { + // NOTE: Do not add any non-user submitted variables to the hash. We don't + // want the hash to change after validation. + return Objects.hashCode(aggregator, metric, tsuids, tags, downsample, rate, + rate_options); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TSSubQuery)) { + return false; + } + if (obj == this) { + return true; + } + + // NOTE: Do not add any non-user submitted variables to the comparator. We + // don't want the value to change after validation. + final TSSubQuery query = (TSSubQuery)obj; + return Objects.equal(aggregator, query.aggregator) + && Objects.equal(metric, query.metric) + && Objects.equal(tsuids, query.tsuids) + && Objects.equal(tags, query.tags) + && Objects.equal(downsample, query.downsample) + && Objects.equal(rate, query.rate) + && Objects.equal(rate_options, query.rate_options); + } public String toString() { final StringBuilder buf = new StringBuilder(); diff --git a/test/core/TestTSQuery.java b/test/core/TestTSQuery.java index 894ae23586..1e905a7490 100644 --- a/test/core/TestTSQuery.java +++ b/test/core/TestTSQuery.java @@ -13,10 +13,16 @@ package net.opentsdb.core; 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.Matchers.anyString; import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.HashMap; + +import net.opentsdb.utils.DateTime; import org.junit.Test; import org.junit.runner.RunWith; @@ -25,7 +31,7 @@ import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) -@PrepareForTest({ TSQuery.class }) +@PrepareForTest({ TSQuery.class, DateTime.class }) public final class TestTSQuery { @Test @@ -102,6 +108,474 @@ public void validateEmptyQueries() { q.validateAndSetQuery(); } + // NOTE: Each of the hash and equals tests should make sure that we the code + // doesn't change after validation. + + @Test + public void testHashCodeandEqualsStart() { + TSQuery sub1 = getMetricForValidate(); + final int hash_a = sub1.hashCode(); + sub1.setStart("1356998300"); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setStart("1356998300"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsStartNull() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setStart(null); + assertTrue(hash_a != sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsStartInvalid() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setStart("1h-ago"); + assertTrue(hash_a != sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test + public void testHashCodeandEqualsEnd() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setEnd("1356998490"); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setEnd("1356998490"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsEndNull() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setEnd(null); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + // this is ok since we assume "now" if end is missing + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setEnd(null); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsEndInvalid() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setEnd("1356998300"); + assertTrue(hash_a != sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test + public void testHashCodeandEqualsTimezone() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setTimezone("America/New_York"); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setTimezone("America/New_York"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsTimezoneInvalid() throws Exception { + // silly test isn't calling into the real method, mockit! + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.parseDateTimeString(anyString(), anyString())) + .thenThrow(new IllegalArgumentException("Invalid timezone")); + + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setTimezone("Not a timezone"); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setTimezone("Not a timezone"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsOptions() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + HashMap> options = + new HashMap>(2); + ArrayList params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + options.put("label", params); + sub1.setOptions(options); + + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + + options = new HashMap>(2); + params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + options.put("label", params); + sub2.setOptions(options); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsOptionsNewPut() { + TSQuery sub1 = getMetricForValidate(); + HashMap> options = + new HashMap>(3); + ArrayList params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + options.put("label", params); + sub1.setOptions(options); + + final int hash_a = sub1.hashCode(); + + params = new ArrayList(1); + params.add("top"); + options.put("key", params); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + + options = new HashMap>(2); + params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + options.put("label", params); + params = new ArrayList(1); + params.add("top"); + options.put("key", params); + sub2.setOptions(options); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsOptionsNewParam() { + TSQuery sub1 = getMetricForValidate(); + HashMap> options = + new HashMap>(3); + ArrayList params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + options.put("label", params); + sub1.setOptions(options); + + final int hash_a = sub1.hashCode(); + + params = new ArrayList(1); + params.add("cycles"); + options.put("label", params); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + + options = new HashMap>(2); + params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("cycles"); + options.put("label", params); + params = new ArrayList(1);; + sub2.setOptions(options); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsOptionsExtraParam() { + TSQuery sub1 = getMetricForValidate(); + HashMap> options = + new HashMap>(3); + ArrayList params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + options.put("label", params); + sub1.setOptions(options); + + final int hash_a = sub1.hashCode(); + + options.get("label").add("extra"); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + + options = new HashMap>(2); + params = new ArrayList(1); + params.add("1419x576"); + options.put("wxh", params); + params = new ArrayList(1); + params.add("latency"); + params.add("extra"); + options.put("label", params); + params = new ArrayList(1);; + sub2.setOptions(options); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsPadding() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setPadding(true); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setPadding(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsNoAnnotations() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setNoAnnotations(true); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setNoAnnotations(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsWithGlobalAnnotations() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setGlobalAnnotations(true); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setGlobalAnnotations(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsShowTSUIDs() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setShowTSUIDs(true); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setShowTSUIDs(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsMSResolution() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setMsResolution(true); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setMsResolution(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsNewSubQuery() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.getQueries().add(TestTSSubQuery.getBaseQuery()); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.getQueries().add(TestTSSubQuery.getBaseQuery()); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsChangeSubQuery() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.getQueries().get(0).setMetric("foo"); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.getQueries().get(0).setMetric("foo"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsEmptySubQueries() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setQueries(new ArrayList(0)); + assertTrue(hash_a != sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsNullSubQueries() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setQueries(null); + assertTrue(hash_a != sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test + public void testEqualsNull() { + TSQuery sub1 = getMetricForValidate(); + assertFalse(sub1.equals(null)); + } + + @Test + public void testEqualsWrongType() { + TSQuery sub1 = getMetricForValidate(); + assertFalse(sub1.equals(new String("Foobar"))); + } + + @Test + public void testEqualsSame() { + TSQuery sub1 = getMetricForValidate(); + assertTrue(sub1.equals(sub1)); + } + + /** + * Sets up an object with good, common values for testing the validation + * function with an query string query. Each test can "set" the + * method it wants to fool with and call .validateAndSetQuery() + * Warning: This method calls into {@link TestTSQuery} + * @return A query object + */ private TSQuery getMetricForValidate() { final TSQuery query = new TSQuery(); query.setStart("1356998400"); diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index eac7bcf291..235a3c21e8 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -13,11 +13,14 @@ package net.opentsdb.core; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import org.junit.Test; @@ -113,6 +116,360 @@ public void validateBadDS() { sub.validateAndSetQuery(); } + // NOTE: Each of the hash and equals tests should make sure that we the code + // doesn't change after validation. + + @Test + public void testHashCodeandEqualsAggregator() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setAggregator("max"); + final int has_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(has_b, sub1.hashCode()); + + final TSSubQuery sub2 = getBaseQuery(); + sub2.setAggregator("max"); + + assertEquals(has_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsAggregatorNull() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setAggregator(null); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsAggregatorNonExistant() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setAggregator("nosuchagg"); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test + public void testHashCodeandEqualsMetric() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setMetric("foo"); + assertEquals(hash_a, sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_a, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setMetric("foo"); + + assertEquals(hash_a, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsMetricNull() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setMetric(null); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test + public void testHashCodeandEqualsTSUIDs() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + List tsuids = new ArrayList(2); + tsuids.add("01010101"); + tsuids.add("01010102"); + sub1.setTsuids(tsuids); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + List tsuids2 = new ArrayList(2); + tsuids2.add("01010101"); + tsuids2.add("01010102"); + sub2.setTsuids(tsuids2); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsTSUIDsChange() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + List tsuids = new ArrayList(2); + tsuids.add("01010101"); + tsuids.add("01010102"); + sub1.setTsuids(tsuids); + + tsuids.set(1, "01010103"); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + List tsuids2 = new ArrayList(2); + tsuids2.add("01010101"); + tsuids2.add("01010103"); + sub2.setTsuids(tsuids2); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsTag() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.getTags().put("host", "web02"); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.getTags().put("host", "web02"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsTags() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.getTags().put("host", "web02"); + sub1.getTags().put("foo", "bar"); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.getTags().put("host", "web02"); + sub2.getTags().put("foo", "bar"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsTagsNull() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setTags(null); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setTags(null); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsDownsampler() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setDownsample("1h-avg"); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setDownsample("1h-avg"); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test (expected = IllegalArgumentException.class) + public void testHashCodeandEqualsDownsamplerInvalid() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setDownsample("bad ds"); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + } + + @Test + public void testHashCodeandEqualsRate() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setRate(false); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setRate(false); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsRateOptionsSameNew() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setRateOptions(new RateOptions(true, 1024, 16)); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsRateOptionsNotCounter() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setRateOptions(new RateOptions(false, 1024, 16)); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setRateOptions(new RateOptions(false, 1024, 16)); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsRateOptionsNewMax() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setRateOptions(new RateOptions(true, 768, 16)); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setRateOptions(new RateOptions(true, 768, 16)); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsRateOptionsNewReset() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setRateOptions(new RateOptions(true, 1024, 32)); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setRateOptions(new RateOptions(true, 1024, 32)); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testHashCodeandEqualsRateOptionsNull() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setRateOptions(null); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setRateOptions(null); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + + @Test + public void testEqualsNull() { + final TSSubQuery sub1 = getBaseQuery(); + assertFalse(sub1.equals(null)); + } + + @Test + public void testEqualsWrongType() { + final TSSubQuery sub1 = getBaseQuery(); + assertFalse(sub1.equals(new String("Foobar"))); + } + + @Test + public void testEqualsSame() { + final TSSubQuery sub1 = getBaseQuery(); + assertTrue(sub1.equals(sub1)); + } + + /** @return a sub query object with some defaults set for testing */ + public static TSSubQuery getBaseQuery() { + TSSubQuery query = new TSSubQuery(); + query.setAggregator("sum"); + query.setMetric("foo"); + HashMap tags = new HashMap(2); + tags.put("host", "web01"); + tags.put("dc", "lax"); + query.setTags(tags); + query.setRate(true); + query.setRateOptions(new RateOptions(true, 1024, 16)); + return query; + } + /** * Sets up an object with good, common values for testing the validation * function with an "m" type query (no tsuids). Each test can "set" the From 89a0e3c3c7f8e62b671d9a74bbb87b4ec7370367 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 20:50:39 -0700 Subject: [PATCH 115/826] Add the MockDataPoints class for unit tests --- Makefile.am | 1 + test/storage/MockDataPoints.java | 137 +++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 test/storage/MockDataPoints.java diff --git a/Makefile.am b/Makefile.am index b92af5d065..8a03860985 100644 --- a/Makefile.am +++ b/Makefile.am @@ -193,6 +193,7 @@ test_SRC := \ test/stats/TestHistogram.java \ test/stats/TestQueryStats.java \ test/storage/MockBase.java \ + test/storage/MockDataPoints.java \ test/tools/TestDumpSeries.java \ test/tools/TestFsck.java \ test/tools/TestTextImporter.java \ diff --git a/test/storage/MockDataPoints.java b/test/storage/MockDataPoints.java new file mode 100644 index 0000000000..dc52724088 --- /dev/null +++ b/test/storage/MockDataPoints.java @@ -0,0 +1,137 @@ +// 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.storage; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Ignore; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.SeekableView; +import net.opentsdb.meta.Annotation; + +/** + * A class that implements a mock of the DataPoints and DataPoint interfaces + * for use in serializing data out to the user. + */ +@Ignore +public class MockDataPoints { + + private final DataPoints dps = mock(DataPoints.class); + private final DataPoint dp = mock(DataPoint.class); + private final SeekableView it = mock(SeekableView.class); + + private long timestamp = 1356998400000L; + private int interval = 300000; // in ms + private long value = 0; + private int limit = 400; // only checks timeout every 100 dps + + private final String metric = "system.cpu.user"; + private final Map tags = new HashMap(1); + private final List agg_tags = new ArrayList(1); + private final List tsuids = new ArrayList(2); + private final List annotations = new ArrayList(1); + + /** + * Default Ctor that stores some values in the tags and tsuid as well as a + * single annotation. + */ + public MockDataPoints() { + tags.put("dc", "lga"); + agg_tags.add("host"); + tsuids.add("000001000001000001"); + tsuids.add("000001000001000002"); + + final Annotation note = new Annotation(); + note.setTSUID("000001000001000001"); + note.setStartTime(1356998401000L); + note.setDescription("Just a simple note"); + annotations.add(note); + } + + /** + * Retrieves the DataPoints object and sets up the mock calls + * @return A DataPoints object for iteration + */ + public DataPoints getMock() { + when(dps.metricName()).thenReturn(metric); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(metric)); + when(dps.getTags()).thenReturn(tags); + when(dps.getTagsAsync()).thenReturn(Deferred.fromResult(tags)); + when(dps.getAggregatedTags()).thenReturn(agg_tags); + when(dps.getAggregatedTagsAsync()).thenReturn(Deferred.fromResult(agg_tags)); + when(dps.getTSUIDs()).thenReturn(tsuids); + when(dps.getAnnotations()).thenReturn(annotations); + when(dps.size()).thenReturn(limit); + when(dps.aggregatedSize()).thenReturn(limit * 2); + when(dps.iterator()).thenReturn(it); + when(dps.getQueryIndex()).thenReturn(0); + + // iterator mocking + when(it.hasNext()).thenAnswer(new Answer() { + @Override + public Boolean answer(final InvocationOnMock args) throws Throwable { + if (value > limit) { + return false; + } + return true; + } + }); + when(it.next()).thenAnswer(new Answer() { + @Override + public DataPoint answer(final InvocationOnMock args) throws Throwable { + value++; + timestamp += interval; + return dp; + } + }); + doThrow(new RuntimeException("OpenTSDB doesn't support remove")) + .when(it).remove(); + + // data point mocking + when(dp.timestamp()).thenAnswer(new Answer() { + @Override + public Long answer(final InvocationOnMock args) throws Throwable { + return timestamp; + } + }); + when(dp.isInteger()).thenReturn(true); + when(dp.longValue()).thenAnswer(new Answer() { + @Override + public Long answer(final InvocationOnMock args) throws Throwable { + return value; + } + }); + return dps; + } + + /** + * Returns the DataPoint object so that the test can override the mocks. + * @return A DataPoint in the DataPoints class + */ + public DataPoint getMockDP() { + return dp; + } +} From 3ec02a5bc6ba1de6ab5c385dd62a13b8a828aa3a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 21:59:24 -0700 Subject: [PATCH 116/826] Add an exception to the QueryStats class for greater detail into issues with queries. --- src/stats/QueryStats.java | 29 ++++++++++++++++++++++++----- test/stats/TestQueryStats.java | 8 ++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 6da9e04ee5..42b4045afd 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -97,6 +97,9 @@ public class QueryStats { /** How many times this exact query was executed. Only updated on completion */ private long executed; + /** A possible exception if thrown when this query completes */ + private Throwable exception; + /** * Default CTor * @param remote_address Remote address of the client @@ -156,7 +159,9 @@ public String toString() { .append(", query=") .append(query) .append(", start=") - .append(query_start); + .append(query_start) + .append(", exception=") + .append(exception == null ? "null" : exception.getMessage()); return buf.toString(); } @@ -166,15 +171,18 @@ public String toString() { * existed. */ public void markComplete() { - markComplete(HttpResponseStatus.OK); + markComplete(HttpResponseStatus.OK, null); } /** * Marks a query as completed with the given HTTP code and moves it from the * running map to the cache, updating the cache if it already existed. - * @param response + * @param response The HttpStatus code to store + * @param exception An optional exception */ - public void markComplete(final HttpResponseStatus response) { + public void markComplete(final HttpResponseStatus response, + final Throwable exception) { + this.exception = exception; LOG.debug("Marking query as complete for " + remote_address + " with hash " + hashCode() + " on thread " + Thread.currentThread().getId() + " And q: " + query.toString()); @@ -200,7 +208,7 @@ public void markComplete(final HttpResponseStatus response) { old_query.executed++; } } - LOG.info("query=" + JSON.serializeToString(buildStats())); + LOG.info("completed_query=" + JSON.serializeToString(this)); } /** @@ -253,6 +261,7 @@ public static Map>> buildStats() { obj.put("timeStorage", stats.time_storage); obj.put("timeAggregation", stats.time_aggregation); obj.put("timeSerialization", stats.time_serialization); + obj.put("exception", stats.exception); running.add(obj); } } @@ -341,4 +350,14 @@ public long getTimeSerialization() { public long getTimeTotal() { return time_total; } + + /** @return the Http status code */ + public HttpResponseStatus getStatus() { + return response; + } + + /** @return an exception if it was associated with this query */ + public Throwable getException() { + return exception; + } } diff --git a/test/stats/TestQueryStats.java b/test/stats/TestQueryStats.java index aec567932f..5ee4dcf163 100644 --- a/test/stats/TestQueryStats.java +++ b/test/stats/TestQueryStats.java @@ -181,7 +181,7 @@ public void markCompleteTimeout() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); final Map>> map = QueryStats.buildStats(); assertNotNull(map); assertEquals(0, map.get("running").size()); @@ -215,7 +215,7 @@ public void executed() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); final Map>> map = QueryStats.buildStats(); assertNotNull(map); assertEquals(0, map.get("running").size()); @@ -229,7 +229,7 @@ public void executedTwice() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); Map>> map = QueryStats.buildStats(); assertNotNull(map); assertEquals(0, map.get("running").size()); @@ -238,7 +238,7 @@ public void executedTwice() throws Exception { assertEquals(1L, completed.get("executed")); stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT); + stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); map = QueryStats.buildStats(); assertNotNull(map); assertEquals(0, map.get("running").size()); From 256c142674a453796ea26d43a65f458f0ab77588 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:00:53 -0700 Subject: [PATCH 117/826] Add a fully asynchronous serializer for queries in the HttpSerializer class to stop blocking threads when a TSD is handling multiple queries. --- src/tsd/HttpJsonSerializer.java | 256 ++++++++++++++-- src/tsd/HttpSerializer.java | 34 +++ test/tsd/TestHttpJsonSerializer.java | 437 ++++++++++++++++++++++++++- 3 files changed, 695 insertions(+), 32 deletions(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index c0510a576f..996f0328c7 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -26,28 +26,31 @@ import org.jboss.netty.buffer.ChannelBufferOutputStream; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.handler.codec.http.HttpResponseStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.type.TypeReference; +import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.core.QueryException; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; +import net.opentsdb.core.TSSubQuery; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; import net.opentsdb.search.SearchQuery; +import net.opentsdb.stats.QueryStats; import net.opentsdb.tree.Branch; import net.opentsdb.tree.Tree; import net.opentsdb.tree.TreeRule; import net.opentsdb.tsd.AnnotationRpc.AnnotationBulkDelete; import net.opentsdb.tsd.QueryRpc.LastPointQuery; import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; /** @@ -59,9 +62,7 @@ * @since 2.0 */ class HttpJsonSerializer extends HttpSerializer { - private static final Logger LOG = - LoggerFactory.getLogger(HttpJsonSerializer.class); - + /** Type reference for incoming data points */ private static TypeReference> TR_INCOMING = new TypeReference>() {}; @@ -540,31 +541,125 @@ public ChannelBuffer formatUidAssignV1(final */ public ChannelBuffer formatQueryV1(final TSQuery data_query, final List results, final List globals) { + try { + return formatQueryAsyncV1(data_query, results, globals) + .joinUninterruptibly(); + } catch (QueryException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Shouldn't be here", e); + } + } + + /** + * Format the results from a timeseries data query + * @param data_query The TSQuery object used to fetch the results + * @param results The data fetched from storage + * @param globals An optional list of global annotation objects + * @return A Deferred object to pass on to the caller + * @throws IOException if serialization failed + * @since 2.2 + */ + public Deferred formatQueryAsyncV1(final TSQuery data_query, + final List results, final List globals) + throws IOException { + final long start = DateTime.currentTimeMillis(); final boolean as_arrays = this.query.hasQueryStringParam("arrays"); final String jsonp = this.query.getQueryStringParam("jsonp"); - // todo - this should be streamed at some point since it could be HUGE + // buffers and an array list to stored the deferreds final ChannelBuffer response = ChannelBuffers.dynamicBuffer(); final OutputStream output = new ChannelBufferOutputStream(response); - try { - // don't forget jsonp - if (jsonp != null && !jsonp.isEmpty()) { - output.write((jsonp + "(").getBytes(query.getCharset())); + // too bad an inner class can't modify a primitive. This is a work around + final List timeout_flag = new ArrayList(1); + timeout_flag.add(false); + + // start with JSONp if we're told to + if (jsonp != null && !jsonp.isEmpty()) { + output.write((jsonp + "(").getBytes(query.getCharset())); + } + + // start the JSON generator and write the opening array + final JsonGenerator json = JSON.getFactory().createGenerator(output); + json.writeStartArray(); + + /** + * Every individual data point set (the result of a query and possibly a + * group by) will initiate an asynchronous metric/tag UID to name resolution + * and then print to the buffer. + * NOTE that because this is asynchronous, the order of results is + * indeterminate. + */ + class DPsResolver implements Callback, Object> { + /** Has to be final to be shared with the nested classes */ + final StringBuilder metric = new StringBuilder(256); + /** Resolved tags */ + final Map tags = new HashMap(); + /** Resolved aggregated tags */ + final List agg_tags = new ArrayList(); + /** A list storing the metric and tag resolve calls */ + final List> resolve_deferreds = + new ArrayList>(); + /** The data points to serialize */ + final DataPoints dps; + + public DPsResolver(final DataPoints dps) { + this.dps = dps; } - JsonGenerator json = JSON.getFactory().createGenerator(output); - json.writeStartArray(); - for (DataPoints[] separate_dps : results) { - for (DataPoints dps : separate_dps) { + /** Resolves the metric UID to a name*/ + class MetricResolver implements Callback { + public Object call(final String metric) throws Exception { + DPsResolver.this.metric.append(metric); + return null; + } + } + + /** Resolves the tag UIDs to a key/value string set */ + class TagResolver implements Callback> { + public Object call(final Map tags) throws Exception { + DPsResolver.this.tags.putAll(tags); + return null; + } + } + + /** Resolves aggregated tags */ + class AggTagResolver implements Callback> { + public Object call(final List tags) throws Exception { + DPsResolver.this.agg_tags.addAll(tags); + return null; + } + } + + /** After the metric and tags have been resolved, this will print the + * results to the output buffer in the proper format. + */ + class WriteToBuffer implements Callback> { + final DataPoints dps; + + /** + * Default ctor that takes a data point set + * @param dps Datapoints to print + */ + public WriteToBuffer(final DataPoints dps) { + this.dps = dps; + } + + /** + * Handles writing the data to the output buffer. The results of the + * deferreds don't matter as they will be stored in the class final + * variables. + */ + public Object call(final ArrayList deferreds) throws Exception { + json.writeStartObject(); - - json.writeStringField("metric", dps.metricName()); + json.writeStringField("metric", metric.toString()); json.writeFieldName("tags"); json.writeStartObject(); if (dps.getTags() != null) { - for (Map.Entry tag : dps.getTags().entrySet()) { + for (Map.Entry tag : tags.entrySet()) { json.writeStringField(tag.getKey(), tag.getValue()); } } @@ -573,12 +668,18 @@ public ChannelBuffer formatQueryV1(final TSQuery data_query, json.writeFieldName("aggregateTags"); json.writeStartArray(); if (dps.getAggregatedTags() != null) { - for (String atag : dps.getAggregatedTags()) { + for (String atag : agg_tags) { json.writeString(atag); } } json.writeEndArray(); + if (data_query.getShowQuery()) { + final TSSubQuery orig_query = data_query.getQueries() + .get(dps.getQueryIndex()); + json.writeObjectField("query", orig_query); + } + if (data_query.getShowTSUIDs()) { json.writeFieldName("tsuids"); json.writeStartArray(); @@ -611,11 +712,13 @@ public ChannelBuffer formatQueryV1(final TSQuery data_query, } } - // now the fun stuff, dump the data + // now the fun stuff, dump the data and time just the iteration over + // the data points + final long dps_start = DateTime.currentTimeMillis(); json.writeFieldName("dps"); // default is to write a map, otherwise write arrays - if (as_arrays) { + if (!timeout_flag.get(0) && as_arrays) { json.writeStartArray(); for (final DataPoint dp : dps) { if (dp.timestamp() < data_query.startTime() || @@ -634,7 +737,7 @@ public ChannelBuffer formatQueryV1(final TSQuery data_query, json.writeEndArray(); } json.writeEndArray(); - } else { + } else if (!timeout_flag.get(0)) { json.writeStartObject(); for (final DataPoint dp : dps) { if (dp.timestamp() < (data_query.startTime()) || @@ -650,25 +753,104 @@ public ChannelBuffer formatQueryV1(final TSQuery data_query, } } json.writeEndObject(); + } else { + // skipping data points all together due to timeout + json.writeStartObject(); + json.writeEndObject(); + } + + final long agg_time = DateTime.currentTimeMillis() - dps_start; + data_query.getQueryStats().addTimeAggregation(agg_time); + data_query.getQueryStats().addAggregatedSize(dps.aggregatedSize()); + data_query.getQueryStats().addSize(dps.size()); + + if (!timeout_flag.get(0) && data_query.getShowStats()) { + json.writeFieldName("stats"); + json.writeStartObject(); + json.writeNumberField("datapoints", dps.size()); + json.writeNumberField("rawDatapoints", dps.aggregatedSize()); + json.writeNumberField("aggregationTime", agg_time); + json.writeNumberField("timeSeries", dps.getTSUIDs().size()); + // todo - timing for just this query + json.writeEndObject(); } // close the results for this particular query json.writeEndObject(); + return null; } } - - // close - json.writeEndArray(); - json.close(); - if (jsonp != null && !jsonp.isEmpty()) { - output.write(")".getBytes()); + /** + * When called, initiates a resolution of metric and tag UIDs to names, + * then prints to the output buffer once they are completed. + */ + public Deferred call(final Object obj) throws Exception { + resolve_deferreds.add(dps.metricNameAsync() + .addCallback(new MetricResolver())); + resolve_deferreds.add(dps.getTagsAsync() + .addCallback(new TagResolver())); + resolve_deferreds.add(dps.getAggregatedTagsAsync() + .addCallback(new AggTagResolver())); + return Deferred.group(resolve_deferreds) + .addCallback(new WriteToBuffer(dps)); + } + + } + + // We want the serializer to execute serially so we need to create a callback + // chain so that when one DPsResolver is finished, it triggers the next to + // start serializing. + final Deferred cb_chain = new Deferred(); + + for (DataPoints[] separate_dps : results) { + for (DataPoints dps : separate_dps) { + try { + cb_chain.addCallback(new DPsResolver(dps)); + } catch (Exception e) { + throw new RuntimeException("Unexpected error durring resolution", e); + } + } + } + + /** Final callback to close out the JSON array and return our results */ + class FinalCB implements Callback { + public ChannelBuffer call(final Object obj) + throws Exception { + data_query.getQueryStats().setTimeSerialization( + DateTime.currentTimeMillis() - start); + data_query.getQueryStats().markComplete(); + + // dump overall stats as an extra object in the array + if (data_query.getShowSummary()) { + final QueryStats stats = data_query.getQueryStats(); + json.writeStartObject(); + json.writeFieldName("statsSummary"); + json.writeStartObject(); + json.writeNumberField("datapoints", stats.getSize()); + json.writeNumberField("rawDatapoints", stats.getAggregatedSize()); + json.writeNumberField("aggregationTime", stats.getTimeAggregation()); + json.writeNumberField("serializationTime", stats.getTimeSerialization()); + json.writeNumberField("storageTime", stats.getTimeStorage()); + json.writeNumberField("timeTotal", stats.getTimeTotal()); + json.writeEndObject(); + json.writeEndObject(); + } + + // IMPORTANT Make sure the close the JSON array and the generator + json.writeEndArray(); + json.close(); + + if (jsonp != null && !jsonp.isEmpty()) { + output.write(")".getBytes()); + } + return response; } - return response; - } catch (IOException e) { - LOG.error("Unexpected exception", e); - throw new RuntimeException(e); } + + // trigger the callback chain here + cb_chain.callback(null); + return cb_chain.addCallback(new FinalCB()); } /** @@ -845,6 +1027,18 @@ public ChannelBuffer formatJVMStatsV1(final Map> sta return serializeJSON(stats); } + /** + * Format the query stats + * @param query_stats Map of query statistics + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.2 + */ + public ChannelBuffer formatQueryStatsV1( + final Map>> query_stats) { + return serializeJSON(query_stats); + } + /** * Format the response from a search query * @param note The query (hopefully filled with results) to serialize diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 05fc82fa50..ba391d6c2e 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.tsd; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -458,6 +459,24 @@ public ChannelBuffer formatQueryV1(final TSQuery query, " has not implemented formatQueryV1"); } + /** + * Format the results from a timeseries data query + * @param query The TSQuery object used to fetch the results + * @param results The data fetched from storage + * @param globals An optional list of global annotation objects + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.2 + */ + public Deferred formatQueryAsyncV1(final TSQuery query, + final List results, final List globals) + throws IOException { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatQueryV1"); + } + /** * Format a list of last data points * @param data_points The results of the query @@ -680,6 +699,21 @@ public ChannelBuffer formatJVMStatsV1(final Map> map " has not implemented formatJVMStatsV1"); } + /** + * Format the query stats + * @param query_stats Map of query statistics + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.2 + */ + public ChannelBuffer formatQueryStatsV1( + final Map>> query_stats) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatQueryStatsV1"); + } + /** * Format the response from a search query * @param results The query (hopefully filled with results) to serialize diff --git a/test/tsd/TestHttpJsonSerializer.java b/test/tsd/TestHttpJsonSerializer.java index 4d4c355d62..a4669a87b1 100644 --- a/test/tsd/TestHttpJsonSerializer.java +++ b/test/tsd/TestHttpJsonSerializer.java @@ -13,32 +13,78 @@ 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.Matchers.anyString; +import static org.mockito.Mockito.when; +import java.lang.Thread.State; +import java.lang.reflect.Field; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.opentsdb.core.DataPoints; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.TSSubQuery; +import net.opentsdb.meta.Annotation; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.storage.MockDataPoints; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; import org.jboss.netty.buffer.ChannelBuffer; 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 com.google.common.cache.CacheBuilder; +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + /** * Unit tests for the JSON serializer. * Note: Tests for the default error handlers are in the TestHttpQuery * class */ @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, HttpQuery.class}) +@PrepareForTest({ HttpJsonSerializer.class, TSDB.class, Config.class, + HttpQuery.class, TSQuery.class, TSSubQuery.class, QueryStats.class, + DateTime.class }) public final class TestHttpJsonSerializer { private TSDB tsdb = null; + private final List timestamp = new ArrayList(1); + private static String remote = "192.168.1.1:4242"; + private static Field running_queries; + static { + try { + running_queries = QueryStats.class.getDeclaredField("running_queries"); + running_queries.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException("Failed in static initializer", e); + } + } + private static Field completed_queries; + static { + try { + completed_queries = QueryStats.class.getDeclaredField("completed_queries"); + completed_queries.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException("Failed in static initializer", e); + } + } @Before public void before() throws Exception { @@ -157,4 +203,393 @@ public void formatSerializersV1() throws Exception { serdes.formatSerializersV1().toString(Charset.forName("UTF-8")) .substring(0, 15)); } + + @Test + public void formatQueryAsyncV1() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357058700\":201")); + assertFalse(json.contains("\"timeTotal\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertFalse(json.contains("\"query\":")); + } + + @Test + public void formatQueryAsyncV1wQuery() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + data_query.setShowQuery(true); + validateTestQuery(data_query); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357058700\":201")); + assertFalse(json.contains("\"timeTotal\"")); + assertTrue(json.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(json.contains("\"query\":")); + } + + @Test + public void formatQueryAsyncV1wStatsSummary() throws Exception { + setupFormatQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, ""); + final HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(true, true); + validateTestQuery(data_query); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + final ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357058700\":201")); + + //assert stats + assertTrue(json.contains("\"stats\":{")); + assertTrue(json.contains("\"datapoints\":400")); + assertTrue(json.contains("\"rawDatapoints\":800")); + assertTrue(json.contains("\"timeSeries\":2")); + + //assert stats summary + assertTrue(json.contains("{\"statsSummary\":{")); + assertTrue(json.contains("\"serializationTime\":1500")); + assertTrue(json.contains("\"storageTime\":0")); + assertTrue(json.contains("\"timeTotal\":2500")); + } + + @Test + public void formatQueryAsyncV1wStatsWoSummary() throws Exception { + setupFormatQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, ""); + final HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(true, false); + validateTestQuery(data_query); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + final ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"stats\":{")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357058700\":201")); + + + //assert stats + assertTrue(json.contains("\"stats\":{")); + assertTrue(json.contains("\"datapoints\":400")); + assertTrue(json.contains("\"rawDatapoints\":800")); + assertTrue(json.contains("\"timeSeries\":2")); + + //assert stats summary + assertFalse(json.contains("{\"statsSummary\":{")); + } + + @Test + public void formatQueryAsyncV1woStatsWSummary() throws Exception { + setupFormatQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, ""); + final HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false, true); + validateTestQuery(data_query); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + final ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357058700\":201")); + + //assert stats + assertFalse(json.contains("\"stats\":{")); + + //assert stats summary + assertTrue(json.contains("{\"statsSummary\":{")); + assertTrue(json.contains("\"serializationTime\":1500")); + assertTrue(json.contains("\"storageTime\":0")); + assertTrue(json.contains("\"timeTotal\":2500")); + } + + @Test + public void formatQueryAsyncV1woStatsWoSummary() throws Exception { + setupFormatQuery(); + final HttpQuery query = NettyMocks.getQuery(tsdb, ""); + final HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false, false); + validateTestQuery(data_query); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + final ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357058700\":201")); + + //assert stats + assertFalse(json.contains("\"stats\":{")); + + //assert stats summary + assertFalse(json.contains("{\"statsSummary\":{")); + } + + @Test + public void formatQueryAsyncTimeFilterV1() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false, false); + final List results = new ArrayList(1); + results.add(new DataPoints[] { new MockDataPoints().getMock() }); + + data_query.setEnd("1357000500"); + validateTestQuery(data_query); + + ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); + assertTrue(json.contains("\"1356998700\":1,")); + assertTrue(json.contains("\"1357000500\":7")); + } + + @Test + public void formatQueryAsyncV1EmptyDPs() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + final List results = new ArrayList(1); + + ChannelBuffer cb = serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertEquals("[]", json); + } + + @Test (expected = DeferredGroupException.class) + public void formatQueryAsyncV1NoSuchMetricId() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + + final DataPoints dps = new MockDataPoints().getMock(); + final List results = new ArrayList(1); + results.add(new DataPoints[] { dps }); + + when(dps.metricNameAsync()) + .thenReturn(Deferred.fromError( + new NoSuchUniqueId("No such metric", new byte[] { 0, 0, 1 }))); + + serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void formatQueryAsyncV1NoSuchTagId() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + + final DataPoints dps = new MockDataPoints().getMock(); + final List results = new ArrayList(1); + results.add(new DataPoints[] { dps }); + + when(dps.getTagsAsync()) + .thenReturn(Deferred.>fromError( + new NoSuchUniqueId("No such tagv", new byte[] { 0, 0, 1 }))); + + serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void formatQueryAsyncV1NoSuchAggTagId() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + + final DataPoints dps = new MockDataPoints().getMock(); + final List results = new ArrayList(1); + results.add(new DataPoints[] { dps }); + + when(dps.getAggregatedTagsAsync()) + .thenReturn(Deferred.>fromError( + new NoSuchUniqueId("No such tagk", new byte[] { 0, 0, 1 }))); + + serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + } + + @Test (expected = NullPointerException.class) + public void formatQueryAsyncV1NullIterator() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + + final DataPoints dps = new MockDataPoints().getMock(); + final List results = new ArrayList(1); + results.add(new DataPoints[] { dps }); + + when(dps.iterator()).thenReturn(null); + + serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + } + + @Test (expected = RuntimeException.class) + public void formatQueryAsyncV1UnexpectedAggException() throws Exception { + setupFormatQuery(); + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final TSQuery data_query = getTestQuery(false); + validateTestQuery(data_query); + + final MockDataPoints mdps = new MockDataPoints(); + final List results = new ArrayList(1); + results.add(new DataPoints[] { mdps.getMock() }); + + when(mdps.getMockDP().timestamp()).thenThrow( + new RuntimeException("Unexpected error")); + + serdes.formatQueryAsyncV1(data_query, results, + Collections. emptyList()).joinUninterruptibly(); + } + + @Test + public void formatThreadStats() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + + final List> output = + new ArrayList>(1); + Map status = new HashMap(); + status.put("threadID", 1); + status.put("name", "Test Thread 1"); + status.put("state", State.RUNNABLE); + status.put("interrupted", false); + status.put("priority", 1); + + List stack = new ArrayList(2); + stack.add("net.opentsdb.tsd(TestHttpJsonSerializer.java:0)"); + stack.add("java.lang.Thread.run(Thread.java:695)"); + status.put("stack", stack); + output.add(status); + + ChannelBuffer cb = serdes.formatThreadStatsV1(output); + assertNotNull(cb); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"threadID\":1")); + assertTrue(json.contains("\"name\":\"Test Thread 1\"")); + } + + @Test (expected = IllegalArgumentException.class) + public void formatThreadStatsNull() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + serdes.formatThreadStatsV1(null); + } + + /** + * Helper to reset the query stats and mock the time calls before each + * data point query. + */ + private void setupFormatQuery() throws Exception { + mockTime(); + running_queries.set(null, new ConcurrentHashMap()); + completed_queries.set(null, CacheBuilder.newBuilder().maximumSize(2).build()); + } + + /** @return Returns a test TSQuery object to pass on to the serializer */ + private TSQuery getTestQuery(final boolean show_stats) { + return getTestQuery(show_stats, false); + } + + /** @return Returns a test TSQuery object to pass on to the serializer */ + private TSQuery getTestQuery(final boolean show_stats, final boolean show_summary) { + final TSQuery data_query = new TSQuery(); + data_query.setStart("1356998400"); + data_query.setEnd("1388534400"); + data_query.setShowStats(show_stats); + data_query.setShowSummary(show_summary); + + final TSSubQuery sub_query = new TSSubQuery(); + sub_query.setMetric("sys.cpu.user"); + sub_query.setAggregator("sum"); + final ArrayList sub_queries = new ArrayList(1); + sub_queries.add(sub_query); + data_query.setQueries(sub_queries); + + return data_query; + } + + /** + * Helper to validate (set) the time series query + * @param data_query The query to validate + */ + private void validateTestQuery(final TSQuery data_query) { + data_query.validateAndSetQuery(); + data_query.setQueryStats(new QueryStats(remote, data_query)); + } + + /** + * Mocks out the DateTime class and increments the timestamp by 500ms every + * time it's called. + */ + private void mockTime() { + timestamp.add(1388534400000L); + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.parseDateTimeString(anyString(), anyString())) + .thenCallRealMethod(); + PowerMockito.when(DateTime.currentTimeMillis()) + .thenAnswer(new Answer () { + public Long answer(InvocationOnMock invocation) throws Throwable { + long ts = timestamp.get(0); + timestamp.set(0, ts + 500); + return ts; + } + }); + } + } From d21c2ead97b7b0ff2a5e2311f38c0d3b249349f0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:01:38 -0700 Subject: [PATCH 118/826] Add getRemoteAddress() to the abstract HTTP query class --- src/tsd/AbstractHttpQuery.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 27fcace893..31eacd90b4 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -106,6 +106,11 @@ public DefaultHttpResponse response() { public Channel channel() { return chan; } + + /** @return The remote address and port in the format : */ + public String getRemoteAddress() { + return chan.getRemoteAddress().toString(); + } /** Return the time in nanoseconds that this query object was * created. From ccb951f0ddcc3c1be6dc11c604f082914f691754 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:03:37 -0700 Subject: [PATCH 119/826] Add a query index getter to the DataPoints class for use in matching results up to their sub queries in situations where we answer questions like "m=sum:metric&m=avg:metric" Thanks to @Sy Le --- src/core/BatchedDataPoints.java | 4 ++++ src/core/DataPoints.java | 8 +++++++ src/core/IncomingDataPoints.java | 4 ++++ src/core/RowSeq.java | 4 ++++ src/core/Span.java | 4 ++++ src/core/SpanGroup.java | 39 +++++++++++++++++++++++++++++++- 6 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 80b3a92120..a3236049ce 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -487,4 +487,8 @@ public String toString() { buf.append("])"); return buf.toString(); } + + public int getQueryIndex() { + throw new UnsupportedOperationException("Not mapped to a query"); + } } diff --git a/src/core/DataPoints.java b/src/core/DataPoints.java index 896499444a..b8b8963906 100644 --- a/src/core/DataPoints.java +++ b/src/core/DataPoints.java @@ -185,4 +185,12 @@ public interface DataPoints extends Iterable { */ double doubleValue(int i); + /** + * 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 + */ + int getQueryIndex(); } diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 56687bf1aa..7ba7a3ee27 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -531,4 +531,8 @@ public String toString() { public Deferred persist() { return Deferred.fromResult((Object) null); } + + public int getQueryIndex() { + throw new UnsupportedOperationException("Not mapped to a query"); + } } diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index 86233069ea..5e97c430ce 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -659,4 +659,8 @@ public String toString() { } } + + public int getQueryIndex() { + throw new UnsupportedOperationException("Not mapped to a query"); + } } diff --git a/src/core/Span.java b/src/core/Span.java index 8620aa16ce..47211090c4 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -436,4 +436,8 @@ Downsampler downsampler(final long interval_ms, final Aggregator downsampler) { return new Downsampler(spanIterator(), interval_ms, downsampler); } + + public int getQueryIndex() { + throw new UnsupportedOperationException("Not mapped to a query"); + } } diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 976bab108b..4d96ad7d20 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -92,6 +92,9 @@ final class SpanGroup implements DataPoints { /** Minimum time interval (in seconds) wanted between each data point. */ private final long sample_interval; + /** Index of the query in the TSQuery class */ + private final int query_index; + /** * Ctor. * @param tsdb The TSDB we belong to. @@ -143,6 +146,36 @@ final class SpanGroup implements DataPoints { final boolean rate, final RateOptions rate_options, final Aggregator aggregator, final long interval, final Aggregator downsampler) { + this(tsdb, start_time, end_time, spans, rate, rate_options, aggregator, + interval, downsampler, -1); + } + + /** + * Ctor. + * @param tsdb The TSDB we belong to. + * @param start_time Any data point strictly before this timestamp will be + * ignored. + * @param end_time Any data point strictly after this timestamp will be + * ignored. + * @param spans A sequence of initial {@link Spans} to add to this group. + * Ignored if {@code null}. Additional spans can be added with {@link #add}. + * @param rate If {@code true}, the rate of the series will be used instead + * of the actual values. + * @param rate_options Specifies the optional additional rate calculation options. + * @param aggregator The aggregation function to use. + * @param interval Number of milliseconds wanted between each data point. + * @param downsampler Aggregation function to use to group data points + * within an interval. + * @param query_index The index of this query in the TSQuery array + * @since 2.2 + */ + SpanGroup(final TSDB tsdb, + final long start_time, final long end_time, + final Iterable spans, + final boolean rate, final RateOptions rate_options, + final Aggregator aggregator, + final long interval, final Aggregator downsampler, + final int query_index) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; this.end_time = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; @@ -156,8 +189,9 @@ final class SpanGroup implements DataPoints { this.aggregator = aggregator; this.downsampler = downsampler; this.sample_interval = interval; + this.query_index = query_index; } - + /** * Adds a span to this group, provided that it's in the right time range. * Must not be called once {@link #getTags} or @@ -439,4 +473,7 @@ private String toStringSharedAttributes() { + ')'; } + public int getQueryIndex() { + return query_index; + } } From 0dec0ac44c3de2b13cc27dd1bce612630e67a59d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:05:31 -0700 Subject: [PATCH 120/826] Add the /api/stats/query endpoint for printing out the query stats --- src/tsd/StatsRpc.java | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index bb5b4ec327..536363108f 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -26,6 +26,7 @@ import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; +import net.opentsdb.stats.QueryStats; import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.JSON; @@ -88,6 +89,9 @@ public void execute(final TSDB tsdb, final HttpQuery query) { } else if ("jvm".equals(endpoint)) { printJVMStats(tsdb, query); return; + } else if ("query".equals(endpoint)) { + printQueryStats(query); + return; } } catch (IllegalArgumentException e) { // this is thrown if the url doesn't start with /api. To maintain backwards @@ -281,6 +285,26 @@ private static String formatStatName(final String stat) { return name.substring(0, 1).toLowerCase() + name.substring(1); } + /** + * Print the detailed query stats to the caller using the proper serializer + * @param query The query to answer to + * @throws BadRequestException if the API version hasn't been implemented + * yet + */ + private void printQueryStats(final HttpQuery query) { + switch (query.apiVersion()) { + case 0: + case 1: + query.sendReply(query.serializer().formatQueryStatsV1( + QueryStats.buildStats())); + break; + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + + query.apiVersion() + " is not implemented"); + } + } + /** * Implements the StatsCollector with ASCII style output. Builds a string * buffer response to send to the caller From 9140b47eb125ba10d3d3a782a2e2cd8b447fa9a2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:06:27 -0700 Subject: [PATCH 121/826] Add a new method to TsdbQuery to compile from a TSQuery object. Also modify the tag lookup code for the GroupBy call to run asynchronously so we don't block any threads while waiting for storage. --- src/core/Query.java | 18 +++ src/core/TsdbQuery.java | 288 ++++++++++++++++++++++++++++++++--- test/core/TestTsdbQuery.java | 218 ++++++++++++++++++++++++++ 3 files changed, 506 insertions(+), 18 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 01e08b969c..9bd924edd2 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -140,6 +140,24 @@ public void setTimeSeries(final List tsuids, final Aggregator function, final boolean rate, final RateOptions 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.2 + */ + public Deferred configureFromQuery(final TSQuery query, + final int index); + /** * Downsamples the results by specifying a fixed interval between points. *

diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index ac62e0c2c8..f5b4a0a356 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -28,12 +29,12 @@ import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; +import org.hbase.async.Bytes.ByteMap; import com.google.common.annotations.VisibleForTesting; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; -import static org.hbase.async.Bytes.ByteMap; import net.opentsdb.stats.Histogram; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; @@ -122,6 +123,9 @@ final class TsdbQuery implements Query { /** 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; + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -202,9 +206,19 @@ public void setTimeSeries(final String metric, final boolean rate, final RateOptions rate_options) throws NoSuchUniqueName { - findGroupBys(tags); + final Map tags_copy = new HashMap(tags.size()); + tags_copy.putAll(tags); + try { + findGroupBys(tags_copy).join(); + } catch (final InterruptedException e) { + LOG.warn("Interrupted", e); + Thread.currentThread().interrupt(); + } catch (final Exception e) { + LOG.error("Unexpected exception processing group bys", e); + throw new RuntimeException(e); + } this.metric = tsdb.metrics.getId(metric); - this.tags = Tags.resolveAll(tsdb, tags); + this.tags = Tags.resolveAll(tsdb, tags_copy); aggregator = function; this.rate = rate; this.rate_options = rate_options; @@ -248,6 +262,133 @@ public void setTimeSeries(final List tsuids, this.rate_options = rate_options; } + public Deferred configureFromQuery(final TSQuery query, + final int index) { + if (query.getQueries() == null || query.getQueries().isEmpty()) { + throw new IllegalArgumentException("Missing sub queries"); + } + if (index < 0 || index > query.getQueries().size()) { + throw new IllegalArgumentException("Query index was out of range"); + } + + final TSSubQuery sub_query = query.getQueries().get(index); + setStartTime(query.startTime()); + setEndTime(query.endTime()); + query_index = index; + + // set common options + aggregator = sub_query.aggregator(); + rate = sub_query.getRate(); + rate_options = sub_query.getRateOptions(); + if (rate_options == null) { + rate_options = new RateOptions(); + } + downsampler = sub_query.downsampler(); + sample_interval_ms = sub_query.downsampleInterval(); + + // if we have tsuids set, that takes precedence + if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { + tsuids = new ArrayList(sub_query.getTsuids()); + String first_metric = ""; + for (final String tsuid : tsuids) { + if (first_metric.isEmpty()) { + first_metric = tsuid.substring(0, TSDB.metrics_width() * 2) + .toUpperCase(); + continue; + } + + final String metric = tsuid.substring(0, TSDB.metrics_width() * 2) + .toUpperCase(); + if (!first_metric.equals(metric)) { + throw new IllegalArgumentException( + "One or more TSUIDs did not share the same metric [" + first_metric + + "] [" + metric + "]"); + } + } + return Deferred.fromResult(null); + } else { + // copy the tags to a new map as the groupby method will modify the map + // and doing so would cause the TSQuery to change it's hash code, making + // it impossible to remove from the query stats. + final Map tags_copy = + new HashMap(sub_query.getTags()); + + /** Adds the tagk and tagv in the array list in the proper order */ + class TagVCB implements Callback { + final byte[] tagk; + public TagVCB(final byte[] tagk) { + this.tagk = tagk; + } + @Override + public Object call(final byte[] tagv) { + // multiple threads can call us back so make sure we lock the array + // to avoid concurrent modifications or add keys and values out of + // order + synchronized(tags) { + final byte[] pair = new byte[tagk.length + tagv.length]; + System.arraycopy(tagk, 0, pair, 0, tagk.length); + System.arraycopy(tagv, 0, pair, tagk.length, tagv.length); + tags.add(pair); + } + return null; + } + } + + /** Triggers the tagv resolution after resolving a tagk */ + class TagKCB implements Callback, byte[]> { + final String tagv; + public TagKCB(final String tagv) { + this.tagv = tagv; + } + @Override + public Deferred call(final byte[] tagk) { + return tsdb.tag_values.getIdAsync(tagv).addCallback(new TagVCB(tagk)); + } + } + + /** Resolves explicit tagk/tagv pairs after group bys */ + class GroupBy implements Callback>, + ArrayList> { + @Override + public Deferred> call(final ArrayList group) { + final List> tags = + new ArrayList>(tags_copy.size()); + TsdbQuery.this.tags = new ArrayList(tags.size()); + for (Map.Entry entry : tags_copy.entrySet()) { + tags.add(tsdb.tag_names.getIdAsync(entry.getKey()) + .addCallbackDeferring(new TagKCB(entry.getValue()))); + } + return Deferred.group(tags); + } + } + + /** Sort the tag array after resolution is complete */ + class SortTags implements Callback, ArrayList> { + @Override + public Deferred call(final ArrayList notused) + throws Exception { + Collections.sort(tags, Bytes.MEMCMP); + return null; + } + } + + /** Resolve and group by tags after resolving the metric */ + class MetricCB implements Callback { + @Override + public Object call(final byte[] uid) throws Exception { + metric = uid; + return findGroupBys(tags_copy) + .addCallbackDeferring(new GroupBy()) + .addCallback(new SortTags()); + } + } + + // fire off the callback chain by resolving the metric first + return tsdb.metrics.getIdAsync(sub_query.getMetric()) + .addCallback(new MetricCB()); + } + } + /** * Sets an optional downsampling function on this query * @param interval The interval, in milliseconds to rollup data points @@ -281,8 +422,94 @@ public void downsample(final long interval, final Aggregator downsampler) { * @param tags The tags from which to extract the 'GROUP BY's. * Each tag that represents a 'GROUP BY' will be removed from the map * passed in argument. + * @return A deferred to wait on, the results are not important and should be + * discarded. */ - private void findGroupBys(final Map tags) { + private Deferred> findGroupBys(final Map tags) { + + /** + * Used to continue processing when we have a tag value that wasn't assigned + * a UID and the config explicitly allows unknown tags. + */ + class Errback implements Callback { + final boolean is_tagv; + public Errback(final boolean is_tagv) { + this.is_tagv = is_tagv; + } + + @Override + public byte[] call(final Exception e) throws Exception { + if (is_tagv && + tsdb.getConfig().getBoolean("tsd.query.skip_unresolved_tagvs")) { + LOG.warn("Query tag value not found: " + e.getMessage()); + return null; + } else { + throw e; + } + } + } + + /** Adds the tagk to the group bys and passes along the UID */ + class ResolveTagKCB implements Callback { + @Override + public byte[] call(final byte[] uid) { + group_bys.add(uid); + return uid; + } + } + + /** Writes the resolved tagv to the proper group_by_values array */ + class ResoveTagVCB implements Callback { + final byte[] tagk; + final int index; + public ResoveTagVCB(final byte[] tagk, final int index) { + this.tagk = tagk; + this.index = index; + } + @Override + public byte[] call(final byte[] uid) { + final byte[][] value_ids = group_by_values.get(tagk); + System.arraycopy(uid, 0, value_ids[index], 0, tsdb.tag_values.width()); + return null; + } + } + + /** + * Only here to cast the {@code ArrayList} to a {@code byte[]} for + * typing purposes. + */ + class PipedGroupCB implements Callback> { + @Override + public byte[] call(final ArrayList tagvs) { + return null; + } + } + + /** Resolves a piped list of tag values after resolving the tagk */ + class ResolvePipedGroupBy implements Callback, byte[]> { + final String[] values; + public ResolvePipedGroupBy(final String[] values) { + this.values = values; + } + @Override + public Deferred call(final byte[] uid) { + final byte[][] value_ids = new byte[values.length][tsdb.tag_values.width()]; + group_by_values.put(uid, value_ids); + + final List> tagvs = + new ArrayList>(values.length); + for (int j = 0; j < values.length; j++) { + tagvs.add(tsdb.tag_values.getIdAsync(values[j]) + .addCallback(new ResoveTagVCB(uid, j)) + .addErrback(new Errback(true))); + } + return Deferred.group(tagvs).addCallback(new PipedGroupCB()); + } + } + + final List> deferreds = !tags.isEmpty() ? + new ArrayList>(tags.size()) : null; + final Iterator> i = tags.entrySet().iterator(); while (i.hasNext()) { final Map.Entry tag = i.next(); @@ -292,27 +519,31 @@ private void findGroupBys(final Map tags) { if (group_bys == null) { group_bys = new ArrayList(); } - group_bys.add(tsdb.tag_names.getId(tag.getKey())); + final Deferred resolve_tagk = + tsdb.tag_names.getIdAsync(tag.getKey()) + .addCallback(new ResolveTagKCB()) + .addErrback(new Errback(false)); + deferreds.add(resolve_tagk); i.remove(); if (tagvalue.charAt(0) == '*') { continue; // For a 'GROUP BY' with any value, we're done. } + // 'GROUP BY' with specific values. Need to split the values // to group on and store their IDs in group_by_values. final String[] values = Tags.splitString(tagvalue, '|'); if (group_by_values == null) { group_by_values = new ByteMap(); } - final short value_width = tsdb.tag_values.width(); - final byte[][] value_ids = new byte[values.length][value_width]; - group_by_values.put(tsdb.tag_names.getId(tag.getKey()), - value_ids); - for (int j = 0; j < values.length; j++) { - final byte[] value_id = tsdb.tag_values.getId(values[j]); - System.arraycopy(value_id, 0, value_ids[j], 0, value_width); - } + resolve_tagk.addCallback(new ResolvePipedGroupBy(values)); } } + + if (deferreds == null) { + return Deferred.fromResult(null); + } else { + return Deferred.group(deferreds); + } } /** @@ -487,7 +718,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { spans.values(), rate, rate_options, aggregator, - sample_interval_ms, downsampler); + sample_interval_ms, downsampler, + query_index); return new SpanGroup[] { group }; } @@ -531,7 +763,7 @@ public DataPoints[] call(final TreeMap spans) throws Exception { thegroup = new SpanGroup(tsdb, getScanStartTimeSeconds(), getScanEndTimeSeconds(), null, rate, rate_options, aggregator, - sample_interval_ms, downsampler); + sample_interval_ms, downsampler, query_index); // 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]; @@ -906,18 +1138,38 @@ public int compare(final byte[] a, final byte[] b) { static class ForTesting { /** @return the start time of the HBase scan for unit tests. */ - static long getScanStartTimeSeconds(TsdbQuery query) { + static long getScanStartTimeSeconds(final TsdbQuery query) { return query.getScanStartTimeSeconds(); } /** @return the end time of the HBase scan for unit tests. */ - static long getScanEndTimeSeconds(TsdbQuery query) { + static long getScanEndTimeSeconds(final TsdbQuery query) { return query.getScanEndTimeSeconds(); } /** @return the downsampling interval for unit tests. */ - static long getDownsampleIntervalMs(TsdbQuery query) { + static long getDownsampleIntervalMs(final TsdbQuery query) { return query.sample_interval_ms; } + + static byte[] getMetric(final TsdbQuery query) { + return query.metric; + } + + static RateOptions getRateOptions(final TsdbQuery query) { + return query.rate_options; + } + + static ArrayList getTags(final TsdbQuery query) { + return query.tags; + } + + static ArrayList getGroupBys(final TsdbQuery query) { + return query.group_bys; + } + + static ByteMap getGroupByValues(final TsdbQuery query) { + return query.group_by_values; + } } } diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index c027d8bd34..62b62cfc6e 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -12,12 +12,17 @@ // 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.assertTrue; import java.util.ArrayList; import java.util.List; +import net.opentsdb.core.TsdbQuery.ForTesting; +import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.DateTime; @@ -28,6 +33,8 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.stumbleupon.async.DeferredGroupException; + /** * 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 @@ -181,4 +188,215 @@ public void setTimeSeriesTSDifferentMetrics() throws Exception { query.setTimeSeries(tsuids, Aggregators.SUM, false); } + @Test + public void configureFromQuery() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(1, ForTesting.getTags(query).size()); + assertArrayEquals(MockBase.concatByteArrays(TAGK_BYTES, TAGV_BYTES), + ForTesting.getTags(query).get(0)); + assertNull(ForTesting.getGroupBys(query)); + assertNull(ForTesting.getGroupByValues(query)); + assertNotNull(ForTesting.getRateOptions(query)); + } + + @Test + public void configureFromQueryWithRate() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + final RateOptions rate_options = new RateOptions(); + rate_options.setResetValue(1024); + ts_query.getQueries().get(0).setRateOptions(rate_options); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(1, ForTesting.getTags(query).size()); + assertArrayEquals(MockBase.concatByteArrays(TAGK_BYTES, TAGV_BYTES), + ForTesting.getTags(query).get(0)); + assertNull(ForTesting.getGroupBys(query)); + assertNull(ForTesting.getGroupByValues(query)); + assertTrue(rate_options == ForTesting.getRateOptions(query)); + } + + @Test + public void configureFromQueryNoTags() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).setTags(null); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(0, ForTesting.getTags(query).size()); + assertNull(ForTesting.getGroupBys(query)); + assertNull(ForTesting.getGroupByValues(query)); + } + + @Test + public void configureFromQueryGroupByAll() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(TAGK_STRING, "*"); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertArrayEquals(TAGK_BYTES, + ForTesting.getGroupBys(query).get(0)); + assertNull(ForTesting.getGroupByValues(query)); + } + + @Test + public void configureFromQueryGroupByPipe() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(TAGK_STRING, + TAGV_STRING + "|" + TAGV_B_STRING); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertArrayEquals(TAGK_BYTES, + ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getGroupByValues(query).size()); + final byte[][] tag_values = ForTesting.getGroupByValues(query) + .iterator().next().getValue(); + assertEquals(2, tag_values.length); + assertArrayEquals(TAGK_BYTES, tag_values[0]); + assertArrayEquals(new byte[] { 0, 0, 2 }, tag_values[1]); + } + + @Test (expected = IllegalArgumentException.class) + public void configureFromQueryNullSubs() throws Exception { + final TSQuery ts_query = new TSQuery(); + new TsdbQuery(tsdb).configureFromQuery(ts_query, 0); + } + + @Test (expected = IllegalArgumentException.class) + public void configureFromQueryEmptySubs() throws Exception { + final TSQuery ts_query = new TSQuery(); + ts_query.setQueries(new ArrayList(0)); + new TsdbQuery(tsdb).configureFromQuery(ts_query, 0); + } + + @Test (expected = IllegalArgumentException.class) + public void configureFromQueryNegativeIndex() throws Exception { + final TSQuery ts_query = getTSQuery(); + new TsdbQuery(tsdb).configureFromQuery(ts_query, -1); + } + + @Test (expected = IllegalArgumentException.class) + public void configureFromQueryIndexOutOfBounds() throws Exception { + final TSQuery ts_query = getTSQuery(); + new TsdbQuery(tsdb).configureFromQuery(ts_query, 2); + } + + @Test (expected = NoSuchUniqueName.class) + public void configureFromQueryNSUMetric() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).setMetric(NSUN_METRIC); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void configureFromQueryNSUTagk() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(NSUN_TAGK, TAGV_STRING); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void configureFromQueryNSUTagv() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(TAGK_STRING, NSUN_TAGV); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void configureFromQueryGroupByPipeNSUTagk() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(NSUN_TAGK, + TAGV_STRING + "|" + TAGV_B_STRING); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void configureFromQueryGroupByPipeNSUTagv() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(TAGK_STRING, + TAGV_STRING + "|" + NSUN_TAGV); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + } + + @Test + public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() + throws Exception { + config.overrideConfig("tsd.query.skip_unresolved_tagvs", "true"); + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + ts_query.getQueries().get(0).getTags().put(TAGK_STRING, + TAGV_STRING + "|" + NSUN_TAGV); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertArrayEquals(TAGK_BYTES, + ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getGroupByValues(query).size()); + final byte[][] tag_values = ForTesting.getGroupByValues(query) + .iterator().next().getValue(); + assertEquals(2, tag_values.length); + assertArrayEquals(TAGV_BYTES, tag_values[0]); + assertArrayEquals(new byte[] { 0, 0, 0 }, tag_values[1]); + } + + /** @return a simple TSQuery object for testing */ + private TSQuery getTSQuery() { + final TSQuery ts_query = new TSQuery(); + ts_query.setStart("1356998400"); + + 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); + + ts_query.setQueries(sub_queries); + return ts_query; + } } From 1005bdae079149b6c7f89787f1a9a1b75e639d18 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:07:39 -0700 Subject: [PATCH 122/826] Add flags to the TSQuery class to show stats, summaries and the original query with results. Also modify it to use the new TsdbQuery compilation method --- src/core/TSQuery.java | 121 ++++++++++++++++++++++++++++++--------- src/core/TSSubQuery.java | 5 +- 2 files changed, 96 insertions(+), 30 deletions(-) diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 1178f53998..378c3df455 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -17,8 +17,12 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Objects; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import net.opentsdb.stats.QueryStats; import net.opentsdb.utils.DateTime; /** @@ -77,6 +81,18 @@ public final class TSQuery { /** Whether or not the user wasn't millisecond resolution */ private boolean ms_resolution; + /** Whether or not to show the sub query with the results */ + private boolean show_query; + + /** Whether or not to include stats in the output */ + private boolean show_stats; + + /** Whether or not to include stats summary in the output */ + private boolean show_summary; + + /** The query status for tracking over all performance of this query */ + private QueryStats query_stats; + /** * Default constructor necessary for POJO de/serialization */ @@ -88,6 +104,7 @@ public TSQuery() { public int hashCode() { // NOTE: Do not add any non-user submitted variables to the hash. We don't // want the hash to change after validation. + // We also don't care about stats or summary return Objects.hashCode(start, end, timezone, options, padding, no_annotations, with_global_annotations, show_tsuids, queries, ms_resolution); @@ -107,6 +124,7 @@ public boolean equals(final Object obj) { // NOTE: Do not add any non-user submitted variables to the comparator. We // don't want the value to change after validation. + // We also don't care about stats or summary final TSQuery query = (TSQuery)obj; return Objects.equal(start, query.start) && Objects.equal(end, query.end) @@ -167,37 +185,45 @@ public void validateAndSetQuery() { * @return An array of queries */ public Query[] buildQueries(final TSDB tsdb) { - final Query[] queries = new Query[this.queries.size()]; - int i = 0; - for (TSSubQuery sub : this.queries) { + try { + return buildQueriesAsync(tsdb).joinUninterruptibly(); + } catch (final Exception e) { + throw new RuntimeException("Unexpected exception", e); + } + } + + /** + * Compiles the TSQuery into an array of Query objects for execution. + * If the user has not set a down sampler explicitly, and they don't want + * millisecond resolution, then we set the down sampler to 1 second to handle + * situations where storage may have multiple data points per second. + * @param tsdb The tsdb to use for {@link TSDB#newQuery} + * @return A deferred array of queries to wait on for compilation. + * @since 2.2 + */ + public Deferred buildQueriesAsync(final TSDB tsdb) { + final Query[] tsdb_queries = new Query[queries.size()]; + + final List> deferreds = + new ArrayList>(queries.size()); + for (int i = 0; i < queries.size(); i++) { final Query query = tsdb.newQuery(); - query.setStartTime(start_time); - query.setEndTime(end_time); - if (sub.downsampler() != null) { - query.downsample(sub.downsampleInterval(), sub.downsampler()); - } else if (!ms_resolution) { - // we *may* have multiple millisecond data points in the set so we have - // to downsample. use the sub query's aggregator - query.downsample(1000, sub.aggregator()); + deferreds.add(query.configureFromQuery(this, i)); + tsdb_queries[i] = query; + } + + class GroupFinished implements Callback> { + @Override + public Query[] call(final ArrayList deferreds) { + return tsdb_queries; } - if (sub.getTsuids() != null && !sub.getTsuids().isEmpty()) { - if (sub.getRateOptions() != null) { - query.setTimeSeries(sub.getTsuids(), sub.aggregator(), sub.getRate(), - sub.getRateOptions()); - } else { - query.setTimeSeries(sub.getTsuids(), sub.aggregator(), sub.getRate()); - } - } else if (sub.getRateOptions() != null) { - query.setTimeSeries(sub.getMetric(), sub.getTags(), sub.aggregator(), - sub.getRate(), sub.getRateOptions()); - } else { - query.setTimeSeries(sub.getMetric(), sub.getTags(), sub.aggregator(), - sub.getRate()); + @Override + public String toString() { + return "Query compile group callback"; } - queries[i] = query; - i++; } - return queries; + + return Deferred.group(deferreds).addCallback(new GroupFinished()); } public String toString() { @@ -309,6 +335,27 @@ public boolean getMsResolution() { return ms_resolution; } + /** @return whether or not to show the query with the results */ + public boolean getShowQuery() { + return show_query; + } + + /** @return whether or not to return stats per query */ + public boolean getShowStats() { + return show_stats; + } + + /** @return Whether or not to show the query summary */ + public boolean getShowSummary() { + return this.show_summary; + } + + /** @return the query stats object. Ignored during JSON serialization */ + @JsonIgnore + public QueryStats getQueryStats() { + return query_stats; + } + /** * Sets the start time for further parsing. This can be an absolute or * relative value. See {@link DateTime#parseDateTimeString} for details. @@ -367,4 +414,24 @@ public void setQueries(ArrayList queries) { public void setMsResolution(boolean ms_resolution) { this.ms_resolution = ms_resolution; } + + /** @param show_query whether or not to show the query with the serialization */ + public void setShowQuery(boolean show_query) { + this.show_query = show_query; + } + + /** @param show_stats whether or not to show stats in the serialization */ + public void setShowStats(boolean show_stats) { + this.show_stats = show_stats; + } + + /** @param show_summary whether or not to show the query summary */ + public void setShowSummary(boolean show_summary) { + this.show_summary = show_summary; + } + + /** @param query_stats the query stats object to associate with this query */ + public void setQueryStats(final QueryStats query_stats) { + this.query_stats = query_stats; + } } diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index d2126cf528..c1142aa882 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -13,7 +13,6 @@ package net.opentsdb.core; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -51,7 +50,7 @@ public final class TSSubQuery { /** User supplied list of tags for specificity or grouping. May be null or * empty */ - private HashMap tags; + private Map tags; /** User given downsampler */ private String downsample; @@ -268,7 +267,7 @@ public void setTsuids(List tsuids) { } /** @param tags an optional list of tags for specificity or grouping */ - public void setTags(HashMap tags) { + public void setTags(Map tags) { this.tags = tags; } From e547b49634649deb40cc175913cb72738a684ef1 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 4 Apr 2015 22:08:33 -0700 Subject: [PATCH 123/826] Modify QueryRpc to track queries in the QueryStats class and run the queries asynchronously to avoid blocking threads. --- src/tsd/QueryRpc.java | 175 ++++++++++++++++++++++++++----------- test/tsd/TestQueryRpc.java | 123 +++++++++++++++++--------- 2 files changed, 208 insertions(+), 90 deletions(-) diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 3c19897025..2fa95524f5 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -21,6 +21,7 @@ import java.util.Map; import org.hbase.async.Bytes.ByteMap; +import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.slf4j.Logger; @@ -33,17 +34,18 @@ import net.opentsdb.core.DataPoints; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.Query; +import net.opentsdb.core.QueryException; import net.opentsdb.core.RateOptions; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; -import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.TSUIDQuery; +import net.opentsdb.stats.QueryStats; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.JSON; +import net.opentsdb.utils.DateTime; /** * Handles queries for timeseries datapoints. Each request is parsed into a @@ -91,6 +93,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) * @param query The HTTP query to parse/respond */ private void handleQuery(final TSDB tsdb, final HttpQuery query) { + final long start = DateTime.currentTimeMillis(); final TSQuery data_query; if (query.method() == HttpMethod.POST) { switch (query.apiVersion()) { @@ -116,70 +119,132 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { e.getMessage(), data_query.toString(), e); } - Query[] tsdbqueries; - try { - tsdbqueries = data_query.buildQueries(tsdb); - } catch(NoSuchUniqueName ex) { - throw new BadRequestException(ex); - } - final int nqueries = tsdbqueries.length; - final ArrayList results = - new ArrayList(nqueries); - final ArrayList> deferreds = - new ArrayList>(nqueries); - for (int i = 0; i < nqueries; i++) { - deferreds.add(tsdbqueries[i].runAsync()); + // if the user tried this query multiple times from the same IP and src port + // they'll be rejected on subsequent calls + final QueryStats query_stats = + new QueryStats(query.getRemoteAddress(), data_query); + data_query.setQueryStats(query_stats); + + final int nqueries = data_query.getQueries().size(); + final ArrayList results = new ArrayList(nqueries); + final List globals = new ArrayList(); + + /** This has to be attached to callbacks or we may never respond to clients */ + class ErrorCB implements Callback { + public Object call(final Exception e) throws Exception { + try { + if (e instanceof DeferredGroupException) { + Throwable ex = e.getCause(); + while (ex != null && ex instanceof DeferredGroupException) { + ex = ex.getCause(); + } + if (ex != null) { + if (ex instanceof NoSuchUniqueName) { + query_stats.markComplete(HttpResponseStatus.BAD_REQUEST, ex); + query.badRequest(new BadRequestException( + HttpResponseStatus.NOT_FOUND, ex.getMessage())); + return null; + } + LOG.error("Query failed", ex); + query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); + query.badRequest(new BadRequestException(ex)); + } else { + LOG.error("Unable to find the cause of the DGE", e); + query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); + query.badRequest(new BadRequestException(e)); + } + } else if (e.getClass() == QueryException.class) { + query_stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); + query.badRequest(new BadRequestException((QueryException)e)); + } else { + LOG.error("Query failed", e); + query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); + query.badRequest(new BadRequestException(e)); + } + return null; + } catch (RuntimeException ex) { + LOG.error("Exception thrown during exception handling", ex); + query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); + query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, + ex.getMessage().getBytes()); + return null; + } + } } - + /** - * After all of the queries have run, we get the results in the order given - * and add dump the results in an array - */ + * After all of the queries have run, we get the results in the order given + * and add dump the results in an array + */ class QueriesCB implements Callback> { public Object call(final ArrayList query_results) throws Exception { results.addAll(query_results); + + /** Simply returns the buffer once serialization is complete and logs it */ + class SendIt implements Callback { + public Object call(final ChannelBuffer buffer) throws Exception { + query.sendReply(buffer); + return null; + } + } + + query_stats.setTimeStorage(System.currentTimeMillis() - start); + switch (query.apiVersion()) { + case 0: + case 1: + query.serializer().formatQueryAsyncV1(data_query, results, + globals).addCallback(new SendIt()).addErrback(new ErrorCB()); + break; + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + + query.apiVersion() + " is not implemented"); + } return null; } } - // if the user wants global annotations, we need to scan and fetch - // TODO(cl) need to async this at some point. It's not super straight - // forward as we can't just add it to the "deferreds" queue since the types - // are different. - List globals = null; - if (!data_query.getNoAnnotations() && data_query.getGlobalAnnotations()) { - try { - globals = Annotation.getGlobalAnnotations(tsdb, - data_query.startTime() / 1000, data_query.endTime() / 1000) - .joinUninterruptibly(); - } catch (Exception e) { - throw new RuntimeException("Shouldn't be here", e); + /** + * Callback executed after we have resolved the metric, tag names and tag + * values to their respective UIDs. This callback then runs the actual + * queries and fetches their results. + */ + class BuildCB implements Callback, Query[]> { + @Override + public Deferred call(final Query[] queries) { + final ArrayList> deferreds = + new ArrayList>(queries.length); + for (final Query query : queries) { + deferreds.add(query.runAsync()); + } + return Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()); } } - - try { - Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()) - .joinUninterruptibly(); - } catch (Exception e) { - throw new RuntimeException("Shouldn't be here", e); - } - switch (query.apiVersion()) { - case 0: - case 1: - query.sendReply(query.serializer().formatQueryV1(data_query, results, - globals)); - break; - default: - throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, - "Requested API version not implemented", "Version " + - query.apiVersion() + " is not implemented"); + /** Handles storing the global annotations after fetching them */ + class GlobalCB implements Callback> { + public Object call(final List annotations) throws Exception { + globals.addAll(annotations); + return data_query.buildQueriesAsync(tsdb).addCallback(new BuildCB()); + } + } + + // if we the caller wants to search for global annotations, fire that off + // first then scan for the notes, then pass everything off to the formatter + // when complete + if (!data_query.getNoAnnotations() && data_query.getGlobalAnnotations()) { + Annotation.getGlobalAnnotations(tsdb, + data_query.startTime() / 1000, data_query.endTime() / 1000) + .addCallback(new GlobalCB()).addErrback(new ErrorCB()); + } else { + data_query.buildQueriesAsync(tsdb).addCallback(new BuildCB()) + .addErrback(new ErrorCB()); } } /** - * + * Returns the last data point for each sub query if found. * @param tsdb The TSDB to which we belong * @param query The HTTP query to parse/respond */ @@ -366,6 +431,18 @@ private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { data_query.setMsResolution(true); } + if (query.hasQueryStringParam("show_query")) { + data_query.setShowQuery(true); + } + + if (query.hasQueryStringParam("show_stats")) { + data_query.setShowStats(true); + } + + if (query.hasQueryStringParam("show_summary")) { + data_query.setShowSummary(true); + } + // handle tsuid queries first if (query.hasQueryStringParam("tsuid")) { final List tsuids = query.getQueryStringParams("tsuid"); diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index d07627788a..6806680b47 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -15,29 +15,35 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; -import org.powermock.api.mockito.PowerMockito; -import org.mockito.Matchers; + import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; -import java.util.ArrayList; +import java.nio.charset.Charset; + import net.opentsdb.core.DataPoints; import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; +import net.opentsdb.storage.MockDataPoints; import net.opentsdb.utils.Config; -import org.hbase.async.HBaseClient; +import net.opentsdb.utils.DateTime; + 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; + import net.opentsdb.uid.NoSuchUniqueName; + import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + /** * Unit tests for the Query RPC class that handles parsing user queries for * timeseries data and returning that data @@ -45,12 +51,13 @@ * core.TestTSQuery and TestTSSubQuery classes */ @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, HttpQuery.class, Query.class, - Deferred.class, TSQuery.class}) +@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, Query.class, + Deferred.class, TSQuery.class, DateTime.class, DeferredGroupException.class }) public final class TestQueryRpc { private TSDB tsdb = null; - final private QueryRpc rpc = new QueryRpc(); - final private Query empty_query = mock(Query.class); + private QueryRpc rpc; + private Query empty_query = mock(Query.class); + private Query query_result; private static final Method parseQuery; static { @@ -66,8 +73,16 @@ public final class TestQueryRpc { @Before public void before() throws Exception { tsdb = NettyMocks.getMockedHTTPTSDB(); - when(tsdb.newQuery()).thenReturn(empty_query); + empty_query = mock(Query.class); + query_result = mock(Query.class); + rpc = new QueryRpc(); + + when(tsdb.newQuery()).thenReturn(query_result); when(empty_query.run()).thenReturn(new DataPoints[0]); + when(query_result.configureFromQuery((TSQuery)any(), anyInt())) + .thenReturn(Deferred.fromResult(null)); + when(query_result.runAsync()) + .thenReturn(Deferred.fromResult(new DataPoints[0])); } @Test @@ -273,16 +288,11 @@ public void parseQueryNoSubQuery() throws Exception { @Test public void postQuerySimplePass() throws Exception { - Deferred> deferredMock = - (Deferred>)mock(Deferred.class); - PowerMockito.mockStatic(Deferred.class); - PowerMockito.when(Deferred.groupInOrder(Matchers.anyCollection())) - .thenReturn(deferredMock); - PowerMockito.when(deferredMock.joinUninterruptibly()) - .thenReturn(null); - PowerMockito.when(deferredMock.addCallback(Matchers.any(com.stumbleupon.async.Callback.class))) - .thenReturn(deferredMock); - + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query", "{\"start\":1425440315306,\"queries\":" + "[{\"metric\":\"somemetric\",\"aggregator\":\"sum\",\"rate\":true," + @@ -291,35 +301,66 @@ public void postQuerySimplePass() throws Exception { assertEquals(HttpResponseStatus.OK, query.response().getStatus()); } - @Test (expected = BadRequestException.class) + @Test public void postQueryNoMetricBadRequest() throws Exception { - Deferred> deferredMock = - (Deferred>)mock(Deferred.class); - PowerMockito.mockStatic(Deferred.class); - PowerMockito.when(Deferred.groupInOrder(Matchers.anyCollection())) - .thenReturn(deferredMock); - PowerMockito.when(deferredMock.joinUninterruptibly()) - .thenReturn(null); - PowerMockito.when(deferredMock.addCallback( - Matchers.any(com.stumbleupon.async.Callback.class))) - .thenReturn(deferredMock); + final DeferredGroupException dge = mock(DeferredGroupException.class); + when(dge.getCause()).thenReturn(new NoSuchUniqueName("foo", "metrics")); - Query mockQuery = mock(Query.class); - PowerMockito.doThrow(new NoSuchUniqueName("metric", "nonexistent")) - .when(mockQuery).setTimeSeries( - Matchers.anyString(), - Matchers.anyMap(), - Matchers.any(net.opentsdb.core.Aggregator.class), - Matchers.anyBoolean(), - Matchers.any(net.opentsdb.core.RateOptions.class)); - when(tsdb.newQuery()).thenReturn(mockQuery); + when(query_result.configureFromQuery((TSQuery)any(), anyInt())) + .thenReturn(Deferred.fromError(dge)); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query", "{\"start\":1425440315306,\"queries\":" + "[{\"metric\":\"nonexistent\",\"aggregator\":\"sum\",\"rate\":true," + "\"rateOptions\":{\"counter\":false}}]}"); rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("No such name for 'foo': 'metrics'")); + } + + @Test + public void executeEmpty() throws Exception { + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + rpc.execute(tsdb, query); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertEquals("[]", json); + } + + @Test + public void execute() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + rpc.execute(tsdb, query); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } + + @Test + public void executeNSU() throws Exception { + final DeferredGroupException dge = mock(DeferredGroupException.class); + when(dge.getCause()).thenReturn(new NoSuchUniqueName("foo", "metrics")); + when(query_result.configureFromQuery((TSQuery)any(), anyInt())) + .thenReturn(Deferred.fromError(dge)); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("No such name for 'foo': 'metrics'")); + } + //TODO(cl) add unit tests for the rate options parsing } \ No newline at end of file From 9a06780b56afa82a85786356d44d05dc0b269be5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 5 Apr 2015 12:37:49 -0700 Subject: [PATCH 124/826] Add Tags.getTagUids() to parse out the tag UID pairs from a row key --- src/core/Tags.java | 26 ++++++++++++ test/core/TestTags.java | 90 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index 1fadced742..f60b1e1491 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -26,6 +26,7 @@ import com.stumbleupon.async.Deferred; import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; @@ -394,6 +395,31 @@ public Map call(final ArrayList names) return Deferred.groupInOrder(deferreds).addCallback(new NameCB()); } + /** + * Returns the tag key and value pairs as a byte map given a row key + * @param row The row key to parse the UIDs from + * @return A byte map with tagk and tagv pairs as raw UIDs + * @since 2.2 + */ + public static ByteMap getTagUids(final byte[] row) { + final ByteMap uids = new ByteMap(); + final short name_width = TSDB.tagk_width(); + final short value_width = TSDB.tagv_width(); + final short tag_bytes = (short) (name_width + value_width); + final short metric_ts_bytes = (short) (TSDB.metrics_width() + + Const.TIMESTAMP_BYTES + + Const.SALT_WIDTH()); + + for (short pos = metric_ts_bytes; pos < row.length; pos += tag_bytes) { + final byte[] tmp_name = new byte[name_width]; + final byte[] tmp_value = new byte[value_width]; + System.arraycopy(row, pos, tmp_name, 0, name_width); + System.arraycopy(row, pos + name_width, tmp_value, 0, value_width); + uids.put(tmp_name, tmp_value); + } + return uids; + } + /** * Ensures that a given string is a valid metric name or tag name/value. * @param what A human readable description of what's being validated. diff --git a/test/core/TestTags.java b/test/core/TestTags.java index f1ae95b992..973641dc60 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -25,13 +25,16 @@ import net.opentsdb.utils.Config; import net.opentsdb.utils.Pair; +import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; +import org.hbase.async.Bytes.ByteMap; 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; @@ -51,7 +54,8 @@ "com.sum.*", "org.xml.*"}) @RunWith(PowerMockRunner.class) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class}) + GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, + Const.class}) public final class TestTags { private TSDB tsdb; private Config config; @@ -688,4 +692,88 @@ private void setupResolveAll() throws Exception { when(tag_values.getId("invalidhost")) .thenThrow(new NoSuchUniqueName("tagk", "invalidhost")); } + + @Test + public void getTagUids() throws Exception { + byte[] row = new byte[] { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2}; + ByteMap uids = Tags.getTagUids(row); + assertEquals(1, uids.size()); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, uids.firstEntry() + .getValue())); + + row = new byte[] { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4}; + uids = Tags.getTagUids(row); + assertEquals(2, uids.size()); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, uids.firstEntry() + .getValue())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 3 }, uids.lastKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 4 }, uids.lastEntry() + .getValue())); + + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + row = new byte[] { 1, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2}; + uids = Tags.getTagUids(row); + assertEquals(1, uids.size()); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, uids.firstEntry() + .getValue())); + + row = new byte[] { 1, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4}; + uids = Tags.getTagUids(row); + assertEquals(2, uids.size()); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, uids.firstEntry() + .getValue())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 3 }, uids.lastKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 4 }, uids.lastEntry() + .getValue())); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void getTagUidsMissingTagV() throws Exception { + byte[] row = new byte[] { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1 }; + Tags.getTagUids(row); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void getTagUidsMissingTagVSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + byte[] row = new byte[] { 1, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1 }; + Tags.getTagUids(row); + } + + @Test + public void getTagUidsMissingTags() throws Exception { + byte[] row = new byte[] { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0 }; + ByteMap uids = Tags.getTagUids(row); + assertEquals(0, uids.size()); + + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + row = new byte[] { 1, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0 }; + uids = Tags.getTagUids(row); + assertEquals(0, uids.size()); + } + + @Test (expected = NullPointerException.class) + public void getTagUidsNullRow() throws Exception { + Tags.getTagUids(null); + } + + @Test + public void getTagUidsEmptyRow() throws Exception { + final ByteMap uids = Tags.getTagUids(new byte[] {}); + assertEquals(0, uids.size()); + } } From db03936045bd4992bf83796422ea39d8d4f8a48b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 5 Apr 2015 12:38:56 -0700 Subject: [PATCH 125/826] Add the getTagUids() method to the DataPoints interface to fetch the UIDs from a row key. Modify the SpanGroup class to resolve tag names AFTER computing the common tags and aggregated tags list. This is a big help for queries with high cardinality as we avoid looking up the names for UIDs that we discard when serializing the query. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/core/BatchedDataPoints.java | 6 + src/core/DataPoints.java | 12 ++ src/core/IncomingDataPoints.java | 6 + src/core/RowSeq.java | 6 + src/core/Span.java | 7 + src/core/SpanGroup.java | 215 ++++++++++++++++++++++--------- test/core/TestSpanGroup.java | 97 ++++++++++++++ 8 files changed, 286 insertions(+), 64 deletions(-) create mode 100644 test/core/TestSpanGroup.java diff --git a/Makefile.am b/Makefile.am index 8a03860985..ffbd686509 100644 --- a/Makefile.am +++ b/Makefile.am @@ -171,6 +171,7 @@ test_SRC := \ test/core/TestRowSeq.java \ test/core/TestSaltScanner.java \ test/core/TestSpan.java \ + test/core/TestSpanGroup.java \ test/core/TestTags.java \ test/core/TestTSDB.java \ test/core/TestTsdbQueryDownsample.java \ diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index a3236049ce..2f0629691b 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -21,6 +21,7 @@ import com.stumbleupon.async.Deferred; import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; import net.opentsdb.meta.Annotation; @@ -317,6 +318,11 @@ public Map getTags() { throw new RuntimeException("Should never be here", e); } } + + @Override + public ByteMap getTagUids() { + return Tags.getTagUids(row_key); + } @Override public Deferred> getTagsAsync() { diff --git a/src/core/DataPoints.java b/src/core/DataPoints.java index b8b8963906..cef3c69790 100644 --- a/src/core/DataPoints.java +++ b/src/core/DataPoints.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.Map; +import org.hbase.async.Bytes.ByteMap; + import com.stumbleupon.async.Deferred; import net.opentsdb.meta.Annotation; @@ -49,6 +51,16 @@ public interface DataPoints extends Iterable { * @since 1.2 */ Deferred> getTagsAsync(); + + /** + * 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 + */ + ByteMap getTagUids(); /** * Returns the tags associated with some but not all of the data points. diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 7ba7a3ee27..1d0f55f578 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -24,6 +24,7 @@ import org.hbase.async.Bytes; import org.hbase.async.PutRequest; +import org.hbase.async.Bytes.ByteMap; import net.opentsdb.meta.Annotation; import net.opentsdb.stats.Histogram; @@ -418,6 +419,11 @@ public Map getTags() { throw new RuntimeException("Should never be here", e); } } + + @Override + public ByteMap getTagUids() { + return Tags.getTagUids(row); + } public Deferred> getTagsAsync() { return Tags.getTagsAsync(tsdb, row); diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index 5e97c430ce..42194aab3b 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -24,6 +24,7 @@ import org.hbase.async.Bytes; import org.hbase.async.KeyValue; +import org.hbase.async.Bytes.ByteMap; import com.stumbleupon.async.Deferred; @@ -294,6 +295,11 @@ public Map getTags() { } } + @Override + public ByteMap getTagUids() { + return Tags.getTagUids(key); + } + public Deferred> getTagsAsync() { return Tags.getTagsAsync(tsdb, key); } diff --git a/src/core/Span.java b/src/core/Span.java index 47211090c4..a7aafc2236 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -24,6 +24,7 @@ import org.hbase.async.Bytes; import org.hbase.async.KeyValue; +import org.hbase.async.Bytes.ByteMap; import com.stumbleupon.async.Deferred; @@ -105,6 +106,12 @@ public Deferred> getTagsAsync() { return rows.get(0).getTagsAsync(); } + @Override + public ByteMap getTagUids() { + checkNotEmpty(); + return rows.get(0).getTagUids(); + } + /** @return an empty list since aggregated tags cannot exist on a single span */ public List getAggregatedTags() { return Collections.emptyList(); diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 4d96ad7d20..be2eb7e1bc 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -18,9 +18,14 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; + +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; /** @@ -61,7 +66,8 @@ final class SpanGroup implements DataPoints { * in this group. * @see #computeTags */ - private HashMap tags; + private Map tags; + private ByteMap tag_uids; /** * The names of the tags that aren't shared by every single data point. @@ -69,7 +75,8 @@ final class SpanGroup implements DataPoints { * in this group. * @see #computeTags */ - private ArrayList aggregated_tags; + private List aggregated_tags; + private Set aggregated_tag_uids; /** Spans in this group. They must all be for the same metric. */ private final ArrayList spans = new ArrayList(); @@ -95,6 +102,9 @@ final class SpanGroup implements DataPoints { /** Index of the query in the TSQuery class */ private final int query_index; + /** The TSDB to which we belong, used for resolution */ + private final TSDB tsdb; + /** * Ctor. * @param tsdb The TSDB we belong to. @@ -176,6 +186,7 @@ final class SpanGroup implements DataPoints { final Aggregator aggregator, final long interval, final Aggregator downsampler, final int query_index) { + this.tsdb = tsdb; annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; this.end_time = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; @@ -247,63 +258,50 @@ void add(final Span span) { /** * Computes the intersection set + symmetric difference of tags in all spans. - * @param spans A collection of spans for which to find the common tags. - * @return A (possibly empty) map of the tags common to all the spans given. + * This method loads the UID aggregated list and tag pair maps with byte arrays + * but does not actually resolve the UIDs to strings. + * On the first run, it will initialize the UID collections (which may be empty) + * and subsequent calls will skip processing. */ - private Deferred computeTags() { + private void computeTags() { + if (tag_uids != null && aggregated_tag_uids != null) { + return; + } if (spans.isEmpty()) { - tags = new HashMap(0); - aggregated_tags = new ArrayList(0); - return Deferred.fromResult(null); + tag_uids = new ByteMap(); + aggregated_tag_uids = new HashSet(); + return; } - - final Iterator it = spans.iterator(); - /** - * This is the last callback that will determine what tags are aggregated in - * the results. - */ - class SpanTagsCB implements Callback>> { - public Object call(final ArrayList> lookups) - throws Exception { - final HashSet discarded_tags = new HashSet(tags.size()); - for (Map lookup : lookups) { - final Iterator> i = tags.entrySet().iterator(); - while (i.hasNext()) { - final Map.Entry entry = i.next(); - final String name = entry.getKey(); - final String value = lookup.get(name); - if (value == null || !value.equals(entry.getValue())) { - i.remove(); - discarded_tags.add(name); - } - } - } - SpanGroup.this.aggregated_tags = new ArrayList(discarded_tags); - return null; - } - } + // local tag uids + final ByteMap tag_set = new ByteMap(); - /** - * We have to wait for the first set of tags to be resolved so we can - * create a map with the proper size. Then we iterate through the rest of - * the tags for the different spans and work on each set. - */ - class FirstTagSetCB implements Callback> { - public Object call(final Map first_tags) throws Exception { - tags = new HashMap(first_tags); - final ArrayList>> deferreds = - new ArrayList>>(tags.size()); - - while (it.hasNext()) { - deferreds.add(it.next().getTagsAsync()); + // value is always null, we just want the set of unique keys + final ByteMap discards = new ByteMap(); + final Iterator it = spans.iterator(); + while (it.hasNext()) { + final Span span = it.next(); + final ByteMap uids = span.getTagUids(); + + for (final Map.Entry tag_pair : uids.entrySet()) { + // we already know it's an aggregated tag + if (discards.containsKey(tag_pair.getKey())) { + continue; } - return Deferred.groupInOrder(deferreds).addCallback(new SpanTagsCB()); + final byte[] tag_value = tag_set.get(tag_pair.getKey()); + if (tag_value == null) { + tag_set.put(tag_pair.getKey(), tag_pair.getValue()); + } else if (Bytes.memcmp(tag_value, tag_pair.getValue()) != 0) { + // bump to aggregated tags + discards.put(tag_pair.getKey(), null); + tag_set.remove(tag_pair.getKey()); + } } } - - return it.next().getTagsAsync().addCallback(new FirstTagSetCB()); + + aggregated_tag_uids = discards.keySet(); + tag_uids = tag_set; } public String metricName() { @@ -333,19 +331,29 @@ public Map getTags() { public Deferred> getTagsAsync() { if (tags != null) { - final Map local_tags = tags; - return Deferred.fromResult(local_tags); + return Deferred.fromResult(tags); } - class ComputeCB implements Callback, Object> { - public Map call(final Object obj) { - return tags; - } + if (spans.isEmpty()) { + tags = new HashMap(0); + return Deferred.fromResult(tags); } - return computeTags().addCallback(new ComputeCB()); + if (tag_uids == null) { + computeTags(); + } + + return resolveTags(tag_uids); } + @Override + public ByteMap getTagUids() { + if (tag_uids == null) { + computeTags(); + } + return tag_uids; + } + public List getAggregatedTags() { try { return getAggregatedTagsAsync().joinUninterruptibly(); @@ -358,17 +366,19 @@ public List getAggregatedTags() { public Deferred> getAggregatedTagsAsync() { if (aggregated_tags != null) { - final List agg_tags = aggregated_tags; - return Deferred.fromResult(agg_tags); + return Deferred.fromResult(aggregated_tags); } - class ComputeCB implements Callback, Object> { - public List call(final Object obj) { - return aggregated_tags; - } + if (spans.isEmpty()) { + aggregated_tags = new ArrayList(0); + return Deferred.fromResult(aggregated_tags); + } + + if (aggregated_tag_uids == null) { + computeTags(); } - return computeTags().addCallback(new ComputeCB()); + return resolveAggTags(aggregated_tag_uids); } public List getTSUIDs() { @@ -476,4 +486,81 @@ private String toStringSharedAttributes() { public int getQueryIndex() { return query_index; } + + /** + * Resolves the set of tag keys to their string names. + * @param tagks The set of unique tag names + * @return a deferred to wait on for all of the tag keys to be resolved. The + * result should be null. + */ + private Deferred> resolveAggTags(final Set tagks) { + if (aggregated_tags != null) { + return Deferred.fromResult(null); + } + aggregated_tags = new ArrayList(tagks.size()); + + final List> names = + new ArrayList>(tagks.size()); + for (final byte[] tagk : tagks) { + names.add(tsdb.tag_names.getNameAsync(tagk)); + } + + /** Adds the names to the aggregated_tags list */ + final class ResolveCB implements Callback, ArrayList> { + @Override + public List call(final ArrayList names) throws Exception { + for (final String name : names) { + aggregated_tags.add(name); + } + return aggregated_tags; + } + } + + return Deferred.group(names).addCallback(new ResolveCB()); + } + + /** + * Resolves the tags to their names, loading them into {@link tags} after + * initializing that map. + * @param tag_uids The tag UIDs + * @return A defeferred to wait on for resolution to complete, the result + * should be null. + */ + private Deferred> resolveTags(final ByteMap tag_uids) { + if (tags != null) { + return Deferred.fromResult(null); + } + tags = new HashMap(tag_uids.size()); + + final List> deferreds = + new ArrayList>(tag_uids.size()); + + /** Dumps the pairs into the map in the correct order */ + final class PairCB implements Callback> { + @Override + public Object call(final ArrayList pair) throws Exception { + tags.put(pair.get(0), pair.get(1)); + return null; + } + } + + /** Callback executed once all of the pairs are resolved and stored in the map */ + final class GroupCB implements Callback, ArrayList> { + @Override + public Map call(final ArrayList group) + throws Exception { + return tags; + } + } + + for (Map.Entry tag_pair : tag_uids.entrySet()) { + final List> resolve_pair = + new ArrayList>(2); + resolve_pair.add(tsdb.tag_names.getNameAsync(tag_pair.getKey())); + resolve_pair.add(tsdb.tag_values.getNameAsync(tag_pair.getValue())); + deferreds.add(Deferred.groupInOrder(resolve_pair).addCallback(new PairCB())); + } + + return Deferred.group(deferreds).addCallback(new GroupCB()); + } } diff --git a/test/core/TestSpanGroup.java b/test/core/TestSpanGroup.java new file mode 100644 index 0000000000..8b0d9d848e --- /dev/null +++ b/test/core/TestSpanGroup.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.core; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; + +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.HBaseClient; +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; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, HBaseClient.class, Config.class, SpanGroup.class, + Span.class }) +public final class TestSpanGroup { + private static long start_ts = 1356998400L; + private static long end_ts = 1356998600L; + + private TSDB tsdb; + + @Before + public void before() { + tsdb = PowerMockito.mock(TSDB.class); + } + + @Test + public void getTagUids() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 2 }); + final Span span = mock(Span.class); + when(span.getTagUids()).thenReturn(uids); + + final SpanGroup group = PowerMockito.spy(new SpanGroup(tsdb, start_ts, + end_ts, null, false, Aggregators.SUM, 0, null)); + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, + uids_read.firstEntry().getValue())); + } + + @Test + public void getTagUidsAggedOut() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 2 }); + final Span span = mock(Span.class); + when(span.getTagUids()).thenReturn(uids); + + final ByteMap uids2 = new ByteMap(); + uids2.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 3 }); + final Span span2 = mock(Span.class); + when(span2.getTagUids()).thenReturn(uids2); + + final SpanGroup group = PowerMockito.spy(new SpanGroup(tsdb, start_ts, + end_ts, null, false, Aggregators.SUM, 0, null)); + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + spans.add(span2); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getTagUidsNoSpans() throws Exception { + final SpanGroup group = new SpanGroup(tsdb, start_ts, end_ts, null, + false, Aggregators.SUM, 0, null); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(0, uids_read.size()); + } +} From 85c9c02d5f92dd6ca1c3ef2af10fce484845d030 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 5 Apr 2015 13:11:32 -0700 Subject: [PATCH 126/826] Check the raw socket writeable state before responding with exceptions or error messages. This can cause an OOM eventually by filling up the netty socket. Signed-off-by: Chris Larsen --- src/tsd/PutDataPointRpc.java | 14 ++++++++++-- test/tsd/TestPutRpc.java | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 6942e006b5..6a539510e9 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -41,6 +41,7 @@ final class PutDataPointRpc implements TelnetRpc, HttpRpc { private static final AtomicLong invalid_values = new AtomicLong(); private static final AtomicLong illegal_arguments = new AtomicLong(); private static final AtomicLong unknown_metrics = new AtomicLong(); + private static final AtomicLong writes_blocked = new AtomicLong(); public Deferred execute(final TSDB tsdb, final Channel chan, final String[] cmd) { @@ -53,7 +54,11 @@ public Exception call(final Exception arg) { // another callback object on every data point. handleStorageException(tsdb, getDataPointFromString(cmd), arg); if (chan.isConnected()) { - chan.write("put: HBase error: " + arg.getMessage() + '\n'); + if (chan.isWritable()) { + chan.write("put: HBase error: " + arg.getMessage() + '\n'); + } else { + writes_blocked.incrementAndGet(); + } } hbase_errors.incrementAndGet(); return null; @@ -76,7 +81,11 @@ public String toString() { if (errmsg != null) { LOG.debug(errmsg); if (chan.isConnected()) { - chan.write(errmsg); + if (chan.isWritable()) { + chan.write(errmsg); + } else { + writes_blocked.incrementAndGet(); + } } } return Deferred.fromResult(null); @@ -231,6 +240,7 @@ public static void collectStats(final StatsCollector collector) { 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"); + collector.record("rpc.errors", writes_blocked, "type=socket_writes_blocked"); } /** diff --git a/test/tsd/TestPutRpc.java b/test/tsd/TestPutRpc.java index 7d20d84ece..31655d2caf 100644 --- a/test/tsd/TestPutRpc.java +++ b/test/tsd/TestPutRpc.java @@ -66,6 +66,7 @@ public final class TestPutRpc { private AtomicLong invalid_values = new AtomicLong(); private AtomicLong illegal_arguments = new AtomicLong(); private AtomicLong unknown_metrics = new AtomicLong(); + private AtomicLong writes_blocked = new AtomicLong(); private StorageExceptionHandler handler; @Before @@ -102,6 +103,8 @@ public void before() throws Exception { illegal_arguments.set(0); unknown_metrics = Whitebox.getInternalState(PutDataPointRpc.class, "unknown_metrics"); unknown_metrics.set(0); + writes_blocked = Whitebox.getInternalState(PutDataPointRpc.class, "writes_blocked"); + writes_blocked.set(0); handler = mock(StorageExceptionHandler.class); when(tsdb.getStorageExceptionHandler()).thenReturn(handler); @@ -154,6 +157,22 @@ public void executeMissingMetric() throws Exception { verify(tsdb, never()).getStorageExceptionHandler(); } + @Test + public void executeMissingMetricNotWriteable() throws Exception { + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + when(chan.isWritable()).thenReturn(false); + assertNull(put.execute(tsdb, chan, new String[] { "put", "", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, illegal_arguments.get()); + assertEquals(1, writes_blocked.get()); + verify(chan, never()).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, never()).getStorageExceptionHandler(); + } + @Test public void executeUnknownMetric() throws Exception { final PutDataPointRpc put = new PutDataPointRpc(); @@ -201,6 +220,28 @@ public void executeHBaseError() throws Exception { verify(tsdb, times(1)).getStorageExceptionHandler(); } + @SuppressWarnings("unchecked") + @Test + public void executeHBaseErrorNotWriteable() throws Exception { + when(tsdb.addPoint(anyString(), anyLong(), anyLong(), + (HashMap)any())) + .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + + final PutDataPointRpc put = new PutDataPointRpc(); + final Channel chan = NettyMocks.fakeChannel(); + when(chan.isWritable()).thenReturn(false); + assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", + "1365465600", "42", "host=web01" }).joinUninterruptibly()); + + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + assertEquals(1, hbase_errors.get()); + assertEquals(1, writes_blocked.get()); + verify(chan, never()).write(any()); + verify(chan, times(1)).isConnected(); + verify(tsdb, times(1)).getStorageExceptionHandler(); + } + @SuppressWarnings("unchecked") @Test public void executeHBaseErrorHandler() throws Exception { From 1d747b883646eccd8f8fc7ff0a15cfeed1acdf69 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 13 Apr 2015 17:56:01 -0700 Subject: [PATCH 127/826] Fix the path to TestStatsRpc in the makefile --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index ffbd686509..633045ea1a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -217,7 +217,7 @@ test_SRC := \ test/tsd/TestRpcManager.java \ test/tsd/TestRTPublisher.java \ test/tsd/TestSearchRpc.java \ - test/uid/TestStatsRpc.java \ + test/tsd/TestStatsRpc.java \ test/tsd/TestSuggestRpc.java \ test/tsd/TestTreeRpc.java \ test/tsd/TestUniqueIdRpc.java \ From 590bf57ac584bfaa9a32f5061d723bf9a1617854 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 18 Apr 2015 21:09:49 -0700 Subject: [PATCH 128/826] Remove links to the old Google code repo in the third party includes. Signed-off-by: Chris Larsen --- third_party/logback/include.mk | 77 ++++++++----------- .../logback/logback-classic-1.0.13.jar.md5 | 2 +- .../logback/logback-core-1.0.13.jar.md5 | 2 +- third_party/mockito/include.mk | 4 +- .../mockito/mockito-core-1.9.5.jar.md5 | 2 +- third_party/suasync/async-1.4.0.jar.md5 | 1 + third_party/suasync/include.mk | 6 +- third_party/suasync/suasync-1.2.0.jar.md5 | 1 - third_party/suasync/suasync-1.3.1.jar.md5 | 1 - third_party/suasync/suasync-1.3.2.jar.md5 | 1 - third_party/suasync/suasync-1.4.0.jar.md5 | 1 - third_party/zookeeper/include.mk | 4 +- third_party/zookeeper/zookeeper-3.3.6.jar.md5 | 2 +- 13 files changed, 46 insertions(+), 58 deletions(-) create mode 100644 third_party/suasync/async-1.4.0.jar.md5 delete mode 100644 third_party/suasync/suasync-1.2.0.jar.md5 delete mode 100644 third_party/suasync/suasync-1.3.1.jar.md5 delete mode 100644 third_party/suasync/suasync-1.3.2.jar.md5 delete mode 100644 third_party/suasync/suasync-1.4.0.jar.md5 diff --git a/third_party/logback/include.mk b/third_party/logback/include.mk index 83074aba4f..de025c59ff 100644 --- a/third_party/logback/include.mk +++ b/third_party/logback/include.mk @@ -1,43 +1,34 @@ -# Copyright (C) 2011-2012 The Async HBase Authors. All rights reserved. -# This file is part of Async HBase. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# - Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# - Neither the name of the StumbleUpon nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -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 := $(OPENTSDB_THIRD_PARTY_BASE_URL) - -$(LOGBACK_CLASSIC): $(LOGBACK_CLASSIC).md5 - set dummy "$(LOGBACK_CLASSIC_BASE_URL)" "$(LOGBACK_CLASSIC)"; shift; $(FETCH_DEPENDENCY) - - -LOGBACK_CORE_VERSION := $(LOGBACK_VERSION) -LOGBACK_CORE := third_party/logback/logback-core-$(LOGBACK_CORE_VERSION).jar -LOGBACK_CORE_BASE_URL := $(OPENTSDB_THIRD_PARTY_BASE_URL) - -$(LOGBACK_CORE): $(LOGBACK_CORE).md5 - set dummy "$(LOGBACK_CORE_BASE_URL)" "$(LOGBACK_CORE)"; shift; $(FETCH_DEPENDENCY) - -THIRD_PARTY += $(LOGBACK_CLASSIC) $(LOGBACK_CORE) +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +http://central.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): $(LOGBACK_CLASSIC).md5 + set dummy "$(LOGBACK_CLASSIC_BASE_URL)" "$(LOGBACK_CLASSIC)"; shift; $(FETCH_DEPENDENCY) + + +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): $(LOGBACK_CORE).md5 + set dummy "$(LOGBACK_CORE_BASE_URL)" "$(LOGBACK_CORE)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(LOGBACK_CLASSIC) $(LOGBACK_CORE) diff --git a/third_party/logback/logback-classic-1.0.13.jar.md5 b/third_party/logback/logback-classic-1.0.13.jar.md5 index 66fd767e18..ae8b69cb80 100644 --- a/third_party/logback/logback-classic-1.0.13.jar.md5 +++ b/third_party/logback/logback-classic-1.0.13.jar.md5 @@ -1 +1 @@ -b4dc8eb42150aafd6d9fd3d211807621 +18586a078b51918942002ec085338e19 diff --git a/third_party/logback/logback-core-1.0.13.jar.md5 b/third_party/logback/logback-core-1.0.13.jar.md5 index 19107c10a7..d4093eb2a6 100644 --- a/third_party/logback/logback-core-1.0.13.jar.md5 +++ b/third_party/logback/logback-core-1.0.13.jar.md5 @@ -1 +1 @@ -3d5f8ce8dca36e493d39177b71958bd4 +945c6dc3c10d3ce784d456a8bbbd0262 diff --git a/third_party/mockito/include.mk b/third_party/mockito/include.mk index ee48e0dfa0..aa99f81071 100644 --- a/third_party/mockito/include.mk +++ b/third_party/mockito/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2013 The OpenTSDB Authors. +# Copyright (C) 2015 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -15,7 +15,7 @@ MOCKITO_VERSION := 1.9.5 MOCKITO := third_party/mockito/mockito-core-$(MOCKITO_VERSION).jar -MOCKITO_BASE_URL := $(OPENTSDB_THIRD_PARTY_BASE_URL) +MOCKITO_BASE_URL := http://central.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/mockito/mockito-core-1.9.5.jar.md5 b/third_party/mockito/mockito-core-1.9.5.jar.md5 index 3d0c520c1a..2b419c7325 100644 --- a/third_party/mockito/mockito-core-1.9.5.jar.md5 +++ b/third_party/mockito/mockito-core-1.9.5.jar.md5 @@ -1 +1 @@ -98f3076e2a691d1ac291624e5a46b80b +6f73cf04a56eb60aaa996506e7c10fc7 diff --git a/third_party/suasync/async-1.4.0.jar.md5 b/third_party/suasync/async-1.4.0.jar.md5 new file mode 100644 index 0000000000..122987eda5 --- /dev/null +++ b/third_party/suasync/async-1.4.0.jar.md5 @@ -0,0 +1 @@ +90aa9cc566423f12af88e205804d5161 diff --git a/third_party/suasync/include.mk b/third_party/suasync/include.mk index 53c137e1eb..599fe6e89d 100644 --- a/third_party/suasync/include.mk +++ b/third_party/suasync/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2012 The OpenTSDB Authors. +# Copyright (C) 2015 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -14,8 +14,8 @@ # along with this library. If not, see . SUASYNC_VERSION := 1.4.0 -SUASYNC := third_party/suasync/suasync-$(SUASYNC_VERSION).jar -SUASYNC_BASE_URL := $(OPENTSDB_THIRD_PARTY_BASE_URL) +SUASYNC := third_party/suasync/async-$(SUASYNC_VERSION).jar +SUASYNC_BASE_URL := http://central.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/suasync/suasync-1.2.0.jar.md5 b/third_party/suasync/suasync-1.2.0.jar.md5 deleted file mode 100644 index 25f85ade2e..0000000000 --- a/third_party/suasync/suasync-1.2.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -abca5dfd6c71c6cc02ffa830ede9c4bc diff --git a/third_party/suasync/suasync-1.3.1.jar.md5 b/third_party/suasync/suasync-1.3.1.jar.md5 deleted file mode 100644 index 0a89e20b7c..0000000000 --- a/third_party/suasync/suasync-1.3.1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -68b67af908e534476b9baa6ae2edb929 \ No newline at end of file diff --git a/third_party/suasync/suasync-1.3.2.jar.md5 b/third_party/suasync/suasync-1.3.2.jar.md5 deleted file mode 100644 index 65dc463d92..0000000000 --- a/third_party/suasync/suasync-1.3.2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -62cf94994a0a6c2c9e3ed32b2cef837f diff --git a/third_party/suasync/suasync-1.4.0.jar.md5 b/third_party/suasync/suasync-1.4.0.jar.md5 deleted file mode 100644 index 0f63f6efb5..0000000000 --- a/third_party/suasync/suasync-1.4.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -289ce3f3e6a9bb17857981eacf6d74b6 diff --git a/third_party/zookeeper/include.mk b/third_party/zookeeper/include.mk index 95f36c479d..69368ea853 100644 --- a/third_party/zookeeper/include.mk +++ b/third_party/zookeeper/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2013 The OpenTSDB Authors. +# Copyright (C) 2015 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -15,7 +15,7 @@ ZOOKEEPER_VERSION := 3.3.6 ZOOKEEPER := third_party/zookeeper/zookeeper-$(ZOOKEEPER_VERSION).jar -ZOOKEEPER_BASE_URL := $(OPENTSDB_THIRD_PARTY_BASE_URL) +ZOOKEEPER_BASE_URL := http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/$(ZOOKEEPER_VERSION) $(ZOOKEEPER): $(ZOOKEEPER).md5 set dummy "$(ZOOKEEPER_BASE_URL)" "$(ZOOKEEPER)"; shift; $(FETCH_DEPENDENCY) diff --git a/third_party/zookeeper/zookeeper-3.3.6.jar.md5 b/third_party/zookeeper/zookeeper-3.3.6.jar.md5 index 9b12197af3..4a38122639 100644 --- a/third_party/zookeeper/zookeeper-3.3.6.jar.md5 +++ b/third_party/zookeeper/zookeeper-3.3.6.jar.md5 @@ -1 +1 @@ -02786e11c19d1671640992f1bda4a858 +a4425412297adf88c157a263d61e2cf1 From ac096ebe0e6b6d742d894b9debc482d6506c6217 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 19 Apr 2015 12:44:57 -0700 Subject: [PATCH 129/826] Fix #492 where the bufferedAtomicIncrement returns the final value of all buffered increments instead of the actual values. This will result in a lot of calls to HBase so I need to get the local buffer code out there quick. Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 86e974ad76..825270a5f7 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -528,7 +528,7 @@ final class TSMetaCB implements Callback, Long> { @Override public Deferred call(final Long incremented_value) throws Exception { - +LOG.info("Value: " + incremented_value); if (incremented_value > 1) { // TODO - maybe update the search index every X number of increments? // Otherwise the search engine would only get last_updated/count @@ -611,9 +611,9 @@ public Deferred call(Boolean success) throws Exception { // if the user has disabled real time TSMeta tracking (due to OOM issues) // then we only want to increment the data point count. if (!tsdb.getConfig().enable_realtime_ts()) { - return tsdb.getClient().bufferAtomicIncrement(inc); + return tsdb.getClient().atomicIncrement(inc); } - return tsdb.getClient().bufferAtomicIncrement(inc).addCallbackDeferring( + return tsdb.getClient().atomicIncrement(inc).addCallbackDeferring( new TSMetaCB()); } From 0486d480a1c8e3ad5888de31080bb7012db7c50a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 19 Apr 2015 13:12:38 -0700 Subject: [PATCH 130/826] Fix the TSMeta unit test post #492 Signed-off-by: Chris Larsen --- test/meta/TestTSMeta.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/meta/TestTSMeta.java b/test/meta/TestTSMeta.java index 034115feb6..1dc7db3989 100644 --- a/test/meta/TestTSMeta.java +++ b/test/meta/TestTSMeta.java @@ -332,7 +332,7 @@ public void counterExistsInStorageNot() throws Exception { public void incrementAndGetCounter() throws Exception { final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 1 }; TSMeta.incrementAndGetCounter(tsdb, tsuid).joinUninterruptibly(); - verify(client).bufferAtomicIncrement((AtomicIncrementRequest)any()); + verify(client).atomicIncrement((AtomicIncrementRequest)any()); } @Test (expected = NoSuchUniqueId.class) From 20087de303e686d47062522a3840d4ba60d45c19 Mon Sep 17 00:00:00 2001 From: nickman Date: Sun, 19 Apr 2015 19:44:34 -0700 Subject: [PATCH 131/826] Fix #455 by adding a thread factory to the hashed wheel timer. Signed-off-by: Chris Larsen --- src/tsd/PipelineFactory.java | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index e706c302f0..b238d1397c 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -14,6 +14,8 @@ import static org.jboss.netty.channel.Channels.pipeline; +import java.util.concurrent.ThreadFactory; + import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; @@ -47,7 +49,7 @@ public final class PipelineFactory implements ChannelPipelineFactory { // PipelineFactory is needed. private final ConnectionManager connmgr = new ConnectionManager(); private final DetectHttpOrRpc HTTP_OR_RPC = new DetectHttpOrRpc(); - private final Timer timer = new HashedWheelTimer(); + private final Timer timer = new HashedWheelTimer(new PipelineThreadFactory()); private final ChannelHandler timeoutHandler; /** Stateless handler for RPCs. */ @@ -149,5 +151,21 @@ protected Object decode(final ChannelHandlerContext ctx, } + /** + * A class to generate a daemon thread for the idle connection timer. + */ + class PipelineThreadFactory implements ThreadFactory { + @Override + public Thread newThread(final Runnable r) { + final Thread t = new Thread(r, "PipelineFactoryTimer"); + t.setDaemon(true); + return t; + } + + @Override + public String toString() { + return "Pipeline timer thread factory"; + } + } } \ No newline at end of file From cfa518d2b98995759a5f7628b8acd6f4a1df2297 Mon Sep 17 00:00:00 2001 From: Gabriel Nicolas Avellaneda Date: Fri, 17 Apr 2015 20:27:49 +0000 Subject: [PATCH 132/826] Modify the Config class to trim space from booleans and integers. Added some tests for Config property parsing and property value trim for accepting properties with additional blanks. Signed-off-by: Chris Larsen --- src/utils/Config.java | 25 +++++++++++---- test/utils/TestConfig.java | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index 1829eb92bb..cab00edcf3 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -256,7 +256,20 @@ public final String getString(final String property) { * @throws NullPointerException if the property did not exist */ public final int getInt(final String property) { - return Integer.parseInt(properties.get(property)); + return Integer.parseInt(sanitize(properties.get(property))); + } + + /** + * Returns the given string trimed or null if is null + * @param string The string be trimmed of + * @return The string trimed or null + */ + private final String sanitize(final String string) { + if (string == null) { + return null; + } + + return string.trim(); } /** @@ -267,7 +280,7 @@ public final int getInt(final String property) { * @throws NullPointerException if the property did not exist */ public final short getShort(final String property) { - return Short.parseShort(properties.get(property)); + return Short.parseShort(sanitize(properties.get(property))); } /** @@ -278,7 +291,7 @@ public final short getShort(final String property) { * @throws NullPointerException if the property did not exist */ public final long getLong(final String property) { - return Long.parseLong(properties.get(property)); + return Long.parseLong(sanitize(properties.get(property))); } /** @@ -289,7 +302,7 @@ public final long getLong(final String property) { * @throws NullPointerException if the property did not exist */ public final float getFloat(final String property) { - return Float.parseFloat(properties.get(property)); + return Float.parseFloat(sanitize(properties.get(property))); } /** @@ -300,7 +313,7 @@ public final float getFloat(final String property) { * @throws NullPointerException if the property did not exist */ public final double getDouble(final String property) { - return Double.parseDouble(properties.get(property)); + return Double.parseDouble(sanitize(properties.get(property))); } /** @@ -316,7 +329,7 @@ public final double getDouble(final String property) { * @throws NullPointerException if the property was not found */ public final boolean getBoolean(final String property) { - final String val = properties.get(property).toUpperCase(); + final String val = properties.get(property).trim().toUpperCase(); if (val.equals("1")) return true; if (val.equals("TRUE")) diff --git a/test/utils/TestConfig.java b/test/utils/TestConfig.java index b30e17ddbb..604777d1cb 100644 --- a/test/utils/TestConfig.java +++ b/test/utils/TestConfig.java @@ -142,6 +142,15 @@ public void getInt() throws Exception { config.getInt("tsd.int")); } + @Test + public void getIntWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.int", + " " + Integer.toString(Integer.MAX_VALUE) + " "); + assertEquals(Integer.MAX_VALUE, + config.getInt("tsd.int")); + } + @Test public void getIntNegative() throws Exception { final Config config = new Config(false); @@ -181,6 +190,15 @@ public void getShort() throws Exception { config.getShort("tsd.short")); } + @Test + public void getShortWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.short", + " " + Short.toString(Short.MAX_VALUE) + " "); + assertEquals(Short.MAX_VALUE, + config.getShort("tsd.short")); + } + @Test public void getShortNegative() throws Exception { final Config config = new Config(false); @@ -218,6 +236,13 @@ public void getLong() throws Exception { assertEquals(Long.MAX_VALUE, config.getLong("tsd.long")); } + @Test + public void getLongWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.long", " " + Long.toString(Long.MAX_VALUE) + " "); + assertEquals(Long.MAX_VALUE, config.getLong("tsd.long")); + } + @Test public void getLongNegative() throws Exception { final Config config = new Config(false); @@ -254,6 +279,14 @@ public void getFloat() throws Exception { config.getFloat("tsd.float"), 0.000001); } + @Test + public void getFloatWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.float", " " + Float.toString(Float.MAX_VALUE) + " "); + assertEquals(Float.MAX_VALUE, + config.getFloat("tsd.float"), 0.000001); + } + @Test public void getFloatNegative() throws Exception { final Config config = new Config(false); @@ -322,6 +355,14 @@ public void getDouble() throws Exception { config.getDouble("tsd.double"), 0.000001); } + @Test + public void getDoubleWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.double", " " + Double.toString(Double.MAX_VALUE) + " "); + assertEquals(Double.MAX_VALUE, + config.getDouble("tsd.double"), 0.000001); + } + @Test public void getDoubleNegative() throws Exception { final Config config = new Config(false); @@ -397,6 +438,13 @@ public void getBool1() throws Exception { assertTrue(config.getBoolean("tsd.bool")); } + @Test + public void getBool1WithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", " 1 "); + assertTrue(config.getBoolean("tsd.bool")); + } + @Test public void getBoolTrueCaseInsensitive() throws Exception { final Config config = new Config(false); @@ -404,6 +452,13 @@ public void getBoolTrueCaseInsensitive() throws Exception { assertTrue(config.getBoolean("tsd.bool")); } + @Test + public void getBoolTrueWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", "TrUe "); + assertTrue(config.getBoolean("tsd.bool")); + } + @Test public void getBoolYes() throws Exception { final Config config = new Config(false); @@ -411,6 +466,13 @@ public void getBoolYes() throws Exception { assertTrue(config.getBoolean("tsd.bool")); } + @Test + public void getBoolYesWithSpaces() throws Exception { + final Config config = new Config(false); + config.overrideConfig("tsd.bool", " yes "); + assertTrue(config.getBoolean("tsd.bool")); + } + @Test public void getBoolYesCaseInsensitive() throws Exception { final Config config = new Config(false); From ce1463b30c66ad136dd1f2e841cc6cf5fc735e10 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Mon, 27 Apr 2015 21:13:14 -0700 Subject: [PATCH 133/826] Add the FillingDownsampler and FillPolicy classes to allow for returning NaNs, Nulls or zeros when data points are "missing" as determined by a downsampling interval. Modify the Downsampler class to be extendable. Signed-off-by: Chris Larsen --- src/core/Downsampler.java | 61 ++++++---- src/core/FillPolicy.java | 56 +++++++++ src/core/FillingDownsampler.java | 146 ++++++++++++++++++++++++ test/core/TestFillingDownsampler.java | 158 ++++++++++++++++++++++++++ 4 files changed, 398 insertions(+), 23 deletions(-) create mode 100644 src/core/FillPolicy.java create mode 100644 src/core/FillingDownsampler.java create mode 100644 test/core/TestFillingDownsampler.java diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index bd707d362e..d4d56ff51d 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -14,20 +14,19 @@ import java.util.NoSuchElementException; - /** * Iterator that downsamples data points using an {@link Aggregator}. */ public class Downsampler implements SeekableView, DataPoint { /** Function to use for downsampling. */ - private final Aggregator downsampler; + protected final Aggregator downsampler; /** Iterator to iterate the values of the current interval. */ - private final ValuesInInterval values_in_interval; + protected final ValuesInInterval values_in_interval; /** Last normalized timestamp */ - private long timestamp; + protected long timestamp; /** Last value as a double */ - private double value; + protected double value; /** * Ctor. @@ -47,10 +46,15 @@ public class Downsampler implements SeekableView, DataPoint { // Iterator interface // // ------------------ // + @Override public boolean hasNext() { return values_in_interval.hasNextValue(); } + /** + * @throws NoSuchElementException if no data points remain. + */ + @Override public DataPoint next() { if (hasNext()) { value = downsampler.runDouble(values_in_interval); @@ -61,6 +65,7 @@ public DataPoint next() { throw new NoSuchElementException("no more data points in " + this); } + @Override public void remove() { throw new UnsupportedOperationException(); } @@ -69,49 +74,59 @@ public void remove() { // SeekableView interface // // ---------------------- // + @Override public void seek(final long timestamp) { values_in_interval.seekInterval(timestamp); } - @Override - public String toString() { - final StringBuilder buf = new StringBuilder(); - buf.append("Downsampler: ") - .append("interval_ms=").append(values_in_interval.interval_ms) - .append(", downsampler=").append(downsampler) - .append(", current data=(timestamp=").append(timestamp) - .append(", value=").append(value) - .append("), values_in_interval=").append(values_in_interval); - return buf.toString(); - } + // ------------------- // + // DataPoint interface // + // ------------------- // + @Override public long timestamp() { return timestamp; } + @Override public boolean isInteger() { return false; } + @Override public long longValue() { throw new ClassCastException("Downsampled values are doubles"); } + @Override public double doubleValue() { return value; } + @Override public double toDouble() { return value; } - + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("Downsampler: ") + .append("interval_ms=").append(values_in_interval.interval_ms) + .append(", downsampler=").append(downsampler) + .append(", current data=(timestamp=").append(timestamp) + .append(", value=").append(value) + .append("), values_in_interval=").append(values_in_interval); + return buf.toString(); + } + /** Iterates source values for an interval. */ - private static class ValuesInInterval implements Aggregator.Doubles { + protected static class ValuesInInterval implements Aggregator.Doubles { /** The iterator of original source values. */ private final SeekableView source; /** The sampling interval in milliseconds. */ - private final long interval_ms; + protected final long interval_ms; /** The end of the current interval. */ private long timestamp_end_interval = Long.MIN_VALUE; /** True if the last value was successfully extracted from the source. */ @@ -134,7 +149,7 @@ private static class ValuesInInterval implements Aggregator.Doubles { } /** Initializes to iterate intervals. */ - private void initializeIfNotDone() { + protected void initializeIfNotDone() { // NOTE: Delay initialization is required to not access any data point // from the source until a user requests it explicitly to avoid the severe // performance penalty by accessing the unnecessary first data of a span. @@ -174,7 +189,7 @@ void moveToNextInterval() { } /** Advances the interval iterator to the given timestamp. */ - void seekInterval(long timestamp) { + void seekInterval(final long timestamp) { // To make sure that the interval of the given timestamp is fully filled, // rounds up the seeking timestamp to the smallest timestamp that is // a multiple of the interval and is greater than or equal to the given @@ -184,7 +199,7 @@ void seekInterval(long timestamp) { } /** Returns the representative timestamp of the current interval. */ - private long getIntervalTimestamp() { + protected long getIntervalTimestamp() { // NOTE: It is well-known practice taking the start time of // a downsample interval as a representative timestamp of it. It also // provides the correct context for seek. @@ -192,7 +207,7 @@ private long getIntervalTimestamp() { } /** Returns timestamp aligned by interval. */ - private long alignTimestamp(long timestamp) { + protected long alignTimestamp(final long timestamp) { return timestamp - (timestamp % interval_ms); } diff --git a/src/core/FillPolicy.java b/src/core/FillPolicy.java new file mode 100644 index 0000000000..7cba9bc5d9 --- /dev/null +++ b/src/core/FillPolicy.java @@ -0,0 +1,56 @@ +// 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; + +/** + * Specification of how to deal with missing intervals when downsampling. + * @since 2.2 + */ +public enum FillPolicy { + NONE("none"), + ZERO("zero"), + NOT_A_NUMBER("nan"), + NULL("null"); + + // The user-friendly name of this policy. + private final String name; + + FillPolicy(final String name) { + this.name = name; + } + + /** + * Get this fill policy's user-friendly name. + * @return this fill policy's user-friendly name. + */ + public String getName() { + return name; + } + + /** + * Get an instance of this enumeration from a user-friendly name. + * @param name The user-friendly name of a fill policy. + * @return an instance of {@link FillPolicy}, or {@code null} if the name + * does not match any instance. + */ + public static FillPolicy fromString(final String name) { + for (final FillPolicy policy : FillPolicy.values()) { + if (policy.name.equalsIgnoreCase(name)) { + return policy; + } + } + + return null; + } +} + diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java new file mode 100644 index 0000000000..0e4d77ad10 --- /dev/null +++ b/src/core/FillingDownsampler.java @@ -0,0 +1,146 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2015 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see . +package net.opentsdb.core; + +import java.util.NoSuchElementException; + +/** + * A specialized downsampler that returns special values, based on the fill + * policy, for intervals for which no data could be found. The default + * implementation, {@link Downsampler}, simply skips intervals that have no + * data, which causes the {@link AggregationIterator} up the chain to + * interpolate. + * @since 2.2 + */ +public class FillingDownsampler extends Downsampler { + /** Track when the downsampled data should end. */ + protected long end_timestamp; + + /** Downsampling fill policy. */ + protected final FillPolicy fill_policy; + + /** + * Create a new nulling downsampler. + * @param source The iterator to access the underlying data. + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. + * @param interval_ms The interval in milli seconds wanted between each data + * point. + * @param downsampler The downsampling function to use. + * @param fill_policy Policy specifying whether to interpolate or to fill + * missing intervals with special values. + * @throws IllegalArgumentException if fill_policy is interpolation. + */ + FillingDownsampler(final SeekableView source, final long start_time, + final long end_time, final long interval_ms, + final Aggregator downsampler, final FillPolicy fill_policy) { + // Lean on the superclass implementation. + super(source, interval_ms, downsampler); + + // Ensure we aren't given a bogus fill policy. + if (FillPolicy.NONE == fill_policy) { + throw new IllegalArgumentException("Cannot instantiate this class with" + + " linear-interpolation fill policy"); + } + this.fill_policy = fill_policy; + + // Use the values-in-interval object to align the timestamps at which we + // expect data to arrive for the first and last intervals. + this.timestamp = values_in_interval.alignTimestamp(start_time); + this.end_timestamp = values_in_interval.alignTimestamp(end_time); + } + + /** + * Please note that when this method returns true, the value yielded by the + * object returned by {@link #next()} might be NaN, which indicates no data + * could be found for the current interval. + * @return true if this iterator has not yet reached the end of the specified + * range of data; otherwise, false. + */ + @Override + public boolean hasNext() { + // No matter the state of the values-in-interval object, if our current + // timestamp hasn't reached the end of the requested overall interval, then + // we still have iterating to do. + return timestamp < end_timestamp; + } + + /** + * Please note that the object returned by this method may return the value + * NaN, which indicates that no data count be found for the interval. This is + * intentional. Future intervals, if any, may still hava data and thus yield + * non-NaN values. + * @return the next data point, which might yield a NaN value. + * @throws NoSuchElementException if no more intervals remain. + */ + @Override + public DataPoint next() { + // Don't proceed if we've already completed iteration. + if (hasNext()) { + // Ensure that the timestamp we request is valid. + values_in_interval.initializeIfNotDone(); + + // Skip any leading data outside the query bounds. + long actual = values_in_interval.getIntervalTimestamp(); + while (values_in_interval.hasNextValue() && actual < timestamp) { + // The actual timestamp precedes our expected, so there's data in the + // values-in-interval object that we wish to ignore. + downsampler.runDouble(values_in_interval); + values_in_interval.moveToNextInterval(); + actual = values_in_interval.getIntervalTimestamp(); + } + + // Check whether the timestamp of the calculation interval matches what + // we expect. + if (actual == timestamp) { + // The calculated interval timestamp matches what we expect, so we can + // do normal processing. + value = downsampler.runDouble(values_in_interval); + values_in_interval.moveToNextInterval(); + } else { + // Our expected timestamp precedes the actual, so the interval is + // missing. We will use a special value, based on the fill policy, to + // represent this case. + switch (fill_policy) { + case NOT_A_NUMBER: + case NULL: + value = Double.NaN; + break; + + case ZERO: + value = 0.0; + break; + + default: + throw new RuntimeException("unhandled fill policy"); + } + } + + // Advance the expected timestamp to the next interval. + timestamp += values_in_interval.interval_ms; + + // This object also represents the data. + return this; + } + + // Ideally, the user will not call this method when no data remains, but + // we can't enforce that. + throw new NoSuchElementException("no more data points in " + this); + } + + @Override + public long timestamp() { + return timestamp - values_in_interval.interval_ms; + } +} + diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java new file mode 100644 index 0000000000..70dd698aca --- /dev/null +++ b/test/core/TestFillingDownsampler.java @@ -0,0 +1,158 @@ +// 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** Tests {@link FillingDownsampler}. */ +public class TestFillingDownsampler { + private static final Aggregator SUM = Aggregators.get("sum"); + + private static final FillPolicy NAN = FillPolicy.fromString("nan"); + private static final FillPolicy ZERO = FillPolicy.fromString("zero"); + + /** Data with gaps: before, during, and after. */ + @Test + public void testNaNMissingInterval() { + final long baseTime = 500L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime + 25L * 4L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 5L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 7L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 12L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 15L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 24L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 25L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 26L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), + }); + + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 36 * 25L, 100L, SUM, NAN); + + step(downsampler, Double.NaN); + step(downsampler, 3.); + step(downsampler, Double.NaN); + step(downsampler, 2.); + step(downsampler, Double.NaN); + step(downsampler, Double.NaN); + step(downsampler, 4.); + step(downsampler, Double.NaN); + step(downsampler, Double.NaN); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testZeroMissingInterval() { + final long baseTime = 500L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime + 25L * 4L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 5L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 7L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 12L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 15L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 24L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 25L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 26L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), + }); + + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 36 * 25L, 100L, SUM, ZERO); + + step(downsampler, 0.); + step(downsampler, 3.); + step(downsampler, 0.); + step(downsampler, 2.); + step(downsampler, 0.); + step(downsampler, 0.); + step(downsampler, 4.); + step(downsampler, 0.); + step(downsampler, 0.); + assertFalse(downsampler.hasNext()); + } + + /** Contiguous data, i.e., nothing missing. */ + @Test + public void testWithoutMissingIntervals() { + final long baseTime = 1000L; + 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.), + }); + + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 12L * 25L, 100L, SUM, NAN); + + step(downsampler, 42.); + step(downsampler, 26.); + step(downsampler, 10.); + assertFalse(downsampler.hasNext()); + } + + /** Data up to five minutes out of query time bounds. */ + @Test + public void testWithOutOfBoundsData() { + final long baseTime = 1425335895000L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime - 60000L * 5L + 320L, 53.), + MutableDataPoint.ofDoubleValue(baseTime - 60000L * 2L + 8839L, 16.), + + // start query + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 0L + 849L, 9.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 0L + 3849L, 8.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 0L + 6210L, 7.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 0L + 42216L, 6.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 1L + 167L, 5.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 1L + 28593L, 4.), + // end query + + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 2L + 30384L, 37.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 4L + 1530L, 86.) + }); + + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 60000L * 2L, 60000L, SUM, NAN); + + step(downsampler, 30.); + step(downsampler, 9.); + assertFalse(downsampler.hasNext()); + } + + private void step(final Downsampler downsampler, final double expected) { + assertTrue(downsampler.hasNext()); + final DataPoint point = downsampler.next(); + assertNotNull(point); + assertEquals(expected, point.doubleValue(), 0.01); + } +} + From 4ba6faddc94f4bfd0fa516fc03d3b9ca86c82ea3 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Mon, 27 Apr 2015 21:40:49 -0700 Subject: [PATCH 134/826] Add the DownsamplingSpecification class Signed-off-by: Chris Larsen --- Makefile.am | 5 + src/core/DownsamplingSpecification.java | 173 +++++++++++++++++++ test/core/TestDownsamplingSpecification.java | 69 ++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/core/DownsamplingSpecification.java create mode 100644 test/core/TestDownsamplingSpecification.java diff --git a/Makefile.am b/Makefile.am index 633045ea1a..61e04b52b5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -43,6 +43,9 @@ tsdb_SRC := \ src/core/DataPoints.java \ src/core/DataPointsIterator.java \ src/core/Downsampler.java \ + src/core/DownsamplingSpecification.java \ + src/core/FillingDownsampler.java \ + src/core/FillPolicy.java \ src/core/IncomingDataPoint.java \ src/core/IncomingDataPoints.java \ src/core/IllegalDataException.java \ @@ -163,6 +166,8 @@ test_SRC := \ test/core/TestBatchedDataPoints.java \ test/core/TestCompactionQueue.java \ test/core/TestDownsampler.java \ + test/core/TestDownsamplingSpecification.java \ + test/core/TestFillingDownsampler.java \ test/core/TestIncomingDataPoints.java \ test/core/TestInternal.java \ test/core/TestMutableDataPoint.java \ diff --git a/src/core/DownsamplingSpecification.java b/src/core/DownsamplingSpecification.java new file mode 100644 index 0000000000..3f590d20f0 --- /dev/null +++ b/src/core/DownsamplingSpecification.java @@ -0,0 +1,173 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2015 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see . +package net.opentsdb.core; + +import java.util.NoSuchElementException; + +import com.google.common.base.MoreObjects; +import net.opentsdb.utils.DateTime; + +/** + * Representation of a downsampling specification in a TSDB query. + * @since 2.2 + */ +public final class DownsamplingSpecification { + /** Instance of a specification indicating no downsampling requested. */ + public static final DownsamplingSpecification NO_DOWNSAMPLER = + new DownsamplingSpecification(); + + /** Special value representing no downsampling interval given. */ + public static final long NO_INTERVAL = 0L; + + /** Special value representing no downsampling function given. */ + public static final Aggregator NO_FUNCTION = null; + + /** The default fill policy. */ + public static final FillPolicy DEFAULT_FILL_POLICY = FillPolicy.NONE; + + // Parsed downsample interval. + private final long interval; + + // Parsed downsampler function. + private final Aggregator function; + + // Parsed fill policy: whether to interpolate or to fill. + private final FillPolicy fill_policy; + + /** + * A specification indicating no downsampling is requested. + */ + private DownsamplingSpecification() { + interval = NO_INTERVAL; + function = NO_FUNCTION; + fill_policy = DEFAULT_FILL_POLICY; + } + + /** + * Non-stringified, piecewise c-tor. + * @param interval The downsampling interval, in milliseconds. + * @param function The downsampling function. + * @param fill_policy The policy specifying how to deal with missing data. + * @throws IllegalArgumentException if any argument is invalid. + */ + public DownsamplingSpecification(final long interval, + final Aggregator function, final FillPolicy fill_policy) { + if (null == function) { + throw new IllegalArgumentException("downsampling function cannot be null"); + } + if (interval <= 0L) { + throw new IllegalArgumentException("interval not > 0: " + interval); + } + if (null == fill_policy) { + throw new IllegalArgumentException("fill policy cannot be null"); + } + + this.interval = interval; + this.function = function; + this.fill_policy = fill_policy; + } + + /** + * C-tor for string representations. + * The argument to this c-tor should have the following format: + * {@code interval-function[-fill_policy]}. + * @param specification String representation of a downsample specifier. + * @throws IllegalArgumentException if the specification is null or invalid. + */ + public DownsamplingSpecification(final String specification) { + if (null == specification) { + throw new IllegalArgumentException("Downsampling specifier cannot be " + + "null"); + } + + final String[] parts = specification.split("-"); + if (parts.length < 2) { + // Too few items. + throw new IllegalArgumentException("Invalid downsampling specifier '" + + specification + "': must provide at least interval and function"); + } else if (parts.length > 3) { + // Too many items. + throw new IllegalArgumentException("Invalid downsampling specifier '" + + specification + "': must consist of interval, function, and optional " + + "fill policy"); + } + + // This porridge is just right. + + // INTERVAL. + // This will throw if interval is invalid. + interval = DateTime.parseDuration(parts[0]); + + // FUNCTION. + try { + function = Aggregators.get(parts[1]); + } catch (final NoSuchElementException e) { + throw new IllegalArgumentException("No such downsampling function: " + + parts[1]); + } + + // FILL POLICY. + if (3 == parts.length) { + // If the user gave us three parts, then the third must be a fill + // policy. + fill_policy = FillPolicy.fromString(parts[2]); + if (null == fill_policy) { + final StringBuilder oss = new StringBuilder(); + oss.append("No such fill policy: '").append(parts[2]) + .append("': must be one of:"); + for (final FillPolicy policy : FillPolicy.values()) { + oss.append(" ").append(policy.getName()); + } + + throw new IllegalArgumentException(oss.toString()); + } + } else { + // Default to linear interpolation. + fill_policy = FillPolicy.NONE; + } + } + + /** + * Get the downsampling interval, in milliseconds. + * @return the downsampling interval, in milliseconds. + */ + public long getInterval() { + return interval; + } + + /** + * Get the downsampling function. + * @return the downsampling function. + */ + public Aggregator getFunction() { + return function; + } + + /** + * Get the policy specifying how to deal with missing data. + * @return the policy specifying how to deal with missing data. + */ + public FillPolicy getFillPolicy() { + return fill_policy; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("interval", getInterval()) + .add("function", getFunction()) + .add("fillPolicy", getFillPolicy()) + .toString(); + } +} + diff --git a/test/core/TestDownsamplingSpecification.java b/test/core/TestDownsamplingSpecification.java new file mode 100644 index 0000000000..a149a08c34 --- /dev/null +++ b/test/core/TestDownsamplingSpecification.java @@ -0,0 +1,69 @@ +// 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestDownsamplingSpecification { + @Test + public void testCtor() { + final long interval = 1234567L; + final Aggregator function = Aggregators.SUM; + final FillPolicy fill_policy = FillPolicy.ZERO; + + final DownsamplingSpecification ds = new DownsamplingSpecification( + interval, function, fill_policy); + + assertEquals(interval, ds.getInterval()); + assertEquals(function, ds.getFunction()); + assertEquals(fill_policy, ds.getFillPolicy()); + } + + @Test + public void testStringCtor() { + final DownsamplingSpecification ds = new DownsamplingSpecification( + "15m-avg-nan"); + + assertEquals(900000L, ds.getInterval()); + assertEquals(Aggregators.AVG, ds.getFunction()); + assertEquals(FillPolicy.NOT_A_NUMBER, ds.getFillPolicy()); + } + + @Test + public void testToString() { + assertEquals("DownsamplingSpecification{interval=4532019, function=zimsum, " + + "fillPolicy=NOT_A_NUMBER}", + new DownsamplingSpecification( + 4532019L, + Aggregators.ZIMSUM, + FillPolicy.NOT_A_NUMBER).toString()); + } + + @Test(expected = RuntimeException.class) + public void testBadInterval() { + new DownsamplingSpecification("blah-avg-lerp"); + } + + @Test(expected = RuntimeException.class) + public void testBadFunction() { + new DownsamplingSpecification("1m-hurp-lerp"); + } + + @Test(expected = RuntimeException.class) + public void testBadFillPolicy() { + new DownsamplingSpecification("10m-avg-max"); + } +} + From 2b8cc1699d4f82b1a9452726286608f5a62d1289 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Mon, 27 Apr 2015 22:16:18 -0700 Subject: [PATCH 135/826] Modify the Aggregator interface to be an Abstract. Modify the Aggregators to handle NaNs passed up from the downsampler so we can fill values when "missing". Signed-off-by: Chris Larsen --- src/core/Aggregator.java | 31 ++++- src/core/Aggregators.java | 255 ++++++++++++++++++-------------------- 2 files changed, 146 insertions(+), 140 deletions(-) diff --git a/src/core/Aggregator.java b/src/core/Aggregator.java index bb2c1124ef..baf1721b62 100644 --- a/src/core/Aggregator.java +++ b/src/core/Aggregator.java @@ -23,8 +23,24 @@ * sequence of {@link Longs Longs} or {@link Doubles Doubles} and return an * aggregated value. */ -public interface Aggregator { +public abstract class Aggregator { + + /** Interpolation method this aggregator uses across time series */ + private final Interpolation interpolation_method; + + /** String name of the aggregator */ + private final String name; + /** + * Create a new instance of this class. + * @param interpolationMethod The interpolation method to use. + * @param name The name of this aggregator. + */ + protected Aggregator(final Interpolation interpolationMethod, final String name) { + this.interpolation_method = interpolationMethod; + this.name = name; + } + /** * A sequence of {@code long}s. *

@@ -76,19 +92,26 @@ public interface Doubles { * @param values The sequence to aggregate. * @return The aggregated value. */ - long runLong(Longs values); + abstract long runLong(Longs values); /** * Aggregates a sequence of {@code double}s. * @param values The sequence to aggregate. * @return The aggregated value. */ - double runDouble(Doubles values); + abstract double runDouble(Doubles values); /** * Returns the interpolation method to use when working with data points * across time series. * @return The interpolation method to use */ - Interpolation interpolationMethod(); + Interpolation interpolationMethod() { + return interpolation_method; + } + + @Override + public String toString() { + return name; + } } diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index eaa529300b..5edb598aa7 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -151,7 +151,7 @@ public enum Interpolation { ep999r7, ep99r7, ep95r7, ep90r7, ep75r7, ep50r7 }; for (PercentileAgg agg : percentiles) { - aggregators.put(agg.getName(), agg); + aggregators.put(agg.toString(), agg); } } @@ -180,15 +180,13 @@ public static Aggregator get(final String name) { throw new NoSuchElementException("No such aggregator: " + name); } - private static final class Sum implements Aggregator { - private final Interpolation method; - private final String name; - + + private static final class Sum extends Aggregator { public Sum(final Interpolation method, final String name) { - this.method = method; - this.name = name; + super(method, name); } - + + @Override public long runLong(final Longs values) { long result = values.nextLongValue(); while (values.hasNextValue()) { @@ -197,33 +195,30 @@ public long runLong(final Longs values) { return result; } + @Override public double runDouble(final Doubles values) { - double result = values.nextDoubleValue(); + double result = 0.; + long n = 0L; + while (values.hasNextValue()) { - result += values.nextDoubleValue(); + final double val = values.nextDoubleValue(); + if (!Double.isNaN(val)) { + result += val; + ++n; + } } - return result; - } - public String toString() { - return name; - } - - public Interpolation interpolationMethod() { - return method; + return (0L == n) ? Double.NaN : result; } } - private static final class Min implements Aggregator { - private final Interpolation method; - private final String name; - + private static final class Min extends Aggregator { public Min(final Interpolation method, final String name) { - this.method = method; - this.name = name; + super(method, name); } - + + @Override public long runLong(final Longs values) { long min = values.nextLongValue(); while (values.hasNextValue()) { @@ -235,36 +230,29 @@ public long runLong(final Longs values) { return min; } + @Override public double runDouble(final Doubles values) { - double min = values.nextDoubleValue(); + final double initial = values.nextDoubleValue(); + double min = Double.isNaN(initial) ? Double.POSITIVE_INFINITY : initial; + while (values.hasNextValue()) { final double val = values.nextDoubleValue(); - if (val < min) { + if (!Double.isNaN(val) && val < min) { min = val; } } - return min; - } - public String toString() { - return name; - } - - public Interpolation interpolationMethod() { - return method; + return (Double.POSITIVE_INFINITY == min) ? Double.NaN : min; } } - private static final class Max implements Aggregator { - private final Interpolation method; - private final String name; - + private static final class Max extends Aggregator { public Max(final Interpolation method, final String name) { - this.method = method; - this.name = name; + super(method, name); } - + + @Override public long runLong(final Longs values) { long max = values.nextLongValue(); while (values.hasNextValue()) { @@ -276,36 +264,29 @@ public long runLong(final Longs values) { return max; } + @Override public double runDouble(final Doubles values) { - double max = values.nextDoubleValue(); + final double initial = values.nextDoubleValue(); + double max = Double.isNaN(initial) ? Double.NEGATIVE_INFINITY : initial; + while (values.hasNextValue()) { final double val = values.nextDoubleValue(); - if (val > max) { + if (!Double.isNaN(val) && val > max) { max = val; } } - return max; - } - public String toString() { - return name; - } - - public Interpolation interpolationMethod() { - return method; + return (Double.NEGATIVE_INFINITY == max) ? Double.NaN : max; } } - private static final class Avg implements Aggregator { - private final Interpolation method; - private final String name; - + private static final class Avg extends Aggregator { public Avg(final Interpolation method, final String name) { - this.method = method; - this.name = name; + super(method, name); } - + + @Override public long runLong(final Longs values) { long result = values.nextLongValue(); int n = 1; @@ -316,22 +297,18 @@ public long runLong(final Longs values) { return result / n; } + @Override public double runDouble(final Doubles values) { - double result = values.nextDoubleValue(); - int n = 1; + double result = 0.; + int n = 0; while (values.hasNextValue()) { - result += values.nextDoubleValue(); - n++; + final double val = values.nextDoubleValue(); + if (!Double.isNaN(val)) { + result += val; + n++; + } } - return result / n; - } - - public String toString() { - return name; - } - - public Interpolation interpolationMethod() { - return method; + return (0 == n) ? Double.NaN : result / n; } } @@ -345,15 +322,12 @@ public Interpolation interpolationMethod() { * paper by B. P. Welford and is presented in Donald Knuth's Art of * Computer Programming, Vol 2, page 232, 3rd edition */ - private static final class StdDev implements Aggregator { - private final Interpolation method; - private final String name; - + private static final class StdDev extends Aggregator { public StdDev(final Interpolation method, final String name) { - this.method = method; - this.name = name; + super(method, name); } - + + @Override public long runLong(final Longs values) { double old_mean = values.nextLongValue(); @@ -362,57 +336,70 @@ public long runLong(final Longs values) { } long n = 2; - double new_mean = 0; - double variance = 0; + double new_mean = 0.; + double M2 = 0.; do { final double x = values.nextLongValue(); new_mean = old_mean + (x - old_mean) / n; - variance += (x - old_mean) * (x - new_mean); + M2 += (x - old_mean) * (x - new_mean); old_mean = new_mean; n++; } while (values.hasNextValue()); - return (long) Math.sqrt(variance / (n - 1)); + return (long) Math.sqrt(M2 / (n - 1)); } + @Override public double runDouble(final Doubles values) { + // Try to get at least one non-NaN value. double old_mean = values.nextDoubleValue(); + while (Double.isNaN(old_mean) && values.hasNextValue()) { + old_mean = values.nextDoubleValue(); + } + if (Double.isNaN(old_mean)) { + // Couldn't find any non-NaN values. + // The stddev of NaNs is NaN. + return Double.NaN; + } if (!values.hasNextValue()) { - return 0; + // Only found one non-NaN value. + // The stddev of one value is zero. + return 0.; } + // If we got here, then we have one non-NaN value, and there are more + // values to aggregate; however, some or all of these values may be NaNs. + long n = 2; - double new_mean = 0; - double variance = 0; + double new_mean = 0.; + + // This is not strictly the second central moment (i.e., variance), but + // rather a multiple of it. + double M2 = 0.; do { final double x = values.nextDoubleValue(); - new_mean = old_mean + (x - old_mean) / n; - variance += (x - old_mean) * (x - new_mean); - old_mean = new_mean; - n++; + if (!Double.isNaN(x)) { + new_mean = old_mean + (x - old_mean) / n; + M2 += (x - old_mean) * (x - new_mean); + old_mean = new_mean; + n++; + } } while (values.hasNextValue()); - return Math.sqrt(variance / (n - 1)); + // If n is still 2, then we never found another non-NaN value; therefore, + // we should return zero. + // + // Otherwise, we calculate the actual variance, and then we find its + // positive square root, which is the standard deviation. + return (2 == n) ? 0. : Math.sqrt(M2 / (n - 1)); } - public String toString() { - return name; - } - - public Interpolation interpolationMethod() { - return method; - } - } - private static final class Count implements Aggregator { - private final Interpolation method; - private final String name; - + private static final class Count extends Aggregator { public Count(final Interpolation method, final String name) { - this.method = method; - this.name = name; + super(method, name); } @Override @@ -429,19 +416,14 @@ public long runLong(Longs values) { public double runDouble(Doubles values) { double result = 0; while (values.hasNextValue()) { - values.nextDoubleValue(); - result++; + final double val = values.nextDoubleValue(); + if (!Double.isNaN(val)) { + result++; + } } return result; } - public String toString() { - return name; - } - - public Interpolation interpolationMethod() { - return method; - } } /** @@ -452,24 +434,24 @@ public Interpolation interpolationMethod() { * minLimit=0 * maxLimit=1 */ - private static final class PercentileAgg implements Aggregator { + private static final class PercentileAgg extends Aggregator { private final Double percentile; - private final String name; private final EstimationType estimation; - PercentileAgg(final Double percentile, final String name) { + public PercentileAgg(final Double percentile, final String name) { this(percentile, name, null); } - public String getName() { - return name; - } - PercentileAgg(final Double percentile, final String name, final EstimationType est) { - Preconditions.checkArgument(percentile > 0 && percentile <= 100, "Invalid percentile value"); + + public PercentileAgg(final Double percentile, final String name, + final EstimationType est) { + super(Aggregators.Interpolation.LERP, name); + Preconditions.checkArgument(percentile > 0 && percentile <= 100, + "Invalid percentile value"); this.percentile = percentile; - this.name = name; this.estimation = est; } + @Override public long runLong(final Longs values) { final Percentile percentile = this.estimation == null @@ -483,23 +465,24 @@ public long runLong(final Longs values) { return (long) percentile.evaluate(); } + @Override public double runDouble(final Doubles values) { - final Percentile percentile = new Percentile(this.percentile); - final ResizableDoubleArray local_values = new ResizableDoubleArray(); - while(values.hasNextValue()) { - local_values.addElement(values.nextDoubleValue()); + final Percentile percentile = new Percentile(this.percentile); + final ResizableDoubleArray local_values = new ResizableDoubleArray(); + int n = 0; + while(values.hasNextValue()) { + final double val = values.nextDoubleValue(); + if (!Double.isNaN(val)) { + local_values.addElement(val); + n++; } + } + if (n > 0) { percentile.setData(local_values.getElements()); return percentile.evaluate(); - } - - public String toString() { - return name; - } - - @Override - public Interpolation interpolationMethod() { - return Aggregators.Interpolation.LERP; + } else { + return Double.NaN; + } } } From a28e97da38c31fd52f101e88679b79d31dc6fed7 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:05:06 -0700 Subject: [PATCH 136/826] Add a ctor override to AggregationIterator for the fill policy. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 47 +++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 2c6f6e3433..363e828f3f 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -222,6 +222,42 @@ public static AggregationIterator create(final List spans, final long sample_interval_ms, final boolean rate, final RateOptions rate_options) { + return create(spans, start_time, end_time, aggregator, method, downsampler, + sample_interval_ms, rate, rate_options, null); + } + + /** + * Creates a new iterator for a {@link SpanGroup}. + * @param spans Spans in a group. + * @param start_time Any data point strictly before this timestamp will be + * ignored. + * @param end_time Any data point strictly after this timestamp will be + * ignored. + * @param aggregator The aggregation function to use. + * @param method Interpolation method to use when aggregating time series + * @param downsampler Aggregation function to use to group data points + * within an interval. + * @param sample_interval_ms Number of milliseconds wanted between each data + * point. + * @param rate If {@code true}, the rate of the series will be used instead + * of the actual values. + * @param rate_options Specifies the optional additional rate calculation + * options. + * @param fill_policy Policy specifying whether to interpolate or to fill + * missing intervals with special values. + * @return An {@link AggregationIterator} object. + * @since 2.2 + */ + public static AggregationIterator create(final List spans, + final long start_time, + final long end_time, + final Aggregator aggregator, + final Interpolation method, + final Aggregator downsampler, + final long sample_interval_ms, + final boolean rate, + final RateOptions rate_options, + final FillPolicy fill_policy) { final int size = spans.size(); final SeekableView[] iterators = new SeekableView[size]; for (int i = 0; i < size; i++) { @@ -229,7 +265,8 @@ public static AggregationIterator create(final List spans, if (downsampler == null) { it = spans.get(i).spanIterator(); } else { - it = spans.get(i).downsampler(sample_interval_ms, downsampler); + it = spans.get(i).downsampler(start_time, end_time, sample_interval_ms, + downsampler, fill_policy); } if (rate) { it = new RateSpan(it, rate_options); @@ -281,7 +318,7 @@ private AggregationIterator(final SeekableView[] iterators, } catch (NoSuchElementException e) { // It should be rare but could happen after downsampling when // we throw away some data points at the beginning after aligning - // start time by downsmpling interval and there are no data points + // start time by downsampling interval and there are no data points // left for the current span. ++num_empty_spans; endReached(i); @@ -492,8 +529,8 @@ public double doubleValue() { pos = -1; final double value = aggregator.runDouble(this); //LOG.debug("aggregator returned " + value); - if (value != value || Double.isInfinite(value)) { - throw new IllegalStateException("Got NaN or Infinity: " + if (Double.isInfinite(value)) { + throw new IllegalStateException("Got Infinity: " + value + " in this " + this); } return value; @@ -573,7 +610,7 @@ public long nextLongValue() { r = Long.MIN_VALUE; break; default: - throw new IllegalDataException("Invalid interploation somehow??"); + throw new IllegalDataException("Invalid interpolation somehow??"); } return r; } From 708ac58e91790450067be170398f2a2f921363b9 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:05:21 -0700 Subject: [PATCH 137/826] Add a downsample setter override to the Query interface for fills Signed-off-by: Chris Larsen --- src/core/Query.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 9bd924edd2..751ff085b4 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -40,8 +40,8 @@ public interface Query { /** * Returns the start time of the graph. * @return A strictly positive integer. - * @throws IllegalStateException if {@link #setStartTime} was never called on - * this instance before. + * @throws IllegalStateException if {@link #setStartTime(long)} was never + * called on this instance before. */ long getStartTime(); @@ -173,6 +173,18 @@ public Deferred configureFromQuery(final TSQuery query, */ void downsample(long interval, Aggregator 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.2 + */ + void downsample(long interval, Aggregator downsampler, FillPolicy fill_policy); + /** * Runs this query. * @return The data points matched by this query. From 4dd0910a1d31a078660cfd62ab8be55308b0bf8c Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:05:51 -0700 Subject: [PATCH 138/826] Modify the Span and SpanGroup classes to account for downsampling fill policies Signed-off-by: Chris Larsen --- src/core/Span.java | 39 +++++++++++++++++++++++++++++---- src/core/SpanGroup.java | 48 ++++++++++++++++++++++++----------------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/core/Span.java b/src/core/Span.java index a7aafc2236..c49ee773cb 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -397,11 +397,17 @@ final class Iterator implements SeekableView { current_row = rows.get(0).internalIterator(); } + // ------------------ // + // Iterator interface // + // ------------------ // + + @Override public boolean hasNext() { return (current_row.hasNext() // more points in this row || row_index < rows.size() - 1); // or more rows } + @Override public DataPoint next() { if (current_row.hasNext()) { return current_row.next(); @@ -413,10 +419,16 @@ public DataPoint next() { throw new NoSuchElementException("no more elements"); } + @Override public void remove() { throw new UnsupportedOperationException(); } + // ---------------------- // + // SeekableView interface // + // ---------------------- // + + @Override public void seek(final long timestamp) { int row_index = seekRow(timestamp); if (row_index != this.row_index) { @@ -426,6 +438,7 @@ public void seek(final long timestamp) { current_row.seek(timestamp); } + @Override public String toString() { return "Span.Iterator(row_index=" + row_index + ", current_row=" + current_row + ", span=" + Span.this + ')'; @@ -434,14 +447,32 @@ public String toString() { } /** - * Package private iterator method to access data while downsampling. + * Package private iterator method to access data while downsampling with the + * option to force interpolation. + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. * @param interval_ms The interval in milli seconds wanted between each data * point. * @param downsampler The downsampling function to use. + * @param fill_policy Policy specifying whether to interpolate or to fill + * missing intervals with special values. + * @return A new downsampler. */ - Downsampler downsampler(final long interval_ms, - final Aggregator downsampler) { - return new Downsampler(spanIterator(), interval_ms, downsampler); + Downsampler downsampler(final long start_time, + final long end_time, + final long interval_ms, + final Aggregator downsampler, + final FillPolicy fill_policy) { + if (FillPolicy.NONE == fill_policy) { + // The default downsampler simply skips missing intervals, causing the + // span group to linearly interpolate. + return new Downsampler(spanIterator(), interval_ms, downsampler); + } else { + // Otherwise, we need to instantiate a downsampler that can fill missing + // intervals with special values. + return new FillingDownsampler(spanIterator(), start_time, end_time, + interval_ms, downsampler, fill_policy); + } } public int getQueryIndex() { diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index be2eb7e1bc..026cb0fedc 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -102,6 +102,9 @@ final class SpanGroup implements DataPoints { /** Index of the query in the TSQuery class */ private final int query_index; + /** Downsampling fill policy. */ + private final FillPolicy fill_policy; + /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; @@ -157,7 +160,7 @@ final class SpanGroup implements DataPoints { final Aggregator aggregator, final long interval, final Aggregator downsampler) { this(tsdb, start_time, end_time, spans, rate, rate_options, aggregator, - interval, downsampler, -1); + interval, downsampler, -1, FillPolicy.NONE); } /** @@ -176,7 +179,9 @@ final class SpanGroup implements DataPoints { * @param interval Number of milliseconds wanted between each data point. * @param downsampler Aggregation function to use to group data points * within an interval. - * @param query_index The index of this query in the TSQuery array + * @param query_index index of the original query + * @param fill_policy Policy specifying whether to interpolate or to fill + * missing intervals with special values. * @since 2.2 */ SpanGroup(final TSDB tsdb, @@ -184,23 +189,26 @@ final class SpanGroup implements DataPoints { final Iterable spans, final boolean rate, final RateOptions rate_options, final Aggregator aggregator, - final long interval, final Aggregator downsampler, - final int query_index) { - this.tsdb = tsdb; - annotations = new ArrayList(); - this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; - this.end_time = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; - if (spans != null) { - for (final Span span : spans) { - add(span); - } - } - this.rate = rate; - this.rate_options = rate_options; - this.aggregator = aggregator; - this.downsampler = downsampler; - this.sample_interval = interval; - this.query_index = query_index; + final long interval, final Aggregator downsampler, final int query_index, + final FillPolicy fill_policy) { + annotations = new ArrayList(); + this.start_time = (start_time & Const.SECOND_MASK) == 0 ? + start_time * 1000 : start_time; + this.end_time = (end_time & Const.SECOND_MASK) == 0 ? + end_time * 1000 : end_time; + if (spans != null) { + for (final Span span : spans) { + add(span); + } + } + this.rate = rate; + this.rate_options = rate_options; + this.aggregator = aggregator; + this.downsampler = downsampler; + this.sample_interval = interval; + this.query_index = query_index; + this.fill_policy = fill_policy; + this.tsdb = tsdb; } /** @@ -423,7 +431,7 @@ public SeekableView iterator() { return AggregationIterator.create(spans, start_time, end_time, aggregator, aggregator.interpolationMethod(), downsampler, sample_interval, - rate, rate_options); + rate, rate_options, fill_policy); } /** From 9013072d2fc2fd605b1f93fb8c3184b3f6e5aecf Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:06:03 -0700 Subject: [PATCH 139/826] Add support to the TSSubQuery class for parsing out the fill policy Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 49 ++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index c1142aa882..5d2c3cd6a0 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -19,8 +19,6 @@ import com.google.common.base.Objects; -import net.opentsdb.utils.DateTime; - /** * Represents the parameters for an individual sub query on a metric or specific * timeseries. When setting up a query, use the setter methods to store user @@ -35,7 +33,7 @@ * the {@link TSQuery} object will call this for you when the entire set of * queries has been compiled. * Note: If using POJO deserialization, make sure to avoid setting the - * {@code agg}, {@code downsampler} and {@code downsample_interval} fields. + * {@code agg} and {@code downsample_specifier} fields. * @since 2.0 */ public final class TSSubQuery { @@ -64,17 +62,15 @@ public final class TSSubQuery { /** Parsed aggregation function */ private Aggregator agg; - /** Parsed downsampler function */ - private Aggregator downsampler; - - /** Parsed downsample interval */ - private long downsample_interval; + /** Parsed downsampling specification. */ + private DownsamplingSpecification downsample_specifier; /** * Default constructor necessary for POJO de/serialization */ public TSSubQuery() { - + // Assume no downsampling until told otherwise. + downsample_specifier = DownsamplingSpecification.NO_DOWNSAMPLER; } @Override @@ -142,7 +138,7 @@ public String toString() { .append(", downsample=") .append(downsample) .append(", ds_interval=") - .append(downsample_interval) + .append(downsample_specifier.getInterval()) .append(", rate=") .append(rate) .append(", rate_options=") @@ -180,20 +176,11 @@ public void validateAndSetQuery() { // parse the downsampler if we have one if (downsample != null && !downsample.isEmpty()) { - final int dash = downsample.indexOf('-', 1); // 1st char can't be - // `-'. - if (dash < 0) { - throw new IllegalArgumentException("Invalid downsampling specifier '" - + downsample + "' in [" + downsample + "]"); - } - try { - downsampler = Aggregators.get(downsample.substring(dash + 1)); - } catch (NoSuchElementException e) { - throw new IllegalArgumentException("No such downsampling function: " - + downsample.substring(dash + 1)); - } - downsample_interval = DateTime.parseDuration( - downsample.substring(0, dash)); + // downsampler given, so parse it + downsample_specifier = new DownsamplingSpecification(downsample); + } else { + // no downsampler + downsample_specifier = DownsamplingSpecification.NO_DOWNSAMPLER; } } @@ -204,12 +191,20 @@ public Aggregator aggregator() { /** @return the parsed downsampler aggregation function */ public Aggregator downsampler() { - return this.downsampler; + return downsample_specifier.getFunction(); } /** @return the parsed downsample interval in seconds */ public long downsampleInterval() { - return this.downsample_interval; + return downsample_specifier.getInterval(); + } + + /** + * @return the downsampling fill policy + * @since 2.2 + */ + public FillPolicy fillPolicy() { + return downsample_specifier.getFillPolicy(); } /** @return the user supplied aggregator */ @@ -236,7 +231,7 @@ public Map getTags() { } /** @return the raw downsampling function request from the user, - * e.g. "1h-avg" */ + * e.g. "1h-avg" or "15m-sum-nan" */ public String getDownsample() { return downsample; } From d78cddabdd88e6a506b25c26e27b3f512ea41ca9 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:06:14 -0700 Subject: [PATCH 140/826] Modify the TsdbQuery class to support filling downsamplers. Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 130 ++++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 38 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index f5b4a0a356..7e0cd04875 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -119,6 +119,9 @@ final class TsdbQuery implements Query { /** Minimum time interval (in milliseconds) wanted between each data point. */ private long sample_interval_ms; + + /** Downsampling fill policy. */ + private FillPolicy fill_policy; /** Optional list of TSUIDs to fetch and aggregate instead of a metric */ private List tsuids; @@ -129,6 +132,9 @@ final class TsdbQuery implements Query { /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; + + // By default, we should interpolate. + fill_policy = DownsamplingSpecification.DEFAULT_FILL_POLICY; } /** @@ -285,6 +291,7 @@ public Deferred configureFromQuery(final TSQuery query, } downsampler = sub_query.downsampler(); sample_interval_ms = sub_query.downsampleInterval(); + fill_policy = sub_query.fillPolicy(); // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { @@ -389,15 +396,10 @@ public Object call(final byte[] uid) throws Exception { } } - /** - * 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 - * @throws NullPointerException if the aggregation function is null - * @throws IllegalArgumentException if the interval is not greater than 0 - */ + @Override - public void downsample(final long interval, final Aggregator downsampler) { + public void downsample(final long interval, final Aggregator downsampler, + final FillPolicy fill_policy) { if (downsampler == null) { throw new NullPointerException("downsampler"); } else if (interval <= 0) { @@ -405,6 +407,19 @@ public void downsample(final long interval, final Aggregator downsampler) { } this.downsampler = downsampler; this.sample_interval_ms = interval; + this.fill_policy = fill_policy; + } + + /** + * Sets an optional downsampling function with interpolation 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 + * @throws NullPointerException if the aggregation function is null + * @throws IllegalArgumentException if the interval is not greater than 0 + */ + @Override + public void downsample(final long interval, final Aggregator downsampler) { + downsample(interval, downsampler, FillPolicy.NONE); } /** @@ -719,7 +734,7 @@ public DataPoints[] call(final TreeMap spans) throws Exception { rate, rate_options, aggregator, sample_interval_ms, downsampler, - query_index); + query_index, fill_policy); return new SpanGroup[] { group }; } @@ -763,7 +778,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { thegroup = new SpanGroup(tsdb, getScanStartTimeSeconds(), getScanEndTimeSeconds(), null, rate, rate_options, aggregator, - sample_interval_ms, downsampler, query_index); + sample_interval_ms, downsampler, query_index, + fill_policy); // 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]; @@ -850,42 +866,80 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { /** Returns the UNIX timestamp from which we must start scanning. */ private long getScanStartTimeSeconds() { - // The reason we look before by `MAX_TIMESPAN * 2' seconds is because of - // the following. Let's assume MAX_TIMESPAN = 600 (10 minutes) and the - // start_time = ... 12:31:00. If we initialize the scanner to look - // only 10 minutes before, we'll start scanning at time=12:21, which will - // give us the row that starts at 12:30 (remember: rows are always aligned - // on MAX_TIMESPAN boundaries -- so in this example, on 10m boundaries). - // But we need to start scanning at least 1 row before, so we actually - // look back by twice MAX_TIMESPAN. Only when start_time is aligned on a - // MAX_TIMESPAN boundary then we'll mistakenly scan back by an extra row, - // but this doesn't really matter. - // Additionally, in case our sample_interval_ms is large, we need to look - // even further before/after, so use that too. + // Begin with the raw query start time. long start = getStartTime(); - // down cast to seconds if we have a query in ms - if ((start & Const.SECOND_MASK) != 0) { - start /= 1000; + + // Convert to seconds if we have a query in ms. + if ((start & Const.SECOND_MASK) != 0L) { + start /= 1000L; + } + + // First, we align the start timestamp to its representative value for the + // interval in which it appears, if downsampling. + long interval_aligned_ts = start; + if (0L != sample_interval_ms) { + // Downsampling enabled. + final long interval_offset = (1000L * start) % sample_interval_ms; + interval_aligned_ts -= interval_offset / 1000L; } - final long ts = start - Const.MAX_TIMESPAN * 2 - sample_interval_ms / 1000; - return ts > 0 ? ts : 0; + + // Then snap that timestamp back to its representative value for the + // timespan in which it appears. + final long timespan_offset = interval_aligned_ts % Const.MAX_TIMESPAN; + final long timespan_aligned_ts = interval_aligned_ts - timespan_offset; + + // Don't return negative numbers. + return timespan_aligned_ts > 0L ? timespan_aligned_ts : 0L; } /** Returns the UNIX timestamp at which we must stop scanning. */ private long getScanEndTimeSeconds() { - // For the end_time, we have a different problem. For instance if our - // end_time = ... 12:30:00, we'll stop scanning when we get to 12:40, but - // once again we wanna try to look ahead one more row, so to avoid this - // problem we always add 1 second to the end_time. Only when the end_time - // is of the form HH:59:59 then we will scan ahead an extra row, but once - // again that doesn't really matter. - // Additionally, in case our sample_interval_ms is large, we need to look - // even further before/after, so use that too. + // Begin with the raw query end time. long end = getEndTime(); - if ((end & Const.SECOND_MASK) != 0) { - end /= 1000; + + // Convert to seconds if we have a query in ms. + if ((end & Const.SECOND_MASK) != 0L) { + end /= 1000L; + } + + // The calculation depends on whether we're downsampling. + if (0L != sample_interval_ms) { + // Downsampling enabled. + // + // First, we align the end timestamp to its representative value for the + // interval FOLLOWING the one in which it appears. + // + // OpenTSDB's query bounds are inclusive, but HBase scan bounds are half- + // open. The user may have provided an end bound that is already + // interval-aligned (i.e., its interval offset is zero). If so, the user + // wishes for that interval to appear in the output. In that case, we + // skip forward an entire extra interval. + // + // This can be accomplished by simply not testing for zero offset. + final long interval_offset = (1000L * end) % sample_interval_ms; + final long interval_aligned_ts = end + + (sample_interval_ms - interval_offset) / 1000L; + + // Then, if we're now aligned on a timespan boundary, then we need no + // further adjustment: we are guaranteed to have always moved the end time + // forward, so the scan will find the data we need. + // + // Otherwise, we need to align to the NEXT timespan to ensure that we scan + // the needed data. + final long timespan_offset = interval_aligned_ts % Const.MAX_TIMESPAN; + return (0L == timespan_offset) ? + interval_aligned_ts : + interval_aligned_ts + (Const.MAX_TIMESPAN - timespan_offset); + } else { + // Not downsampling. + // + // Regardless of the end timestamp's position within the current timespan, + // we must always align to the beginning of the next timespan. This is + // true even if it's already aligned on a timespan boundary. Again, the + // reason for this is OpenTSDB's closed interval vs. HBase's half-open. + final long timespan_offset = end % Const.MAX_TIMESPAN; + return end + (Const.MAX_TIMESPAN - timespan_offset); } - return end + Const.MAX_TIMESPAN + 1 + sample_interval_ms / 1000; } /** From b2f707092d7e7a0481fa30c87c6bef34295f09de Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:06:26 -0700 Subject: [PATCH 141/826] Modify the Plot and GraphHandler classes to handle NaNs properly when a fill policy is in use. Signed-off-by: Chris Larsen --- src/graph/Plot.java | 27 ++++++++++++------ src/tsd/GraphHandler.java | 60 +++++++++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/src/graph/Plot.java b/src/graph/Plot.java index 5777a70d73..2d69db5b7b 100644 --- a/src/graph/Plot.java +++ b/src/graph/Plot.java @@ -17,7 +17,6 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -205,23 +204,33 @@ public int dumpToFiles(final String basepath) throws IOException { final PrintWriter datafile = new PrintWriter(datafiles[i]); try { for (final DataPoint d : datapoints.get(i)) { - final long ts = d.timestamp() / 1000; - if (ts >= (start_time & UNSIGNED) && ts <= (end_time & UNSIGNED)) { - npoints++; - } - datafile.print(ts + utc_offset); - datafile.print(' '); + final long ts = d.timestamp() / 1000; if (d.isInteger()) { + datafile.print(ts + utc_offset); + datafile.print(' '); datafile.print(d.longValue()); } else { final double value = d.doubleValue(); - if (value != value || Double.isInfinite(value)) { - throw new IllegalStateException("NaN or Infinity found in" + + if (Double.isInfinite(value)) { + // Infinity is invalid. + throw new IllegalStateException("Infinity found in" + " datapoints #" + i + ": " + value + " d=" + d); + } else if (Double.isNaN(value)) { + // NaNs should be skipped. + continue; } + + datafile.print(ts + utc_offset); + datafile.print(' '); datafile.print(value); } + datafile.print('\n'); + + if (ts >= (start_time & UNSIGNED) && ts <= (end_time & UNSIGNED)) { + npoints++; + } } } finally { datafile.close(); diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 08b642bcce..a69bb7f693 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -30,10 +30,12 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; + import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +44,7 @@ import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; +import net.opentsdb.core.DownsamplingSpecification; import net.opentsdb.core.Query; import net.opentsdb.core.RateOptions; import net.opentsdb.core.TSDB; @@ -796,20 +799,27 @@ private static void respondAsciiQuery(final HttpQuery query, .append('=').append(tag.getValue()); } for (final DataPoint d : dp) { - asciifile.print(metric); - asciifile.print(' '); - asciifile.print((d.timestamp() / 1000)); - asciifile.print(' '); if (d.isInteger()) { + printMetricHeader(asciifile, metric, d.timestamp()); asciifile.print(d.longValue()); } else { + // Doubles require extra processing. final double value = d.doubleValue(); - if (value != value || Double.isInfinite(value)) { - throw new IllegalStateException("NaN or Infinity:" + value + + // Value might be NaN or infinity. + if (Double.isInfinite(value)) { + // Infinity is invalid. + throw new IllegalStateException("Infinity:" + value + " d=" + d + ", query=" + query); + } else if (Double.isNaN(value)) { + // NaNs should be skipped. + continue; } + + printMetricHeader(asciifile, metric, d.timestamp()); asciifile.print(value); } + asciifile.print(tagbuf); asciifile.print('\n'); } @@ -824,6 +834,20 @@ private static void respondAsciiQuery(final HttpQuery query, } } + /** + * Helper method to write metric name and timestamp. + * @param writer The writer to which to write. + * @param metric The metric name. + * @param timestamp The timestamp. + */ + private static void printMetricHeader(final PrintWriter writer, final String metric, + final long timestamp) { + writer.print(metric); + writer.print(' '); + writer.print(timestamp / 1000L); + writer.print(' '); + } + /** * Parses the {@code /q} query in a list of {@link Query} objects. * @param tsdb The TSDB to use. @@ -867,22 +891,16 @@ private static Query[] parseQuery(final TSDB tsdb, final HttpQuery query) { } // downsampling function & interval. if (i > 0) { - final int dash = parts[1].indexOf('-', 1); // 1st char can't be `-'. - if (dash < 0) { - throw new BadRequestException("Invalid downsampling specifier '" - + parts[1] + "' in m=" + m); - } - Aggregator downsampler; - try { - downsampler = Aggregators.get(parts[1].substring(dash + 1)); - } catch (NoSuchElementException e) { - throw new BadRequestException("No such downsampling function: " - + parts[1].substring(dash + 1)); - } - final long interval = DateTime.parseDuration(parts[1].substring(0, dash)); - tsdbquery.downsample(interval, downsampler); + // downsampler given, so parse it + final DownsamplingSpecification ds_spec = + new DownsamplingSpecification(parts[1]); + + tsdbquery.downsample(ds_spec.getInterval(), ds_spec.getFunction(), + ds_spec.getFillPolicy()); } else { - tsdbquery.downsample(1000, agg); + // no downsampler + tsdbquery.downsample(1000, agg, + DownsamplingSpecification.DEFAULT_FILL_POLICY); } tsdbqueries[nqueries++] = tsdbquery; } From 7e6d41dd52858d58505007c577f438d3176fb215 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:06:40 -0700 Subject: [PATCH 142/826] Modify the built-in UI to allow for selecting a fill policy when downsampling. Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 48 ++++++++++++++++++++++++++++++---- src/tsd/client/QueryUi.java | 10 +++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index e409d3e0fa..a397b61fca 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -14,6 +14,8 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.List; + import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; @@ -50,6 +52,7 @@ public static interface MetricChangeHandler extends EventHandler { private final CheckBox downsample = new CheckBox("Downsample"); private final ListBox downsampler = new ListBox(); private final ValidatedTextBox interval = new ValidatedTextBox(); + private final ListBox fill_policy = new ListBox(); private final CheckBox rate = new CheckBox("Rate"); private final CheckBox rate_counter = new CheckBox("Rate Ctr"); private final TextBox counter_max = new TextBox(); @@ -66,6 +69,7 @@ public MetricForm(final EventsHandler handler) { downsampler.addChangeHandler(handler); interval.addBlurHandler(handler); interval.addKeyPressHandler(handler); + fill_policy.addChangeHandler(handler); rate.addClickHandler(handler); rate_counter.addClickHandler(handler); counter_max.addBlurHandler(handler); @@ -179,18 +183,38 @@ public void updateFromQueryString(final String m, final String o) { // downsampling function & interval. if (i > 0) { - final int dash = parts[1].indexOf('-', 1); // 1st char can't be `-'. - if (dash < 0) { + // First dash should have been given. + final int first_dash = parts[1].indexOf('-', 1); // 1st char can't be `-'. + if (first_dash < 0) { disableDownsample(); return; // Invalid downsampling specifier. } + + // Second dash (and subsequent fill policy) are optional. + final int second_dash = parts[1].indexOf('-', first_dash + 1); + downsample.setValue(true, false); downsampler.setEnabled(true); - setSelectedItem(downsampler, parts[1].substring(dash + 1)); + fill_policy.setEnabled(true); + if (-1 == second_dash) { + // No fill policy given. + setSelectedItem(downsampler, parts[1].substring(first_dash + 1)); + + // So use a default. + // TODO: don't assume this exists. + setSelectedItem(fill_policy, "lerp"); + } else { + // User specified fill policy. + setSelectedItem(downsampler, parts[1].substring(first_dash + 1, + second_dash)); + + // So use what was given. + setSelectedItem(fill_policy, parts[1].substring(second_dash + 1)); + } interval.setEnabled(true); - interval.setText(parts[1].substring(0, dash)); + interval.setText(parts[1].substring(0, first_dash)); } else { disableDownsample(); } @@ -202,6 +226,7 @@ private void disableDownsample() { downsample.setValue(false, false); interval.setEnabled(false); downsampler.setEnabled(false); + fill_policy.setEnabled(false); } public CheckBox x1y2() { @@ -264,6 +289,7 @@ private void assembleUi() { final HorizontalPanel hbox = new HorizontalPanel(); hbox.add(downsampler); hbox.add(interval); + hbox.add(fill_policy); vbox.add(hbox); } add(vbox); @@ -281,9 +307,18 @@ public void setAggregators(final ArrayList aggs) { aggregators.addItem((String)agg); downsampler.addItem((String)agg); } + // TODO: don't assume we will get these. setSelectedItem(aggregators, "sum"); setSelectedItem(downsampler, "avg"); } + + public void setFillPolicies(final List policies) { + for (final String policy : policies) { + fill_policy.addItem(policy); + } + // TODO: don't assume we will get this. + setSelectedItem(fill_policy, "lerp"); + } public boolean buildQueryString(final StringBuilder url) { final String metric = getMetric(); @@ -294,7 +329,8 @@ public boolean buildQueryString(final StringBuilder url) { url.append(selectedValue(aggregators)); if (downsample.getValue()) { url.append(':').append(interval.getValue()) - .append('-').append(selectedValue(downsampler)); + .append('-').append(selectedValue(downsampler)) + .append('-').append(selectedValue(fill_policy)); } if (rate.getValue()) { url.append(":rate"); @@ -496,6 +532,7 @@ public void onClick(final ClickEvent event) { private void setupDownsampleWidgets() { downsampler.setEnabled(false); + fill_policy.setEnabled(false); interval.setEnabled(false); interval.setMaxLength(5); interval.setVisibleLength(5); @@ -505,6 +542,7 @@ private void setupDownsampleWidgets() { public void onClick(final ClickEvent event) { final boolean checked = ((CheckBox) event.getSource()).getValue(); downsampler.setEnabled(checked); + fill_policy.setEnabled(checked); interval.setEnabled(checked); if (checked) { downsampler.setFocus(true); diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index f173e01106..ad09380bb5 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -19,10 +19,12 @@ */ import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -197,6 +199,13 @@ protected void onEvent(final DomEvent event) { /** List of known aggregation functions. Fetched once from the server. */ private final ArrayList aggregators = new ArrayList(); + /** + * List of known downsampling fill policies. + * TODO: fetch from server. + */ + private final List fill_policies = Arrays.asList("none", "nan", + "zero", "null"); + private final DecoratedTabPanel metrics = new DecoratedTabPanel(); /** Panel to place generated graphs and a box for zoom highlighting. */ @@ -517,6 +526,7 @@ private MetricForm addMetricForm(final String label, final int item) { metric.x1y2().addClickHandler(updatey2range); metric.setMetricChangeHandler(metric_change_handler); metric.setAggregators(aggregators); + metric.setFillPolicies(fill_policies); metrics.insert(metric, label, item); return metric; } From 84fdad2cb85ed9d28ab2d4b22669a1405e49117c Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:04:23 -0700 Subject: [PATCH 143/826] Add and fix unit tests for downsampling fill policies. Signed-off-by: Chris Larsen --- test/core/BaseTsdbTest.java | 37 +++ test/core/TestSpan.java | 3 +- test/core/TestTsdbQueryAggregators.java | 63 ++++ test/core/TestTsdbQueryDownsample.java | 393 ++++++++++++++++++++++-- test/core/TestTsdbQueryQueries.java | 3 +- test/tsd/TestQueryRpc.java | 28 ++ 6 files changed, 506 insertions(+), 21 deletions(-) diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 002f1dedfa..9bf987b7ea 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -275,6 +275,43 @@ protected void storeLongTimeSeriesMs() throws Exception { } } + /** + * Create two metrics with same name, skipping every third point in host=web01 + * and every other point in host=web02. To wit: + * + * METRIC TAG t0 t1 t2 t3 t4 t5 ... + * sys.cpu.user web01 X 2 3 X 5 6 ... + * sys.cpu.user web02 X 299 X 297 X 295 ... + */ + protected void storeLongTimeSeriesWithMissingData() throws Exception { + setDataPointStorage(); + + // host=web01 + HashMap tags_local = new HashMap(tags); + long timestamp = 1356998400L; + for (int i = 0; i < 300; ++i) { + // Skip every third point. + if (0 != (i % 3)) { + tsdb.addPoint(METRIC_STRING, timestamp, i + 1, tags_local) + .joinUninterruptibly(); + } + timestamp += 10L; + } + + // host=web02 + tags_local.clear(); + tags_local.put(TAGK_STRING, TAGV_B_STRING); + timestamp = 1356998400L; + for (int i = 300; i > 0; --i) { + // Skip every other point. + if (0 != (i % 2)) { + tsdb.addPoint(METRIC_STRING, timestamp, i, tags_local) + .joinUninterruptibly(); + } + timestamp += 10L; + } + } + protected void storeFloatTimeSeriesSeconds(final boolean two_metrics, final boolean offset) throws Exception { setDataPointStorage(); diff --git a/test/core/TestSpan.java b/test/core/TestSpan.java index 8d04a883aa..dbfc247ce5 100644 --- a/test/core/TestSpan.java +++ b/test/core/TestSpan.java @@ -357,7 +357,8 @@ public void downsampler() throws Exception { assertEquals(6, span.size()); long interval_ms = 1000000; Aggregator downsampler = Aggregators.get("avg"); - final SeekableView it = span.downsampler(interval_ms, downsampler); + final SeekableView it = span.downsampler(1356998000L, 1357007000L, + interval_ms, downsampler, FillPolicy.NONE); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); while (it.hasNext()) { diff --git a/test/core/TestTsdbQueryAggregators.java b/test/core/TestTsdbQueryAggregators.java index 7532411cd4..eed999fc66 100644 --- a/test/core/TestTsdbQueryAggregators.java +++ b/test/core/TestTsdbQueryAggregators.java @@ -13,6 +13,11 @@ package net.opentsdb.core; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; import org.hbase.async.Scanner; import org.junit.Before; @@ -135,6 +140,64 @@ public void runZimSumFloatOffset() throws Exception { assertEquals(600, dps[0].size()); } + @Test + public void runZimSumWithMissingData() throws Exception { + storeLongTimeSeriesWithMissingData(); + + HashMap tags = new HashMap(0); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.ZIMSUM, false); + final DataPoints[] dps = query.run(); + assertNotNull(dps); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertEquals(TAGK_STRING, dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + /* INPUT: + * t0 t1 t2 t3 t4 t5 ... + * web01 X 2 3 X 5 6 ... + * web02 X 299 X 297 X 295 ... + * + * OUTPUT: + * zimsum X 301 3 297 5 301 ... + */ + + int i = 0; + long ts = 1356998400000L; + for (final DataPoint dp : dps[0]) { + // Every sixth position, both elements are missing, so the aggregation + // will have a gap. + int offset = i % 6; + if (0 == offset) { + // We have skipped a timestamp, so we should update the state. + ts += 10000; + ++i; + ++offset; + } + + if (1 == offset || 5 == offset) { + // The second and last elements in each cycle should be the expected + // value, which is 301. + assertEquals(301, dp.longValue()); + } else if (2 == offset || 4 == offset) { + // The third and fifth elements in each cycle should be taken from the + // ascending series. + assertEquals(i + 1, dp.longValue()); + } else { + // Otherwise, the element should be taken from the descending series. + assertEquals(300 - i, dp.longValue()); + } + + assertEquals(ts, dp.timestamp()); + ts += 10000; + ++i; + } + + assertEquals(250, dps[0].size()); + } + @Test public void runMin() throws Exception { storeLongTimeSeriesSeconds(false, false); diff --git a/test/core/TestTsdbQueryDownsample.java b/test/core/TestTsdbQueryDownsample.java index 82af981886..b213a73466 100644 --- a/test/core/TestTsdbQueryDownsample.java +++ b/test/core/TestTsdbQueryDownsample.java @@ -14,6 +14,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; import net.opentsdb.utils.DateTime; @@ -24,6 +29,8 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.google.common.math.DoubleMath; + /** * Tests downsampling with query. */ @@ -38,29 +45,81 @@ public void beforeLocal() throws Exception { } @Test - public void downsample() throws Exception { - int downsampleInterval = (int)DateTime.parseDuration("60s"); - query.downsample(downsampleInterval, Aggregators.SUM); - query.setStartTime(1356998400); - query.setEndTime(1357041600); - assertEquals(60000, TsdbQuery.ForTesting.getDownsampleIntervalMs(query)); - long scanStartTime = 1356998400 - Const.MAX_TIMESPAN * 2 - 60; - assertEquals(scanStartTime, TsdbQuery.ForTesting.getScanStartTimeSeconds(query)); - long scanEndTime = 1357041600 + Const.MAX_TIMESPAN + 1 + 60; - assertEquals(scanEndTime, TsdbQuery.ForTesting.getScanEndTimeSeconds(query)); + public void downsampleFullyAligned() { + testDownsampleScanBounds( + 60000L, + 1356998400L, 1357041600L, + + // The scan start time should be exactly the same as the query start time + // because it is aligned on both boundaries, timespan and interval. + 1356998400L, + + // However, because the query end time is already aligned on an interval, + // it should be snapped forward yet another interval and then snapped + // forward to the next timespan, which is an entire extra hour. + 1357045200L); + } + + @Test + public void downsampleUnaligned() { + final long fifteen_minutes = 60L * 15L; + final long twelve_hours = 3600L * 12L; + final long now = 1427415547L; // Thu Mar 26 17:19:07 2015 GMT-7:00 DST + + testDownsampleScanBounds( + 1000L * fifteen_minutes, + now - twelve_hours, now, + + // Start time should have been snapped back to 1427415300 (5:15a) for the + // interval, then back to 1427371200 (5:00a) for the timespan. + 1427371200L, + + // End time should have been snapped forward to 1427416200 (5:30p) for + // the interval, then forward to 1427418000 (6:00p) for the timespan. + 1427418000L); + } + + @Test + public void downsampleWeirdly() { + final long day = 3600L * 24L; + final long twelve_hours = 3600L * 12L; + + // Thu Mar 26 17:19:07 2015 GMT-7:00 DST + // Fri, 27 Mar 2015 00:19:07 GMT + final long now = 1427415547L; + + testDownsampleScanBounds( + 1000L * day, + now - twelve_hours, now, + + // Start time should be midnight UTC, 26 March. + 1427328000L, + + // End time should be midnight UTC, 28 March. + 1427500800L); } @Test public void downsampleMilliseconds() throws Exception { - int downsampleInterval = (int)DateTime.parseDuration("60s"); - query.downsample(downsampleInterval, Aggregators.SUM); - query.setStartTime(1356998400000L); - query.setEndTime(1357041600000L); - assertEquals(60000, TsdbQuery.ForTesting.getDownsampleIntervalMs(query)); - long scanStartTime = 1356998400 - Const.MAX_TIMESPAN * 2 - 60; - assertEquals(scanStartTime, TsdbQuery.ForTesting.getScanStartTimeSeconds(query)); - long scanEndTime = 1357041600 + Const.MAX_TIMESPAN + 1 + 60; - assertEquals(scanEndTime, TsdbQuery.ForTesting.getScanEndTimeSeconds(query)); + final long start_time = 1356998400000L; + final long end_time = 1357041600000L; + final long downsample_interval = DateTime.parseDuration("60s"); + + query.downsample(downsample_interval, Aggregators.SUM); + query.setStartTime(start_time); + query.setEndTime(end_time); + assertEquals(60000L, TsdbQuery.ForTesting.getDownsampleIntervalMs(query)); + + // The scan start time should be exactly the same as the query start time + // because it is aligned on both boundaries, timespan and interval. + assertEquals(start_time / 1000L, + TsdbQuery.ForTesting.getScanStartTimeSeconds(query)); + + // However, because the query end time is already aligned on an interval, + // it should be snapped forward yet another interval and then snapped + // forward to the next timespan, which is an entire extra hour. + assertEquals((end_time + 3600000L) / 1000L, + TsdbQuery.ForTesting.getScanEndTimeSeconds(query)); } @Test (expected = NullPointerException.class) @@ -438,4 +497,300 @@ public void runFloatSingleTSDownsampleAndRateAndCount() throws Exception { assertEquals(150, dps[0].size()); } + /** + * A helper interface to be used by the filling-test code. + */ + interface Validator { + /** @return true if the argument is valid. */ + boolean isValidValue(double value); + + /** @return the fill policy to be used while downsampling. */ + FillPolicy getFillPolicy(); + + /** @return true if the argument is the sentinel for empty intervals. */ + boolean isMissingValue(double value); + } + + // Fill missing intervals with NaNs. + abstract class NaNValidator implements Validator { + @Override + public FillPolicy getFillPolicy() { + return FillPolicy.NOT_A_NUMBER; + } + + @Override + public boolean isMissingValue(final double value) { + return Double.isNaN(value); + } + } + + // Fill missing intervals with zeroes. + abstract class ZeroValidator implements Validator { + @Override + public FillPolicy getFillPolicy() { + return FillPolicy.ZERO; + } + + @Override + public boolean isMissingValue(final double value) { + return DoubleMath.fuzzyEquals(0.0, value, 0.0001); + } + } + + @Test + public void runSumAvgLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.SUM, Aggregators.AVG, + // 301.5, 301.5, 301.5, ... + new NaNValidator() { + @Override + public boolean isValidValue(final double value) { + return DoubleMath.fuzzyEquals(301.5, value, 0.0001); + } + }); + } + + @Test + public void runAvgSumLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.AVG, Aggregators.SUM, + // 152, 301.5, 155, 301.5, 158, 301.5, ... + new NaNValidator() { + private boolean even = false; + private double even_expected = 149.0; + + @Override + public boolean isValidValue(final double value) { + even = !even; + if (even) { + even_expected += 3.0; + return DoubleMath.fuzzyEquals(even_expected, value, 0.0001); + } else { + return DoubleMath.fuzzyEquals(301.5, value, 0.0001); + } + } + }); + } + + @Test + public void runAvgAvgLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.AVG, Aggregators.AVG, + // 150.75, 150.75, 150.75, ... + new ZeroValidator() { + @Override + public boolean isValidValue(final double value) { + return DoubleMath.fuzzyEquals(150.75, value, 0.0001); + } + }); + } + + @Test + public void runSumSumLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.SUM, Aggregators.SUM, + // 304, 603, 310, 603, 316, 603, ... + new NaNValidator() { + private double even_expected = 298.0; + private final double odd_expected = 603.0; + private boolean even = false; + + @Override + public boolean isValidValue(final double value) { + even = !even; + if (even) { + even_expected += 6.0; + return DoubleMath.fuzzyEquals(even_expected, value, 0.0001); + } else { + return DoubleMath.fuzzyEquals(odd_expected, value, 0.0001); + } + } + }); + } + + @Test + public void runMinMinLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.MIN, Aggregators.MIN, + // 2, 5, 8, ..., 143, 146, 149, 149, 145, 143, 139, 133, 131, ... + new ZeroValidator() { + private double even_expected = -4.0; + private double even_change = 6.0; + private double odd_expected = -1.0; + private double odd_change = 6.0; + private boolean even = false; + + @Override + public boolean isValidValue(final double value) { + even = !even; + if (even) { + even_expected += even_change; + + // Check for the point at which even terms change. + if (DoubleMath.fuzzyEquals(even_expected, 152.0, 0.0001)) { + // After this point, even terms begin decreasing by six. + even_expected = 149.0; + even_change = -6.0; + } + + return DoubleMath.fuzzyEquals(even_expected, value, 0.0001); + } else { + odd_expected += odd_change; + + // Check for the point at which odd terms change. + if (DoubleMath.fuzzyEquals(odd_expected, 155.0, 0.0001)) { + // After this point, odd terms begin decreasing by six. + odd_expected = 145.0; + odd_change = -6.0; + } + + return DoubleMath.fuzzyEquals(odd_expected, value, 0.0001); + } + } + }); + } + + @Test + public void runMinSumLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.MIN, Aggregators.SUM, + // 5, 11, 17, 23, ..., 197, 203, 197, 215, 191, 227, 185, 239, ..., + // 287, 155, 299, 149, 292, 143, 280, ... + new NaNValidator() { + private double even_expected = -7.0; + private double even_change = 12.0; + private double odd_expected = -1.0; + private double odd_change = 12.0; + private boolean even = false; + + @Override + public boolean isValidValue(final double value) { + even = !even; + if (even) { + even_expected += even_change; + + // Check for the point at which even terms change. + if (DoubleMath.fuzzyEquals(even_expected, 209.0, 0.0001)) { + // After this point, even terms begin decreasing by six. + even_expected = 197.0; + even_change = -6.0; + } + + return DoubleMath.fuzzyEquals(even_expected, value, 0.0001); + } else { + odd_expected += odd_change; + + // Check for the point at which odd terms change. + if (DoubleMath.fuzzyEquals(odd_expected, 311.0, 0.0001)) { + // After this point, odd terms begin decreasing by twelve. + odd_expected = 292.0; + odd_change = -12.0; + } + + return DoubleMath.fuzzyEquals(odd_expected, value, 0.0001); + } + } + }); + } + + @Test + public void runSumMinLongSingleTSDownsampleWNulls() throws Exception { + storeLongTimeSeriesWithMissingData(); + + runTSDownsampleWithMissingData(Aggregators.SUM, Aggregators.MIN, + // 301, 300, 301, 300, ... + new NaNValidator() { + private boolean even = false; + + @Override + public boolean isValidValue(final double value) { + even = !even; + return DoubleMath.fuzzyEquals( + even ? 301.0 : 300.0, value, 0.0001); + } + }); + } + + /** + * Precondition: the time series have been stored. + */ + public void runTSDownsampleWithMissingData(final Aggregator queryAggregator, + final Aggregator downsampleAggregator, final Validator validator) + throws Exception { + final long start_time = 1356998400L; + final long end_time = 1357041600L; + final int ds_interval = 30; + final long ds_interval_ms = 1000L * ds_interval; + final String metric = METRIC_STRING; + + final HashMap tags = new HashMap(0); + query.setStartTime(start_time); + query.setEndTime(end_time); + query.downsample(ds_interval_ms, downsampleAggregator, + validator.getFillPolicy()); + query.setTimeSeries(metric, tags, queryAggregator, false); + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals(metric, dps[0].metricName()); + assertFalse(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + + // For the reasoning behind this calculation, see the following methods: + // TsdbQuery#getScanStartTimeSeconds() + // TsdbQuery#getScanEndTimeSeconds() + + int i = 0; + long expected_timestamp_ms = 1000L * start_time; + for (final DataPoint dp : dps[0]) { + // Downsampler outputs just doubles. + assertFalse(dp.isInteger()); + + // There should be only one hundred valid values. + if (i++ < 100) { + // Check the value. + assertTrue(validator.isValidValue(dp.doubleValue())); + } else { + // Otherwise, the value should be the special missing value. + assertTrue(validator.isMissingValue(dp.doubleValue())); + } + + // The timestamp should match our expectation based on the interval. + assertEquals(expected_timestamp_ms, dp.timestamp()); + + // Move to the next expected interval. + expected_timestamp_ms += ds_interval_ms; + } + + // Ensure we got the number of points we expected. + assertEquals((end_time - start_time + 3600L) / ds_interval, dps[0].size()); + } + + /** + * Helper to test the start and stop times in a query for downsampling + * @param downsample_interval The downsample interval + * @param start_time The start of the query + * @param end_time The end of the query + * @param expected_start_time What we expect the TSDBQuery class to give us + * @param expected_end_time What we expect the TSDBQuery class to give us + */ + private void testDownsampleScanBounds(final long downsample_interval, + final long start_time, final long end_time, + final long expected_start_time, final long expected_end_time) { + query.downsample(downsample_interval, Aggregators.SUM); + query.setStartTime(start_time); + query.setEndTime(end_time); + + assertEquals(expected_start_time, + TsdbQuery.ForTesting.getScanStartTimeSeconds(query)); + + assertEquals(expected_end_time, + TsdbQuery.ForTesting.getScanEndTimeSeconds(query)); + } } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 464cb91bff..a6005daebf 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -539,12 +539,13 @@ public void runEndTime() throws Exception { int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { + System.out.println(timestamp); assertEquals(value, dp.longValue()); assertEquals(timestamp, dp.timestamp()); value++; timestamp += 30000; } - assertEquals(236, dps[0].size()); + assertEquals(119, dps[0].size()); } @Test diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index 6806680b47..8abb43c2fd 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.when; @@ -142,6 +143,15 @@ public void parseQueryMTypeWDS() throws Exception { assertEquals("1h-avg", sub.getDownsample()); } + @Test + public void parseQueryMTypeWDSAndFill() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:1h-avg-lerp:sys.cpu.0"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + assertEquals("1h-avg-lerp", sub.getDownsample()); + } + @Test public void parseQueryMTypeWRateAndDS() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, @@ -362,5 +372,23 @@ public void executeNSU() throws Exception { assertTrue(json.contains("No such name for 'foo': 'metrics'")); } + @Test + public void executeWithBadDSFill() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + try { + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:10m-avg-badbadbad:sys.cpu.user"); + rpc.execute(tsdb, query); + fail("expected BadRequestException"); + } catch (final BadRequestException exn) { + assertTrue(exn.getMessage().startsWith( + "No such fill policy: 'badbadbad': must be one of:")); + } + } + //TODO(cl) add unit tests for the rate options parsing } \ No newline at end of file From 53560deff93408c44783e53f427f046277a7c351 Mon Sep 17 00:00:00 2001 From: Sean Patrick Miller Date: Tue, 28 Apr 2015 12:57:01 -0700 Subject: [PATCH 144/826] Enable the JSON serializer to emit NaNs or Nulls based on the fill policy. Signed-off-by: Chris Larsen --- src/tsd/HttpJsonSerializer.java | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 996f0328c7..94d8d6a0f5 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -34,6 +34,7 @@ import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; +import net.opentsdb.core.FillPolicy; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.QueryException; import net.opentsdb.core.TSDB; @@ -652,7 +653,9 @@ public WriteToBuffer(final DataPoints dps) { * variables. */ public Object call(final ArrayList deferreds) throws Exception { - + final TSSubQuery orig_query = data_query.getQueries() + .get(dps.getQueryIndex()); + json.writeStartObject(); json.writeStringField("metric", metric.toString()); @@ -675,8 +678,6 @@ public Object call(final ArrayList deferreds) throws Exception { json.writeEndArray(); if (data_query.getShowQuery()) { - final TSSubQuery orig_query = data_query.getQueries() - .get(dps.getQueryIndex()); json.writeObjectField("query", orig_query); } @@ -732,7 +733,14 @@ public Object call(final ArrayList deferreds) throws Exception { if (dp.isInteger()) { json.writeNumber(dp.longValue()); } else { - json.writeNumber(dp.doubleValue()); + // Report missing intervals as null or NaN. + final double value = dp.doubleValue(); + if (Double.isNaN(value) && + orig_query.fillPolicy() == FillPolicy.NULL) { + json.writeNull(); + } else { + json.writeNumber(dp.doubleValue()); + } } json.writeEndArray(); } @@ -749,7 +757,14 @@ public Object call(final ArrayList deferreds) throws Exception { if (dp.isInteger()) { json.writeNumberField(Long.toString(timestamp), dp.longValue()); } else { - json.writeNumberField(Long.toString(timestamp), dp.doubleValue()); + // Report missing intervals as null or NaN. + final double value = dp.doubleValue(); + if (Double.isNaN(value) && + orig_query.fillPolicy() == FillPolicy.NULL) { + json.writeNumberField(Long.toString(timestamp), null); + } else { + json.writeNumberField(Long.toString(timestamp), dp.doubleValue()); + } } } json.writeEndObject(); From 4be59101e3797842c5be89044f2a186ee13ed9e3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 13:21:23 -0700 Subject: [PATCH 145/826] Add an option to the rate counter object to simply drop data points when the value resets. This addresses both #460 and #465 --- src/core/RateOptions.java | 37 ++++++++++++++++++- src/core/RateSpan.java | 5 +++ src/tsd/QueryRpc.java | 5 ++- src/tsd/client/MetricForm.java | 9 ++++- test/core/TestRateSpan.java | 57 +++++++++++++++++++++++++++++ test/core/TestTsdbQueryQueries.java | 23 ++++++++++++ 6 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/core/RateOptions.java b/src/core/RateOptions.java index 0265e19aee..07ae817f6a 100644 --- a/src/core/RateOptions.java +++ b/src/core/RateOptions.java @@ -33,6 +33,9 @@ public class RateOptions { * some maximum */ private boolean counter; + + /** Whether or not to simply drop rolled-over or reset data points */ + private boolean drop_resets; /** * If calculating a rate of change over a metric that is a counter, then this @@ -55,6 +58,7 @@ public RateOptions() { this.counter = false; this.counter_max = Long.MAX_VALUE; this.reset_value = DEFAULT_RESET_VALUE; + this.drop_resets = false; } /** @@ -69,14 +73,32 @@ public RateOptions() { */ public RateOptions(final boolean counter, final long counter_max, final long reset_value) { + this(counter, counter_max, reset_value, false); + } + + /** + * Ctor + * @param counter If true, indicates that the rate calculation should assume + * that the underlying data is from a counter + * @param counter_max Specifies the maximum value for the counter before it + * will roll over and restart at 0 + * @param reset_value Specifies the largest rate change that is considered + * acceptable, if a rate change is seen larger than this value then the + * counter is assumed to have been reset + * @param drop_resets Whether or not to drop rolled-over or reset counters + * @since 2.2 + */ + public RateOptions(final boolean counter, final long counter_max, + final long reset_value, final boolean drop_resets) { this.counter = counter; this.counter_max = counter_max; this.reset_value = reset_value; + this.drop_resets = drop_resets; } @Override public int hashCode() { - return Objects.hashCode(counter, counter_max, reset_value); + return Objects.hashCode(counter, counter_max, reset_value, drop_resets); } @Override @@ -93,7 +115,8 @@ public boolean equals(final Object obj) { final RateOptions options = (RateOptions)obj; return Objects.equal(counter, options.counter) && Objects.equal(counter_max, options.counter_max) - && Objects.equal(reset_value, options.reset_value); + && Objects.equal(reset_value, options.reset_value) + && Objects.equal(drop_resets, options.drop_resets); } /** @return Whether or not the counter flag is set */ @@ -111,6 +134,11 @@ public long getResetValue() { return reset_value; } + /** @return Whether or not to drop rolled-over or reset counters */ + public boolean getDropResets() { + return drop_resets; + } + /** @param counter Whether or not the time series should be considered counters */ public void setIsCounter(boolean counter) { this.counter = counter; @@ -126,6 +154,11 @@ public void setResetValue(long reset_value) { this.reset_value = reset_value; } + /** @param drop_resets Whether or not to drop rolled-over or reset counters */ + public void setDropResets(boolean drop_resets) { + this.drop_resets = drop_resets; + } + /** * Generates a String version of the rate option instance in a format that * can be utilized in a query. diff --git a/src/core/RateSpan.java b/src/core/RateSpan.java index ed3540e6da..c9fc43f350 100644 --- a/src/core/RateSpan.java +++ b/src/core/RateSpan.java @@ -147,6 +147,11 @@ private void populateNextRate() { } if (options.isCounter() && difference < 0) { + if (options.getDropResets()) { + populateNextRate(); + return; + } + if (prev_data.isInteger() && next_data.isInteger()) { // NOTE: Calculates in the long type to avoid precision loss // while converting long values to double values if both values are long. diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 2fa95524f5..5da8dae855 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -604,14 +604,15 @@ static final public RateOptions parseRateOptions(final boolean rate, + parts.length + " parts"); } - final boolean counter = "counter".equals(parts[0]); + final boolean counter = parts[0].endsWith("counter"); try { final long max = (parts.length >= 2 && parts[1].length() > 0 ? Long .parseLong(parts[1]) : Long.MAX_VALUE); try { final long reset = (parts.length >= 3 && parts[2].length() > 0 ? Long .parseLong(parts[2]) : RateOptions.DEFAULT_RESET_VALUE); - return new RateOptions(counter, max, reset); + final boolean drop_counter = parts[0].equals("dropcounter"); + return new RateOptions(counter, max, reset, drop_counter); } catch (NumberFormatException e) { throw new BadRequestException( "Reset value of counter was not a number, received '" + parts[2] diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index a397b61fca..e12421a6a1 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -335,9 +335,14 @@ public boolean buildQueryString(final StringBuilder url) { if (rate.getValue()) { url.append(":rate"); if (rate_counter.getValue()) { - url.append('{').append("counter"); + url.append('{');//.append("counter"); final String max = counter_max.getValue().trim(); final String reset = counter_reset_value.getValue().trim(); + if (max.isEmpty() && (reset.equals("0") || reset.isEmpty())) { + url.append("dropcounter"); + } else { + url.append("counter"); + } if (max.length() > 0 && reset.length() > 0) { url.append(',').append(max).append(',').append(reset); } else if (max.length() > 0 && reset.length() == 0) { @@ -605,7 +610,7 @@ static final public LocalRateOptions parseRateOptions(boolean rate, String spec) try { LocalRateOptions options = new LocalRateOptions(); - options.is_counter = "counter".equals(parts[0]); + options.is_counter = parts[0].endsWith("counter"); options.counter_max = (parts.length >= 2 && parts[1].length() > 0 ? Long .parseLong(parts[1]) : Long.MAX_VALUE); options.reset_value = (parts.length >= 3 && parts[2].length() > 0 ? Long diff --git a/test/core/TestRateSpan.java b/test/core/TestRateSpan.java index afa1d59488..296b322dce 100644 --- a/test/core/TestRateSpan.java +++ b/test/core/TestRateSpan.java @@ -254,4 +254,61 @@ public void testNext_counterWithResetValue() { assertFalse(rate_span.hasNext()); assertFalse(rate_span.hasNext()); } + + @Test + public void testNext_counterDroResets() { + final long RESET_VALUE = 1; + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(1356998400000L, 40), + MutableDataPoint.ofLongValue(1356998401000L, 50), + MutableDataPoint.ofLongValue(1356998402000L, 40), + MutableDataPoint.ofLongValue(1356998403000L, 50) + }); + DataPoint[] rates = new DataPoint[] { + MutableDataPoint.ofDoubleValue(1356998400000L, 40 / 1356998400.0), + MutableDataPoint.ofDoubleValue(1356998401000L, 10), + // drop the point before + MutableDataPoint.ofDoubleValue(1356998403000L, 10) + }; + options = new RateOptions(true, COUNTER_MAX, RESET_VALUE, true); + RateSpan rate_span = new RateSpan(source, options); + for (DataPoint rate : rates) { + assertTrue(rate_span.hasNext()); + assertTrue(rate_span.hasNext()); + DataPoint dp = rate_span.next(); + String msg = String.format("expected rate = '%s' ", rate); + assertFalse(msg, dp.isInteger()); + assertEquals(msg, rate.timestamp(), dp.timestamp()); + assertEquals(msg, rate.doubleValue(), dp.doubleValue(), 0.0000001); + } + assertFalse(rate_span.hasNext()); + assertFalse(rate_span.hasNext()); + } + + @Test + public void testNext_counterDroResetsNothingAfter() { + final long RESET_VALUE = 1; + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(1356998400000L, 40), + MutableDataPoint.ofLongValue(1356998401000L, 50), + MutableDataPoint.ofLongValue(1356998402000L, 40) + }); + DataPoint[] rates = new DataPoint[] { + MutableDataPoint.ofDoubleValue(1356998400000L, 40 / 1356998400.0), + MutableDataPoint.ofDoubleValue(1356998401000L, 10), + }; + options = new RateOptions(true, COUNTER_MAX, RESET_VALUE, true); + RateSpan rate_span = new RateSpan(source, options); + for (DataPoint rate : rates) { + assertTrue(rate_span.hasNext()); + assertTrue(rate_span.hasNext()); + DataPoint dp = rate_span.next(); + String msg = String.format("expected rate = '%s' ", rate); + assertFalse(msg, dp.isInteger()); + assertEquals(msg, rate.timestamp(), dp.timestamp()); + assertEquals(msg, rate.doubleValue(), dp.doubleValue(), 0.0000001); + } + assertFalse(rate_span.hasNext()); + assertFalse(rate_span.hasNext()); + } } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index a6005daebf..32effffc3c 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -1059,6 +1059,29 @@ public void runRateCounterAnomally() throws Exception { assertEquals(2, dps[0].size()); } + @Test + public void runRateCounterAnomallyDrop() throws Exception { + setDataPointStorage(); + long timestamp = 1356998400; + tsdb.addPoint(METRIC_STRING, timestamp += 30, 45, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 75, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 25, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, timestamp += 30, 55, tags).joinUninterruptibly(); + + final RateOptions ro = new RateOptions(true, 10000, 35, true); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + assertEquals(1.0, dps[0].doubleValue(0), 0.001); + assertEquals(1356998460000L, dps[0].timestamp(0)); + assertEquals(1, dps[0].doubleValue(1), 0.001); + assertEquals(1356998520000L, dps[0].timestamp(1)); + assertEquals(2, dps[0].size()); + } + @Test public void runMultiCompact() throws Exception { final byte[] qual1 = { 0x00, 0x17 }; From ecbae070bc07517b4672dd22052487dc3c433e4a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 19:21:33 -0700 Subject: [PATCH 146/826] Add Internal.getMaxUnsignedValueOnBytes() and deprecate the old one in UniqueId. Signed-off-by: Chris Larsen --- src/core/Internal.java | 14 ++++++++++++++ src/uid/UniqueId.java | 8 ++++++-- test/core/TestInternal.java | 28 ++++++++++++++++++++++++++++ test/uid/TestUniqueId.java | 7 ------- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index bde1e57a53..f57ed5dc8e 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -860,4 +860,18 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, scanner.setKeyRegexp(buf.toString(), Charset.forName("ISO-8859-1")); } + /** + * Simple helper to calculate the max value for any width of long from 0 to 7 + * bytes. + * @param width The width of the byte array we're comparing + * @return The maximum unsigned integer value on {@link width} bytes. + * @since 2.2 + */ + public static long getMaxUnsignedValueOnBytes(final int width) { + if (width < 0 || width > 7) { + throw new IllegalArgumentException("Width must be from 1 to 7 bytes: " + + width); + } + return ((long) 1 << width * Byte.SIZE) - 1; + } } diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 0a1096d73c..780645b61f 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory; import net.opentsdb.core.Const; +import net.opentsdb.core.Internal; import net.opentsdb.core.TSDB; import net.opentsdb.meta.UIDMeta; import net.opentsdb.stats.StatsCollector; @@ -190,9 +191,12 @@ public void setTSDB(final TSDB tsdb) { this.tsdb = tsdb; } - /** The largest possible ID given the number of bytes the IDs are represented on. */ + /** The largest possible ID given the number of bytes the IDs are + * represented on. + * @deprecated Use {@link Internal.getMaxUnsignedValueOnBytes} + */ public long maxPossibleId() { - return ((long) 1 << id_width * Byte.SIZE) - 1; + return Internal.getMaxUnsignedValueOnBytes(id_width); } /** diff --git a/test/core/TestInternal.java b/test/core/TestInternal.java index 4259072b89..40a08f8192 100644 --- a/test/core/TestInternal.java +++ b/test/core/TestInternal.java @@ -15,7 +15,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertArrayEquals; 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 java.util.ArrayList; @@ -810,6 +812,32 @@ public void extractQualifierMilliSeconds() { Internal.extractQualifier(qual, 2)); } + @Test + public void getMaxUnsignedValueOnBytes() throws Exception { + assertEquals(0, Internal.getMaxUnsignedValueOnBytes(0)); + assertEquals(255, Internal.getMaxUnsignedValueOnBytes(1)); + assertEquals(65535, Internal.getMaxUnsignedValueOnBytes(2)); + assertEquals(16777215, Internal.getMaxUnsignedValueOnBytes(3)); + assertEquals(4294967295L, Internal.getMaxUnsignedValueOnBytes(4)); + assertEquals(1099511627775L, Internal.getMaxUnsignedValueOnBytes(5)); + assertEquals(281474976710655L, Internal.getMaxUnsignedValueOnBytes(6)); + assertEquals(72057594037927935L, Internal.getMaxUnsignedValueOnBytes(7)); + + try { + Internal.getMaxUnsignedValueOnBytes(8); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertNotNull(e); + } + + try { + Internal.getMaxUnsignedValueOnBytes(-1); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertNotNull(e); + } + } + /** Shorthand to create a {@link KeyValue}. */ private static KeyValue makekv(final byte[] qualifier, final byte[] value) { return new KeyValue(KEY, FAMILY, qualifier, value); diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index bccb35c284..682dcac70a 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -111,13 +111,6 @@ public void widthEqual() { uid = new UniqueId(client, table, kind, 3); assertEquals(3, uid.width()); } - - @Test - public void testMaxPossibleId() { - assertEquals(255, (new UniqueId(client, table, kind, 1)).maxPossibleId()); - assertEquals(65535, (new UniqueId(client, table, kind, 2)).maxPossibleId()); - assertEquals(16777215L, (new UniqueId(client, table, kind, 3)).maxPossibleId()); - } @Test public void getNameSuccessfulHBaseLookup() { From b01c0145fc5d57ce8f6e1db9682358b0e89145de Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 19:22:05 -0700 Subject: [PATCH 147/826] Modify TSDB to use the new getMaxUnsignedValueOnBytes() and also emit "0" for the metric UID used and available stats so we don't confuse people too much with odd, negative values. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 11cd0cb3ca..64249578e1 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -395,25 +395,31 @@ public void collectStats(final StatsCollector collector) { .joinUninterruptibly(); collectUidStats(metrics, collector); - collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), - "kind=" + METRICS_QUAL); - collector.record("uid.ids-available", - (metrics.maxPossibleId() - used_uids.get(METRICS_QUAL)), - "kind=" + METRICS_QUAL); + if (config.getBoolean("tsd.core.uid.random_metrics")) { + collector.record("uid.ids-used", 0, "kind=" + METRICS_QUAL); + collector.record("uid.ids-available", 0, "kind=" + METRICS_QUAL); + } else { + collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), + "kind=" + METRICS_QUAL); + collector.record("uid.ids-available", + (Internal.getMaxUnsignedValueOnBytes(metrics.width()) - + used_uids.get(METRICS_QUAL)), "kind=" + METRICS_QUAL); + } collectUidStats(tag_names, collector); collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), "kind=" + TAG_NAME_QUAL); collector.record("uid.ids-available", - (tag_names.maxPossibleId() - used_uids.get(TAG_NAME_QUAL)), + (Internal.getMaxUnsignedValueOnBytes(tag_names.width()) - + used_uids.get(TAG_NAME_QUAL)), "kind=" + TAG_NAME_QUAL); collectUidStats(tag_values, collector); collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), "kind=" + TAG_VALUE_QUAL); collector.record("uid.ids-available", - (tag_values.maxPossibleId() - used_uids.get(TAG_VALUE_QUAL)), - "kind=" + TAG_VALUE_QUAL); + (Internal.getMaxUnsignedValueOnBytes(tag_values.width()) - + used_uids.get(TAG_VALUE_QUAL)), "kind=" + TAG_VALUE_QUAL); } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); From 3e4bed2006230685ec52e5660746b9daca77aead Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 19:24:11 -0700 Subject: [PATCH 148/826] Add a method to CliUtils to get proper scanners for iterating over the entire TSDB table whether it's salted or not. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/tools/CliUtils.java | 95 +++++++++++++++++ test/tools/TestCliUtils.java | 201 +++++++++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 test/tools/TestCliUtils.java diff --git a/Makefile.am b/Makefile.am index 61e04b52b5..9ad3da7732 100644 --- a/Makefile.am +++ b/Makefile.am @@ -201,6 +201,7 @@ test_SRC := \ test/storage/MockBase.java \ test/storage/MockDataPoints.java \ test/tools/TestDumpSeries.java \ + test/tools/TestCliUtils.java \ test/tools/TestFsck.java \ test/tools/TestTextImporter.java \ test/tools/TestUID.java \ diff --git a/src/tools/CliUtils.java b/src/tools/CliUtils.java index 3fc78b3cc5..f67c966dd4 100644 --- a/src/tools/CliUtils.java +++ b/src/tools/CliUtils.java @@ -17,12 +17,17 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import net.opentsdb.core.Const; +import net.opentsdb.core.Internal; +import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes; import org.hbase.async.GetRequest; +import org.hbase.async.HBaseClient; import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; @@ -140,6 +145,96 @@ static final Scanner getDataTableScanner(final TSDB tsdb, final long start_id, return scanner; } + /** + * Generates a list of Scanners to use for iterating over the full TSDB + * data table. If salting is enabled then {@link Const.SaltBukets()} scanners + * will be returned. If salting is disabled then {@link num_scanners} + * scanners will be returned. + * @param tsdb The TSDB to generate scanners from + * @param num_scanners The max number of scanners if salting is disabled + * @return A list of scanners to use for scanning the table. + */ + static final List getDataTableScanners(final TSDB tsdb, + final int num_scanners) { + if (num_scanners < 1) { + throw new IllegalArgumentException( + "Number of scanners must be 1 or more: " + num_scanners); + } + // TODO - It would be neater to get a list of regions then create scanners + // on those boundaries. We'll have to modify AsyncHBase for that to avoid + // creating lots of custom HBase logic in here. + final short metric_width = TSDB.metrics_width(); + final List scanners = new ArrayList(); + + if (Const.SALT_WIDTH() > 0) { + // salting is enabled so we'll create one scanner per salt for now + byte[] start_key = HBaseClient.EMPTY_ARRAY; + byte[] stop_key = HBaseClient.EMPTY_ARRAY; + + for (int i = 1; i < Const.SALT_BUCKETS() + 1; i++) { + // move stop key to start key + if (i > 1) { + start_key = Arrays.copyOf(stop_key, stop_key.length); + } + + if (i >= Const.SALT_BUCKETS()) { + stop_key = HBaseClient.EMPTY_ARRAY; + } else { + stop_key = RowKey.getSaltBytes(i); + } + final Scanner scanner = tsdb.getClient().newScanner(tsdb.dataTable()); + scanner.setStartKey(Arrays.copyOf(start_key, start_key.length)); + scanner.setStopKey(Arrays.copyOf(stop_key, stop_key.length)); + scanner.setFamily(TSDB.FAMILY()); + scanners.add(scanner); + } + + } else { + // No salt, just go by the max metric ID + long max_id = CliUtils.getMaxMetricID(tsdb); + if (max_id < 1) { + max_id = Internal.getMaxUnsignedValueOnBytes(metric_width); + } + final long quotient = max_id % num_scanners == 0 ? max_id / num_scanners : + (max_id / num_scanners) + 1; + + byte[] start_key = HBaseClient.EMPTY_ARRAY; + byte[] stop_key = new byte[metric_width]; + + for (int i = 0; i < num_scanners; i++) { + // move stop key to start key + if (i > 0) { + start_key = Arrays.copyOf(stop_key, stop_key.length); + } + + // setup the next stop key + final byte[] stop_id; + if ((i +1) * quotient > max_id) { + stop_id = null; + } else { + stop_id = Bytes.fromLong((i + 1) * quotient); + } + if ((i +1) * quotient >= max_id) { + stop_key = HBaseClient.EMPTY_ARRAY; + } else { + System.arraycopy(stop_id, stop_id.length - metric_width, stop_key, + 0, metric_width); + } + + final Scanner scanner = tsdb.getClient().newScanner(tsdb.dataTable()); + scanner.setStartKey(Arrays.copyOf(start_key, start_key.length)); + if (stop_key != null) { + scanner.setStopKey(Arrays.copyOf(stop_key, stop_key.length)); + } + scanner.setFamily(TSDB.FAMILY()); + scanners.add(scanner); + } + + } + + return scanners; + } + /** * Invokes the reflected {@code UniqueId.toBytes()} method with the given * string using the UniqueId character set. diff --git a/test/tools/TestCliUtils.java b/test/tools/TestCliUtils.java new file mode 100644 index 0000000000..7745494961 --- /dev/null +++ b/test/tools/TestCliUtils.java @@ -0,0 +1,201 @@ +package net.opentsdb.tools; + +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; + +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.core.Const; +import net.opentsdb.core.RowKey; +import net.opentsdb.core.TSDB; + +import org.hbase.async.Bytes; +import org.hbase.async.GetRequest; +import org.hbase.async.HBaseClient; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +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.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +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, Scanner.class, Const.class }) +public class TestCliUtils { + + private TSDB tsdb = null; + private HBaseClient client = null; + private List start_keys; + private List stop_keys; + + @Before + public void before() throws Exception { + tsdb = mock(TSDB.class); + client = mock(HBaseClient.class); + when(tsdb.getClient()).thenReturn(client); + when(tsdb.uidTable()).thenReturn("tsdb-uid".getBytes()); + + } + + @Test + public void getDataTableScanners1Thread() throws Exception { + setupGetDataTableScanners(256); + + final List scanners = CliUtils.getDataTableScanners(tsdb, 1); + assertEquals(1, scanners.size()); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, start_keys.get(0)); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, stop_keys.get(0)); + } + + @Test + public void getDataTableScannersMultiThreaded() throws Exception { + setupGetDataTableScanners(256); + final List scanners = CliUtils.getDataTableScanners(tsdb, 15); + assertEquals(15, scanners.size()); + byte[] key = new byte[3]; + int last_value = 0; + for (int i = 0; i < 15; i++) { + if (i == 0) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, start_keys.get(i)); + } else { + assertArrayEquals(key, start_keys.get(i)); + } + last_value += 18; + if (i == 14) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, stop_keys.get(i)); + } else { + System.arraycopy(Bytes.fromInt(last_value), 1, key, 0, 3); + assertArrayEquals(key, stop_keys.get(i)); + } + } + } + + @Test + public void getDataTableScannersRandom1Thread() throws Exception { + setupGetDataTableScanners(0); + + final List scanners = CliUtils.getDataTableScanners(tsdb, 1); + assertEquals(1, scanners.size()); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, start_keys.get(0)); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, stop_keys.get(0)); + } + + @Test + public void getDataTableScannersRandomMultiThreaded() throws Exception { + setupGetDataTableScanners(0); + final List scanners = CliUtils.getDataTableScanners(tsdb, 15); + assertEquals(15, scanners.size()); + byte[] key = new byte[3]; + int last_value = 0; + for (int i = 0; i < 15; i++) { + if (i == 0) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, start_keys.get(i)); + } else { + assertArrayEquals(key, start_keys.get(i)); + } + last_value += 1118481; + if (i == 14) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, stop_keys.get(i)); + } else { + System.arraycopy(Bytes.fromInt(last_value), 1, key, 0, 3); + assertArrayEquals(key, stop_keys.get(i)); + } + } + } + + @Test + public void getDataTableScannersSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + setupGetDataTableScanners(256); + final List scanners = CliUtils.getDataTableScanners(tsdb, 15); + assertEquals(20, scanners.size()); + byte[] key = new byte[1]; + for (int i = 0; i < 20; i++) { + if (i == 0) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, start_keys.get(i)); + } else { + assertArrayEquals(key, start_keys.get(i)); + } + if (i == 19) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, stop_keys.get(i)); + } else { + key = RowKey.getSaltBytes(i + 1); + assertArrayEquals(key, stop_keys.get(i)); + } + } + } + + @Test + public void getDataTableScannersSaltedRandom() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + setupGetDataTableScanners(0); + final List scanners = CliUtils.getDataTableScanners(tsdb, 15); + assertEquals(20, scanners.size()); + byte[] key = new byte[1]; + for (int i = 0; i < 20; i++) { + if (i == 0) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, start_keys.get(i)); + } else { + assertArrayEquals(key, start_keys.get(i)); + } + if (i == 19) { + assertArrayEquals(HBaseClient.EMPTY_ARRAY, stop_keys.get(i)); + } else { + key = RowKey.getSaltBytes(i + 1); + assertArrayEquals(key, stop_keys.get(i)); + } + } + } + + private void setupGetDataTableScanners(final long max) { + final KeyValue kv = new KeyValue(new byte[] {}, + TSDB.FAMILY(), "metrics".getBytes(), Bytes.fromLong(max)); + final ArrayList kvs = new ArrayList(1); + kvs.add(kv); + when(client.get(any(GetRequest.class))) + .thenReturn(Deferred.>fromResult(kvs)); + + start_keys = new ArrayList(); + stop_keys = new ArrayList(); + + final Scanner scanner = mock(Scanner.class); + when(client.newScanner(any(byte[].class))).thenReturn(scanner); + + PowerMockito.doAnswer(new Answer() { + @Override + public Void answer(final InvocationOnMock invocation) throws Throwable { + start_keys.add((byte[])invocation.getArguments()[0]); + return null; + } + }).when(scanner).setStartKey(any(byte[].class)); + + PowerMockito.doAnswer(new Answer() { + @Override + public Void answer(final InvocationOnMock invocation) throws Throwable { + stop_keys.add((byte[])invocation.getArguments()[0]); + return null; + } + }).when(scanner).setStopKey(any(byte[].class)); + } +} From 817ae54d0313cf55d50d79f6eea767285d4784ed Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 19:24:51 -0700 Subject: [PATCH 149/826] Fix the scanner in MockBase to properly handle an empty byte array for the stop key when we want to scan from the start key all the way to the end of the table. Signed-off-by: Chris Larsen --- test/storage/MockBase.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index e6568c1446..c6493a262e 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -989,17 +989,20 @@ public Deferred>> answer( new ArrayList>(); for (Map.Entry>>> row : storage.entrySet()) { - + // if it's before the start row, after the end row or doesn't // match the given regex, continue on to the next row if (start != null && Bytes.memcmp(row.getKey(), start) < 0) { continue; } // asynchbase Scanner's logic: - // - start_key is inclusive, stop key is exclusive, - // - when start key is equal to the stop key, include the key in scan result, - if (stop != null && Bytes.memcmp(row.getKey(), stop) >= 0 - && Bytes.memcmp(start, stop) != 0) { + // - start_key is inclusive, stop key is exclusive + // - when start key is equal to the stop key, + // include the key in scan result + // - if stop key is empty, scan till the end + if (stop != null && stop.length > 0 && + Bytes.memcmp(row.getKey(), stop) >= 0 && + Bytes.memcmp(start, stop) != 0) { continue; } if (pattern != null) { @@ -1008,7 +1011,7 @@ public Deferred>> answer( continue; } } - + // loop on the column families final ArrayList kvs = new ArrayList(row.getValue().size()); From 55b13bd2ff460ab719317465d57fa0a01a77d1b4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 19:25:28 -0700 Subject: [PATCH 150/826] Modify the Fsck utility to properly handle salted keys. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/tools/Fsck.java | 60 +++++++++++++--------------------- test/tools/TestFsck.java | 60 ++++++++++++++-------------------- test/tools/TestFsckSalted.java | 25 ++++++++++++++ 4 files changed, 73 insertions(+), 73 deletions(-) create mode 100644 test/tools/TestFsckSalted.java diff --git a/Makefile.am b/Makefile.am index 9ad3da7732..adb81c5593 100644 --- a/Makefile.am +++ b/Makefile.am @@ -203,6 +203,7 @@ test_SRC := \ test/tools/TestDumpSeries.java \ test/tools/TestCliUtils.java \ test/tools/TestFsck.java \ + test/tools/TestFsckSalted.java \ test/tools/TestTextImporter.java \ test/tools/TestUID.java \ test/tree/TestBranch.java \ diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 1b1c619ee7..7e0aa64e6e 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -112,8 +112,8 @@ final class Fsck { final AtomicLong vle_fixed = new AtomicLong(); /** Length of the metric + timestamp for key validation */ - private static int key_prefix_length = TSDB.metrics_width() + - Const.TIMESTAMP_BYTES; + private static int key_prefix_length = Const.SALT_WIDTH() + + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; /** Length of a tagk + tagv pair for key validation */ private static int key_tags_length = TSDB.tagk_width() + TSDB.tagv_width(); @@ -140,30 +140,25 @@ public Fsck(final TSDB tsdb, final FsckOptions options) { public void runFullTable() throws Exception { LOG.info("Starting full table scan"); final long start_time = System.currentTimeMillis() / 1000; - final long max_id = CliUtils.getMaxMetricID(tsdb); - final int workers = options.threads() > 0 ? options.threads() : Runtime.getRuntime().availableProcessors() * 2; - final double quotient = (double)max_id / (double)workers; - LOG.info("Max metric ID is [" + max_id + "]"); - LOG.info("Spooling up [" + workers + "] worker threads"); - long index = 1; - final Thread[] threads = new Thread[workers]; - for (int i = 0; i < workers; i++) { - threads[i] = new FsckWorker(index, quotient, i); - threads[i].setName("Fsck #" + i); - threads[i].start(); - index += quotient; - if (index < max_id) { - index++; - } + + final List scanners = CliUtils.getDataTableScanners(tsdb, workers); + LOG.info("Spooling up [" + scanners.size() + "] worker threads"); + final List threads = new ArrayList(scanners.size()); + int i = 0; + for (final Scanner scanner : scanners) { + final FsckWorker worker = new FsckWorker(scanner, i++); + worker.setName("Fsck #" + i); + worker.start(); + threads.add(worker); } final Thread reporter = new ProgressReporter(); reporter.start(); - for (int i = 0; i < workers; i++) { - threads[i].join(); - LOG.info("Thread [" + i + "] Finished"); + for (final Thread thread : threads) { + thread.join(); + LOG.info("Thread [" + thread + "] Finished"); } reporter.interrupt(); @@ -224,15 +219,12 @@ long correctable() { * performs the actual FSCK process. */ final class FsckWorker extends Thread { - /** Optional value of the first metric this worker should start on, should - * be >0 */ - final long start_id; - /** Value of the metric this worker should end on */ - final long end_id; /** Id of the thread this worker belongs to */ final int thread_id; /** Optional query to execute instead of a full table scan */ final Query query; + /** The scanner to use for iterating over a chunk of the table */ + final Scanner scanner; /** Set of TSUIDs this worker has seen. Used to avoid UID resolution for * previously processed row keys */ final Set tsuids = new HashSet(); @@ -248,13 +240,11 @@ final class FsckWorker extends Thread { /** * Ctor for running a worker on a chunk of the data table - * @param start_id The first metric this worker should start on - * @param quotient How many metrics the worker should cover + * @param scanner The scanner to use for iterationg * @param thread_id Id of the thread this worker is assigned for logging */ - FsckWorker(final long start_id, final double quotient, final int thread_id) { - this.start_id = start_id; - this.end_id = start_id + (long) quotient + 1; // teensy bit of overlap + FsckWorker(final Scanner scanner, final int thread_id) { + this.scanner = scanner; this.thread_id = thread_id; query = null; } @@ -266,10 +256,9 @@ final class FsckWorker extends Thread { * @param thread_id Id of the thread this worker is assigned for logging */ FsckWorker(final Query query, final int thread_id) { - start_id = 0; - end_id = 0; this.thread_id = thread_id; this.query = query; + scanner = Internal.getScanner(query); } /** @@ -279,9 +268,6 @@ final class FsckWorker extends Thread { * appropriate. */ public void run() { - final Scanner scanner = query != null ? Internal.getScanner(query) : - CliUtils.getDataTableScanner(tsdb, start_id, end_id); - // store every data point for the row in here final TreeMap> datapoints = new TreeMap>(); @@ -348,7 +334,7 @@ private void fsckRow(final ArrayList row, } final long base_time = Bytes.getUnsignedInt(row.get(0).key(), - TSDB.metrics_width()); + Const.SALT_WIDTH() + TSDB.metrics_width()); for (final KeyValue kv : row) { kvs_processed.getAndIncrement(); @@ -512,7 +498,7 @@ private boolean fsckKey(final byte[] key) throws Exception { } // Process the time series ID by resolving the UIDs to names if we haven't - // already seen this particular TSUID + // already seen this particular TSUID. Note that getTSUID accounts for salt final byte[] tsuid = UniqueId.getTSUIDFromKey(key, TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (!tsuids.contains(tsuid)) { diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 31b078b1ee..d9c95d098b 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -26,7 +26,6 @@ import java.util.List; import net.opentsdb.core.Query; -import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; @@ -36,7 +35,6 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; @@ -57,17 +55,16 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, +@PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, GetRequest.class, PutRequest.class, KeyValue.class, Fsck.class, - FsckOptions.class, Scanner.class, DeleteRequest.class, Annotation.class, - RowKey.class, Tags.class}) -public final class TestFsck { - private final static byte[] ROW = - MockBase.stringToBytes("00000150E22700000001000001"); - private final static byte[] ROW2 = - MockBase.stringToBytes("00000150E23510000001000001"); - private final static byte[] ROW3 = - MockBase.stringToBytes("00000150E24320000001000001"); + FsckOptions.class, Scanner.class, Annotation.class, Tags.class }) +public class TestFsck { + protected byte[] GLOBAL_ROW = + new byte[] {0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}; + protected byte[] ROW = MockBase.stringToBytes("00000150E22700000001000001"); + protected byte[] ROW2 = MockBase.stringToBytes("00000150E23510000001000001"); + protected byte[] ROW3 = MockBase.stringToBytes("00000150E24320000001000001"); + protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; private Config config; private TSDB tsdb = null; private HBaseClient client = mock(HBaseClient.class); @@ -119,7 +116,8 @@ public void before() throws Exception { // mock UniqueId when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getName(new byte[] { 0, 0, 1 })).thenReturn("sys.cpu.user"); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")); when(metrics.getId("sys.cpu.system")) .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); @@ -136,19 +134,11 @@ public void before() throws Exception { when(tag_values.getOrCreateId("web02")).thenReturn(new byte[] { 0, 0, 2 }); when(tag_values.getId("web03")) .thenThrow(new NoSuchUniqueName("web03", "metric")); - - PowerMockito.mockStatic(RowKey.class); - when(RowKey.metricNameAsync((TSDB)any(), (byte[])any())) - .thenReturn(Deferred.fromResult("sys.cpu.user")); PowerMockito.mockStatic(Tags.class); when(Tags.resolveIds((TSDB)any(), (ArrayList)any())) .thenReturn(null); // don't care - -// PowerMockito.mockStatic(Thread.class); -// PowerMockito.doNothing().when(Thread.class); -// Thread.sleep(anyLong()); - + when(metrics.width()).thenReturn((short)3); when(tag_names.width()).thenReturn((short)3); when(tag_values.width()).thenReturn((short)3); @@ -156,10 +146,7 @@ public void before() throws Exception { @Test public void globalAnnotation() throws Exception { - // make sure we don't catch this during a query. We should start with - // the first metric (0, 0, 1) whereas globals are on metric (0, 0, 0). - storage.addColumn(new byte[] {0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}, - new byte[] {1, 0, 0}, "{}".getBytes()); + storage.addColumn(GLOBAL_ROW, new byte[] {1, 0, 0}, "{}".getBytes()); final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); @@ -204,6 +191,7 @@ public void noErrorsMultipleRows() throws Exception { storage.addColumn(ROW2, qual2, val2); storage.addColumn(ROW3, qual1, val1); storage.addColumn(ROW3, qual2, val2); +storage.dumpToSystemOut(); final Fsck fsck = new Fsck(tsdb, options); fsck.runFullTable(); assertEquals(6, fsck.kvs_processed.get()); @@ -410,7 +398,7 @@ public void singleValueCompactedFix() throws Exception { @Test public void noSuchMetricId() throws Exception { when(options.fix()).thenReturn(true); - when(RowKey.metricNameAsync((TSDB)any(), (byte[])any())) + when(metrics.getNameAsync((byte[])any())) .thenThrow(new NoSuchUniqueId("metric", new byte[] { 0, 0, 1 })); final byte[] qual1 = { 0x00, 0x07 }; @@ -433,7 +421,7 @@ public void noSuchMetricId() throws Exception { public void noSuchMetricIdFix() throws Exception { when(options.fix()).thenReturn(true); when(options.deleteOrphans()).thenReturn(true); - when(RowKey.metricNameAsync((TSDB)any(), (byte[])any())) + when(metrics.getNameAsync((byte[])any())) .thenThrow(new NoSuchUniqueId("metric", new byte[] { 0, 0, 1 })); final byte[] qual1 = { 0x00, 0x07 }; @@ -506,11 +494,11 @@ public void badRowKey() throws Exception { final byte[] val1 = Bytes.fromLong(4L); final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); - final byte[] bad_key = { 0x00, 0x00, 0x01 }; + storage.addColumn(ROW, qual1, val1); storage.addColumn(ROW, qual2, val2); - storage.addColumn(bad_key, qual1, val1); - storage.addColumn(bad_key, qual2, val2); + storage.addColumn(BAD_KEY, qual1, val1); + storage.addColumn(BAD_KEY, qual2, val2); storage.addColumn(ROW3, qual1, val1); storage.addColumn(ROW3, qual2, val2); @@ -520,7 +508,7 @@ public void badRowKey() throws Exception { assertEquals(3, fsck.rows_processed.get()); assertEquals(1, fsck.totalErrors()); assertEquals(2, storage.numColumns(ROW)); - assertEquals(2, storage.numColumns(bad_key)); + assertEquals(2, storage.numColumns(BAD_KEY)); assertEquals(2, storage.numColumns(ROW3)); } @@ -533,11 +521,11 @@ public void badRowKeyFix() throws Exception { final byte[] val1 = Bytes.fromLong(4L); final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); - final byte[] bad_key = { 0x00, 0x00, 0x01 }; + storage.addColumn(ROW, qual1, val1); storage.addColumn(ROW, qual2, val2); - storage.addColumn(bad_key, qual1, val1); - storage.addColumn(bad_key, qual2, val2); + storage.addColumn(BAD_KEY, qual1, val1); + storage.addColumn(BAD_KEY, qual2, val2); storage.addColumn(ROW3, qual1, val1); storage.addColumn(ROW3, qual2, val2); @@ -547,7 +535,7 @@ public void badRowKeyFix() throws Exception { assertEquals(3, fsck.rows_processed.get()); assertEquals(1, fsck.totalErrors()); assertEquals(2, storage.numColumns(ROW)); - assertEquals(-1, storage.numColumns(bad_key)); + assertEquals(-1, storage.numColumns(BAD_KEY)); assertEquals(2, storage.numColumns(ROW3)); } diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java new file mode 100644 index 0000000000..226ca4fedb --- /dev/null +++ b/test/tools/TestFsckSalted.java @@ -0,0 +1,25 @@ +package net.opentsdb.tools; + +import org.junit.Before; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; + +import net.opentsdb.core.Const; +import net.opentsdb.storage.MockBase; + +@PrepareForTest({ Const.class }) +public class TestFsckSalted extends TestFsck { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + + GLOBAL_ROW = new byte[] {0, 0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}; + ROW = MockBase.stringToBytes("0000000150E22700000001000001"); + ROW2 = MockBase.stringToBytes("0100000150E23510000001000001"); + ROW3 = MockBase.stringToBytes("0100000150E24320000001000001"); + BAD_KEY = new byte[] { 0x01, 0x00, 0x00, 0x01 }; + } +} From aacb22b6ac3dd568947df9d5d4decc4b3eb5d3c5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 19:59:57 -0700 Subject: [PATCH 151/826] Add Internal.getScanners() to get all of the salt scanners from a query and use that in the FSCK class for salting support. Signed-off-by: Chris Larsen --- src/core/Internal.java | 16 ++++++++++++++++ src/tools/Fsck.java | 28 ++++++++++++++-------------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index f57ed5dc8e..072e2072ec 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -86,6 +86,22 @@ public static Scanner getScanner(final Query query) { return ((TsdbQuery) query).getScanner(); } + /** Returns a set of scanners, one for each bucket if salted, or one scanner + * if salting is disabled. + * @see TsdbQuery#getScanner() */ + public static List getScanners(final Query query) { + final List scanners = new ArrayList( + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1); + if (Const.SALT_WIDTH() > 0) { + for (int i = 0; i < Const.SALT_BUCKETS(); i++) { + scanners.add(((TsdbQuery) query).getScanner(i)); + } + } else { + scanners.add(((TsdbQuery) query).getScanner()); + } + return scanners; + } + /** @see RowKey#metricName */ public static String metricName(final TSDB tsdb, final byte[] id) { return RowKey.metricName(tsdb, id); diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 7e0aa64e6e..830c5c2d25 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -183,8 +183,20 @@ public void runQueries(final List queries) throws Exception { reporter.start(); for (final Query query : queries) { - final FsckWorker worker = new FsckWorker(query, 0); - worker.run(); + final List scanners = Internal.getScanners(query); + final List threads = new ArrayList(scanners.size()); + int i = 0; + for (final Scanner scanner : scanners) { + final FsckWorker worker = new FsckWorker(scanner, i++); + worker.setName("Fsck #" + i); + worker.start(); + threads.add(worker); + } + + for (final Thread thread : threads) { + thread.join(); + LOG.info("Thread [" + thread + "] Finished"); + } } reporter.interrupt(); @@ -249,18 +261,6 @@ final class FsckWorker extends Thread { query = null; } - /** - * Ctor for running an FSCK over a specific query, scanning only rows that - * match the filter. - * @param query The query to execute - * @param thread_id Id of the thread this worker is assigned for logging - */ - FsckWorker(final Query query, final int thread_id) { - this.thread_id = thread_id; - this.query = query; - scanner = Internal.getScanner(query); - } - /** * Determines the type of scanner to use, i.e. a specific query scanner or * for a portion of the whole table. It then performs the actual scan, From efde901eb77a11b54a53336091443dc2f38f192d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 2 May 2015 21:55:25 -0700 Subject: [PATCH 152/826] Modify the MetaSync utility to account for salting and use the new scanner code. Signed-off-by: Chris Larsen --- src/tools/MetaSync.java | 60 +++++++++------------------------------ src/tools/UidManager.java | 41 +++++++++++--------------- 2 files changed, 31 insertions(+), 70 deletions(-) diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index 75bf1b2957..2303650a0e 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.tools; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,7 +27,6 @@ import net.opentsdb.uid.UniqueId.UniqueIdType; import org.hbase.async.Bytes; -import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; import org.slf4j.Logger; @@ -54,13 +52,7 @@ final class MetaSync extends Thread { /** TSDB to use for storage access */ final TSDB tsdb; - - /** The ID to start the sync with for this thread */ - final long start_id; - - /** The end of the ID block to work on */ - final long end_id; - + /** A shared list of TSUIDs that have been processed by this or other * threads. It stores hashes instead of the bytes or strings to save * on space */ @@ -78,22 +70,27 @@ final class MetaSync extends Thread { /** Diagnostic ID for this thread */ final int thread_id; + /** The scanner for this worker */ + final Scanner scanner; + /** * Constructor that sets local variables * @param tsdb The TSDB to process with - * @param start_id The starting ID of the block we'll work on - * @param quotient The total number of IDs in our block + * @param scanner The scanner to use for this worker + * @param processed_tsuids TSUIDs that have been processed already + * @param metric_uids List of metric UIDs + * @param tagk_uids List of tag key UIDs + * @param tagv_uids List of tag value UIDs * @param thread_id The ID of this thread (starts at 0) */ - public MetaSync(final TSDB tsdb, final long start_id, final double quotient, + public MetaSync(final TSDB tsdb, final Scanner scanner, final Set processed_tsuids, ConcurrentHashMap metric_uids, ConcurrentHashMap tagk_uids, ConcurrentHashMap tagv_uids, final int thread_id) { this.tsdb = tsdb; - this.start_id = start_id; - this.end_id = start_id + (long) quotient + 1; // teensy bit of overlap + this.scanner = scanner; this.processed_tsuids = processed_tsuids; this.metric_uids = metric_uids; this.tagk_uids = tagk_uids; @@ -342,17 +339,9 @@ public Deferred call(final Boolean exists) throws Exception { final class MetaScanner implements Callback>> { - private final Scanner scanner; private byte[] last_tsuid = null; private String tsuid_string = ""; - - /** - * Default constructor that initializes the data row scanner - */ - public MetaScanner() { - scanner = getScanner(); - } - + /** * Fetches the next set of rows from the scanner and adds this class as * a callback @@ -396,14 +385,14 @@ public Object call(ArrayList> rows) // row for use as the "created" time. Depending on speed we could // parse datapoints, but for now the hourly row time is enough final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), - TSDB.metrics_width()); + Const.SALT_WIDTH() + TSDB.metrics_width()); LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + " row timestamp: " + timestamp); // now process the UID metric meta data final byte[] metric_uid_bytes = - Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + Arrays.copyOfRange(tsuid, 0, Const.SALT_WIDTH() + TSDB.metrics_width()); final String metric_uid = UniqueId.uidToString(metric_uid_bytes); Long last_get = metric_uids.get(metric_uid); @@ -558,26 +547,5 @@ public Object call(Exception e) throws Exception { throw new RuntimeException("[" + thread_id + "] Scanner exception", e); } } - - /** - * Returns a scanner set to scan the range configured for this thread - * @return A scanner on the "t" CF configured for the specified range - * @throws HBaseException if something goes boom - */ - private Scanner getScanner() throws HBaseException { - final short metric_width = TSDB.metrics_width(); - final byte[] start_row = - Arrays.copyOfRange(Bytes.fromLong(start_id), 8 - metric_width, 8); - final byte[] end_row = - Arrays.copyOfRange(Bytes.fromLong(end_id), 8 - metric_width, 8); - - LOG.debug("[" + thread_id + "] Start row: " + UniqueId.uidToString(start_row)); - LOG.debug("[" + thread_id + "] End row: " + UniqueId.uidToString(end_row)); - final Scanner scanner = tsdb.getClient().newScanner(tsdb.dataTable()); - scanner.setStartKey(start_row); - scanner.setStopKey(end_row); - scanner.setFamily("t".getBytes(Charset.forName("ISO-8859-1"))); - return scanner; - } } diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 3e8a001ef6..c71b56d0d2 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -24,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; @@ -955,11 +955,9 @@ private static int extactLookupName(final HBaseClient client, */ private static int metaSync(final TSDB tsdb) throws Exception { final long start_time = System.currentTimeMillis() / 1000; - final long max_id = CliUtils.getMaxMetricID(tsdb); - + // now figure out how many IDs to divy up between the workers final int workers = Runtime.getRuntime().availableProcessors() * 2; - final double quotient = (double)max_id / (double)workers; final Set processed_tsuids = Collections.synchronizedSet(new HashSet()); final ConcurrentHashMap metric_uids = @@ -968,27 +966,22 @@ private static int metaSync(final TSDB tsdb) throws Exception { new ConcurrentHashMap(); final ConcurrentHashMap tagv_uids = new ConcurrentHashMap(); - - long index = 1; - - LOG.info("Max metric ID is [" + max_id + "]"); - LOG.info("Spooling up [" + workers + "] worker threads"); - final Thread[] threads = new Thread[workers]; - for (int i = 0; i < workers; i++) { - threads[i] = new MetaSync(tsdb, index, quotient, processed_tsuids, - metric_uids, tagk_uids, tagv_uids, i); - threads[i].setName("MetaSync # " + i); - threads[i].start(); - index += quotient; - if (index < max_id) { - index++; - } + + final List scanners = CliUtils.getDataTableScanners(tsdb, workers); + LOG.info("Spooling up [" + scanners.size() + "] worker threads"); + final List threads = new ArrayList(scanners.size()); + int i = 0; + for (final Scanner scanner : scanners) { + final MetaSync worker = new MetaSync(tsdb, scanner, processed_tsuids, + metric_uids, tagk_uids, tagv_uids, i++); + worker.setName("Sync #" + i); + worker.start(); + threads.add(worker); } - - // wait till we're all done - for (int i = 0; i < workers; i++) { - threads[i].join(); - LOG.info("[" + i + "] Finished"); + + for (final Thread thread : threads) { + thread.join(); + LOG.info("Thread [" + thread + "] Finished"); } // make sure buffered data is flushed to storage before exiting From 5b79d6610f438e026a1c0a9563aa1abb771f2dd5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 27 Apr 2015 18:44:49 -0700 Subject: [PATCH 153/826] Modify the logging output of the FSCK command to dump the qualifier and value for columns that we keep during duplicates resolution. This is towards a fix for #436. Also add a couple of UTs to make sure we handle the case where two (or more) compacted columns exist in the row but one has an extra DP. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 11 ++++++-- test/tools/TestFsck.java | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 830c5c2d25..80e9cffb60 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -695,13 +695,20 @@ private void fsckDataPoints(final Map> datapoints) TSDB.FAMILY(), new_qualifier, new_value); // it's *possible* that the hash of our new compacted qualifier is in - // the delete list so double check. + // the delete list so double check before we delete everything if (unique_columns.containsKey(new_qualifier)) { - LOG.info("Our qualifier was in the delete list!!!"); if (Bytes.memcmp(unique_columns.get(new_qualifier), new_value) != 0) { + LOG.info("Overwriting column " + Bytes.pretty(new_qualifier) + + " with new value " + + Bytes.pretty(put.value()) + ". Old value " + + Bytes.pretty(unique_columns.get(new_qualifier))); // Important: Make sure to wait for the write to complete before // proceeding with the deletes. tsdb.getClient().put(put).joinUninterruptibly(); + } else { + LOG.debug("Column " + Bytes.pretty(new_qualifier) + + " had the same value after repair: " + + Bytes.pretty(put.value()) + ". Skipping deletion."); } unique_columns.remove(new_qualifier); } else { diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index d9c95d098b..4ed79b94a9 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -2478,6 +2478,65 @@ public void twoCompactedColumnsWSameTSFix() throws Exception { MockBase.concatByteArrays(qual1, qual2, qual4))); } + @Test + public void twoCompactedColumnsOneWExtraDP() throws Exception { + when(options.resolveDupes()).thenReturn(true); + final byte[] qual1 = { 0x0, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { 0x0, 0x20 }; + final byte[] val2 = { 5 }; + final byte[] qual3 = { 0x0, 0x30 }; + final byte[] val3 = { 7 }; + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2), + MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, + new byte[] { 0 }), storage.getColumn(ROW, + MockBase.concatByteArrays(qual1, qual2))); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, + new byte[] { 0 }), storage.getColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3))); + } + + @Test + public void twoCompactedColumnsOneWExtraDPFix() throws Exception { + when(options.fix()).thenReturn(true); + when(options.resolveDupes()).thenReturn(true); + final byte[] qual1 = { 0x0, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { 0x0, 0x20 }; + final byte[] val2 = { 5 }; + final byte[] qual3 = { 0x0, 0x30 }; + final byte[] val3 = { 7 }; + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2), + MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(2, fsck.duplicates.get()); + assertEquals(2, fsck.totalErrors()); + assertEquals(2, fsck.correctable()); + assertNull(storage.getColumn(ROW, MockBase.concatByteArrays(qual1, qual2))); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, + new byte[] { 0 }), storage.getColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3))); + } + @Test public void twoCompactedColumnsWSameTSLWW() throws Exception { when(options.lastWriteWins()).thenReturn(true); From 65ea6b8dcde54f91a91587e1d59256b4a77d9870 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 3 May 2015 12:34:48 -0700 Subject: [PATCH 154/826] Another stab at #436. Turns out regular compacted columns without issues were triggering the "Our qualifier was in the delete list!" message even if there weren't any other issues in the row. That's because the column is split and re-compacted then compared. Now I only log that message if there were duplicates. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 45 ++++++++++++++++----- test/tools/TestFsck.java | 86 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 80e9cffb60..c722845819 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -32,6 +32,8 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import com.stumbleupon.async.Deferred; + import net.opentsdb.core.Const; import net.opentsdb.core.IllegalDataException; import net.opentsdb.core.Internal; @@ -698,17 +700,30 @@ private void fsckDataPoints(final Map> datapoints) // the delete list so double check before we delete everything if (unique_columns.containsKey(new_qualifier)) { if (Bytes.memcmp(unique_columns.get(new_qualifier), new_value) != 0) { - LOG.info("Overwriting column " + Bytes.pretty(new_qualifier) + - " with new value " + - Bytes.pretty(put.value()) + ". Old value " + - Bytes.pretty(unique_columns.get(new_qualifier))); + final StringBuilder buf = new StringBuilder(); + buf.append("Overwriting compacted column with new value: ") + .append("\n row key: (") + .append(UniqueId.uidToString(key)) + .append(")\n qualifier: ") + .append(Bytes.pretty(new_qualifier)) + .append("\n value: ") + .append(Bytes.pretty(new_value)); + LOG.info(buf.toString()); // Important: Make sure to wait for the write to complete before // proceeding with the deletes. tsdb.getClient().put(put).joinUninterruptibly(); - } else { - LOG.debug("Column " + Bytes.pretty(new_qualifier) + - " had the same value after repair: " + - Bytes.pretty(put.value()) + ". Skipping deletion."); + } else if (has_duplicates) { + if (LOG.isDebugEnabled()) { + final StringBuilder buf = new StringBuilder(); + buf.append("Re-compacted column is the same as the existing column: ") + .append("\n row key: (") + .append(UniqueId.uidToString(key)) + .append(")\n qualifier: ") + .append(Bytes.pretty(new_qualifier)) + .append("\n value: ") + .append(Bytes.pretty(new_value)); + LOG.debug(buf.toString()); + } } unique_columns.remove(new_qualifier); } else { @@ -717,11 +732,23 @@ private void fsckDataPoints(final Map> datapoints) tsdb.getClient().put(put).joinUninterruptibly(); } + final List> deletes = + new ArrayList>(unique_columns.size()); for (byte[] qualifier : unique_columns.keySet()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key, TSDB.FAMILY(), qualifier); - tsdb.getClient().delete(delete); + if (LOG.isDebugEnabled()) { + final StringBuilder buf = new StringBuilder(); + buf.append("Deleting column: ") + .append("\n row key: (") + .append(UniqueId.uidToString(key)) + .append(")\n qualifier: ") + .append(Bytes.pretty(qualifier)); + LOG.debug(buf.toString()); + } + deletes.add(tsdb.getClient().delete(delete)); } + Deferred.group(deletes).joinUninterruptibly(); duplicates_fixed.getAndAdd(duplicates_fixed_comp.longValue()); duplicates_fixed_comp.set(0); } diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 4ed79b94a9..6fd7de5a17 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -3203,6 +3203,92 @@ public void compactedAndSingleMixedWSameTSLWWFix() throws Exception { assertNull(storage.getColumn(ROW, qual4)); } + @Test + public void compactedAndSinglesDeleteFailed() throws Exception { + when(options.resolveDupes()).thenReturn(true); + final byte[] qual1 = { 0x0, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { 0x0, 0x20 }; + final byte[] val2 = { 5 }; + final byte[] qual3 = { 0x0, 0x30 }; + final byte[] val3 = { 7 }; + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + storage.addColumn(ROW, qual3, val3); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(4, fsck.kvs_processed.get()); + assertEquals(3, fsck.duplicates.get()); + assertEquals(3, fsck.totalErrors()); + assertEquals(3, fsck.correctable()); + assertArrayEquals(val1, storage.getColumn(ROW, qual1)); + assertArrayEquals(val2, storage.getColumn(ROW, qual2)); + assertArrayEquals(val3, storage.getColumn(ROW, qual3)); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, + new byte[] { 0 }), storage.getColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3))); + } + + @Test + public void compactedAndSinglesDeleteFailedFix() throws Exception { + when(options.fix()).thenReturn(true); + when(options.resolveDupes()).thenReturn(true); + final byte[] qual1 = { 0x0, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { 0x0, 0x20 }; + final byte[] val2 = { 5 }; + final byte[] qual3 = { 0x0, 0x30 }; + final byte[] val3 = { 7 }; + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + storage.addColumn(ROW, qual3, val3); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(4, fsck.kvs_processed.get()); + assertEquals(3, fsck.duplicates.get()); + assertEquals(3, fsck.totalErrors()); + assertEquals(3, fsck.correctable()); + assertNull(storage.getColumn(ROW, qual1)); + assertNull(storage.getColumn(ROW, qual2)); + assertNull(storage.getColumn(ROW, qual3)); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, + new byte[] { 0 }), storage.getColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3))); + } + + @Test + public void compactedNoErrors() throws Exception { + when(options.fix()).thenReturn(true); + when(options.resolveDupes()).thenReturn(true); + final byte[] qual1 = { 0x0, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { 0x0, 0x20 }; + final byte[] val2 = { 5 }; + final byte[] qual3 = { 0x0, 0x30 }; + final byte[] val3 = { 7 }; + storage.addColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3), + MockBase.concatByteArrays(val1, val2, val3, new byte[] { 0 })); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.duplicates.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, + new byte[] { 0 }), storage.getColumn(ROW, + MockBase.concatByteArrays(qual1, qual2, qual3))); + } + @Test public void tripleCompactedColumnsWSameTS() throws Exception { when(options.resolveDupes()).thenReturn(true); From a260119c9a2fe7c6bd30b2c2bc34e40e81e1bbd6 Mon Sep 17 00:00:00 2001 From: sidhhu Date: Sun, 3 May 2015 12:37:05 -0700 Subject: [PATCH 155/826] Fix Internal.inMilliseconds() where the offset was a byte instead of an integer, preventing it from functioning properly across columns with more than 127 bytes in their qualifier. Signed-off-by: Chris Larsen --- src/core/Internal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index 072e2072ec..54e2f7306d 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -541,7 +541,7 @@ public static byte[] fixFloatingPointValue(final byte flags, * @since 2.0 */ public static boolean inMilliseconds(final byte[] qualifier, - final byte offset) { + final int offset) { return inMilliseconds(qualifier[offset]); } From b1c32620cd008572fcf9e7ed8c9701c22eac85b2 Mon Sep 17 00:00:00 2001 From: Lois BURG Date: Tue, 21 Apr 2015 14:31:59 +0200 Subject: [PATCH 156/826] Fix missing info from /api/uid/tsmeta - refs #498 Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 19 ++++++++----------- test/meta/TestTSMeta.java | 9 +++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 825270a5f7..9abc89e463 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -405,24 +405,21 @@ public static Deferred parseFromColumn(final TSDB tsdb, if (column.value() == null || column.value().length < 1) { throw new IllegalArgumentException("Empty column value"); } - - final TSMeta meta = JSON.parseToObject(column.value(), TSMeta.class); + + final TSMeta parsed_meta = JSON.parseToObject(column.value(), TSMeta.class); // fix in case the tsuid is missing - if (meta.tsuid == null || meta.tsuid.isEmpty()) { - meta.tsuid = UniqueId.uidToString(column.key()); + if (parsed_meta.tsuid == null || parsed_meta.tsuid.isEmpty()) { + parsed_meta.tsuid = UniqueId.uidToString(column.key()); } + + Deferred meta = getFromStorage(tsdb, UniqueId.stringToUid(parsed_meta.tsuid)); if (!load_uidmetas) { - return Deferred.fromResult(meta); + return meta; } - final LoadUIDs deferred = new LoadUIDs(tsdb, meta.tsuid); - try { - return deferred.call(meta); - } catch (Exception e) { - throw new RuntimeException(e); - } + return meta.addCallbackDeferring(new LoadUIDs(tsdb, parsed_meta.tsuid)); } /** diff --git a/test/meta/TestTSMeta.java b/test/meta/TestTSMeta.java index 1dc7db3989..95c79da54d 100644 --- a/test/meta/TestTSMeta.java +++ b/test/meta/TestTSMeta.java @@ -128,6 +128,15 @@ public void before() throws Exception { NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); + + storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, + NAME_FAMILY, + "ts_meta".getBytes(MockBase.ASCII()), + ("{\"tsuid\":\"000001000001000002\",\"" + + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") + .getBytes(MockBase.ASCII())); } @Test From 0a22eab4fe499728b9af65fecff33fb0d53b92e6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 16 May 2015 18:12:42 -0700 Subject: [PATCH 157/826] Fix QueryRPC post merge upstream. Signed-off-by: Chris Larsen --- src/tsd/QueryRpc.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 71b4001371..9a0c9db370 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -124,7 +124,7 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { final QueryStats query_stats = new QueryStats(query.getRemoteAddress(), data_query); data_query.setQueryStats(query_stats); - } + final int nqueries = data_query.getQueries().size(); final ArrayList results = new ArrayList(nqueries); final List globals = new ArrayList(); @@ -775,4 +775,4 @@ public void setTSUIDs(final List tsuids) { this.tsuids = tsuids; } } -} +} \ No newline at end of file From c472d2794b2a819144a5ae7b7b73d7c849bb5880 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 17 May 2015 22:20:39 -0700 Subject: [PATCH 158/826] Bump AsyncHBase to the 1.7.0 snapshot. Modify the TSDB to use the new AsyncHBase config file, overriding the ZK and ZK base dir settings and allowing for security settings to be passed in. Signed-off-by: Chris Larsen --- src/utils/Config.java | 5 +++++ third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 | 1 + third_party/hbase/include.mk | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 diff --git a/src/utils/Config.java b/src/utils/Config.java index cab00edcf3..4b1905c8f2 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -151,6 +151,11 @@ public Config(final Config parent) { setDefaults(); } + /** @return The file that generated this config. May be null */ + public String configLocation() { + return config_location; + } + /** @return the auto_metric value */ public boolean auto_metric() { return auto_metric; diff --git a/third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 new file mode 100644 index 0000000000..4e13899eda --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 @@ -0,0 +1 @@ +78c317f4457b6add9671bc2a30df7bd1 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index eb7ca564ab..a173db7fd3 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.6.0 +ASYNCHBASE_VERSION := 1.7.0-20150517.200244-1 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://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.0-SNAPSHOT/ $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From 8665d2493d5271aefc679c8d22cd4d155a7efe98 Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Mon, 18 May 2015 21:48:06 -0700 Subject: [PATCH 159/826] Relax the pgrep regex to correctly find and kill the java process. --- build-aux/rpm/init.d/opentsdb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/rpm/init.d/opentsdb b/build-aux/rpm/init.d/opentsdb index d721d483ba..5f4ee1d8d5 100644 --- a/build-aux/rpm/init.d/opentsdb +++ b/build-aux/rpm/init.d/opentsdb @@ -139,7 +139,7 @@ rh_status_q() { } findproc() { - pgrep -f "^java .* net.opentsdb.tools.TSDMain .*${NAME}" + pgrep -f "java .* net.opentsdb.tools.TSDMain .*${NAME}" } case "$1" in From 460da3c92b39035b6df0e8d3fbb521609d7e7f4e Mon Sep 17 00:00:00 2001 From: Pradeep Chhetri Date: Thu, 21 May 2015 01:54:36 +0530 Subject: [PATCH 160/826] Server should just exit whenever there is some unrecognized option --- src/tools/CliOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index aeccb1bb36..49dc34df34 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -77,7 +77,7 @@ static String[] parse(final ArgP argp, String[] args) { args = argp.parse(args); } catch (IllegalArgumentException e) { System.err.println("Invalid usage. " + e.getMessage()); - return null; + System.exit(2); } honorVerboseFlag(argp); return args; From 1e5cc8f8a9268a0e1e36690af6590bb809abb615 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 24 May 2015 12:21:53 -0700 Subject: [PATCH 161/826] Modify the TSDB ctor to use the new AsyncHBase config object. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 26 ++++++++++++++++++++++---- test/core/TestBatchedDataPoints.java | 4 +--- test/meta/TestTSMeta.java | 8 ++------ test/meta/TestTSUIDQuery.java | 8 ++------ test/meta/TestUIDMeta.java | 5 +---- test/search/TestTimeSeriesLookup.java | 6 +----- test/tools/TestDumpSeries.java | 6 +----- test/tools/TestFsck.java | 5 +---- test/tools/TestTextImporter.java | 4 +--- test/tree/TestBranch.java | 8 ++------ test/tree/TestLeaf.java | 6 +----- test/tree/TestTree.java | 4 ---- test/tree/TestTreeBuilder.java | 5 +---- test/tree/TestTreeRule.java | 6 +----- test/tsd/TestAnnotationRpc.java | 8 ++------ test/tsd/TestRpcHandler.java | 6 +----- test/tsd/TestTreeRpc.java | 6 +----- test/tsd/TestUniqueIdRpc.java | 8 ++------ 18 files changed, 43 insertions(+), 86 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 64249578e1..cf696485a1 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.core; +import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -118,7 +119,26 @@ public final class TSDB { */ public TSDB(final HBaseClient client, final Config config) { this.config = config; - this.client = client; + if (client == null) { + final org.hbase.async.Config async_config; + if (config.configLocation() != null && !config.configLocation().isEmpty()) { + try { + async_config = new org.hbase.async.Config(config.configLocation()); + } catch (final IOException e) { + throw new RuntimeException("Failed to read the config file: " + + config.configLocation(), e); + } + } else { + async_config = new org.hbase.async.Config(); + } + async_config.overrideConfig("asynchbase.zk.base_path", + config.getString("tsd.storage.hbase.zk_basedir")); + async_config.overrideConfig("asynchbase.zk.quorum", + config.getString("tsd.storage.hbase.zk_quorum")); + this.client = new HBaseClient(async_config); + } else { + this.client = client; + } this.client.setFlushInterval(config.getShort("tsd.storage.flush_interval")); table = config.getString("tsd.storage.hbase.data_table").getBytes(CHARSET); @@ -162,9 +182,7 @@ public TSDB(final HBaseClient client, final Config config) { * @since 2.0 */ public TSDB(final Config config) { - this(new HBaseClient(config.getString("tsd.storage.hbase.zk_quorum"), - config.getString("tsd.storage.hbase.zk_basedir")), - config); + this(null, config); } /** @return The data point column family name */ diff --git a/test/core/TestBatchedDataPoints.java b/test/core/TestBatchedDataPoints.java index 38e3c472c9..517fd86aae 100644 --- a/test/core/TestBatchedDataPoints.java +++ b/test/core/TestBatchedDataPoints.java @@ -55,10 +55,8 @@ public class TestBatchedDataPoints { @SuppressWarnings("unchecked") @Before public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); config = new Config(false); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); // replace the "real" field objects with mocks Field met = tsdb.getClass().getDeclaredField("metrics"); diff --git a/test/meta/TestTSMeta.java b/test/meta/TestTSMeta.java index 95c79da54d..72693bb99a 100644 --- a/test/meta/TestTSMeta.java +++ b/test/meta/TestTSMeta.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -42,7 +41,6 @@ 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; @@ -74,10 +72,8 @@ public void before() throws Exception { when(config.getString("tsd.storage.hbase.tree_table")).thenReturn("tsdb-tree"); when(config.enable_tsuid_incrementing()).thenReturn(true); when(config.enable_realtime_ts()).thenReturn(true); - - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); storage.addColumn(new byte[] { 0, 0, 1 }, diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index 0b7511b8b5..0da499bc19 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -13,7 +13,6 @@ package net.opentsdb.meta; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -38,7 +37,6 @@ 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; @@ -72,10 +70,8 @@ public void before() throws Exception { when(config.getString("tsd.storage.hbase.tree_table")).thenReturn("tsdb-tree"); when(config.enable_tsuid_incrementing()).thenReturn(true); when(config.enable_realtime_ts()).thenReturn(true); - - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, diff --git a/test/meta/TestUIDMeta.java b/test/meta/TestUIDMeta.java index 85e5c0a977..c628a5872b 100644 --- a/test/meta/TestUIDMeta.java +++ b/test/meta/TestUIDMeta.java @@ -15,7 +15,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -58,9 +57,7 @@ public final class TestUIDMeta { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index e302f09cfb..ad125f093a 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -15,7 +15,6 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -38,7 +37,6 @@ 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; @@ -74,10 +72,8 @@ public class TestTimeSeriesLookup { @Before public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); config = new Config(false); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); // replace the "real" field objects with mocks Field met = tsdb.getClass().getDeclaredField("metrics"); diff --git a/test/tools/TestDumpSeries.java b/test/tools/TestDumpSeries.java index d6366378fa..85100f7852 100644 --- a/test/tools/TestDumpSeries.java +++ b/test/tools/TestDumpSeries.java @@ -14,7 +14,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -42,7 +41,6 @@ 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; @@ -85,10 +83,8 @@ public class TestDumpSeries { @Before public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); config = new Config(false); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); storage.setFamily("t".getBytes(MockBase.ASCII())); diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 2d690c5d79..58cbd374b9 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -16,7 +16,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -81,10 +80,8 @@ public class TestFsck { @SuppressWarnings("unchecked") @Before public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); config = new Config(false); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); when(client.flush()).thenReturn(Deferred.fromResult(null)); storage = new MockBase(tsdb, client, true, true, true, true); diff --git a/test/tools/TestTextImporter.java b/test/tools/TestTextImporter.java index c17208150d..991562a748 100644 --- a/test/tools/TestTextImporter.java +++ b/test/tools/TestTextImporter.java @@ -94,10 +94,8 @@ public class TestTextImporter { @Before public void before() throws Exception { - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); config = new Config(false); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); storage.setFamily("t".getBytes(MockBase.ASCII())); diff --git a/test/tree/TestBranch.java b/test/tree/TestBranch.java index 22e2eec6e8..4aff7fc0d5 100644 --- a/test/tree/TestBranch.java +++ b/test/tree/TestBranch.java @@ -18,7 +18,6 @@ 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.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Field; @@ -39,7 +38,6 @@ import org.hbase.async.Scanner; 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; @@ -565,10 +563,8 @@ public static Branch buildTestBranch(final Tree tree) { private void setupStorage() throws Exception { final HBaseClient client = mock(HBaseClient.class); final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - - storage = new MockBase(new TSDB(config), client, true, true, true, true); + storage = new MockBase(new TSDB(client, config), + client, true, true, true, true); Branch branch = new Branch(1); TreeMap path = new TreeMap(); diff --git a/test/tree/TestLeaf.java b/test/tree/TestLeaf.java index 22f8d80579..e9b32ede0c 100644 --- a/test/tree/TestLeaf.java +++ b/test/tree/TestLeaf.java @@ -16,7 +16,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -35,7 +34,6 @@ 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; @@ -58,9 +56,7 @@ public final class TestLeaf { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); diff --git a/test/tree/TestTree.java b/test/tree/TestTree.java index e3d10e6334..a7bdeccce2 100644 --- a/test/tree/TestTree.java +++ b/test/tree/TestTree.java @@ -18,7 +18,6 @@ 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.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Field; @@ -46,7 +45,6 @@ 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; @@ -76,8 +74,6 @@ public final class TestTree { public void before() throws Exception { final Config config = new Config(false); config.overrideConfig("tsd.storage.enable_compaction", "false"); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); tsdb = new TSDB(client, config); } diff --git a/test/tree/TestTreeBuilder.java b/test/tree/TestTreeBuilder.java index e657c3ac18..ff9ba81659 100644 --- a/test/tree/TestTreeBuilder.java +++ b/test/tree/TestTreeBuilder.java @@ -16,7 +16,6 @@ import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Field; @@ -91,9 +90,7 @@ public final class TestTreeBuilder { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); treebuilder = new TreeBuilder(storage.getTSDB(), tree); diff --git a/test/tree/TestTreeRule.java b/test/tree/TestTreeRule.java index 6aa7ccdd1f..bceabe7ecc 100644 --- a/test/tree/TestTreeRule.java +++ b/test/tree/TestTreeRule.java @@ -16,7 +16,6 @@ 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.powermock.api.mockito.PowerMockito.mock; import java.util.regex.PatternSyntaxException; @@ -37,7 +36,6 @@ 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; @@ -58,9 +56,7 @@ public final class TestTreeRule { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); rule = new TreeRule(); } diff --git a/test/tsd/TestAnnotationRpc.java b/test/tsd/TestAnnotationRpc.java index 0060c1c637..2335ce1fe6 100644 --- a/test/tsd/TestAnnotationRpc.java +++ b/test/tsd/TestAnnotationRpc.java @@ -14,7 +14,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; import static org.powermock.api.mockito.PowerMockito.mock; import java.nio.charset.Charset; @@ -37,7 +36,6 @@ 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; @@ -63,13 +61,11 @@ public final class TestAnnotationRpc { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); - // add a global + // add a global storage.addColumn(global_row_key, new byte[] { 1, 0, 0 }, ("{\"startTime\":1328140800,\"endTime\":1328140801,\"description\":" + diff --git a/test/tsd/TestRpcHandler.java b/test/tsd/TestRpcHandler.java index ec26b0a34f..9407937555 100644 --- a/test/tsd/TestRpcHandler.java +++ b/test/tsd/TestRpcHandler.java @@ -17,7 +17,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -43,7 +42,6 @@ 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.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -69,9 +67,7 @@ public final class TestRpcHandler { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); rpc_manager = RpcManager.instance(tsdb); } diff --git a/test/tsd/TestTreeRpc.java b/test/tsd/TestTreeRpc.java index f706fa67e0..864afecdad 100644 --- a/test/tsd/TestTreeRpc.java +++ b/test/tsd/TestTreeRpc.java @@ -15,7 +15,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Method; @@ -50,7 +49,6 @@ 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; @@ -121,9 +119,7 @@ public final class TestTreeRpc { @Before public void before() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); } diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index 16ef3ec874..f306391689 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -911,9 +911,7 @@ private void setupAssign() throws Exception { */ private void setupUID() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); @@ -942,9 +940,7 @@ private void setupUID() throws Exception { */ private void setupTSUID() throws Exception { final Config config = new Config(false); - PowerMockito.whenNew(HBaseClient.class) - .withArguments(anyString(), anyString()).thenReturn(client); - tsdb = new TSDB(config); + tsdb = new TSDB(client, config); Field met = tsdb.getClass().getDeclaredField("metrics"); met.setAccessible(true); From 9df6f70abc83e50ca9f2c6b8472a2d97d36dd028 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 24 May 2015 15:11:20 -0700 Subject: [PATCH 162/826] Fix for the TSDB class where the client wasn't passed properly to the UID classes. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index cf696485a1..2535d81cc5 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -147,12 +147,13 @@ public TSDB(final HBaseClient client, final Config config) { meta_table = config.getString("tsd.storage.hbase.meta_table").getBytes(CHARSET); if (config.getBoolean("tsd.core.uid.random_metrics")) { - metrics = new UniqueId(client, uidtable, METRICS_QUAL, METRICS_WIDTH, true); + metrics = new UniqueId(this.client, uidtable, METRICS_QUAL, METRICS_WIDTH, + true); } else { - metrics = new UniqueId(client, uidtable, METRICS_QUAL, METRICS_WIDTH); + metrics = new UniqueId(this.client, uidtable, METRICS_QUAL, METRICS_WIDTH); } - tag_names = new UniqueId(client, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH); - tag_values = new UniqueId(client, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH); + tag_names = new UniqueId(this.client, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH); + tag_values = new UniqueId(this.client, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH); compactionq = new CompactionQueue(this); if (config.hasProperty("tsd.core.timezone")) { From 1c4603f2181c62e993cad2a911ffe142e374067d Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Sat, 23 May 2015 20:45:45 -0700 Subject: [PATCH 163/826] Modify the config with flags to support append writes. Signed-off-by: Chris Larsen --- src/utils/Config.java | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/utils/Config.java b/src/utils/Config.java index 4b1905c8f2..e40ea5c5b8 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -73,6 +73,12 @@ public class Config { /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; + /** tsd.storage.enable_appends */ + private boolean enable_appends = false; + + /** tsd.storage.repair_appends */ + private boolean repair_appends = false; + /** tsd.core.meta.enable_realtime_ts */ private boolean enable_realtime_ts = false; @@ -183,6 +189,17 @@ public boolean enable_compactions() { return enable_compactions; } + /** @return whether or not to write data in the append format */ + public boolean enable_appends() { + return enable_appends; + } + + /** @return whether or not to re-write appends with duplicates or out of order + * data when queried. */ + public boolean repair_appends() { + return repair_appends; + } + /** @return whether or not to record new TSMeta objects in real time */ public boolean enable_realtime_ts() { return enable_realtime_ts; @@ -486,6 +503,8 @@ protected void setDefaults() { 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.enable_appends", "false"); + default_map.put("tsd.storage.repair_appends", "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"); @@ -597,6 +616,8 @@ protected 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"); + 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"); enable_realtime_ts = this.getBoolean("tsd.core.meta.enable_realtime_ts"); enable_realtime_uid = this.getBoolean("tsd.core.meta.enable_realtime_uid"); From d1b9f28433a02e3349d9574e11107a0bc5dc20c8 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Sat, 23 May 2015 20:41:07 -0700 Subject: [PATCH 164/826] Add the AppendDataPoints class for appending writes to columns instead of performing post-write compactions. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/core/AppendDataPoints.java | 255 +++++++++++++++++++ test/core/TestAppendDataPoints.java | 373 ++++++++++++++++++++++++++++ test/storage/MockBase.java | 64 ++++- 4 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 src/core/AppendDataPoints.java create mode 100644 test/core/TestAppendDataPoints.java diff --git a/Makefile.am b/Makefile.am index adb81c5593..4dfed202b9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -34,6 +34,7 @@ tsdb_SRC := \ src/core/AggregationIterator.java \ src/core/Aggregator.java \ src/core/Aggregators.java \ + src/core/AppendDataPoints.java \ src/core/BatchedDataPoints.java \ src/core/ByteBufferList.java \ src/core/ColumnDatapointIterator.java \ @@ -163,6 +164,7 @@ test_SRC := \ test/core/BaseTsdbTest.java \ test/core/TestAggregationIterator.java \ test/core/TestAggregators.java \ + test/core/TestAppendDataPoints.java \ test/core/TestBatchedDataPoints.java \ test/core/TestCompactionQueue.java \ test/core/TestDownsampler.java \ diff --git a/src/core/AppendDataPoints.java b/src/core/AppendDataPoints.java new file mode 100644 index 0000000000..8081a507c3 --- /dev/null +++ b/src/core/AppendDataPoints.java @@ -0,0 +1,255 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2015 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see . +package net.opentsdb.core; + +import java.util.Collection; +import java.util.Map; +import java.util.TreeMap; + +import net.opentsdb.core.Internal.Cell; +import net.opentsdb.utils.DateTime; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.hbase.async.PutRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.stumbleupon.async.Deferred; + +/** + * A class that deals with serializing/deserializing appended data point columns. + * In busy TSDB installs appends can save on storage at write time and network + * bandwidth as TSD compactions are no longer necessary. Each data point is + * concatenated to a byte array in storage. At query time, the values are ordered + * and de-duped. Optionally the column can be re-written when out of order or + * duplicates are detected. + * NOTE: This will increase CPU usage on your HBase servers as it has to perform + * the atomic read-modify-write operation on the column. + * @since 2.2 + */ +public class AppendDataPoints { + private static final Logger LOG = LoggerFactory.getLogger(AppendDataPoints.class); + + /** The prefix ID of append columns */ + public static final byte APPEND_COLUMN_PREFIX = 0x05; + + /** The full column qualifier for append columns */ + public static final byte[] APPEND_COLUMN_QUALIFIER = new byte[] { + APPEND_COLUMN_PREFIX, 0x00, 0x00}; + + /** A threshold in seconds where we avoid writing repairs */ + public static final int REPAIR_THRESHOLD = 3600; + + /** Filled with the qualifiers in the compacted data points format after parsing */ + private byte[] qualifier; + + /** Filled with the values in the compacted data points format after parsing */ + private byte[] value; + + /** A deferred that is set if a repaired column was sent to storage */ + private Deferred repaired_deferred = null; + + /** + * Default empty ctor + */ + public AppendDataPoints() { + + } + + /** + * Creates a new AppendDataPoints object from a qualifier and value. You can + * then call {@link #getBytes()} to write to TSDB. + * @param qualifier The qualifier with the time offset, type and length flags. + * @param value The value to append + * @throws IllegalArgumentException if the qualifier or value is null or empty + */ + public AppendDataPoints(final byte[] qualifier, final byte[] value) { + if (qualifier == null || qualifier.length < 1) { + throw new IllegalArgumentException("Qualifier cannot be null or empty"); + } + if (value == null || value.length < 1) { + throw new IllegalArgumentException("Value cannot be null or empty"); + } + this.qualifier = qualifier; + this.value = value; + } + + /** + * Concatenates the qualifier and value for appending to a column in the + * backing data store. + * @return A byte array to append to the value of a column. + */ + public byte[] getBytes() { + final byte[] bytes = new byte[qualifier.length + value.length]; + System.arraycopy(this.qualifier, 0, bytes, 0, qualifier.length); + System.arraycopy(value, 0, bytes, qualifier.length, value.length); + return bytes; + } + + /** + * Parses a column from storage, orders and drops newer duplicate data points. + * The parsing will return both a Cell collection for debugging and add + * the cells to concatenated qualifier and value arrays in the compacted data + * point format so that the results can be merged with other non-append + * columns or rows. + *

+ * WARNING: If the "tsd.core.repair_appends" config is set to true then this + * method will issue puts against the database, overwriting the column with + * sorted and de-duplicated data. It will only do this for rows that are at + * least an hour old so as to avoid pounding current rows. + *

+ * TODO (CL) - allow for newer or older data points depending on a config. + * @param tsdb The TSDB to which we belong + * @param kv The key value t parse + * @throws IllegalArgumentException if the given KV is not an append column + * or we were unable to parse the value. + */ + public final Collection parseKeyValue(final TSDB tsdb, final KeyValue kv) { + if (kv.qualifier().length != 3 || kv.qualifier()[0] != APPEND_COLUMN_PREFIX) { + // it's really not an issue if the offset is not 0, maybe in the future + // we'll support appends at different offsets. + throw new IllegalArgumentException("Can not parse cell, it is not " + + " an appended cell. It has a different qualifier " + + Bytes.pretty(kv.qualifier()) + ", row key " + Bytes.pretty(kv.key())); + } + final boolean repair = tsdb.getConfig().repair_appends(); + final long base_time; + try { + base_time = Internal.baseTime(tsdb, kv.key()); + } catch (ArrayIndexOutOfBoundsException oob) { + throw new IllegalDataException("Corrupted value: invalid row key: " + kv, + oob); + } + + int val_idx = 0; + int val_length = 0; + int qual_length = 0; + int last_delta = -1; // Time delta, extracted from the qualifier. + + final Map deltas = new TreeMap(); + boolean has_duplicates = false; + boolean out_of_order = false; + boolean needs_repair = false; + + try { + while (val_idx < kv.value().length) { + byte[] q = Internal.extractQualifier(kv.value(), val_idx); + System.arraycopy(kv.value(), val_idx, q, 0, q.length); + val_idx=val_idx + q.length; + + int vlen = Internal.getValueLengthFromQualifier(q, 0); + byte[] v = new byte[vlen]; + System.arraycopy(kv.value(), val_idx, v, 0, vlen); + val_idx += vlen; + int delta = Internal.getOffsetFromQualifier(q); + + final Cell duplicate = deltas.get(delta); + if (duplicate != null) { + // This is a duplicate cell, skip it + has_duplicates = true; + qual_length -= duplicate.qualifier.length; + val_length -= duplicate.value.length; + } + + qual_length += q.length; + val_length += vlen; + final Cell cell = new Cell(q, v); + deltas.put(delta, cell); + + if (!out_of_order) { + // Data points needs to be sorted if we find at least one out of + // order data + if (delta <= last_delta) { + out_of_order = true; + } + last_delta = delta; + } + } + } catch (ArrayIndexOutOfBoundsException oob) { + throw new IllegalDataException("Corrupted value: couldn't break down" + + " into individual values (consumed " + val_idx + " bytes, but was" + + " expecting to consume " + (kv.value().length) + "): " + kv + + ", cells so far: " + deltas.values(), oob); + } + + if (has_duplicates || out_of_order) { + if ((DateTime.currentTimeMillis() / 1000) - base_time > REPAIR_THRESHOLD) { + needs_repair = true; + } + } + + // Check we consumed all the bytes of the value. + if (val_idx != kv.value().length) { + throw new IllegalDataException("Corrupted value: couldn't break down" + + " into individual values (consumed " + val_idx + " bytes, but was" + + " expecting to consume " + (kv.value().length) + "): " + kv + + ", cells so far: " + deltas.values()); + } + + val_idx = 0; + int qual_idx = 0; + byte[] healed_cell = null; + int healed_index = 0; + + this.value = new byte[val_length]; + this.qualifier = new byte[qual_length]; + + if (repair && needs_repair) { + healed_cell = new byte[val_length+qual_length]; + } + + for (final Cell cell: deltas.values()) { + System.arraycopy(cell.qualifier, 0, this.qualifier, qual_idx, + cell.qualifier.length); + qual_idx += cell.qualifier.length; + System.arraycopy(cell.value, 0, this.value, val_idx, cell.value.length); + val_idx += cell.value.length; + + if (repair && needs_repair) { + System.arraycopy(cell.qualifier, 0, healed_cell, healed_index, + cell.qualifier.length); + healed_index += cell.qualifier.length; + System.arraycopy(cell.value, 0, healed_cell, healed_index, cell.value.length); + healed_index += cell.value.length; + } + } + + if (repair && needs_repair) { + LOG.debug("Repairing appended data column " + kv); + final PutRequest put = new PutRequest(tsdb.table, kv.key(), + TSDB.FAMILY(), kv.qualifier(), healed_cell); + repaired_deferred = tsdb.getClient().put(put); + } + + return deltas.values(); + } + + /** @return the sorted qualifier in a compacted data point format after + * {@link #parseKeyValue(TSDB, KeyValue)} has been called */ + public byte[] qualifier() { + return qualifier; + } + + /** @return the sorted value in a compacted data point format after + * {@link #parseKeyValue(TSDB, KeyValue)} has been called */ + public byte[] value() { + return value; + } + + /** @return a deferred to wait on if the call to + * {@link #parseKeyValue(TSDB, KeyValue)} triggered a put to storage. */ + public Deferred repairedDeferred() { + return repaired_deferred; + } +} diff --git a/test/core/TestAppendDataPoints.java b/test/core/TestAppendDataPoints.java new file mode 100644 index 0000000000..ee89501575 --- /dev/null +++ b/test/core/TestAppendDataPoints.java @@ -0,0 +1,373 @@ +// 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Collection; +import java.util.Iterator; + +import net.opentsdb.core.Internal.Cell; +import net.opentsdb.storage.MockBase; + +import org.hbase.async.HBaseClient; +import org.hbase.async.KeyValue; +import org.hbase.async.PutRequest; +import org.junit.Test; +import org.powermock.reflect.Whitebox; + +public class TestAppendDataPoints extends BaseTsdbTest { + + private static final byte[] CF = "t".getBytes(); + private static final byte[] DPQ_S = new byte[] { 0, 0x20 }; + private static final byte[] DPQ_MS = new byte[] { (byte) 0xF0, 0, 0x20, 0 }; + private static final byte[] DPV = new byte[] { 42 }; + private static final byte[] DPV2 = new byte[] { 24 }; + private static final byte[] ROW_KEY = new byte[] { + 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1 }; + + @Test + public void ctorForWrites() throws Exception { + assertNotNull(new AppendDataPoints(DPQ_S, DPV)); + assertNotNull(new AppendDataPoints(DPQ_MS, DPV)); + + try { + assertNotNull(new AppendDataPoints(DPQ_S, null)); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + assertNotNull(new AppendDataPoints(null, DPV)); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + assertNotNull(new AppendDataPoints(null, null)); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + } + + @Test + public void toByteArray() throws Exception { + AppendDataPoints adp = new AppendDataPoints(DPQ_S, DPV); + assertArrayEquals(MockBase.concatByteArrays(DPQ_S, DPV), adp.getBytes()); + + adp = new AppendDataPoints(DPQ_MS, DPV); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPV), adp.getBytes()); + + adp = new AppendDataPoints(MockBase.concatByteArrays(DPQ_MS, DPV), + MockBase.concatByteArrays(DPQ_S, DPV2)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2), + adp.getBytes()); + } + + @Test + public void parseKeyValue() throws Exception { + KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_S, DPV)); + AppendDataPoints adp = new AppendDataPoints(); + Collection cells = adp.parseKeyValue(tsdb, kv); + assertEquals(1, cells.size()); + Cell cell = cells.iterator().next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(DPQ_S, adp.qualifier()); + assertArrayEquals(DPV, adp.value()); + verify(client, never()).put(any(PutRequest.class)); + + kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_MS, DPV)); + adp = new AppendDataPoints(); + cells = adp.parseKeyValue(tsdb, kv); + assertEquals(1, cells.size()); + cell = cells.iterator().next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + assertArrayEquals(DPQ_MS, adp.qualifier()); + assertArrayEquals(DPV, adp.value()); + verify(client, never()).put(any(PutRequest.class)); + + // some odd offset + kv = new KeyValue(ROW_KEY, CF, + new byte[] { AppendDataPoints.APPEND_COLUMN_PREFIX, 42, 42 }, + MockBase.concatByteArrays(DPQ_MS, DPV)); + adp = new AppendDataPoints(); + cells = adp.parseKeyValue(tsdb, kv); + assertEquals(1, cells.size()); + cell = cells.iterator().next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + assertArrayEquals(DPQ_MS, adp.qualifier()); + assertArrayEquals(DPV, adp.value()); + verify(client, never()).put(any(PutRequest.class)); + + kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2)); + adp = new AppendDataPoints(); + cells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + Iterator iterator = cells.iterator(); + cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, never()).put(any(PutRequest.class)); + + // out of order + kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_S, DPV2, DPQ_MS, DPV)); + adp = new AppendDataPoints(); + cells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + iterator = cells.iterator(); + cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, never()).put(any(PutRequest.class)); + + // duplicates + kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2, DPQ_S, DPV2)); + adp = new AppendDataPoints(); + cells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + iterator = cells.iterator(); + cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, never()).put(any(PutRequest.class)); + + // duplicates AND out of order, just a mess + kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays( + DPQ_S, DPV2, DPQ_MS, DPV, DPQ_MS, DPV, DPQ_S, DPV2, DPQ_MS, DPV)); + adp = new AppendDataPoints(); + cells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + iterator = cells.iterator(); + cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, never()).put(any(PutRequest.class)); + } + + @Test + public void parseKeyValueNotAppends() throws Exception { + // regular data points + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, DPQ_S, DPV); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, DPQ_MS, DPV); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + // different object + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, new byte[] { 1, 0, 0 }, DPV); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + // bad coder! + try { + new AppendDataPoints().parseKeyValue(tsdb, null); + fail("Expected an NullPointerException"); + } catch (NullPointerException iae) { } + } + + @Test + public void parseKeyValueCorrupt() throws Exception { + // shouldn't happen, but who knows? + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, null, DPV); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an NullPointerException"); + } catch (NullPointerException iae) { } + + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, HBaseClient.EMPTY_ARRAY, DPV); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, DPQ_S, null); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an NullPointerException"); + } catch (NullPointerException iae) { } + + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, DPQ_S, HBaseClient.EMPTY_ARRAY); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + KeyValue kv = new KeyValue(null, CF, DPQ_S, DPV); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an NullPointerException"); + } catch (NullPointerException iae) { } + + try { + KeyValue kv = new KeyValue(METRIC_BYTES, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_S, DPV2)); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalDataException"); + } catch (IllegalDataException iae) { } + + // bad values + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_MS, /*DPV, oops*/ DPQ_S, DPV2)); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalDataException"); + } catch (IllegalDataException iae) { } + + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_MS, DPV2, new byte[] { 0, 0, 0, 0, })); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalDataException"); + } catch (IllegalDataException iae) { } + + try { + KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPV2, DPQ_MS)); + new AppendDataPoints().parseKeyValue(tsdb, kv); + fail("Expected an IllegalDataException"); + } catch (IllegalDataException iae) { } + } + + @Test + public void repairDuplicates() throws Exception { + setDataPointStorage(); + Whitebox.setInternalState(config, "repair_appends", true); + final KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2, DPQ_S, DPV2)); + final AppendDataPoints adp = new AppendDataPoints(); + final Collectioncells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + final Iterator iterator = cells.iterator(); + Cell cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, times(1)).put(any(PutRequest.class)); + + adp.repairedDeferred().join(); + assertArrayEquals( + MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2), + storage.getColumn(ROW_KEY, AppendDataPoints.APPEND_COLUMN_QUALIFIER)); + } + + @Test + public void repairOutOfOrder() throws Exception { + setDataPointStorage(); + Whitebox.setInternalState(config, "repair_appends", true); + final KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(DPQ_S, DPV2, DPQ_MS, DPV)); + final AppendDataPoints adp = new AppendDataPoints(); + final Collectioncells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + final Iterator iterator = cells.iterator(); + Cell cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, times(1)).put(any(PutRequest.class)); + + adp.repairedDeferred().join(); + assertArrayEquals( + MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2), + storage.getColumn(ROW_KEY, AppendDataPoints.APPEND_COLUMN_QUALIFIER)); + } + + @Test + public void repairOutOfOrderAndDuplicates() throws Exception { + setDataPointStorage(); + Whitebox.setInternalState(config, "repair_appends", true); + final KeyValue kv = new KeyValue(ROW_KEY, CF, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays( + DPQ_S, DPV2, DPQ_MS, DPV, DPQ_MS, DPV, DPQ_S, DPV2, DPQ_MS, DPV)); + final AppendDataPoints adp = new AppendDataPoints(); + final Collectioncells = adp.parseKeyValue(tsdb, kv); + assertEquals(2, cells.size()); + final Iterator iterator = cells.iterator(); + Cell cell = iterator.next(); + assertArrayEquals(DPV, cell.value); + assertEquals(1356998400128L, cell.timestamp(1356998400)); + cell = iterator.next(); + assertArrayEquals(DPV2, cell.value); + assertEquals(1356998402000L, cell.timestamp(1356998400)); + assertArrayEquals(MockBase.concatByteArrays(DPQ_MS, DPQ_S), adp.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(DPV, DPV2), adp.value()); + verify(client, times(1)).put(any(PutRequest.class)); + + adp.repairedDeferred().join(); + assertArrayEquals( + MockBase.concatByteArrays(DPQ_MS, DPV, DPQ_S, DPV2), + storage.getColumn(ROW_KEY, AppendDataPoints.APPEND_COLUMN_QUALIFIER)); + } +} diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index c6493a262e..d2243ff259 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -36,6 +36,7 @@ import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.AppendRequest; import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; @@ -72,6 +73,7 @@ *
  • HBaseClient
  • *
  • GetRequest
  • *
  • PutRequest
  • + *
  • AppendRequest
  • *
  • KeyValue
  • *
  • Scanner
  • *
  • DeleteRequest
  • @@ -155,7 +157,8 @@ public Scanner answer(InvocationOnMock arg0) throws Throwable { when(client.atomicIncrement((AtomicIncrementRequest)any())) .then(new MockAtomicIncrement()); when(client.bufferAtomicIncrement((AtomicIncrementRequest)any())) - .then(new MockAtomicIncrement()); + .then(new MockAtomicIncrement()); + when(client.append((AppendRequest)any())).thenAnswer(new MockAppend()); } /** @param family Sets the family for calls that need it */ @@ -678,6 +681,65 @@ public Deferred answer(final InvocationOnMock invocation) } } + /** + * Stores one or more columns in a row. If the row does not exist, it's + * created. + */ + private class MockAppend implements Answer> { + @Override + public Deferred answer(final InvocationOnMock invocation) + throws Throwable { + final Object[] args = invocation.getArguments(); + final AppendRequest append = (AppendRequest)args[0]; + + ByteMap>> row = storage.get(append.key()); + if (row == null) { + row = new ByteMap>>(); + storage.put(append.key(), row); + } + + ByteMap> cf = row.get(append.family()); + if (cf == null) { + cf = new ByteMap>(); + row.put(append.family(), cf); + } + + TreeMap column = cf.get(append.qualifier()); + if (column == null) { + column = new TreeMap(); + cf.put(append.qualifier(), column); + } + + final byte[] values; + long column_timestamp = 0; + if (append.timestamp() != Long.MAX_VALUE) { + values = column.get(append.timestamp()); + column_timestamp = append.timestamp(); + } else { + if (column.isEmpty()) { + values = null; + } else { + values = column.firstEntry().getValue(); + column_timestamp = column.firstKey(); + } + } + if (column_timestamp == 0) { + column_timestamp = current_timestamp++; + } + + final int current_len = values != null ? values.length : 0; + final byte[] append_value = new byte[current_len + append.value().length]; + if (current_len > 0) { + System.arraycopy(values, 0, append_value, 0, values.length); + } + + System.arraycopy(append.value(), 0, append_value, current_len, + append.value().length); + column.put(column_timestamp, append_value); + return Deferred.fromResult(true); + } + } + /** * Imitates the compareAndSet client call where a {@code PutRequest} is passed * along with a byte array to compared the stored value against. If the stored From 9dce414fdd612e2ec3d7f6ce9a56cae9a012a751 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Sat, 23 May 2015 20:43:40 -0700 Subject: [PATCH 165/826] Modify the TSDB and IncomingDataPoints classes to support append writes. Signed-off-by: Chris Larsen --- src/core/IncomingDataPoints.java | 17 +++- src/core/TSDB.java | 19 +++-- test/core/TestTSDB.java | 136 +++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 1d0f55f578..ea415c69c4 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -22,6 +22,7 @@ import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; +import org.hbase.async.AppendRequest; import org.hbase.async.Bytes; import org.hbase.async.PutRequest; import org.hbase.async.Bytes.ByteMap; @@ -296,8 +297,6 @@ private Deferred addPointInternal(final long timestamp, // Java is so stupid with its auto-promotion of int to float. final byte[] qualifier = Internal.buildQualifier(timestamp, flags); - final PutRequest point = new PutRequest(tsdb.table, row, TSDB.FAMILY, - qualifier, value); // TODO(tsuna): The following timing is rather useless. First of all, // the histogram never resets, so it tends to converge to a certain // distribution and never changes. What we really want is a moving @@ -318,8 +317,18 @@ private Deferred addPointInternal(final long timestamp, // }; // TODO(tsuna): Add an errback to handle some error cases here. - point.setDurable(!batch_import); - return tsdb.client.put(point)/* .addBoth(cb) */; + if (tsdb.getConfig().enable_appends()) { + final AppendDataPoints kv = new AppendDataPoints(qualifier, value); + final AppendRequest point = new AppendRequest(tsdb.table, row, TSDB.FAMILY, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); + point.setDurable(!batch_import); + return tsdb.client.append(point);/* .addBoth(cb) */ + } else { + final PutRequest point = new PutRequest(tsdb.table, row, TSDB.FAMILY, + qualifier, value); + point.setDurable(!batch_import); + return tsdb.client.put(point)/* .addBoth(cb) */; + } } private void grow() { diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 2535d81cc5..694a099bbf 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.hbase.async.AppendRequest; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.ClientStats; @@ -139,8 +140,7 @@ public TSDB(final HBaseClient client, final Config config) { } else { this.client = client; } - - this.client.setFlushInterval(config.getShort("tsd.storage.flush_interval")); + table = config.getString("tsd.storage.hbase.data_table").getBytes(CHARSET); uidtable = config.getString("tsd.storage.hbase.uid_table").getBytes(CHARSET); treetable = config.getString("tsd.storage.hbase.tree_table").getBytes(CHARSET); @@ -719,12 +719,21 @@ private Deferred addPointInternal(final String metric, Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); RowKey.prefixKeyWithSalt(row); - scheduleForCompaction(row, (int) base_time); - final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); + Deferred result = null; + if (config.enable_appends()) { + final AppendDataPoints kv = new AppendDataPoints(qualifier, value); + final AppendRequest point = new AppendRequest(table, row, FAMILY, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); + result = client.append(point); + } else { + scheduleForCompaction(row, (int) base_time); + final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); + result = client.put(point); + } // TODO(tsuna): Add a callback to time the latency of HBase and store the // timing in a moving Histogram (once we have a class for this). - Deferred result = client.put(point); + if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && !config.enable_tsuid_tracking() && rt_publisher == null) { return result; diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 55d549c08e..c958a64d5c 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -41,6 +41,7 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @@ -827,6 +828,141 @@ public void addPointWithSaltDifferentTime() throws Exception { assertEquals(42, value[0]); } + @Test + public void addPointAppend() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42 }, value); + } + + @Test + public void addPointAppendWithOffset() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998430, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 1, -32, 42 }, value); + } + + @Test + public void addPointAppendAppending() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); + } + + @Test + public void addPointAppendAppendingOutOfOrder() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 3, -64, 1, 1, -32, 24 }, value); + } + + @Test + public void addPointAppendAppendingDuplicates() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 1, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 1, -32, 1 }, value); + } + + @Test + public void addPointAppendMS() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400050L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { (byte) 0xF0, 0, 12, -128, 42 }, value); + } + + @Test + public void addPointAppendAppendingMixMS() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400050L, 1, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { + 0, 0, 42, (byte) 0xF0, 0, 12, -128, 1, 1, -32, 24 }, value); + } + + @Test + public void addPointAppendWithSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42 }, value); + } + + @Test + public void addPointAppendAppendingWithSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + Whitebox.setInternalState(config, "enable_appends", true); + setupAddPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); + } + /** * Helper to mock the UID caches with valid responses */ From 985c35c22d599797a0f73dca6882295248467175 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 May 2015 20:47:00 -0700 Subject: [PATCH 166/826] Modify the compaction queue to support appends at query time Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/core/CompactionQueue.java | 40 ++- test/core/TestCompactionQueue.java | 381 +++++++++++++++++++++++ test/core/TestTsdbQueryAppend.java | 25 ++ test/core/TestTsdbQueryQueries.java | 77 ++++- test/core/TestTsdbQuerySaltedAppend.java | 29 ++ 6 files changed, 534 insertions(+), 20 deletions(-) create mode 100644 test/core/TestTsdbQueryAppend.java create mode 100644 test/core/TestTsdbQuerySaltedAppend.java diff --git a/Makefile.am b/Makefile.am index 4dfed202b9..1ac310072c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -186,8 +186,10 @@ test_SRC := \ test/core/TestTsdbQuery.java \ test/core/TestTsdbQueryAggregators.java \ test/core/TestTsdbQueryAggregatorsSalted.java \ + test/core/TestTsdbQueryAppend.java \ test/core/TestTsdbQueryQueries.java \ test/core/TestTsdbQuerySalted.java \ + test/core/TestTsdbQuerySaltedAppend.java \ test/core/TestTSQuery.java \ test/core/TestTSSubQuery.java \ test/plugin/DummyPlugin.java \ diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 0b5e54f321..0f23b361d4 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -27,7 +27,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.hbase.async.Bytes; import org.hbase.async.HBaseRpc; import org.hbase.async.KeyValue; @@ -286,6 +285,10 @@ private class Compaction { // KeyValue containing the longest qualifier for the datapoint, used to optimize // checking if the compacted qualifier already exists. private KeyValue longest; + + // the latest append column. If set then we don't want to re-write the row + // and if we only had a single column with a single value, we return this. + private KeyValue last_append_column; public Compaction(ArrayList row, KeyValue[] compacted, List annotations) { nkvs = row.size(); @@ -387,21 +390,26 @@ public Deferred compact() { deferred = deferred.addCallbacks(new DeleteCompactedCB(to_delete), handle_write_error); } return deferred; - } else { + } else if (last_append_column == null) { // We had nothing to write, because one of the cells is already the // correctly compacted version, so we can go ahead and delete the // individual cells directly. new DeleteCompactedCB(to_delete).call(null); return null; + } else { + return null; } } /** - * Find the first datapoint column in a row. + * Find the first datapoint column in a row. It may be an appended column * * @return the first found datapoint column in the row, or null if none */ private KeyValue findFirstDatapointColumn() { + if (last_append_column != null) { + return last_append_column; + } for (final KeyValue kv : row) { if (isDatapoint(kv)) { return kv; @@ -419,12 +427,26 @@ private KeyValue findFirstDatapointColumn() { private int buildHeapProcessAnnotations() { int tot_values = 0; for (final KeyValue kv : row) { - final byte[] qual = kv.qualifier(); - final int len = qual.length; + byte[] qual = kv.qualifier(); + int len = qual.length; if ((len & 1) != 0) { // process annotations and other extended formats if (qual[0] == Annotation.PREFIX()) { annotations.add(JSON.parseToObject(kv.value(), Annotation.class)); + } else if (qual[0] == AppendDataPoints.APPEND_COLUMN_PREFIX){ + final AppendDataPoints adp = new AppendDataPoints(); + tot_values += adp.parseKeyValue(tsdb, kv).size(); + last_append_column = new KeyValue(kv.key(), kv.family(), + adp.qualifier(), kv.timestamp(), adp.value()); + if (longest == null || + longest.qualifier().length < last_append_column.qualifier().length) { + longest = last_append_column; + } + final ColumnDatapointIterator col = + new ColumnDatapointIterator(last_append_column); + if (col.hasMoreData()) { + heap.add(col); + } } else { LOG.warn("Ignoring unexpected extended format type " + qual[0]); } @@ -523,11 +545,19 @@ private KeyValue buildCompactedColumn(ByteBufferList compacted_qual, /** * Make sure we don't delete the row that is the result of the compaction, so we * remove the compacted value from the list of values to delete if it is there. + * Also, if one or more columns were appends then we don't want to mess with + * the row for now. * * @param compact the compacted column * @return true if we need to write the compacted value */ private boolean updateDeletesCheckForWrite(KeyValue compact) { + if (last_append_column != null) { + // TODO appends are involved so we may want to squash dps into the + // append or vice-versa. + return false; + } + // if the longest entry isn't as long as the compacted one, obviously the compacted // one can't have already existed if (longest != null && longest.qualifier().length >= compact.qualifier().length) { diff --git a/test/core/TestCompactionQueue.java b/test/core/TestCompactionQueue.java index d1c1a0e84e..368f1d270b 100644 --- a/test/core/TestCompactionQueue.java +++ b/test/core/TestCompactionQueue.java @@ -88,6 +88,7 @@ public void before() throws Exception { Whitebox.setInternalState(config, "enable_compactions", true); Whitebox.setInternalState(config, "fix_duplicates", true); Whitebox.setInternalState(tsdb, "config", config); + when(tsdb.getConfig()).thenReturn(config); // Stub out the compaction thread, so it doesn't even start. PowerMockito.whenNew(CompactionQueue.Thrd.class).withNoArguments() .thenReturn(mock(CompactionQueue.Thrd.class)); @@ -133,6 +134,25 @@ public void oneCellRow() throws Exception { verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } + @Test + public void oneCellAppend() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(qual, kv.qualifier()); + assertArrayEquals(val, kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + @Test public void oneCellRowWAnnotation() throws Exception { ArrayList kvs = new ArrayList(1); @@ -153,6 +173,27 @@ public void oneCellRowWAnnotation() throws Exception { verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } + @Test + public void oneCellAppendWAnnotiation() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(1); + kvs.add(makekv(note_qual, note)); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(qual, kv.qualifier()); + assertArrayEquals(val, kv.value()); + assertEquals(1, annotations.size()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + @Test public void oneCellRowWAnnotationMS() throws Exception { ArrayList kvs = new ArrayList(1); @@ -231,6 +272,27 @@ public void twoCellRow() throws Exception { verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } + @Test + public void twoCellAppend() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + @Test public void twoCellRowWAnnotation() throws Exception { ArrayList kvs = new ArrayList(2); @@ -255,6 +317,29 @@ public void twoCellRowWAnnotation() throws Exception { verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } + @Test + public void twoCellAppendWAnnotations() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(1); + kvs.add(makekv(note_qual, note)); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); + assertEquals(1, annotations.size()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + @Test public void fullRowSeconds() throws Exception { ArrayList kvs = new ArrayList(3600); @@ -1117,6 +1202,302 @@ public void tripleCompactedSecondsAndMs() throws Exception { verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual34, qual56 })); } + @Test + public void appendsAndLaterPuts() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(qual3, val3)); + kvs.add(makekv(qual4, val4)); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void appendsAndEarlierPuts() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + kvs.add(makekv(qual, val)); + kvs.add(makekv(qual2, val2)); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual3, val3, qual4, val4))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void appendsAndInterspersedPuts() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + kvs.add(makekv(qual, val)); + kvs.add(makekv(qual3, val3)); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual2, val2, qual4, val4))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void doubleAppends() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual3, val3, qual4, val4))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void tripleAppends() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + final byte[] qual5 = { 0x00, 0x47 }; + final byte[] val5 = Bytes.fromLong(1L); + final byte[] qual6 = { 0x00, 0x57 }; + final byte[] val6 = Bytes.fromLong(0L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual3, val3, qual4, val4))); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual5, val5, qual6, val6))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays( + qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays( + val, val2, val3, val4, val5, val6, ZERO), kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void doubleAppendsAndPuts() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + final byte[] qual5 = { 0x00, 0x47 }; + final byte[] val5 = Bytes.fromLong(1L); + final byte[] qual6 = { 0x00, 0x57 }; + final byte[] val6 = Bytes.fromLong(0L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(qual3, val3)); + kvs.add(makekv(qual4, val4)); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual5, val5, qual6, val6))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays( + qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays( + val, val2, val3, val4, val5, val6, ZERO), kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void appendsAndCompacted() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), + MockBase.concatByteArrays(val3, val4, ZERO))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void appendsAndCompactedAndPuts() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(3L); + final byte[] qual4 = { 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(2L); + final byte[] qual5 = { 0x00, 0x47 }; + final byte[] val5 = Bytes.fromLong(1L); + final byte[] qual6 = { 0x00, 0x57 }; + final byte[] val6 = Bytes.fromLong(0L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), + MockBase.concatByteArrays(val3, val4, ZERO))); + kvs.add(makekv(qual5, val5)); + kvs.add(makekv(qual6, val6)); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays( + qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays( + val, val2, val3, val4, val5, val6, ZERO), kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void appendsDuplicatePuts() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(qual, val)); + kvs.add(makekv(qual2, val2)); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + + @Test + public void appendsDuplicateCompacted() throws Exception { + ArrayList kvs = new ArrayList(1); + ArrayList annotations = new ArrayList(0); + final byte[] qual = { 0x00, 0x07 }; + final byte[] val = Bytes.fromLong(42L); + final byte[] qual2 = { 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + MockBase.concatByteArrays(qual, val, qual2, val2))); + kvs.add(makekv(MockBase.concatByteArrays(qual, qual2), + MockBase.concatByteArrays(val, val2, ZERO))); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); + + // We had nothing to do so... + // ... verify there were no put. + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + // ... verify there were no delete. + verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); + } + // ----------------- // // Helper functions. // // ----------------- // diff --git a/test/core/TestTsdbQueryAppend.java b/test/core/TestTsdbQueryAppend.java new file mode 100644 index 0000000000..c7b4928b09 --- /dev/null +++ b/test/core/TestTsdbQueryAppend.java @@ -0,0 +1,25 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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 org.junit.Before; +import org.powermock.reflect.Whitebox; + +public class TestTsdbQueryAppend extends TestTsdbQueryQueries { + + @Before + public void beforeLocal() { + Whitebox.setInternalState(config, "enable_appends", true); + query = new TsdbQuery(tsdb); + } +} diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 32effffc3c..32fde1e46e 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -14,6 +14,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import java.lang.reflect.Field; @@ -479,12 +480,11 @@ public void runMixedSingleTSPostCompaction() throws Exception { final Field compact = Config.class.getDeclaredField("enable_compactions"); compact.setAccessible(true); compact.set(config, true); - query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); assertNotNull(query.run()); - + // this should only compact the rows for the time series that we fetched and // leave the others alone @@ -539,7 +539,6 @@ public void runEndTime() throws Exception { int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { - System.out.println(timestamp); assertEquals(value, dp.longValue()); assertEquals(timestamp, dp.timestamp()); value++; @@ -579,7 +578,11 @@ public void runCompactPostQuery() throws Exception { System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - assertEquals(119, storage.numColumns(key_b)); + if (config.enable_appends()) { + assertEquals(1, storage.numColumns(key_b)); + } else { + assertEquals(119, storage.numColumns(key_b)); + } System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -587,15 +590,23 @@ public void runCompactPostQuery() throws Exception { System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - assertEquals(120, storage.numColumns(key_b)); - + if (config.enable_appends()) { + assertEquals(1, storage.numColumns(key_b)); + } else { + assertEquals(120, storage.numColumns(key_b)); + } + System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key_a)); System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - assertEquals(61, storage.numColumns(key_b)); + if (config.enable_appends()) { + assertEquals(1, storage.numColumns(key_b)); + } else { + assertEquals(61, storage.numColumns(key_b)); + } // run it again to verify the compacted data uncompacts properly dps = query.run(); @@ -618,7 +629,7 @@ public void runStartNotSet() throws Exception { query.run(); } - @Test (expected = IllegalDataException.class) + @Test public void runFloatAndIntSameTSNoFix() throws Exception { // if a row has an integer and a float for the same timestamp, there will be // two different qualifiers that will resolve to the same offset. This no @@ -629,8 +640,32 @@ public void runFloatAndIntSameTSNoFix() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998430, 42.5F, tags).joinUninterruptibly(); query.setStartTime(1356998400); query.setEndTime(1357041600); - query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); - query.run(); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + if (config.enable_appends()) { + DataPoints[] dps = query.run(); + assertMeta(dps, 0, false, false); + + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + if (value == 1) { + // first value was replaced in the append + assertEquals(42.5, dp.doubleValue(), 0.0001); + } else { + assertEquals(value, dp.longValue()); + } + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + } else { + try { + query.run(); + fail("Expected an IllegalDataException"); + } catch (IllegalDataException ide) { } + } } @Test @@ -711,14 +746,18 @@ public void runWithAnnotationPostCompact() throws Exception { final byte[] key_b = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); RowKey.prefixKeyWithSalt(key_b); - + System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(2, storage.numColumns(key_a)); System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - assertEquals(119, storage.numColumns(key_b)); + if (config.enable_appends()) { + assertEquals(1, storage.numColumns(key_b)); + } else { + assertEquals(119, storage.numColumns(key_b)); + } System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -726,7 +765,11 @@ public void runWithAnnotationPostCompact() throws Exception { System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - assertEquals(120, storage.numColumns(key_b)); + if (config.enable_appends()) { + assertEquals(1, storage.numColumns(key_b)); + } else { + assertEquals(120, storage.numColumns(key_b)); + } System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -734,7 +777,11 @@ public void runWithAnnotationPostCompact() throws Exception { System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - assertEquals(61, storage.numColumns(key_b)); + if (config.enable_appends()) { + assertEquals(1, storage.numColumns(key_b)); + } else { + assertEquals(61, storage.numColumns(key_b)); + } dps = query.run(); assertMeta(dps, 0, false, true); @@ -830,7 +877,7 @@ public void runSingleDataPoint() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + storage.dumpToSystemOut(); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); diff --git a/test/core/TestTsdbQuerySaltedAppend.java b/test/core/TestTsdbQuerySaltedAppend.java new file mode 100644 index 0000000000..81e58cdccc --- /dev/null +++ b/test/core/TestTsdbQuerySaltedAppend.java @@ -0,0 +1,29 @@ +// 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 org.junit.Before; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.reflect.Whitebox; + +public class TestTsdbQuerySaltedAppend extends TestTsdbQueryQueries { + + @Before + public void beforeLocal() { + Whitebox.setInternalState(config, "enable_appends", true); + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + query = new TsdbQuery(tsdb); + } +} From 79e0d069940f156b6ef1427c8ee145d9123be00b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 27 May 2015 10:28:43 -0700 Subject: [PATCH 167/826] Fix property names for the AsyncHBase config in the TSDB ctor. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 694a099bbf..8c87395ef9 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -132,9 +132,9 @@ public TSDB(final HBaseClient client, final Config config) { } else { async_config = new org.hbase.async.Config(); } - async_config.overrideConfig("asynchbase.zk.base_path", + async_config.overrideConfig("hbase.zookeeper.znode.parent", config.getString("tsd.storage.hbase.zk_basedir")); - async_config.overrideConfig("asynchbase.zk.quorum", + async_config.overrideConfig("hbase.zookeeper.quorum", config.getString("tsd.storage.hbase.zk_quorum")); this.client = new HBaseClient(async_config); } else { From 45e575a7dbc01f32788baf87b427e92bee453676 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 5 Jun 2015 18:03:01 -0700 Subject: [PATCH 168/826] Improve query performance in the AggregationIterator by calling .hasNext() on the downsampler instead of letting it build a giant exception string that we're just tossing in the bit bucket. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 2c6f6e3433..f806f7b1d2 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -276,17 +276,12 @@ private AggregationIterator(final SeekableView[] iterators, SeekableView it = iterators[i]; it.seek(start_time); final DataPoint dp; - try { - dp = it.next(); - } catch (NoSuchElementException e) { - // It should be rare but could happen after downsampling when - // we throw away some data points at the beginning after aligning - // start time by downsmpling interval and there are no data points - // left for the current span. + if (!it.hasNext()) { ++num_empty_spans; endReached(i); continue; } + dp = it.next(); //LOG.debug("Creating iterator #" + i); if (dp.timestamp() >= start_time) { //LOG.debug("First DP in range for #" + i + ": " From a35d2dda3b3c12d3719d8b1bfc79b4f19453bbaf Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 5 Jun 2015 18:03:01 -0700 Subject: [PATCH 169/826] Improve query performance in the AggregationIterator by calling .hasNext() on the downsampler instead of letting it build a giant exception string that we're just tossing in the bit bucket. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 363e828f3f..28d77b9939 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -313,17 +313,12 @@ private AggregationIterator(final SeekableView[] iterators, SeekableView it = iterators[i]; it.seek(start_time); final DataPoint dp; - try { - dp = it.next(); - } catch (NoSuchElementException e) { - // It should be rare but could happen after downsampling when - // we throw away some data points at the beginning after aligning - // start time by downsampling interval and there are no data points - // left for the current span. + if (!it.hasNext()) { ++num_empty_spans; endReached(i); continue; } + dp = it.next(); //LOG.debug("Creating iterator #" + i); if (dp.timestamp() >= start_time) { //LOG.debug("First DP in range for #" + i + ": " From b97276cda61384494f7600d601a3fe95237c0e4a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 29 May 2015 17:23:18 -0700 Subject: [PATCH 170/826] Add tag value filter classes to handle more complex filtering situations such as wildcards, key exclusions and regular expressions. Signed-off-by: Chris Larsen --- Makefile.am | 12 + src/query/filter/TagVFilter.java | 601 ++++++++++++++++++ src/query/filter/TagVLiteralOrFilter.java | 210 ++++++ src/query/filter/TagVNotKeyFilter.java | 71 +++ src/query/filter/TagVNotLiteralOrFilter.java | 185 ++++++ src/query/filter/TagVRegexFilter.java | 106 +++ src/query/filter/TagVWildcardFilter.java | 228 +++++++ test/query/filter/TestTagVFilter.java | 404 ++++++++++++ .../query/filter/TestTagVLiteralOrFilter.java | 172 +++++ test/query/filter/TestTagVNotKeyFilter.java | 47 ++ .../filter/TestTagVNotLiteralOrFilter.java | 172 +++++ test/query/filter/TestTagVRegexFilter.java | 126 ++++ test/query/filter/TestTagVWildcardFilter.java | 320 ++++++++++ 13 files changed, 2654 insertions(+) create mode 100644 src/query/filter/TagVFilter.java create mode 100644 src/query/filter/TagVLiteralOrFilter.java create mode 100644 src/query/filter/TagVNotKeyFilter.java create mode 100644 src/query/filter/TagVNotLiteralOrFilter.java create mode 100644 src/query/filter/TagVRegexFilter.java create mode 100644 src/query/filter/TagVWildcardFilter.java create mode 100644 test/query/filter/TestTagVFilter.java create mode 100644 test/query/filter/TestTagVLiteralOrFilter.java create mode 100644 test/query/filter/TestTagVNotKeyFilter.java create mode 100644 test/query/filter/TestTagVNotLiteralOrFilter.java create mode 100644 test/query/filter/TestTagVRegexFilter.java create mode 100644 test/query/filter/TestTagVWildcardFilter.java diff --git a/Makefile.am b/Makefile.am index 1ac310072c..59f34534e8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -73,6 +73,12 @@ tsdb_SRC := \ src/meta/TSMeta.java \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ + src/query/TagVFilter.java \ + src/query/TagVLiteralOrFilter.java \ + src/query/TagVNotKeyFilter.java \ + src/query/TagVNotLiteralOrFilter.java \ + src/query/TagVRegexFilter.java \ + src/query/TagVWildcardFilter.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ src/search/TimeSeriesLookup.java \ @@ -197,6 +203,12 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/TestTagVFilter.java \ + test/query/TestTagVLiteralOrFilter.java \ + test/query/TestTagVNotKeyFilter.java \ + test/query/TestTagVNotLiteralOrFilter.java \ + test/query/TestTagVRegexFilter.java \ + test/query/TestTagVWildcardFilter.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java new file mode 100644 index 0000000000..e55a234da1 --- /dev/null +++ b/src/query/filter/TagVFilter.java @@ -0,0 +1,601 @@ +// 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.filter; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hbase.async.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.opentsdb.core.TSDB; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Pair; +import net.opentsdb.utils.PluginLoader; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +/** + * A base class for tag value filters that may execute against rows that + * come out of a scanner to determine if we should include them in the results + * or not. The filters should be prefixed with something to differentiate them + * from literal values. + * + * Every filter must be associated with a tag key. During scanning, each time + * a new TSUID is encountered, the map will be passed to {@link match} for + * matching. + * + * Plugins implementing the filter must include the following: + * + * - {@code public static final String FILTER_NAME;} + * A short, unique name without spaces or odd characters that is used to + * invoke the filter. + * - {@code public static String description();} + * A method that returns a description of what the filter does. + * - {@code public static String examples();} + * A method that returns a string with some examples of how to use the filter. + * + * This class also contains the list of configured filters as well as a method + * to load filters from plugin Jars. + * @since 2.2 + */ +@JsonDeserialize(builder = TagVFilter.Builder.class) +public abstract class TagVFilter implements Comparable { + private static final Logger LOG = LoggerFactory.getLogger(TagVFilter.class); + + /** A map of configured filters for use in querying */ + private static Map, Constructor>> + tagv_filter_map = new HashMap, Constructor>>(); + static { + try { + tagv_filter_map.put(TagVLiteralOrFilter.FILTER_NAME, + new Pair, Constructor>(TagVLiteralOrFilter.class, + TagVLiteralOrFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVLiteralOrFilter.TagVILiteralOrFilter.FILTER_NAME, + new Pair, Constructor>(TagVLiteralOrFilter.TagVILiteralOrFilter.class, + TagVLiteralOrFilter.TagVILiteralOrFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVNotLiteralOrFilter.FILTER_NAME, + new Pair, Constructor>(TagVNotLiteralOrFilter.class, + TagVNotLiteralOrFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVNotLiteralOrFilter.TagVNotILiteralOrFilter.FILTER_NAME, + new Pair, Constructor>(TagVNotLiteralOrFilter.TagVNotILiteralOrFilter.class, + TagVNotLiteralOrFilter.TagVNotILiteralOrFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVRegexFilter.FILTER_NAME, + new Pair, Constructor>(TagVRegexFilter.class, + TagVRegexFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVWildcardFilter.FILTER_NAME, + new Pair, Constructor>(TagVWildcardFilter.class, + TagVWildcardFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVWildcardFilter.TagVIWildcardFilter.FILTER_NAME, + new Pair, Constructor>(TagVWildcardFilter.TagVIWildcardFilter.class, + TagVWildcardFilter.TagVIWildcardFilter.class.getDeclaredConstructor(String.class, String.class))); + tagv_filter_map.put(TagVNotKeyFilter.FILTER_NAME, + new Pair, Constructor>(TagVNotKeyFilter.class, + TagVNotKeyFilter.class.getDeclaredConstructor(String.class, String.class))); + } catch (SecurityException e) { + throw new RuntimeException("Failed to load a tag value filter", e); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Failed to load a tag value filter", e); + } + } + + /** The tag key this filter is associated with */ + final protected String tagk; + + /** The raw, unparsed filter */ + final protected String filter; + + /** The tag key converted into a UID */ + protected byte[] tagk_bytes; + + /** An optional list of tag value UIDs if the filter matches on literals. */ + protected List tagv_uids; + + /** Whether or not to also group by this filter */ + @JsonProperty + protected boolean group_by; + + /** Flag the implementation can set to tell the scanner to pick up rows that + * DON'T have the given tagk (regardless of value) + */ + protected boolean not_key; + + /** A flag to indicate whether or not we need to execute a post-scan lookup */ + protected boolean post_scan = true; + + /** + * Default Ctor needed for the service loader. Implementations must override + * and set the filterName(). + */ + public TagVFilter() { + this.tagk = null; + this.filter = null; + } + + /** + * The ctor that validates we have a good tag key to work with + * @param tagk The tag key to associate with this filter + * @param filter The unparsed filter + * @throws IlleglArgumentException if the tag was empty or null. + */ + public TagVFilter(final String tagk, final String filter) { + this.tagk = tagk; + this.filter = filter; + if (tagk == null || tagk.isEmpty()) { + throw new IllegalArgumentException("Filter must have a tagk"); + } + } + + /** + * Looks up the tag key in the given map and determines if the filter matches + * or not. If the tag key doesn't exist in the tag map, then the match fails. + * @param tags The tag map to use for looking up the value for the tagk + * @return True if the tag value matches, false if it doesn't. + */ + public abstract boolean match(final Map tags); + + /** + * The name of this filter as used in queries. When used in URL queries the + * value will be in parentheses, e.g. filter() + * The name will also be lowercased before storing it in the lookup map. + * @return The name of the filter. + */ + public abstract String getType(); + + /** + * A simple string of the filter settings for printing in toString() calls. + * @return A string with the format "{settings=, ...}" + */ + @JsonIgnore + public abstract String debugInfo(); + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("filter_name=") + .append(getType()) + .append(", tagk=").append(tagk) + .append(", group_by=").append(group_by) + .append(", tagk_bytes=").append(Bytes.pretty(tagk_bytes)) + .append(", config=") + .append(debugInfo()); + return buf.toString(); + } + + /** + * Parses the tag value and determines if it's a group by, a literal or a filter. + * @param tagk The tag key associated with this value + * @param filter The tag value, possibly a filter + * @return Null if the value was a group by or a literal, a valid filter object + * if it looked to be a filter. + * @throws IllegalArgumentException if the tag key or filter was null, empty + * or if the filter was malformed, e.g. a bad regular expression. + */ + public static TagVFilter getFilter(final String tagk, final String filter) { + if (tagk == null || tagk.isEmpty()) { + throw new IllegalArgumentException("Tagk cannot be null or empty"); + } + if (filter == null || filter.isEmpty()) { + throw new IllegalArgumentException("Filter cannot be null or empty"); + } + if (filter.length() == 1 && filter.charAt(0) == '*') { + return null; // group by filter + } + + final int paren = filter.indexOf('('); + if (paren > -1) { + final String prefix = filter.substring(0, paren).toLowerCase(); + return new Builder().withTagk(tagk) + .withFilter(stripParentheses(filter)) + .withType(prefix) + .build(); + } else if (filter.contains("*")) { + // a shortcut for wildcards since we don't allow asterisks to be stored + // in strings at this time. + return new TagVWildcardFilter(tagk, filter, true); + } else { + return null; // likely a literal or unknown + } + } + + /** + * Helper to strip parentheses from a filter name passed in over a URL + * or JSON. E.g. "regexp(foo.*)" returns "foo.*". + * @param filter The filter string to parse + * @return The filter value minus the surrounding name and parens. + */ + public static String stripParentheses(final String filter) { + if (filter == null || filter.isEmpty()) { + throw new IllegalArgumentException("Filter string cannot be null or empty"); + } + if (filter.charAt(filter.length() - 1) != ')') { + throw new IllegalArgumentException("Filter must end with a ')': " + filter); + } + final int start_pos = filter.indexOf('('); + if (start_pos < 0) { + throw new IllegalArgumentException("Filter must include a '(': " + filter); + } + return filter.substring(start_pos + 1, filter.length() - 1); + } + + /** + * Loads plugins from the plugin directory and + * @throws ClassNotFoundException If we found a class that we didn't... find? + * @throws NoSuchMethodException If the discovered plugin didn't have the + * proper (tagk, filter) ctor + */ + public static void initializeFilterMap() + throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException { + final List filter_plugins = + PluginLoader.loadPlugins(TagVFilter.class); + if (filter_plugins != null) { + for (final TagVFilter filter : filter_plugins) { + // validate required fields and methods + filter.getClass().getDeclaredMethod("description"); + filter.getClass().getDeclaredMethod("examples"); + filter.getClass().getDeclaredField("FILTER_NAME"); + + final Constructor ctor = + filter.getClass().getDeclaredConstructor(String.class, String.class); + + final Constructor existing = + tagv_filter_map.get(filter.getType()).getValue(); + if (existing != null) { + LOG.warn("Overloading existing filter " + + existing.getClass().getCanonicalName() + + " with new filter " + filter.getClass().getCanonicalName()); + } + tagv_filter_map.put(filter.getType().toLowerCase(), + new Pair, Constructor>( + filter.getClass(), ctor)); + } + LOG.info("Loaded " + tagv_filter_map.size() + " filters"); + } + } + + /** + * Converts the tag map to a filter list. If a filter already exists for a + * tag group by, then the duplicate is skipped. + * @param tags A set of tag keys and values. May be null or empty. + * @param filters A set of filters to add the converted filters to. This may + * not be null. + */ + public static void tagsToFilters(final Map tags, + final List filters) { + if (tags == null || tags.isEmpty()) { + return; + } + + for (final Map.Entry entry : tags.entrySet()) { + TagVFilter filter = getFilter(entry.getKey(), entry.getValue()); + + if (filter == null && entry.getValue().equals("*")) { + filter = new TagVWildcardFilter(entry.getKey(), "*", true); + } else if (filter == null) { + filter = new TagVLiteralOrFilter(entry.getKey(), entry.getValue()); + } + + filter.setGroupBy(true); + boolean duplicate = false; + for (final TagVFilter existing : filters) { + if (filter.equals(existing)) { + LOG.debug("Skipping duplicate filter: " + existing); + existing.setGroupBy(true); + duplicate = true; + break; + } + } + + if (!duplicate) { + filters.add(filter); + } + } + } + + /** + * Runs through the loaded plugin map and dumps the names, description and + * examples into a map to serialize via the API. + * @return A map of filter meta data. + */ + public static Map> loadedFilters() { + final Map> filters = + new HashMap>(tagv_filter_map.size()); + for (final Pair, Constructor> pair : + tagv_filter_map.values()) { + final Map filter_meta = new HashMap(1); + try { + Method method = pair.getKey().getDeclaredMethod("description"); + filter_meta.put("description", (String)method.invoke(null)); + + method = pair.getKey().getDeclaredMethod("examples"); + filter_meta.put("examples", (String)method.invoke(null)); + + final Field filter_name = pair.getKey().getDeclaredField("FILTER_NAME"); + filters.put((String)filter_name.get(null), filter_meta); + } catch (SecurityException e) { + throw new RuntimeException("Unexpected security exception", e); + } catch (NoSuchMethodException e) { + LOG.error("Filter plugin " + pair.getClass().getCanonicalName() + + " did not implement one of the \"description\" or \"examples\" methods"); + } catch (NoSuchFieldException e) { + LOG.error("Filter plugin " + pair.getClass().getCanonicalName() + + " did not have the \"FILTER_NAME\" field"); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Unexpected exception", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unexpected security exception", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Unexpected security exception", e); + } + + } + return filters; + } + + /** + * Asynchronously resolves the tagk name to it's UID. On a successful lookup + * the {@link tagk_bytes} will be set. + * @param tsdb The TSDB to use for the lookup + * @return A deferred to let the caller know that the lookup was completed. + * The value will be the tag UID (unless it's an exception of course) + */ + public Deferred resolveTagkName(final TSDB tsdb) { + class ResolvedCB implements Callback { + @Override + public byte[] call(final byte[] uid) throws Exception { + tagk_bytes = uid; + return uid; + } + } + + return tsdb.getUIDAsync(UniqueIdType.TAGK, tagk) + .addCallback(new ResolvedCB()); + } + + /** + * Resolves both the tagk to it's UID and a list of literal tag values to + * their UIDs. A filter may match a literal set (e.g. the pipe filter) in which + * case we can build the row key scanner with these values. + * Note that if "tsd.query.skip_unresolved_tagvs" is set in the config then + * any tag value UIDs that couldn't be found will be excluded. + * @param tsdb The TSDB to use for the lookup + * @param literals The list of unique strings to lookup + * @return A deferred to let the caller know that the lookup was completed. + * The value will be the tag UID (unless it's an exception of course) + */ + public Deferred resolveTags(final TSDB tsdb, + final Set literals) { + final Config config = tsdb.getConfig(); + + /** + * Allows the filter to avoid killing the entire query when we can't resolve + * a tag value to a UID. + */ + class TagVErrback implements Callback { + @Override + public byte[] call(final Exception e) throws Exception { + if (config.getBoolean("tsd.query.skip_unresolved_tagvs")) { + LOG.warn("Query tag value not found: " + e.getMessage()); + return null; + } else { + throw e; + } + } + } + + /** + * Stores the non-null UIDs in the local list and then sorts them in + * prep for use in the regex filter + */ + class ResolvedTagVCB implements Callback> { + @Override + public byte[] call(final ArrayList results) + throws Exception { + tagv_uids = new ArrayList(results.size() - 1); + for (final byte[] tagv : results) { + if (tagv != null) { + tagv_uids.add(tagv); + } + } + Collections.sort(tagv_uids, Bytes.MEMCMP); + return tagk_bytes; + } + } + + /** + * Super simple callback to set the local tagk and returns null so it won't + * be included in the tag value UID lookups. + */ + class ResolvedTagKCB implements Callback { + @Override + public byte[] call(final byte[] uid) throws Exception { + tagk_bytes = uid; + return null; + } + } + + final List> tagvs = + new ArrayList>(literals.size()); + for (final String tagv : literals) { + tagvs.add(tsdb.getUIDAsync(UniqueIdType.TAGV, tagv) + .addErrback(new TagVErrback())); + } + // ugly hack to resolve the tagk UID. The callback will return null and we'll + // remove it from the UID list. + tagvs.add(tsdb.getUIDAsync(UniqueIdType.TAGK, tagk) + .addCallback(new ResolvedTagKCB())); + return Deferred.group(tagvs).addCallback(new ResolvedTagVCB()); + } + + /** @return the tag key associated with this filter */ + public String getTagk() { + return tagk; + } + + /** @return the tag key UID associated with this filter. + * Call {@link resolveName} first */ + @JsonIgnore + public byte[] getTagkBytes() { + return tagk_bytes; + } + + @JsonIgnore + public List getTagVUids() { + return tagv_uids == null ? Collections.emptyList() : tagv_uids; + } + + /** @return whether or not to group by the results of this filter */ + @JsonIgnore + public boolean isGroupBy() { + return group_by; + } + + /** @param group_by Wether or not to group by the results of this filter */ + public void setGroupBy(final boolean group_by) { + this.group_by = group_by; + } + + public String getFilter() { + return filter; + } + + /** @return the simple class name of this filter */ + @JsonIgnore + public String getName() { + return this.getClass().getSimpleName(); + } + + /** @return Whether or not this filter should be executed against scan results */ + public boolean postScan() { + if (not_key) { + return false; + } + return post_scan; + } + + /** @param post_scan Whether or not this filter should be executed against + * scan results */ + public void setPostScan(final boolean post_scan) { + this.post_scan = post_scan; + } + + @JsonIgnore + public boolean isNotKeyFilter() { + return not_key; + } + + @Override + public int compareTo(final TagVFilter filter) { + return Bytes.memcmpMaybeNull(tagk_bytes, filter.tagk_bytes); + } + + /** + * Builder class used for deserializing filters from JSON queries via Jackson + * since we don't want the user to worry about the class name. The type, + * tagk and filter must be configured or the build will fail. + */ + @JsonPOJOBuilder() + public static class Builder { + private String type; + private String tagk; + private String filter; + @JsonProperty + private boolean group_by; + + /** @param type The type of filter matching a valid filter name */ + public Builder withType(final String type) { + this.type = type; + return this; + } + + /** @param tagk The tag key to match on for this filter */ + public Builder withTagk(final String tagk) { + this.tagk = tagk; + return this; + } + + /** @param filter The filter expression to use for matching */ + public Builder withFilter(final String filter) { + this.filter = filter; + return this; + } + + /** @param group_by Whether or not the filter should group results */ + public Builder withGroupBy(final boolean group_by) { + this.group_by = group_by; + return this; + } + + /** + * Searches the filter map for the given type and returns an instantiated + * filter if found. The caller must set the type, tagk and filter values. + * @return A filter if instantiation was successful + * @throws IllegalArgumentException if one of the required parameters was + * not set or the filter couldn't be found. + * @throws RuntimeException if the filter couldn't be instantiated. Check + * the implementation if it's a plugin. + */ + public TagVFilter build() { + if (type == null || type.isEmpty()) { + throw new IllegalArgumentException( + "The filter type cannot be null or empty"); + } + if (tagk == null || tagk.isEmpty()) { + throw new IllegalArgumentException( + "The tagk cannot be null or empty"); + } + + final Pair, Constructor> filter_meta = + tagv_filter_map.get(type); + if (filter_meta == null) { + throw new IllegalArgumentException( + "Could not find a tag value filter of the type: " + type); + } + final Constructor ctor = filter_meta.getValue(); + final TagVFilter tagv_filter; + try { + tagv_filter = ctor.newInstance(tagk, filter); + } catch (IllegalArgumentException e) { + throw e; + } catch (InstantiationException e) { + throw new RuntimeException("Failed to instantiate filter: " + type, e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to instantiate filter: " + type, e); + } catch (InvocationTargetException e) { + if (e.getCause() != null) { + throw (RuntimeException)e.getCause(); + } + throw new RuntimeException("Failed to instantiate filter: " + type, e); + } + + tagv_filter.setGroupBy(group_by); + return tagv_filter; + } + } +} diff --git a/src/query/filter/TagVLiteralOrFilter.java b/src/query/filter/TagVLiteralOrFilter.java new file mode 100644 index 0000000000..48f25ae439 --- /dev/null +++ b/src/query/filter/TagVLiteralOrFilter.java @@ -0,0 +1,210 @@ +// 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.filter; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.Config; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Objects; +import com.stumbleupon.async.Deferred; + +/** + * A filter that lets the user list one or more explicit strings that should + * be included in a result set for aggregation. + * @since 2.2 + */ +public class TagVLiteralOrFilter extends TagVFilter { + + /** Name of this filter */ + final public static String FILTER_NAME = "literal_or"; + + /** A list of strings to match on */ + final protected Set literals; + + /** Whether or not the match should be case insensitive */ + final protected boolean case_insensitive; + + /** + * The default Ctor that disables case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The filter to match on + * @throws IllegalArgumentException if the tagk or filter were empty or null + */ + public TagVLiteralOrFilter(final String tagk, final String filter) { + this(tagk, filter, false); + } + + /** + * A ctor that allows enabling case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The filter to match on + * @param case_insensitive Whether or not to match on case + * @throws IllegalArgumentException if the tagk or filter were empty or null + */ + public TagVLiteralOrFilter(final String tagk, final String filter, + final boolean case_insensitive) { + super(tagk, filter); + this.case_insensitive = case_insensitive; + + // we have to have at least one character. + if (filter == null || filter.isEmpty()) { + throw new IllegalArgumentException("Filter cannot be null or empty"); + } + if (filter.length() == 1 && filter.charAt(0) == '|') { + throw new IllegalArgumentException("Filter must contain more than just a pipe"); + } + final String[] split = filter.split("\\|"); + if (case_insensitive) { + for (int i = 0; i < split.length; i++) { + split[i] = split[i].toLowerCase(); + } + } + literals = new HashSet(Arrays.asList(split)); + } + + @Override + public boolean match(final Map tags) { + final String tagv = tags.get(tagk); + if (tagv == null) { + return false; + } + return literals.contains(case_insensitive ? tagv.toLowerCase() : tagv); + } + + @Override + public String debugInfo() { + return "{literals=" + literals + ", case=" + case_insensitive + "}"; + } + + /** + * Overridden here so that we can resolve the literal values if we don't have + * too many of them AND we're not searching with case insensitivity. + */ + @Override + public Deferred resolveTagkName(final TSDB tsdb) { + final Config config = tsdb.getConfig(); + + // resolve tag values if the filter is NOT case insensitive and there are + // fewer literals than the expansion limit + if (!case_insensitive && + literals.size() <= config.getInt("tsd.query.filter.expansion_limit")) { + return resolveTags(tsdb, literals); + } else { + return super.resolveTagkName(tsdb); + } + } + + /** @return Whether or not this filter has case insensitivity enabled */ + @JsonIgnore + public boolean isCaseInsensitive() { + return case_insensitive; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVLiteralOrFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVLiteralOrFilter filter = (TagVLiteralOrFilter)obj; + return Objects.equal(tagk, filter.tagk) + && Objects.equal(literals, filter.literals) + && Objects.equal(case_insensitive, filter.case_insensitive); + } + + @Override + public int hashCode() { + return Objects.hashCode(tagk, literals, case_insensitive); + } + + @Override + public String getType() { + return FILTER_NAME; + } + + /** @return a string describing the filter */ + public static String description() { + return "Accepts one or more exact values and matches if the series contains " + + "any of them. Multiple values can be included and must be seperated " + + "by the | (pipe) character. The filter is case sensitive and will not " + + "allow characters that TSDB does not allow at write time."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=literal_or(web01), host=literal_or(web01|web02|web03) " + + "{\"type\":\"literal_or\",\"tagk\":\"host\"," + + "\"filter\":\"web01|web02|web03\",\"groupBy\":false}"; + } + + /** + * Case insensitive version + */ + public static class TagVILiteralOrFilter extends TagVLiteralOrFilter { + + /** Name of this filter */ + final public static String FILTER_NAME = "iliteral_or"; + + public TagVILiteralOrFilter(final String tagk, final String filter) { + super(tagk, filter, true); + } + + @Override + public String getType() { + return FILTER_NAME; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVILiteralOrFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVILiteralOrFilter filter = (TagVILiteralOrFilter)obj; + return Objects.equal(tagk, filter.tagk) + && Objects.equal(literals, filter.literals) + && Objects.equal(case_insensitive, filter.case_insensitive); + } + + /** @return a string describing the filter */ + public static String description() { + return "Accepts one or more exact values and matches if the series contains " + + "any of them. Multiple values can be included and must be seperated " + + "by the | (pipe) character. The filter is case insensitive and will not " + + "allow characters that TSDB does not allow at write time."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=iliteral_or(web01), host=iliteral_or(web01|web02|web03) " + + "{\"type\":\"iliteral_or\",\"tagk\":\"host\"," + + "\"filter\":\"web01|web02|web03\",\"groupBy\":false}"; + } + } +} diff --git a/src/query/filter/TagVNotKeyFilter.java b/src/query/filter/TagVNotKeyFilter.java new file mode 100644 index 0000000000..8c62f64f3f --- /dev/null +++ b/src/query/filter/TagVNotKeyFilter.java @@ -0,0 +1,71 @@ +package net.opentsdb.query.filter; + +import java.util.Map; + +import com.google.common.base.Objects; + +public class TagVNotKeyFilter extends TagVFilter { + /** Name of this filter */ + final public static String FILTER_NAME = "not_key"; + + public TagVNotKeyFilter(final String tagk, final String filter) { + super(tagk, ""); + if (filter != null && filter.length() > 0) { + throw new IllegalArgumentException("The filter must be empty for the " + + FILTER_NAME + " filter"); + } + not_key = true; + } + + @Override + public boolean match(Map tags) { + if (tags.containsKey(tagk)) { + return false; + } + return true; + } + + @Override + public String getType() { + return FILTER_NAME; + } + + @Override + public String debugInfo() { + return "{}"; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVRegexFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVNotKeyFilter filter = (TagVNotKeyFilter)obj; + return Objects.equal(tagk, filter.tagk); + } + + @Override + public int hashCode() { + return Objects.hashCode(tagk); + } + + /** @return a string describing the filter */ + public static String description() { + return "Skips any time series with the given tag key, regardless of the " + + "value. This can be useful for situations where a metric has " + + "inconsistent tag sets. NOTE: The filter value must be null or an " + + "empty string."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=not_key() {\"type\":\"not_key\",\"tagk\":\"host\"," + + "\"filter\":\"\",\"groupBy\":false}"; + } +} diff --git a/src/query/filter/TagVNotLiteralOrFilter.java b/src/query/filter/TagVNotLiteralOrFilter.java new file mode 100644 index 0000000000..8bf3f5da7e --- /dev/null +++ b/src/query/filter/TagVNotLiteralOrFilter.java @@ -0,0 +1,185 @@ +// 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.filter; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Objects; + +/** + * A filter that lets the user list one or more explicit strings that should + * NOT be included in a result set for aggregation. + * @since 2.2 + */ +public class TagVNotLiteralOrFilter extends TagVFilter { + + /** Name of this filter */ + final public static String FILTER_NAME = "not_literal_or"; + + /** A list of strings to match on */ + final protected Set literals; + + /** Whether or not the match should be case insensitive */ + final protected boolean case_insensitive; + + /** + * The default Ctor that disables case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The filter to match on + * @throws IllegalArgumentException if the tagk or filter were empty or null + */ + public TagVNotLiteralOrFilter(final String tagk, final String filter) { + this(tagk, filter, false); + } + + /** + * A ctor that allows enabling case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The filter to match on + * @param case_insensitive Whether or not to match on case + * @throws IllegalArgumentException if the tagk or filter were empty or null + */ + public TagVNotLiteralOrFilter(final String tagk, final String filter, + final boolean case_insensitive) { + super(tagk, filter); + this.case_insensitive = case_insensitive; + + // we have to have at least one character. + if (filter == null || filter.length() < 2) { + throw new IllegalArgumentException("Filter cannot be null or empty"); + } + final String[] split = filter.split("\\|"); + if (case_insensitive) { + for (int i = 0; i < split.length; i++) { + split[i] = split[i].toLowerCase(); + } + } + literals = new HashSet(Arrays.asList(split)); + } + + @Override + public boolean match(final Map tags) { + final String tagv = tags.get(tagk); + if (tagv == null) { + return true; + } + return !(literals.contains(case_insensitive ? tagv.toLowerCase() : tagv)); + } + + @Override + public String debugInfo() { + return "{literals=" + literals + ", case=" + case_insensitive + "}"; + } + + /** @return Whether or not this filter has case insensitivity enabled */ + @JsonIgnore + public boolean isCaseInsensitive() { + return case_insensitive; + } + + @Override + public String getType() { + return FILTER_NAME; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVNotLiteralOrFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVNotLiteralOrFilter filter = (TagVNotLiteralOrFilter)obj; + return Objects.equal(tagk, filter.tagk) + && Objects.equal(literals, filter.literals) + && Objects.equal(case_insensitive, filter.case_insensitive); + } + + @Override + public int hashCode() { + return Objects.hashCode(tagk, literals, case_insensitive); + } + + /** @return a string describing the filter */ + public static String description() { + return "Accepts one or more exact values and matches if the series does NOT " + + "contain any of them. Multiple values can be included and must be " + + "seperated by the | (pipe) character. The filter is case sensitive " + + "and will not allow characters that TSDB does not allow at write time."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=not_literal_or(web01), host=not_literal_or(web01|web02|web03) " + + "{\"type\":\"not_literal_or\",\"tagk\":\"host\"," + + "\"filter\":\"web01|web02|web03\",\"groupBy\":false}"; + } + + /** + * Case insensitive version + */ + public static class TagVNotILiteralOrFilter extends TagVNotLiteralOrFilter { + + /** Name of this filter */ + final public static String FILTER_NAME = "not_iliteral_or"; + + public TagVNotILiteralOrFilter(final String tagk, final String filter) { + super(tagk, filter, true); + } + + @Override + public String getType() { + return FILTER_NAME; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVNotILiteralOrFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVNotILiteralOrFilter filter = (TagVNotILiteralOrFilter)obj; + return Objects.equal(tagk, filter.tagk) + && Objects.equal(literals, filter.literals) + && Objects.equal(case_insensitive, filter.case_insensitive); + } + + /** @return a string describing the filter */ + public static String description() { + return "Accepts one or more exact values and matches if the series does NOT " + + "contain any of them. Multiple values can be included and must be " + + "seperated by the | (pipe) character. The filter is case insensitive " + + "and will not allow characters that TSDB does not allow at write time."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=not_iliteral_or(web01), host=not_iliteral_or(web01|web02|web03) " + + "{\"type\":\"not_iliteral_or\",\"tagk\":\"host\"," + + "\"filter\":\"web01|web02|web03\",\"groupBy\":false}"; + } + } +} diff --git a/src/query/filter/TagVRegexFilter.java b/src/query/filter/TagVRegexFilter.java new file mode 100644 index 0000000000..56f4d089c2 --- /dev/null +++ b/src/query/filter/TagVRegexFilter.java @@ -0,0 +1,106 @@ +// 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.filter; + +import java.util.Map; +import java.util.regex.Pattern; + +import com.google.common.base.Objects; + +/** + * A filter that allows for regular expression matching on tag values. + * @since 2.2 + */ +public class TagVRegexFilter extends TagVFilter { + /** Name of this filter */ + final public static String FILTER_NAME = "regexp"; + + /** The compiled pattern */ + final Pattern pattern; + + /** + * The default Ctor that disables case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The filter to match on + * @throws IllegalArgumentException if the tagk or filter were empty or null + * @throws PatternSyntaxException if the pattern was invalid + */ + public TagVRegexFilter(final String tagk, final String filter) { + super(tagk, filter); + // we have to have at least one character. + if (filter == null || filter.length() < 1) { + throw new IllegalArgumentException("Filter cannot be null or empty"); + } + pattern = Pattern.compile(filter); + } + + @Override + public boolean match(final Map tags) { + final String tagv = tags.get(tagk); + if (tagv == null) { + return false; + } + return pattern.matcher(tagv).find(); + } + + @Override + public String debugInfo() { + return "{pattern=" + pattern.toString() + "}"; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVRegexFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVRegexFilter filter = (TagVRegexFilter)obj; + // NOTE: apparently different pattern objects with the SAME pattern will + // return a different hash. *sigh*. So cast the pattern to a string, THEN + // compare. + return Objects.equal(tagk, filter.tagk) + && Objects.equal(pattern.pattern(), filter.pattern.pattern()); + } + + @Override + public int hashCode() { + // NOTE: apparently different pattern objects with the SAME pattern will + // return a different hash. *sigh*. So cast the pattern to a string, THEN + // compare. + return Objects.hashCode(tagk, pattern.pattern()); + } + + @Override + public String getType() { + return FILTER_NAME; + } + + /** @return a string describing the filter */ + public static String description() { + return "Provides full, POSIX compliant regular expression using the " + + "built in Java Pattern class. Note that an expression containing " + + "curly braces {} will not parse properly in URLs. If the pattern " + + "is not a valid regular expression then an exception will be raised."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=regexp(.*) {\"type\":\"regexp\",\"tagk\":\"host\"," + + "\"filter\":\".*\",\"groupBy\":false}"; + } +} diff --git a/src/query/filter/TagVWildcardFilter.java b/src/query/filter/TagVWildcardFilter.java new file mode 100644 index 0000000000..11898e84d0 --- /dev/null +++ b/src/query/filter/TagVWildcardFilter.java @@ -0,0 +1,228 @@ +// 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.filter; + +import java.util.Arrays; +import java.util.Map; + +import net.opentsdb.core.Tags; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Objects; + +/** + * Performs basic wild card searching. It supports prefix, postfix, infix, + * multi-infix and case insensitive matching. The wildcard character is + * an asterisk. If case insensitivity is enabled, we simply drop everything + * to lower case. + * @since 2.2 + */ +public class TagVWildcardFilter extends TagVFilter { + + /** Name of this filter */ + final public static String FILTER_NAME = "wildcard"; + + /** Whether or not the filter had a postfix asterisk */ + protected final boolean has_postfix; + + /** Whether or not the filter had a prefix asterisk */ + protected final boolean has_prefix; + + /** The individual components to match on */ + protected final String[] components; + + /** Whether or not we'll match case */ + protected boolean case_insensitive; + + /** + * The default Ctor that disables case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The wildcard filter to match on + * @throws IllegalArgumentException if the tagk or filter were empty or null + */ + public TagVWildcardFilter(final String tagk, final String filter) { + this(tagk, filter, false); + } + + /** + * A ctor that allows enabling case insensitivity + * @param tagk The tag key to associate with this filter + * @param filter The wildcard filter to match on + * @param case_insensitive Whether or not to match on case + * @throws IllegalArgumentException if the tagk or filter were empty or null + */ + public TagVWildcardFilter(final String tagk, final String filter, + final boolean case_insensitive) { + super(tagk, filter); + this.case_insensitive = case_insensitive; + + if (filter == null || filter.length() < 1) { + throw new IllegalArgumentException("Filter cannot be null or empty"); + } + String actual = case_insensitive ? filter.toLowerCase() : filter; + if (!actual.contains("*")) { + throw new IllegalArgumentException("Filter must contain an asterisk"); + } + + if (actual.charAt(0) == '*') { + has_postfix = true; + while (actual.charAt(0) == '*') { + if (actual.length() < 2) { + break; + } + actual = actual.substring(1); + } + } else { + has_postfix = false; + } + if (actual.charAt(actual.length() - 1) == '*') { + has_prefix = true; + while(actual.charAt(actual.length() - 1) == '*') { + if (actual.length() < 2) { + break; + } + actual = actual.substring(0, actual.length() - 1); + } + } else { + has_prefix = false; + } + if (actual.indexOf('*') > 0) { + components = Tags.splitString(actual, '*'); + } else { + components = new String[1]; + components[0] = actual; + } + + // avoid resolving UIDs at scan time + if (components.length == 1 && components[0].equals("*")) { + post_scan = false; + } + } + + @Override + public boolean match(final Map tags) { + String tagv = tags.get(tagk); + if (tagv == null) { + return false; + } else if (components.length == 1 && components[0].equals("*")) { + // match all + return true; + } else if (case_insensitive) { + tags.get(tagk).toLowerCase(); + } + if (has_postfix && !has_prefix && + !tagv.endsWith(components[components.length-1])) { + return false; + } + if (has_prefix && !has_postfix && !tagv.startsWith(components[0])) { + return false; + } + int idx = 0; + for (int i = 0; i < components.length; i++) { + if (tagv.indexOf(components[i], idx) < 0) { + return false; + } + idx += components[i].length(); + } + return true; + } + + @Override + public String debugInfo() { + return "{components=" + Arrays.toString(components) + ", case=" + + case_insensitive + "}"; + } + + /** @return Whether or not this filter has case insensitivity enabled */ + @JsonIgnore + public boolean isCaseInsensitive() { + return case_insensitive; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof TagVWildcardFilter)) { + return false; + } + if (obj == this) { + return true; + } + final TagVWildcardFilter filter = (TagVWildcardFilter)obj; + return Objects.equal(tagk, filter.tagk) + && Arrays.equals(components, filter.components) + && Objects.equal(case_insensitive, filter.case_insensitive); + } + + @Override + public int hashCode() { + return Objects.hashCode(tagk, Arrays.hashCode(components), case_insensitive); + } + + @Override + public String getType() { + return FILTER_NAME; + } + + /** @return a string describing the filter */ + public static String description() { + return "Performs pre, post and in-fix glob matching of values. The globs " + + "are case sensitive and multiple wildcards can be used. The wildcard " + + "character is the * (asterisk). At least one wildcard must be " + + "present in the filter value. A wildcard by itself can be used as " + + "well to match on any value for the tag key."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=wildcard(web*), host=wildcard(web*.tsdb.net) " + + "{\"type\":\"wildcard\",\"tagk\":\"host\"," + + "\"filter\":\"web*.tsdb.net\",\"groupBy\":false}"; + } + + /** + * Case insensitive version + */ + public static class TagVIWildcardFilter extends TagVWildcardFilter { + /** Name of this filter */ + final public static String FILTER_NAME = "iwildcard"; + + public TagVIWildcardFilter(final String tagk, final String filter) { + super(tagk, filter, true); + } + + @Override + public String getType() { + return FILTER_NAME; + } + + /** @return a string describing the filter */ + public static String description() { + return "Performs pre, post and in-fix glob matching of values. The globs " + + "are case insensitive and multiple wildcards can be used. The wildcard " + + "character is the * (asterisk). Case insensitivity is achieved by " + + "dropping all values to lower case. At least one wildcard must be " + + "present in the filter value. A wildcard by itself can be used as " + + "well to match on any value for the tag key."; + } + + /** @return a list of examples showing how to use the filter */ + public static String examples() { + return "host=iwildcard(web*), host=iwildcard(web*.tsdb.net) " + + "{\"type\":\"iwildcard\",\"tagk\":\"host\"," + + "\"filter\":\"web*.tsdb.net\",\"groupBy\":false}"; + } + } +} diff --git a/test/query/filter/TestTagVFilter.java b/test/query/filter/TestTagVFilter.java new file mode 100644 index 0000000000..c3f7d13450 --- /dev/null +++ b/test/query/filter/TestTagVFilter.java @@ -0,0 +1,404 @@ +// 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.filter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.opentsdb.core.BaseTsdbTest; +import net.opentsdb.core.TSDB; +import net.opentsdb.uid.NoSuchUniqueName; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.DeferredGroupException; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class }) +public class TestTagVFilter extends BaseTsdbTest { + + @Test (expected = IllegalArgumentException.class) + public void getFilterNullTagk() throws Exception { + TagVFilter.getFilter(null, "myflter"); + } + + @Test (expected = IllegalArgumentException.class) + public void getFilterEmptyTagk() throws Exception { + TagVFilter.getFilter(null, "myflter"); + } + + @Test (expected = IllegalArgumentException.class) + public void getFilterEmptyFilter() throws Exception { + TagVFilter.getFilter(TAGK_STRING, ""); + } + + @Test (expected = IllegalArgumentException.class) + public void getFilterNullFilter() throws Exception { + TagVFilter.getFilter(TAGK_STRING, null); + } + + @Test + public void getFilterGroupBy() throws Exception { + assertNull(TagVFilter.getFilter(TAGK_STRING, "*")); + } + + @Test + public void getFilterLiteral() throws Exception { + assertNull(TagVFilter.getFilter(TAGK_STRING, "web01")); + } + + @Test + public void getFilterGroupByPiped() throws Exception { + assertNull(TagVFilter.getFilter(TAGK_STRING, "web01|web02")); + } + + @Test + public void getFilterWildcard() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + TagVWildcardFilter.FILTER_NAME + "(*bonk.com)"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVWildcardFilter); + assertFalse(((TagVWildcardFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterWildcardInsensitive() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + TagVWildcardFilter.TagVIWildcardFilter.FILTER_NAME + "(*bonk.com)"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVWildcardFilter); + assertTrue(((TagVWildcardFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterWildcardFatfinger() throws Exception { + // falls through to the shortcut + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + "wil@*sugarbean"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVWildcardFilter); + assertTrue(((TagVWildcardFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterWildcardImplicit() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, "*bonk.com"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVWildcardFilter); + assertTrue(((TagVWildcardFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterPipe() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + TagVLiteralOrFilter.FILTER_NAME + "(quirm|bonk)"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVLiteralOrFilter); + assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterPipeInsensitive() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + TagVLiteralOrFilter.TagVILiteralOrFilter.FILTER_NAME + "(quirm|bonk)"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVLiteralOrFilter); + assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterPipeFatfinger() throws Exception { + assertNull(TagVFilter.getFilter(TAGK_STRING, "lite@sugarbean|granny")); + } + + @Test + public void getFilterRegex() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + TagVRegexFilter.FILTER_NAME + "(.*sugarbean)"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVRegexFilter); + } + + @Test + public void getFilterRegexFatFinger() throws Exception { + // falls through to the implicity + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, "rexp@.*sugarbean"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVWildcardFilter); + assertTrue(((TagVWildcardFilter)filter).isCaseInsensitive()); + } + + @Test + public void getFilterRegexCase() throws Exception { + final TagVFilter filter = TagVFilter.getFilter(TAGK_STRING, + TagVRegexFilter.FILTER_NAME.toUpperCase() + "(.*sugarbean)"); + assertEquals(TAGK_STRING, filter.getTagk()); + assertTrue(filter instanceof TagVRegexFilter); + } + + @Test (expected = IllegalArgumentException.class) + public void getFilterMissingClosingParens() throws Exception { + TagVFilter.getFilter(TAGK_STRING, TagVRegexFilter.FILTER_NAME + "(.*sugarbean"); + } + + @Test (expected = IllegalArgumentException.class) + public void getFilterEmptyParens() throws Exception { + TagVFilter.getFilter(TAGK_STRING, TagVRegexFilter.FILTER_NAME + "()"); + } + + @Test (expected = IllegalArgumentException.class) + public void getFilterUnknownType() throws Exception { + TagVFilter.getFilter(TAGK_STRING, "dummyfilter(nothere)"); + } + + @Test + public void resolveName() throws Exception { + final TagVFilter filter = new TagVWildcardFilter(TAGK_STRING, "*omnia"); + filter.resolveTagkName(tsdb).join(); + assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); + assertTrue(filter.getTagVUids().isEmpty()); + } + + @Test + public void resolveNameLiteral() throws Exception { + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01"); + filter.resolveTagkName(tsdb).join(); + assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); + assertEquals(1, filter.getTagVUids().size()); + assertArrayEquals(TAGV_BYTES, filter.getTagVUids().get(0)); + } + + @Test + public void resolveNameLiterals() throws Exception { + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01|web02"); + filter.resolveTagkName(tsdb).join(); + assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); + assertEquals(2, filter.getTagVUids().size()); + assertArrayEquals(TAGV_BYTES, filter.getTagVUids().get(0)); + assertArrayEquals(TAGV_B_BYTES, filter.getTagVUids().get(1)); + } + + @Test (expected = DeferredGroupException.class) + public void resolveNameLiteralsNSUNTagV() throws Exception { + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01|web03"); + filter.resolveTagkName(tsdb).join(); + } + + @Test + public void resolveNameLiteralsNSUNTagvSkipped() throws Exception { + config.overrideConfig("tsd.query.skip_unresolved_tagvs", "true"); + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01|web03"); + filter.resolveTagkName(tsdb).join(); + assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); + assertEquals(1, filter.getTagVUids().size()); + assertArrayEquals(TAGV_BYTES, filter.getTagVUids().get(0)); + } + + @Test + public void resolveNameLiteralsTooMany() throws Exception { + config.overrideConfig("tsd.query.filter.expansion_limit", "1"); + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01|web02"); + filter.resolveTagkName(tsdb).join(); + assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); + assertTrue(filter.getTagVUids().isEmpty()); + } + + @Test + public void resolveNameLiteralsCaseInsensitive() throws Exception { + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01|web02", + true); + filter.resolveTagkName(tsdb).join(); + assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); + assertTrue(filter.getTagVUids().isEmpty()); + } + + @Test (expected = NoSuchUniqueName.class) + public void resolveNameNSUN() throws Exception { + final TagVFilter filter = new TagVWildcardFilter(NSUN_TAGK, "*omnia"); + filter.resolveTagkName(tsdb).join(); + } + + @Test (expected = NullPointerException.class) + public void resolveNameNullTSDB() throws Exception { + new TagVWildcardFilter("host", "*omnia").resolveTagkName(null); + } + + @Test + public void comparableTest() throws Exception { + final TagVFilter filter_a = new TagVWildcardFilter("host", "*omnia"); + Whitebox.setInternalState(filter_a, "tagk_bytes", new byte[] { 0, 0, 0, 1 }); + final TagVFilter filter_b = new TagVRegexFilter("dc", ".*katch"); + Whitebox.setInternalState(filter_b, "tagk_bytes", new byte[] { 0, 0, 0, 2 }); + + assertEquals(0, filter_a.compareTo(filter_a)); + assertEquals(-1, filter_a.compareTo(filter_b)); + assertEquals(1, filter_b.compareTo(filter_a)); + + Whitebox.setInternalState(filter_a, "tagk_bytes", (byte[])null); + assertEquals(0, filter_a.compareTo(filter_a)); + assertEquals(-1, filter_a.compareTo(filter_b)); + assertEquals(1, filter_b.compareTo(filter_a)); + + Whitebox.setInternalState(filter_b, "tagk_bytes", (byte[])null); + assertEquals(0, filter_a.compareTo(filter_a)); + assertEquals(0, filter_a.compareTo(filter_b)); + assertEquals(0, filter_b.compareTo(filter_a)); + + } + + @Test + public void stripParentheses() throws Exception { + assertEquals(".*sugarbean", TagVFilter.stripParentheses( + TagVRegexFilter.FILTER_NAME + "(.*sugarbean)")); + } + + @Test + public void stripParenthesesEmptyParentheses() throws Exception { + // let the filter's ctor handle this case + assertEquals("", TagVFilter.stripParentheses( + TagVRegexFilter.FILTER_NAME + "()")); + } + + @Test (expected = IllegalArgumentException.class) + public void stripParenthesesMissingClosing() throws Exception { + TagVFilter.stripParentheses(TagVRegexFilter.FILTER_NAME + "(.*sugarbean"); + } + + @Test (expected = IllegalArgumentException.class) + public void stripParenthesesMissingOpening() throws Exception { + TagVFilter.stripParentheses("regexp.*sugarbean)"); + } + + @Test (expected = IllegalArgumentException.class) + public void stripParenthesesNull() throws Exception { + TagVFilter.stripParentheses(null); + } + + @Test (expected = IllegalArgumentException.class) + public void stripParenthesesEmpty() throws Exception { + TagVFilter.stripParentheses(""); + } + + @SuppressWarnings("unchecked") + @Test + public void tagsToFiltersOldGroupBy() throws Exception { + final Map tags = new HashMap(3); + tags.put("host", "quirm"); // literal + tags.put("owner", "vimes|vetinary"); // pipe + tags.put("colo", "*"); // group by all + final List filters = new ArrayList(3); + TagVFilter.tagsToFilters(tags, filters); + + assertEquals(3, filters.size()); + for (final TagVFilter filter : filters) { + if (filter.getTagk().equals("host")) { + assertTrue(filter instanceof TagVLiteralOrFilter); + assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + assertEquals(1, ((Set)Whitebox + .getInternalState(filter, "literals")).size()); + } else if (filter.getTagk().equals("owner")) { + assertTrue(filter instanceof TagVLiteralOrFilter); + assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + assertEquals(2, ((Set)Whitebox + .getInternalState(filter, "literals")).size()); + } else if (filter.getTagk().equals("colo")) { + assertTrue(filter instanceof TagVWildcardFilter); + assertTrue(((TagVWildcardFilter)filter).isCaseInsensitive()); + } else { + fail("Unexpected filter type: " + filter); + } + assertTrue(filter.isGroupBy()); + } + } + + @Test + public void tagsToFiltersNewFunctions() throws Exception { + final Map tags = new HashMap(4); + tags.put("host", "*beybi"); + tags.put("owner", "wildcard(*snapcase*)"); + tags.put("colo", "regexp(.*opolis)"); + tags.put("geo", "literal_or(tsort|chalk)"); + final List filters = new ArrayList(3); + TagVFilter.tagsToFilters(tags, filters); + + assertEquals(4, filters.size()); + for (final TagVFilter filter : filters) { + if (filter.getTagk().equals("host")) { + assertTrue(filter instanceof TagVWildcardFilter); + assertTrue(((TagVWildcardFilter)filter).isCaseInsensitive()); + } else if (filter.getTagk().equals("owner")) { + assertTrue(filter instanceof TagVWildcardFilter); + assertFalse(((TagVWildcardFilter)filter).isCaseInsensitive()); + } else if (filter.getTagk().equals("colo")) { + assertTrue(filter instanceof TagVRegexFilter); + } else if (filter.getTagk().equals("geo")) { + assertTrue(filter instanceof TagVLiteralOrFilter); + assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } else { + fail("Unexpected filter type: " + filter); + } + assertTrue(filter.isGroupBy()); + } + } + + @Test (expected = IllegalArgumentException.class) + public void tagsToFiltersNoSuchFunction() throws Exception { + final Map tags = new HashMap(1); + tags.put("host", "doesnotexist(*beybi)"); + final List filters = new ArrayList(1); + TagVFilter.tagsToFilters(tags, filters); + } + + @Test + public void tagsToFiltersDuplicate() throws Exception { + final Map tags = new HashMap(1); + tags.put("host", "*beybi"); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "*beybi", true)); + assertFalse(filters.get(0).isGroupBy()); + TagVFilter.tagsToFilters(tags, filters); + assertEquals(1, filters.size()); + assertTrue(filters.get(0).isGroupBy()); + } + + @Test + public void tagsToFiltersSameTagDiffValues() throws Exception { + final Map tags = new HashMap(1); + tags.put("host", "*beybi"); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "*helit", true)); + assertFalse(filters.get(0).isGroupBy()); + TagVFilter.tagsToFilters(tags, filters); + assertEquals(2, filters.size()); + } + + // TODO - test the plugin loader similar to the other plugins +} diff --git a/test/query/filter/TestTagVLiteralOrFilter.java b/test/query/filter/TestTagVLiteralOrFilter.java new file mode 100644 index 0000000000..6931016d75 --- /dev/null +++ b/test/query/filter/TestTagVLiteralOrFilter.java @@ -0,0 +1,172 @@ +package net.opentsdb.query.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +public class TestTagVLiteralOrFilter { + private static final String TAGK = "host"; + private Map tags; + + @Before + public void before() throws Exception { + tags = new HashMap(1); + tags.put(TAGK, "CMTDibbler"); + } + + @Test + public void matchMiddle() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|CMTDibbler|Slant"); + assertTrue(filter.match(tags)); + assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchStart() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "CMTDibbler|LutZe|Slant"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchEnd() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|Slant|CMTDibbler"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchNoPipes() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "CMTDibbler"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPipeNoValueAfter() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "CMTDibbler|"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPipeNoValueBefore() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "|CMTDibbler"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchFail() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|Keli|Slant"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchFailCase() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|CMtDibbler|Slant"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchCaseInsensitive() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|CMtDibbler|Slant", true); + assertTrue(filter.match(tags)); + assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchCaseInsensitiveFail() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|CMtDibble|Slant", true); + assertFalse(filter.match(tags)); + assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchNoSuchTagk() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|Keli|Slant"); + tags.clear(); + tags.put("colo", "lga"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchNoSuchTagkCaseInsensitive() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|Keli|Slant", true); + tags.clear(); + tags.put("colo", "lga"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchSingle() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, "CMTDibbler"); + assertTrue(filter.match(tags)); + assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchSingleCaseInsensitive() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, "cmtDibbler", true); + assertTrue(filter.match(tags)); + assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTagk() throws Exception { + new TagVLiteralOrFilter(null, "LutZe|Keli|Slant"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyTagk() throws Exception { + new TagVLiteralOrFilter("", "LutZe|Keli|Slant"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullFilter() throws Exception { + new TagVLiteralOrFilter(TAGK, null); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyFilter() throws Exception { + new TagVLiteralOrFilter(TAGK, ""); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorJustAPipe() throws Exception { + new TagVLiteralOrFilter(TAGK, "|"); + } + + @Test + public void toStringTest() throws Exception { + TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); + assertTrue(filter.toString().contains("literal_or")); + } + + @Test + public void hashCodeAndEqualsTest() throws Exception { + TagVFilter filter_a = new TagVLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); + TagVFilter filter_b = new TagVLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); + TagVFilter filter_c = new TagVLiteralOrFilter(TAGK, "LutZe|Slant"); + TagVFilter filter_d = new TagVLiteralOrFilter(TAGK, "LutZe|cmtdibbler|Slant"); + + assertEquals(filter_a.hashCode(), filter_b.hashCode()); + assertFalse(filter_a.hashCode() == filter_c.hashCode()); + assertFalse(filter_a.hashCode() == filter_d.hashCode()); + + assertEquals(filter_a, filter_b); + assertFalse(filter_a.equals(filter_c)); + assertFalse(filter_a.equals(filter_d)); + } +} diff --git a/test/query/filter/TestTagVNotKeyFilter.java b/test/query/filter/TestTagVNotKeyFilter.java new file mode 100644 index 0000000000..25aed9b950 --- /dev/null +++ b/test/query/filter/TestTagVNotKeyFilter.java @@ -0,0 +1,47 @@ +package net.opentsdb.query.filter; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +public class TestTagVNotKeyFilter { + private static final String TAGK = "host"; + private static final String TAGK2 = "owner"; + private Map tags; + + @Before + public void before() throws Exception { + tags = new HashMap(1); + tags.put(TAGK, "ogg-01.ops.ankh.morpork.com"); + tags.put(TAGK2, "Hrun"); + } + + @Test + public void matchHasKey() throws Exception { + TagVFilter filter = new TagVNotKeyFilter(TAGK, ""); + assertFalse(filter.match(tags)); + } + + @Test + public void matchDoesNotHaveKey() throws Exception { + TagVFilter filter = new TagVNotKeyFilter("colo", ""); + assertTrue(filter.match(tags)); + } + + @Test + public void ctorNullFilter() throws Exception { + TagVFilter filter = new TagVNotKeyFilter(TAGK, null); + assertTrue(filter.isNotKeyFilter()); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorFilterHasValue() throws Exception { + assertNotNull(new TagVNotKeyFilter(TAGK, "Evadne")); + } +} diff --git a/test/query/filter/TestTagVNotLiteralOrFilter.java b/test/query/filter/TestTagVNotLiteralOrFilter.java new file mode 100644 index 0000000000..5270afee1c --- /dev/null +++ b/test/query/filter/TestTagVNotLiteralOrFilter.java @@ -0,0 +1,172 @@ +package net.opentsdb.query.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +public class TestTagVNotLiteralOrFilter { + private static final String TAGK = "host"; + private Map tags; + + @Before + public void before() throws Exception { + tags = new HashMap(1); + tags.put(TAGK, "CMTDibbler"); + } + + @Test + public void matchMiddle() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "LutZe|CMTDibbler|Slant"); + assertFalse(filter.match(tags)); + assertFalse(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchStart() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "CMTDibbler|LutZe|Slant"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchEnd() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "LutZe|Slant|CMTDibbler"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchNoPipes() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "CMTDibbler"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPipeNoValueAfter() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "CMTDibbler|"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPipeNoValueBefore() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "|CMTDibbler"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchFail() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "LutZe|Keli|Slant"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchFailCase() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "LutZe|CMtDibbler|Slant"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchCaseInsensitive() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "LutZe|CMtDibbler|Slant", true); + assertFalse(filter.match(tags)); + assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchCaseInsensitiveFail() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, + "LutZe|CMtDibble|Slant", true); + assertTrue(filter.match(tags)); + assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchNoSuchTagk() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|Keli|Slant"); + tags.clear(); + tags.put("colo", "lga"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchNoSuchTagkCaseInsensitive() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|Keli|Slant", true); + tags.clear(); + tags.put("colo", "lga"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchSingle() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "CMTDibbler"); + assertFalse(filter.match(tags)); + assertFalse(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test + public void matchSingleCaseInsensitive() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "cmtDibbler", true); + assertFalse(filter.match(tags)); + assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTagk() throws Exception { + new TagVNotLiteralOrFilter(null, "LutZe|Keli|Slant"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyTagk() throws Exception { + new TagVNotLiteralOrFilter("", "LutZe|Keli|Slant"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullFilter() throws Exception { + new TagVNotLiteralOrFilter(TAGK, null); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyFilter() throws Exception { + new TagVNotLiteralOrFilter(TAGK, ""); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorJustAPipe() throws Exception { + new TagVNotLiteralOrFilter(TAGK, "|"); + } + + @Test + public void toStringTest() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); + assertTrue(filter.toString().contains("literal_or")); + } + + @Test + public void hashCodeAndEqualsTest() throws Exception { + TagVFilter filter_a = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); + TagVFilter filter_b = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); + TagVFilter filter_c = new TagVNotLiteralOrFilter(TAGK, "LutZe|Slant"); + TagVFilter filter_d = new TagVNotLiteralOrFilter(TAGK, "LutZe|cmtdibbler|Slant"); + + assertEquals(filter_a.hashCode(), filter_b.hashCode()); + assertFalse(filter_a.hashCode() == filter_c.hashCode()); + assertFalse(filter_a.hashCode() == filter_d.hashCode()); + + assertEquals(filter_a, filter_b); + assertFalse(filter_a.equals(filter_c)); + assertFalse(filter_a.equals(filter_d)); + } +} diff --git a/test/query/filter/TestTagVRegexFilter.java b/test/query/filter/TestTagVRegexFilter.java new file mode 100644 index 0000000000..ebba4daa0c --- /dev/null +++ b/test/query/filter/TestTagVRegexFilter.java @@ -0,0 +1,126 @@ +package net.opentsdb.query.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.PatternSyntaxException; + +import org.junit.Before; +import org.junit.Test; + +public class TestTagVRegexFilter { + private static final String TAGK = "host"; + private Map tags; + + @Before + public void before() throws Exception { + tags = new HashMap(1); + tags.put(TAGK, "ogg-01.ops.ankh.morpork.com"); + } + + @Test + public void matchExact() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.ankh.morpork.com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPostfix() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, ".*.ops.ankh.morpork.com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPrefix() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.ankh.*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchAnything() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, ".*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchFailed() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.qurim.*"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchGrouping() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, + "ogg-01.ops.(ankh|quirm|tsort).morpork.com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchNumbers() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, + "ogg-\\d+.ops.ankh.morpork.com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchNotEnoughNumbers() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, + "ogg-\\d(3).ops.ankh.morpork.com"); + assertFalse(filter.match(tags)); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTagk() throws Exception { + new TagVRegexFilter(null, "ogg-01.ops.qurim.*"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyTagk() throws Exception { + new TagVRegexFilter("", "ogg-01.ops.qurim.*"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullFilter() throws Exception { + new TagVRegexFilter(TAGK, null); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyFilter() throws Exception { + new TagVRegexFilter(TAGK, ""); + } + + @Test (expected = PatternSyntaxException.class) + public void ctorBadRegex() throws Exception { + new TagVRegexFilter(TAGK, "ogg-\\d(3.ops.ankh.morpork.com"); + } + + @Test + public void toStringTest() throws Exception { + TagVFilter filter = new TagVRegexFilter(TAGK, + "ogg-\\d+.ops.ankh.morpork.com"); + assertTrue(filter.toString().contains("regex")); + } + + @Test + public void hashCodeAndEqualsTest() throws Exception { + TagVFilter filter_a = new TagVRegexFilter(TAGK, + "ogg-\\d+.ops.ankh.morpork.com"); + TagVFilter filter_b = new TagVRegexFilter(TAGK, + "ogg-\\d+.ops.ankh.morpork.com"); + TagVFilter filter_c = new TagVRegexFilter(TAGK, + "ogg-\\d.ops.ankh.morpork.com"); + TagVFilter filter_d = new TagVRegexFilter(TAGK, + "ogg-\\d+.ops.ankh.morpork.co"); + + assertEquals(filter_a.hashCode(), filter_b.hashCode()); + assertFalse(filter_a.hashCode() == filter_c.hashCode()); + assertFalse(filter_a.hashCode() == filter_d.hashCode()); + + assertEquals(filter_a, filter_b); + assertFalse(filter_a.equals(filter_c)); + assertFalse(filter_a.equals(filter_d)); + } +} diff --git a/test/query/filter/TestTagVWildcardFilter.java b/test/query/filter/TestTagVWildcardFilter.java new file mode 100644 index 0000000000..9622557c65 --- /dev/null +++ b/test/query/filter/TestTagVWildcardFilter.java @@ -0,0 +1,320 @@ +package net.opentsdb.query.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +public class TestTagVWildcardFilter { + private static final String TAGK = "host"; + private Map tags; + + @Before + public void before() throws Exception { + tags = new HashMap(1); + tags.put(TAGK, "ogg-01.ops.ankh.morpork.com"); + } + + @Test + public void matchAll() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, "*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchAllNoSuchKey() throws Exception { + TagVFilter filter = new TagVWildcardFilter("hobbes", "*"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPostfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPrefix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchDoubleInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*ops*ank*com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchTripleInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*ops*com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPreAndPostfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*morpork*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPostAndInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*ops*com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPostAndDoubleInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*ops*mor*com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPreAndInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*ops*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPreAndDoubleInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*ops*mor*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchMultiWildcardInfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg***com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchMultiWildcardPrefix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*****"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchMultiWildcardPostfix() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "****com"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchWildcardsEverywhere() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "****ogg*****mor****com****"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchExactPostfix() throws Exception { + tags.put(TAGK, "*ops*mor"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*ops*mor"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchExactPretfix() throws Exception { + tags.put(TAGK, "ogg*ops*"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*ops*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchExactInfix() throws Exception { + tags.put(TAGK, "ogg*ops*mor"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogg*ops*mor"); + assertTrue(filter.match(tags)); + } + + // Make sure this file is encoded in UTF-8 of the following will fail + @Test + public void matchUTF8Postfix() throws Exception { + tags.put(TAGK, "Здравей'_хора"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*хора"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchUTF8Prefix() throws Exception { + tags.put(TAGK, "Здравей'_хора"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "Здр*"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchUTF8Infix() throws Exception { + tags.put(TAGK, "Здравей'_хора"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "Здр*ра"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPostfixFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*.morpork.org"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPrefixFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "magrat*"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchInfixFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "magrat*com"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPreAndPostfixFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*quirm*"); + assertFalse(filter.match(tags)); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullFilter() throws Exception { + new TagVWildcardFilter(TAGK, null); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyFilter() throws Exception { + new TagVWildcardFilter(TAGK, ""); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNoWildcard() throws Exception { + new TagVWildcardFilter(TAGK, "someliteral"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTagk() throws Exception { + new TagVWildcardFilter(null, "*quirm*"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyTagk() throws Exception { + new TagVWildcardFilter("", "*quirm*"); + } + + @Test + public void matchPostfixCaseFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*.MorPork.com"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPrefixCaseFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "Ogg*"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchInfixCaseFail() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogG*Com"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchPostfixCaseInsensitive() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*.MorPork.com", true); + assertTrue(filter.match(tags)); + } + + @Test + public void matchPrefixCaseInsensitive() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "Ogg*", true); + assertTrue(filter.match(tags)); + } + + @Test + public void matchInfixCaseInsensitive() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, + "ogG*Com", true); + assertTrue(filter.match(tags)); + } + + @Test + public void matchNothingButStars() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, "****"); + assertTrue(filter.match(tags)); + } + + @Test + public void matchNoSuchTagk() throws Exception { + final TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.com"); + tags.remove("host"); + tags.put("colo", "lga"); + assertFalse(filter.match(tags)); + } + + @Test + public void matchNoSuchTagkCaseInsensitive() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.com", true); + tags.remove("host"); + tags.put("colo", "lga"); + assertFalse(filter.match(tags)); + } + + @Test + public void toStringTest() throws Exception { + TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*com"); + assertTrue(filter.toString().contains("wild")); + } + + @Test + public void hashCodeAndEqualsTest() throws Exception { + TagVFilter filter_a = new TagVWildcardFilter(TAGK, "ogg*com"); + TagVFilter filter_b = new TagVWildcardFilter(TAGK, "ogg*com"); + TagVFilter filter_c = new TagVWildcardFilter(TAGK, "*com"); + TagVFilter filter_d = new TagVWildcardFilter(TAGK, "Ogg*com"); + + assertEquals(filter_a.hashCode(), filter_b.hashCode()); + assertFalse(filter_a.hashCode() == filter_c.hashCode()); + assertFalse(filter_a.hashCode() == filter_d.hashCode()); + + assertEquals(filter_a, filter_b); + assertFalse(filter_a.equals(filter_c)); + assertFalse(filter_a.equals(filter_d)); + } + +} From 9940da0e482c79a0218662d8e1228c87aa731b59 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 29 May 2015 17:30:46 -0700 Subject: [PATCH 171/826] Add TSDB.getUIDAsync() and cleanup UTS that depend on the old method for mocking. Also add a config default for tagv literal resolution. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 27 +++++- src/utils/Config.java | 1 + test/core/TestTSDB.java | 2 +- test/meta/TestTSUIDQuery.java | 132 +++++--------------------- test/query/filter/TestTagVFilter.java | 4 +- test/search/TestTimeSeriesLookup.java | 41 +++++--- 6 files changed, 77 insertions(+), 130 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 8c87395ef9..8286e4c858 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -339,16 +339,37 @@ public Deferred getUidName(final UniqueIdType type, final byte[] uid) { * @since 2.0 */ public byte[] getUID(final UniqueIdType type, final String name) { + try { + return getUIDAsync(type, name).join(); + } catch (NoSuchUniqueName e) { + throw e; + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + LOG.error("Unexpected exception", e); + throw new RuntimeException(e); + } + } + + /** + * Attempts to find the UID matching a given name asynchronously + * @param type The type of UID + * @param name The name to search for + * @throws IllegalArgumentException if the type is not valid + * @throws NoSuchUniqueName if the name was not found + * @since 2.2 + */ + public Deferred getUIDAsync(final UniqueIdType type, final String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Missing UID name"); } switch (type) { case METRIC: - return this.metrics.getId(name); + return this.metrics.getIdAsync(name); case TAGK: - return this.tag_names.getId(name); + return this.tag_names.getIdAsync(name); case TAGV: - return this.tag_values.getId(name); + return this.tag_values.getIdAsync(name); default: throw new IllegalArgumentException("Unrecognized UID type"); } diff --git a/src/utils/Config.java b/src/utils/Config.java index e40ea5c5b8..e0a389184d 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -490,6 +490,7 @@ protected void setDefaults() { default_map.put("tsd.core.preload_uid_cache.max_entries", "300000"); default_map.put("tsd.core.storage_exception_handler.enable", "false"); default_map.put("tsd.core.uid.random_metrics", "false"); + default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index c958a64d5c..036a456dcb 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -256,7 +256,7 @@ public void getUIDTagvNSU() { tsdb.getUID(UniqueIdType.TAGV, NSUN_TAGV); } - @Test (expected = NullPointerException.class) + @Test (expected = RuntimeException.class) public void getUIDNullType() { tsdb.getUID(null, METRIC_STRING); } diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index 0da499bc19..174352f3f4 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -13,13 +13,11 @@ package net.opentsdb.meta; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; -import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; +import net.opentsdb.core.BaseTsdbTest; import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueName; @@ -41,8 +39,6 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import com.stumbleupon.async.Deferred; - @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @@ -50,33 +46,17 @@ @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, Scanner.class, TSMeta.class, AtomicIncrementRequest.class}) -public final class TestTSUIDQuery { +public final class TestTSUIDQuery extends BaseTsdbTest { private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); - private TSDB tsdb; - private Config config; - private HBaseClient client = mock(HBaseClient.class); - private MockBase storage; - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); private TSUIDQuery query; @Before - public void before() throws Exception { - config = mock(Config.class); - when(config.getString("tsd.storage.hbase.data_table")).thenReturn("tsdb"); - when(config.getString("tsd.storage.hbase.uid_table")).thenReturn("tsdb-uid"); - when(config.getString("tsd.storage.hbase.meta_table")).thenReturn("tsdb-meta"); - when(config.getString("tsd.storage.hbase.tree_table")).thenReturn("tsdb-tree"); - when(config.enable_tsuid_incrementing()).thenReturn(true); - when(config.enable_realtime_ts()).thenReturn(true); - - tsdb = new TSDB(client, config); + public void beforeLocal() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), - "sys.cpu.user".getBytes(MockBase.ASCII())); + METRIC_STRING.getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.user\"," + @@ -95,7 +75,7 @@ public void before() throws Exception { storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), - "host".getBytes(MockBase.ASCII())); + TAGK_STRING.getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGK\",\"name\":\"host\"," + @@ -114,7 +94,7 @@ public void before() throws Exception { storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), - "web01".getBytes(MockBase.ASCII())); + TAGV_STRING.getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGV\",\"name\":\"web01\"," + @@ -169,111 +149,43 @@ public void before() throws Exception { .getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 2, 0, 0, 3, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), - Bytes.fromLong(1L)); - - // replace the "real" field objects with mocks - Field cl = tsdb.getClass().getDeclaredField("client"); - cl.setAccessible(true); - cl.set(tsdb, client); - - Field met = tsdb.getClass().getDeclaredField("metrics"); - met.setAccessible(true); - met.set(tsdb, metrics); - - Field tagk = tsdb.getClass().getDeclaredField("tag_names"); - tagk.setAccessible(true); - tagk.set(tsdb, tag_names); - - Field tagv = tsdb.getClass().getDeclaredField("tag_values"); - tagv.setAccessible(true); - tagv.set(tsdb, tag_values); - - // mock UniqueId - when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("sys.cpu.user")); - when(metrics.getId("sys.cpu.system")) - .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); - when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); - when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("sys.cpu.nice")); - - when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("host")); - when(tag_names.getOrCreateIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getId("dc")) - .thenThrow(new NoSuchUniqueName("dc", "metric")); - when(tag_names.getId("datacenter")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_names.getIdAsync("datacenter")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_names.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("datacenter")); - - when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("web01")); - when(tag_values.getOrCreateIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_values.getIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("web02")); - when(tag_values.getOrCreateIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getId("web03")) - .thenThrow(new NoSuchUniqueName("web03", "metric")); - when(tag_values.getId("dc01")).thenReturn(new byte[] { 0, 0, 3 }); - when(tag_values.getIdAsync("dc01")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 3 })) - .thenReturn(Deferred.fromResult("dc01")); - - when(metrics.width()).thenReturn((short)3); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); + Bytes.fromLong(1L)); } @Test public void setQuery() throws Exception { query = new TSUIDQuery(tsdb); final HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setQuery("sys.cpu.user", tags); + tags.put(TAGK_STRING, TAGV_STRING); + query.setQuery(METRIC_STRING, tags); } @Test public void setQueryEmtpyTags() throws Exception { query = new TSUIDQuery(tsdb); - query.setQuery("sys.cpu.user", new HashMap(0)); + query.setQuery(METRIC_STRING, new HashMap(0)); } @Test (expected = NoSuchUniqueName.class) public void setQueryNSUMetric() throws Exception { query = new TSUIDQuery(tsdb); - query.setQuery("sys.cpu.system", new HashMap(0)); + query.setQuery(NSUN_METRIC, new HashMap(0)); } @Test (expected = NoSuchUniqueName.class) public void setQueryNSUTagk() throws Exception { query = new TSUIDQuery(tsdb); final HashMap tags = new HashMap(1); - tags.put("dc", "web01"); - query.setQuery("sys.cpu.user", tags); + tags.put(NSUN_TAGK, TAGV_STRING); + query.setQuery(METRIC_STRING, tags); } @Test (expected = NoSuchUniqueName.class) public void setQueryNSUTagv() throws Exception { query = new TSUIDQuery(tsdb); final HashMap tags = new HashMap(1); - tags.put("host", "web03"); - query.setQuery("sys.cpu.user", tags); + tags.put(TAGK_STRING, "web03"); + query.setQuery(METRIC_STRING, tags); } @Test (expected = IllegalArgumentException.class) @@ -286,8 +198,8 @@ public void getLastWriteTimesQueryNotSet() throws Exception { public void getTSMetasSingle() throws Exception { query = new TSUIDQuery(tsdb); HashMap tags = new HashMap(); - tags.put("host", "web01"); - query.setQuery("sys.cpu.user", tags); + tags.put(TAGK_STRING, TAGV_STRING); + query.setQuery(METRIC_STRING, tags); List tsmetas = query.getTSMetas().joinUninterruptibly(); assertEquals(1, tsmetas.size()); } @@ -296,7 +208,7 @@ public void getTSMetasSingle() throws Exception { public void getTSMetasMulti() throws Exception { query = new TSUIDQuery(tsdb); HashMap tags = new HashMap(); - query.setQuery("sys.cpu.user", tags); + query.setQuery(METRIC_STRING, tags); List tsmetas = query.getTSMetas().joinUninterruptibly(); assertEquals(2, tsmetas.size()); } @@ -305,11 +217,11 @@ public void getTSMetasMulti() throws Exception { public void getTSMetasMultipleTags() throws Exception { query = new TSUIDQuery(tsdb); HashMap tags = new HashMap(); - query.setQuery("sys.cpu.nice", tags); - tags.put("host", "web01"); - tags.put("datacenter", "dc01"); + query.setQuery(METRIC_STRING, tags); + tags.put(TAGK_STRING, TAGV_STRING); + tags.put(TAGK_B_STRING, TAGV_B_STRING); List tsmetas = query.getTSMetas().joinUninterruptibly(); - assertEquals(1, tsmetas.size()); + assertEquals(2, tsmetas.size()); } @Test (expected = IllegalArgumentException.class) diff --git a/test/query/filter/TestTagVFilter.java b/test/query/filter/TestTagVFilter.java index c3f7d13450..477f65aaa9 100644 --- a/test/query/filter/TestTagVFilter.java +++ b/test/query/filter/TestTagVFilter.java @@ -72,7 +72,7 @@ public void getFilterGroupBy() throws Exception { @Test public void getFilterLiteral() throws Exception { - assertNull(TagVFilter.getFilter(TAGK_STRING, "web01")); + assertNull(TagVFilter.getFilter(TAGK_STRING, TAGV_STRING)); } @Test @@ -189,7 +189,7 @@ public void resolveName() throws Exception { @Test public void resolveNameLiteral() throws Exception { - final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, "web01"); + final TagVFilter filter = new TagVLiteralOrFilter(TAGK_STRING, TAGV_STRING); filter.resolveTagkName(tsdb).join(); assertArrayEquals(TAGK_BYTES, filter.getTagkBytes()); assertEquals(1, filter.getTagVUids().size()); diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index ad125f093a..347ae95ace 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -41,6 +41,8 @@ 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.*", @@ -89,22 +91,33 @@ public void before() throws Exception { tagv.set(tsdb, tag_values); // mock UniqueId - when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getId("sys.cpu.system")) - .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); - when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); - when(metrics.getId("sys.cpu.idle")).thenReturn(new byte[] { 0, 0, 3 }); - when(metrics.getId("no.values")).thenReturn(new byte[] { 0, 0, 11 }); + when(metrics.getIdAsync("sys.cpu.user")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(metrics.getIdAsync("sys.cpu.system")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("sys.cpu.system", "metric"))); + when(metrics.getIdAsync("sys.cpu.nice")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(metrics.getIdAsync("sys.cpu.idle")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); + when(metrics.getIdAsync("no.values")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 11 })); - when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getId("dc")) - .thenThrow(new NoSuchUniqueName("dc", "metric")); - when(tag_names.getId("owner")).thenReturn(new byte[] { 0, 0, 4 }); + when(tag_names.getIdAsync("host")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_names.getIdAsync("dc")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("dc", "metric"))); + when(tag_names.getIdAsync("owner")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 4 })); - when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_values.getId("web03")) - .thenThrow(new NoSuchUniqueName("web03", "metric")); + when(tag_values.getIdAsync("web01")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_values.getIdAsync("web02")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(tag_values.getIdAsync("web03")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("web03", "metric"))); when(metrics.width()).thenReturn((short)3); when(tag_names.width()).thenReturn((short)3); From 020754a7307b696df75f47ee9a791f4da9f3f7ac Mon Sep 17 00:00:00 2001 From: Yulai Fu Date: Sun, 31 May 2015 18:41:55 -0700 Subject: [PATCH 172/826] Modify the TagVFilter class to return a deferred on matching for asynchonous plugins. Add code to initialize TagVFilter plugins on load. Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 6 ++ src/query/filter/TagVFilter.java | 52 ++++++++++---- src/query/filter/TagVLiteralOrFilter.java | 7 +- src/query/filter/TagVNotKeyFilter.java | 7 +- src/query/filter/TagVNotLiteralOrFilter.java | 8 ++- src/query/filter/TagVRegexFilter.java | 7 +- src/query/filter/TagVWildcardFilter.java | 15 ++-- .../query/filter/TestTagVLiteralOrFilter.java | 28 ++++---- test/query/filter/TestTagVNotKeyFilter.java | 4 +- .../filter/TestTagVNotLiteralOrFilter.java | 28 ++++---- test/query/filter/TestTagVRegexFilter.java | 16 ++--- test/query/filter/TestTagVWildcardFilter.java | 70 +++++++++---------- 12 files changed, 142 insertions(+), 106 deletions(-) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 5d2c3cd6a0..e889da71aa 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -17,6 +17,8 @@ import java.util.Map; import java.util.NoSuchElementException; +import net.opentsdb.query.filter.TagVFilter; + import com.google.common.base.Objects; /** @@ -65,6 +67,10 @@ public final class TSSubQuery { /** Parsed downsampling specification. */ private DownsamplingSpecification downsample_specifier; + /** A list of filters for this query. For now these are pulled out of the + * tags map. In the future we'll have special JSON objects for them. */ + private List filters; + /** * Default constructor necessary for POJO de/serialization */ diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java index e55a234da1..efc32a40bb 100644 --- a/src/query/filter/TagVFilter.java +++ b/src/query/filter/TagVFilter.java @@ -23,7 +23,9 @@ import java.util.Map; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; + import org.hbase.async.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -158,7 +160,7 @@ public TagVFilter(final String tagk, final String filter) { * @param tags The tag map to use for looking up the value for the tagk * @return True if the tag value matches, false if it doesn't. */ - public abstract boolean match(final Map tags); + public abstract Deferred match(final Map tags); /** * The name of this filter as used in queries. When used in URL queries the @@ -211,9 +213,9 @@ public static TagVFilter getFilter(final String tagk, final String filter) { final int paren = filter.indexOf('('); if (paren > -1) { final String prefix = filter.substring(0, paren).toLowerCase(); - return new Builder().withTagk(tagk) - .withFilter(stripParentheses(filter)) - .withType(prefix) + return new Builder().setTagk(tagk) + .setFilter(stripParentheses(filter)) + .setType(prefix) .build(); } else if (filter.contains("*")) { // a shortcut for wildcards since we don't allow asterisks to be stored @@ -245,13 +247,23 @@ public static String stripParentheses(final String filter) { } /** - * Loads plugins from the plugin directory and + * Loads plugins from the plugin directory and loads them into the map. + * Built-in filters don't need to go through this process. + * @param tsdb A TSDB to use to initialize plugins * @throws ClassNotFoundException If we found a class that we didn't... find? * @throws NoSuchMethodException If the discovered plugin didn't have the * proper (tagk, filter) ctor + * @throws InvocationTargetException if the static "initialize(tsdb)" method + * doesn't exist. + * @throws IllegalAccessException if something went really pear shaped + * @throws SecurityException if the JVM is really unhappy with the user + * @throws IllegalArgumentException really shouldn't happen but you know, + * checked exceptions... */ - public static void initializeFilterMap() - throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException { + public static void initializeFilterMap(final TSDB tsdb) + throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, + IllegalArgumentException, SecurityException, IllegalAccessException, + InvocationTargetException { final List filter_plugins = PluginLoader.loadPlugins(TagVFilter.class); if (filter_plugins != null) { @@ -261,11 +273,15 @@ public static void initializeFilterMap() filter.getClass().getDeclaredMethod("examples"); filter.getClass().getDeclaredField("FILTER_NAME"); + final Method initialize = filter.getClass() + .getDeclaredMethod("initialize", TSDB.class); + initialize.invoke(null, tsdb); + final Constructor ctor = filter.getClass().getDeclaredConstructor(String.class, String.class); - final Constructor existing = - tagv_filter_map.get(filter.getType()).getValue(); + final Pair, Constructor> existing = + tagv_filter_map.get(filter.getType()); if (existing != null) { LOG.warn("Overloading existing filter " + existing.getClass().getCanonicalName() + @@ -274,6 +290,8 @@ public static void initializeFilterMap() tagv_filter_map.put(filter.getType().toLowerCase(), new Pair, Constructor>( filter.getClass(), ctor)); + LOG.info("Successfully loaded TagVFilter plugin: " + + filter.getClass().getCanonicalName()); } LOG.info("Loaded " + tagv_filter_map.size() + " filters"); } @@ -515,12 +533,18 @@ public int compareTo(final TagVFilter filter) { return Bytes.memcmpMaybeNull(tagk_bytes, filter.tagk_bytes); } + /** @return a TagVFilter builder for constructing filters */ + public static Builder Builder() { + return new Builder(); + } + /** * Builder class used for deserializing filters from JSON queries via Jackson * since we don't want the user to worry about the class name. The type, * tagk and filter must be configured or the build will fail. */ - @JsonPOJOBuilder() + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "set") public static class Builder { private String type; private String tagk; @@ -529,25 +553,25 @@ public static class Builder { private boolean group_by; /** @param type The type of filter matching a valid filter name */ - public Builder withType(final String type) { + public Builder setType(final String type) { this.type = type; return this; } /** @param tagk The tag key to match on for this filter */ - public Builder withTagk(final String tagk) { + public Builder setTagk(final String tagk) { this.tagk = tagk; return this; } /** @param filter The filter expression to use for matching */ - public Builder withFilter(final String filter) { + public Builder setFilter(final String filter) { this.filter = filter; return this; } /** @param group_by Whether or not the filter should group results */ - public Builder withGroupBy(final boolean group_by) { + public Builder setGroupBy(final boolean group_by) { this.group_by = group_by; return this; } diff --git a/src/query/filter/TagVLiteralOrFilter.java b/src/query/filter/TagVLiteralOrFilter.java index 48f25ae439..d80618e3be 100644 --- a/src/query/filter/TagVLiteralOrFilter.java +++ b/src/query/filter/TagVLiteralOrFilter.java @@ -79,12 +79,13 @@ public TagVLiteralOrFilter(final String tagk, final String filter, } @Override - public boolean match(final Map tags) { + public Deferred match(final Map tags) { final String tagv = tags.get(tagk); if (tagv == null) { - return false; + return Deferred.fromResult(false); } - return literals.contains(case_insensitive ? tagv.toLowerCase() : tagv); + return Deferred.fromResult( + literals.contains(case_insensitive ? tagv.toLowerCase() : tagv)); } @Override diff --git a/src/query/filter/TagVNotKeyFilter.java b/src/query/filter/TagVNotKeyFilter.java index 8c62f64f3f..9166bc64c0 100644 --- a/src/query/filter/TagVNotKeyFilter.java +++ b/src/query/filter/TagVNotKeyFilter.java @@ -3,6 +3,7 @@ import java.util.Map; import com.google.common.base.Objects; +import com.stumbleupon.async.Deferred; public class TagVNotKeyFilter extends TagVFilter { /** Name of this filter */ @@ -18,11 +19,11 @@ public TagVNotKeyFilter(final String tagk, final String filter) { } @Override - public boolean match(Map tags) { + public Deferred match(Map tags) { if (tags.containsKey(tagk)) { - return false; + return Deferred.fromResult(false); } - return true; + return Deferred.fromResult(true); } @Override diff --git a/src/query/filter/TagVNotLiteralOrFilter.java b/src/query/filter/TagVNotLiteralOrFilter.java index 8bf3f5da7e..c384697d67 100644 --- a/src/query/filter/TagVNotLiteralOrFilter.java +++ b/src/query/filter/TagVNotLiteralOrFilter.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Objects; +import com.stumbleupon.async.Deferred; /** * A filter that lets the user list one or more explicit strings that should @@ -72,12 +73,13 @@ public TagVNotLiteralOrFilter(final String tagk, final String filter, } @Override - public boolean match(final Map tags) { + public Deferred match(final Map tags) { final String tagv = tags.get(tagk); if (tagv == null) { - return true; + return Deferred.fromResult(true); } - return !(literals.contains(case_insensitive ? tagv.toLowerCase() : tagv)); + return Deferred.fromResult( + !(literals.contains(case_insensitive ? tagv.toLowerCase() : tagv))); } @Override diff --git a/src/query/filter/TagVRegexFilter.java b/src/query/filter/TagVRegexFilter.java index 56f4d089c2..249ec23ed8 100644 --- a/src/query/filter/TagVRegexFilter.java +++ b/src/query/filter/TagVRegexFilter.java @@ -16,6 +16,7 @@ import java.util.regex.Pattern; import com.google.common.base.Objects; +import com.stumbleupon.async.Deferred; /** * A filter that allows for regular expression matching on tag values. @@ -45,12 +46,12 @@ public TagVRegexFilter(final String tagk, final String filter) { } @Override - public boolean match(final Map tags) { + public Deferred match(final Map tags) { final String tagv = tags.get(tagk); if (tagv == null) { - return false; + return Deferred.fromResult(false); } - return pattern.matcher(tagv).find(); + return Deferred.fromResult(pattern.matcher(tagv).find()); } @Override diff --git a/src/query/filter/TagVWildcardFilter.java b/src/query/filter/TagVWildcardFilter.java index 11898e84d0..2fae7a099f 100644 --- a/src/query/filter/TagVWildcardFilter.java +++ b/src/query/filter/TagVWildcardFilter.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Objects; +import com.stumbleupon.async.Deferred; /** * Performs basic wild card searching. It supports prefix, postfix, infix, @@ -110,31 +111,31 @@ public TagVWildcardFilter(final String tagk, final String filter, } @Override - public boolean match(final Map tags) { + public Deferred match(final Map tags) { String tagv = tags.get(tagk); if (tagv == null) { - return false; + return Deferred.fromResult(false); } else if (components.length == 1 && components[0].equals("*")) { // match all - return true; + return Deferred.fromResult(true); } else if (case_insensitive) { tags.get(tagk).toLowerCase(); } if (has_postfix && !has_prefix && !tagv.endsWith(components[components.length-1])) { - return false; + return Deferred.fromResult(false); } if (has_prefix && !has_postfix && !tagv.startsWith(components[0])) { - return false; + return Deferred.fromResult(false); } int idx = 0; for (int i = 0; i < components.length; i++) { if (tagv.indexOf(components[i], idx) < 0) { - return false; + return Deferred.fromResult(false); } idx += components[i].length(); } - return true; + return Deferred.fromResult(true); } @Override diff --git a/test/query/filter/TestTagVLiteralOrFilter.java b/test/query/filter/TestTagVLiteralOrFilter.java index 6931016d75..2208657bc7 100644 --- a/test/query/filter/TestTagVLiteralOrFilter.java +++ b/test/query/filter/TestTagVLiteralOrFilter.java @@ -24,7 +24,7 @@ public void before() throws Exception { public void matchMiddle() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } @@ -32,56 +32,56 @@ public void matchMiddle() throws Exception { public void matchStart() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "CMTDibbler|LutZe|Slant"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchEnd() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|Slant|CMTDibbler"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchNoPipes() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "CMTDibbler"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPipeNoValueAfter() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "CMTDibbler|"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPipeNoValueBefore() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "|CMTDibbler"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchFail() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|Keli|Slant"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchFailCase() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|CMtDibbler|Slant"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchCaseInsensitive() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|CMtDibbler|Slant", true); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } @@ -89,7 +89,7 @@ public void matchCaseInsensitive() throws Exception { public void matchCaseInsensitiveFail() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|CMtDibble|Slant", true); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } @@ -98,7 +98,7 @@ public void matchNoSuchTagk() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|Keli|Slant"); tags.clear(); tags.put("colo", "lga"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test @@ -106,20 +106,20 @@ public void matchNoSuchTagkCaseInsensitive() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "LutZe|Keli|Slant", true); tags.clear(); tags.put("colo", "lga"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchSingle() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "CMTDibbler"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); assertFalse(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } @Test public void matchSingleCaseInsensitive() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, "cmtDibbler", true); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } diff --git a/test/query/filter/TestTagVNotKeyFilter.java b/test/query/filter/TestTagVNotKeyFilter.java index 25aed9b950..ea9631bde8 100644 --- a/test/query/filter/TestTagVNotKeyFilter.java +++ b/test/query/filter/TestTagVNotKeyFilter.java @@ -25,13 +25,13 @@ public void before() throws Exception { @Test public void matchHasKey() throws Exception { TagVFilter filter = new TagVNotKeyFilter(TAGK, ""); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchDoesNotHaveKey() throws Exception { TagVFilter filter = new TagVNotKeyFilter("colo", ""); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test diff --git a/test/query/filter/TestTagVNotLiteralOrFilter.java b/test/query/filter/TestTagVNotLiteralOrFilter.java index 5270afee1c..44df79466a 100644 --- a/test/query/filter/TestTagVNotLiteralOrFilter.java +++ b/test/query/filter/TestTagVNotLiteralOrFilter.java @@ -24,7 +24,7 @@ public void before() throws Exception { public void matchMiddle() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMTDibbler|Slant"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); assertFalse(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } @@ -32,56 +32,56 @@ public void matchMiddle() throws Exception { public void matchStart() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "CMTDibbler|LutZe|Slant"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchEnd() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|Slant|CMTDibbler"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchNoPipes() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "CMTDibbler"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPipeNoValueAfter() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "CMTDibbler|"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPipeNoValueBefore() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "|CMTDibbler"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchFail() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|Keli|Slant"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchFailCase() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMtDibbler|Slant"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchCaseInsensitive() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMtDibbler|Slant", true); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } @@ -89,7 +89,7 @@ public void matchCaseInsensitive() throws Exception { public void matchCaseInsensitiveFail() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|CMtDibble|Slant", true); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } @@ -98,7 +98,7 @@ public void matchNoSuchTagk() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|Keli|Slant"); tags.clear(); tags.put("colo", "lga"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -106,20 +106,20 @@ public void matchNoSuchTagkCaseInsensitive() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "LutZe|Keli|Slant", true); tags.clear(); tags.put("colo", "lga"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchSingle() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "CMTDibbler"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); assertFalse(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } @Test public void matchSingleCaseInsensitive() throws Exception { TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "cmtDibbler", true); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } diff --git a/test/query/filter/TestTagVRegexFilter.java b/test/query/filter/TestTagVRegexFilter.java index ebba4daa0c..8464eb032a 100644 --- a/test/query/filter/TestTagVRegexFilter.java +++ b/test/query/filter/TestTagVRegexFilter.java @@ -24,52 +24,52 @@ public void before() throws Exception { @Test public void matchExact() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.ankh.morpork.com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPostfix() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, ".*.ops.ankh.morpork.com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPrefix() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.ankh.*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchAnything() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, ".*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchFailed() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.qurim.*"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchGrouping() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-01.ops.(ankh|quirm|tsort).morpork.com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchNumbers() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-\\d+.ops.ankh.morpork.com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchNotEnoughNumbers() throws Exception { TagVFilter filter = new TagVRegexFilter(TAGK, "ogg-\\d(3).ops.ankh.morpork.com"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test (expected = IllegalArgumentException.class) diff --git a/test/query/filter/TestTagVWildcardFilter.java b/test/query/filter/TestTagVWildcardFilter.java index 9622557c65..3270d49149 100644 --- a/test/query/filter/TestTagVWildcardFilter.java +++ b/test/query/filter/TestTagVWildcardFilter.java @@ -23,110 +23,110 @@ public void before() throws Exception { @Test public void matchAll() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchAllNoSuchKey() throws Exception { TagVFilter filter = new TagVWildcardFilter("hobbes", "*"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPostfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPrefix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchDoubleInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*ops*ank*com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchTripleInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*ops*com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPreAndPostfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*morpork*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPostAndInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*ops*com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPostAndDoubleInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*ops*mor*com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPreAndInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*ops*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPreAndDoubleInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*ops*mor*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchMultiWildcardInfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg***com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchMultiWildcardPrefix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*****"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchMultiWildcardPostfix() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "****com"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchWildcardsEverywhere() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "****ogg*****mor****com****"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -134,7 +134,7 @@ public void matchExactPostfix() throws Exception { tags.put(TAGK, "*ops*mor"); TagVFilter filter = new TagVWildcardFilter(TAGK, "*ops*mor"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -142,7 +142,7 @@ public void matchExactPretfix() throws Exception { tags.put(TAGK, "ogg*ops*"); TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*ops*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -150,7 +150,7 @@ public void matchExactInfix() throws Exception { tags.put(TAGK, "ogg*ops*mor"); TagVFilter filter = new TagVWildcardFilter(TAGK, "ogg*ops*mor"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } // Make sure this file is encoded in UTF-8 of the following will fail @@ -159,7 +159,7 @@ public void matchUTF8Postfix() throws Exception { tags.put(TAGK, "Здравей'_хора"); TagVFilter filter = new TagVWildcardFilter(TAGK, "*хора"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -167,7 +167,7 @@ public void matchUTF8Prefix() throws Exception { tags.put(TAGK, "Здравей'_хора"); TagVFilter filter = new TagVWildcardFilter(TAGK, "Здр*"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -175,35 +175,35 @@ public void matchUTF8Infix() throws Exception { tags.put(TAGK, "Здравей'_хора"); TagVFilter filter = new TagVWildcardFilter(TAGK, "Здр*ра"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPostfixFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.org"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPrefixFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "magrat*"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchInfixFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "magrat*com"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPreAndPostfixFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*quirm*"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test (expected = IllegalArgumentException.class) @@ -235,48 +235,48 @@ public void ctorEmptyTagk() throws Exception { public void matchPostfixCaseFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*.MorPork.com"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPrefixCaseFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "Ogg*"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchInfixCaseFail() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogG*Com"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test public void matchPostfixCaseInsensitive() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*.MorPork.com", true); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchPrefixCaseInsensitive() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "Ogg*", true); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchInfixCaseInsensitive() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "ogG*Com", true); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test public void matchNothingButStars() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "****"); - assertTrue(filter.match(tags)); + assertTrue(filter.match(tags).join()); } @Test @@ -284,7 +284,7 @@ public void matchNoSuchTagk() throws Exception { final TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.com"); tags.remove("host"); tags.put("colo", "lga"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test @@ -292,7 +292,7 @@ public void matchNoSuchTagkCaseInsensitive() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, "*.morpork.com", true); tags.remove("host"); tags.put("colo", "lga"); - assertFalse(filter.match(tags)); + assertFalse(filter.match(tags).join()); } @Test From 948b2c5873f74da481ac0abc8b7b7b7715d6779d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 14:09:41 -0700 Subject: [PATCH 173/826] Modify the TSSubQuery to use the new filter classes. It replaces the old tag group by map though the API is still supported. Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 77 +++++++++++++++------ test/core/TestTSSubQuery.java | 126 ++++++++++++++++++++++++++++++---- 2 files changed, 168 insertions(+), 35 deletions(-) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index e889da71aa..7438f7d303 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -12,7 +12,9 @@ // see . package net.opentsdb.core; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -20,6 +22,7 @@ import net.opentsdb.query.filter.TagVFilter; import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; /** * Represents the parameters for an individual sub query on a metric or specific @@ -47,11 +50,7 @@ public final class TSSubQuery { /** User provided list of timeseries UIDs */ private List tsuids; - - /** User supplied list of tags for specificity or grouping. May be null or - * empty */ - private Map tags; - + /** User given downsampler */ private String downsample; @@ -83,8 +82,8 @@ public TSSubQuery() { public int hashCode() { // NOTE: Do not add any non-user submitted variables to the hash. We don't // want the hash to change after validation. - return Objects.hashCode(aggregator, metric, tsuids, tags, downsample, rate, - rate_options); + return Objects.hashCode(aggregator, metric, tsuids, downsample, rate, + rate_options, filters); } @Override @@ -105,27 +104,25 @@ public boolean equals(final Object obj) { return Objects.equal(aggregator, query.aggregator) && Objects.equal(metric, query.metric) && Objects.equal(tsuids, query.tsuids) - && Objects.equal(tags, query.tags) && Objects.equal(downsample, query.downsample) && Objects.equal(rate, query.rate) - && Objects.equal(rate_options, query.rate_options); + && Objects.equal(rate_options, query.rate_options) + && Objects.equal(filters, query.filters); } public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("TSSubQuery(metric=") .append(metric == null || metric.isEmpty() ? "" : metric); - buf.append(", tags=["); - if (tags != null && !tags.isEmpty()) { + buf.append(", filters=["); + if (filters != null && !filters.isEmpty()) { int counter = 0; - for (Map.Entry entry : tags.entrySet()) { + for (final TagVFilter filter : filters) { if (counter > 0) { buf.append(", "); } - buf.append(entry.getKey()) - .append("=") - .append(entry.getValue()); - counter++; + buf.append(filter); + ++counter; } } buf.append("], tsuids=["); @@ -180,6 +177,11 @@ public void validateAndSetQuery() { "Missing the metric or tsuids, provide at least one"); } + // Make sure we have a filter list + if (filters == null) { + filters = new ArrayList(); + } + // parse the downsampler if we have one if (downsample != null && !downsample.isEmpty()) { // downsampler given, so parse it @@ -228,12 +230,22 @@ public List getTsuids() { return tsuids; } - /** @return the user supplied list of query tags, may be empty */ + /** @return the user supplied list of group by query tags, may be empty. + * Note that as of version 2.2 this is an immutable list of tags built from + * the filter list. + * @deprecated */ public Map getTags() { - if (tags == null) { + if (filters == null) { return Collections.emptyMap(); } - return tags; + final Map tags = new HashMap(filters.size()); + for (final TagVFilter filter : filters) { + if (filter.isGroupBy()) { + tags.put(filter.getTagk(), filter.getType() + + "(" + filter.getFilter() + ")"); + } + } + return ImmutableMap.copyOf(tags); } /** @return the raw downsampling function request from the user, @@ -252,6 +264,15 @@ public RateOptions getRateOptions() { return rate_options; } + /** @return the filters pulled from the tags object + * @since 2.2 */ + public List getFilters() { + if (filters == null) { + filters = new ArrayList(); + } + return filters; + } + /** @param aggregator the name of an aggregation function */ public void setAggregator(String aggregator) { this.aggregator = aggregator; @@ -267,9 +288,16 @@ public void setTsuids(List tsuids) { this.tsuids = tsuids; } - /** @param tags an optional list of tags for specificity or grouping */ + /** @param tags an optional list of tags for specificity or grouping + * As of 2.2 this will convert the existing tags to filter + * @deprecated */ public void setTags(Map tags) { - this.tags = tags; + if (filters == null) { + filters = new ArrayList(tags.size()); + } else { + filters.clear(); + } + TagVFilter.tagsToFilters(tags, filters); } /** @param downsample the downsampling function to use, e.g. "2h-avg" */ @@ -286,4 +314,11 @@ public void setRate(boolean rate) { public void setRateOptions(RateOptions options) { this.rate_options = options; } + + /** @param filters A list of filters to use when querying + * @since 2.2 */ + public void setFilters(List filters) { + this.filters = filters; + } + } diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 235a3c21e8..3336f18827 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -19,8 +19,14 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; + +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVLiteralOrFilter; +import net.opentsdb.query.filter.TagVWildcardFilter; import org.junit.Test; @@ -36,8 +42,8 @@ public void validate() { TSSubQuery sub = getMetricForValidate(); sub.validateAndSetQuery(); assertEquals("sys.cpu.0", sub.getMetric()); - assertEquals("*", sub.getTags().get("host")); - assertEquals("lga", sub.getTags().get("dc")); + assertEquals("wildcard(*)", sub.getTags().get("host")); + assertEquals("literal_or(lga)", sub.getTags().get("dc")); assertEquals(Aggregators.SUM, sub.aggregator()); assertEquals(Aggregators.AVG, sub.downsampler()); assertEquals(300000, sub.downsampleInterval()); @@ -52,8 +58,8 @@ public void validateTS() { sub.setTsuids(tsuids); sub.validateAndSetQuery(); assertNotNull(sub.getTsuids()); - assertEquals("*", sub.getTags().get("host")); - assertEquals("lga", sub.getTags().get("dc")); + assertEquals("wildcard(*)", sub.getTags().get("host")); + assertEquals("literal_or(lga)", sub.getTags().get("dc")); assertEquals(Aggregators.SUM, sub.aggregator()); assertEquals(Aggregators.AVG, sub.downsampler()); assertEquals(300000, sub.downsampleInterval()); @@ -65,8 +71,8 @@ public void validateNoDS() { sub.setDownsample(null); sub.validateAndSetQuery(); assertEquals("sys.cpu.0", sub.getMetric()); - assertEquals("*", sub.getTags().get("host")); - assertEquals("lga", sub.getTags().get("dc")); + assertEquals("wildcard(*)", sub.getTags().get("host")); + assertEquals("literal_or(lga)", sub.getTags().get("dc")); assertEquals(Aggregators.SUM, sub.aggregator()); assertNull(sub.downsampler()); assertEquals(0, sub.downsampleInterval()); @@ -116,6 +122,69 @@ public void validateBadDS() { sub.validateAndSetQuery(); } + @Test + public void validateWithFilter() { + TSSubQuery sub = getMetricForValidate(); + sub.setFilters(Arrays.asList(TagVFilter.Builder() + .setFilter("*nari").setType("wildcard").setTagk("host").build())); + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + assertEquals(0, sub.getTags().size()); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + + @Test + public void validateWithFilterViaTags() { + TSSubQuery sub = getMetricForValidate(); + + final Map tags = new HashMap(); + tags.put("host", TagVWildcardFilter.FILTER_NAME + "(*nari)"); + sub.setTags(tags); + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + assertEquals("wildcard(*nari)", sub.getTags().get("host")); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + + @Test + public void validateGroupByFilterMissingParensViaTags() { + TSSubQuery sub = getMetricForValidate(); + final Map tags = new HashMap(); + tags.put("host", TagVWildcardFilter.FILTER_NAME); + sub.setTags(tags); + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVLiteralOrFilter); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + + @Test + public void validateWithGroupByFilter() { + TSSubQuery sub = getMetricForValidate(); + sub.setFilters(Arrays.asList(TagVFilter.Builder() + .setFilter("*nari").setType("wildcard").setTagk("host") + .setGroupBy(true).build())); + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals("wildcard(*nari)", sub.getTags().get("host")); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + // NOTE: Each of the hash and equals tests should make sure that we the code // doesn't change after validation. @@ -241,15 +310,18 @@ public void testHashCodeandEqualsTSUIDsChange() { public void testHashCodeandEqualsTag() { final TSSubQuery sub1 = getBaseQuery(); final int hash_a = sub1.hashCode(); - - sub1.getTags().put("host", "web02"); + Map tags = new HashMap(); + tags.put("host", "web02"); + sub1.setTags(tags); final int hash_b = sub1.hashCode(); assertFalse(hash_a == sub1.hashCode()); sub1.validateAndSetQuery(); assertEquals(hash_b, sub1.hashCode()); TSSubQuery sub2 = getBaseQuery(); - sub2.getTags().put("host", "web02"); + tags = new HashMap(); + tags.put("host", "web02"); + sub2.setTags(tags); assertEquals(hash_b, sub2.hashCode()); assertEquals(sub1, sub2); @@ -260,17 +332,20 @@ public void testHashCodeandEqualsTag() { public void testHashCodeandEqualsTags() { final TSSubQuery sub1 = getBaseQuery(); final int hash_a = sub1.hashCode(); - - sub1.getTags().put("host", "web02"); - sub1.getTags().put("foo", "bar"); + Map tags = new HashMap(); + tags.put("host", "web02"); + tags.put("foo", "bar"); + sub1.setTags(tags); final int hash_b = sub1.hashCode(); assertFalse(hash_a == sub1.hashCode()); sub1.validateAndSetQuery(); assertEquals(hash_b, sub1.hashCode()); TSSubQuery sub2 = getBaseQuery(); - sub2.getTags().put("host", "web02"); - sub2.getTags().put("foo", "bar"); + tags = new HashMap(); + tags.put("host", "web02"); + tags.put("foo", "bar"); + sub2.setTags(tags); assertEquals(hash_b, sub2.hashCode()); assertEquals(sub1, sub2); @@ -455,6 +530,29 @@ public void testEqualsSame() { final TSSubQuery sub1 = getBaseQuery(); assertTrue(sub1.equals(sub1)); } + + @Test + public void testHashCodeandEqualsFilter() { + TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + sub1.setFilters(Arrays.asList(TagVFilter.Builder() + .setFilter("*nari").setType("wildcard").setTagk("host") + .setGroupBy(true).build())); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + final int has_b = sub1.hashCode(); + assertEquals(has_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setFilters(Arrays.asList(TagVFilter.Builder() + .setFilter("*nari").setType("wildcard").setTagk("host") + .setGroupBy(true).build())); + sub2.validateAndSetQuery(); + + assertEquals(has_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } /** @return a sub query object with some defaults set for testing */ public static TSSubQuery getBaseQuery() { From adcc53a48b99ab369eed548b4e630da3e201d401 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 14:12:20 -0700 Subject: [PATCH 174/826] Load filter plugins on TSDB startup when initializePlugins() is called. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 8286e4c858..c99d548c69 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -13,6 +13,7 @@ package net.opentsdb.core; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -48,6 +49,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.search.SearchPlugin; import net.opentsdb.search.SearchQuery; import net.opentsdb.stats.Histogram; @@ -212,6 +214,25 @@ public void initializePlugins(final boolean init_rpcs) { plugin_path, e); } } + + try { + TagVFilter.initializeFilterMap(this); + // @#$@%$%#$ing typed exceptions + } catch (SecurityException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Failed to instantiate filters", e); + } // load the search plugin if enabled if (config.getBoolean("tsd.search.enable")) { From 87f7d98b59a64e25dc406a18e3b5977cf65aa698 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 15:47:26 -0700 Subject: [PATCH 175/826] Add TagVFilter.mapToFilters() to help parse from the URI Signed-off-by: Chris Larsen --- src/query/filter/TagVFilter.java | 46 ++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java index efc32a40bb..2797f229fd 100644 --- a/src/query/filter/TagVFilter.java +++ b/src/query/filter/TagVFilter.java @@ -306,11 +306,25 @@ public static void initializeFilterMap(final TSDB tsdb) */ public static void tagsToFilters(final Map tags, final List filters) { - if (tags == null || tags.isEmpty()) { + mapToFilters(tags, filters, true); + } + + /** + * Converts the map to a filter list. If a filter already exists for a + * tag group by and we're told to process group bys, then the duplicate + * is skipped. + * @param map A set of tag keys and values. May be null or empty. + * @param filters A set of filters to add the converted filters to. This may + * not be null. + * @param group_by Whether or not to set the group by flag and kick dupes + */ + public static void mapToFilters(final Map map, + final List filters, final boolean group_by) { + if (map == null || map.isEmpty()) { return; } - for (final Map.Entry entry : tags.entrySet()) { + for (final Map.Entry entry : map.entrySet()) { TagVFilter filter = getFilter(entry.getKey(), entry.getValue()); if (filter == null && entry.getValue().equals("*")) { @@ -319,23 +333,27 @@ public static void tagsToFilters(final Map tags, filter = new TagVLiteralOrFilter(entry.getKey(), entry.getValue()); } - filter.setGroupBy(true); - boolean duplicate = false; - for (final TagVFilter existing : filters) { - if (filter.equals(existing)) { - LOG.debug("Skipping duplicate filter: " + existing); - existing.setGroupBy(true); - duplicate = true; - break; + if (group_by) { + filter.setGroupBy(true); + boolean duplicate = false; + for (final TagVFilter existing : filters) { + if (filter.equals(existing)) { + LOG.debug("Skipping duplicate filter: " + existing); + existing.setGroupBy(true); + duplicate = true; + break; + } } - } - - if (!duplicate) { + + if (!duplicate) { + filters.add(filter); + } + } else { filters.add(filter); } } } - + /** * Runs through the loaded plugin map and dumps the names, description and * examples into a map to serialize via the API. From ba8e2928526c11ba2b56630b5050210e6fa07310 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 15:48:00 -0700 Subject: [PATCH 176/826] Add Tags.parseWithMetricAndFilters() to parse out filters from the URI Signed-off-by: Chris Larsen --- src/core/Tags.java | 67 ++++++++++++++++++++++ test/core/TestTags.java | 121 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/src/core/Tags.java b/src/core/Tags.java index f60b1e1491..a2a84657a1 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -28,6 +28,7 @@ import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.Pair; @@ -203,6 +204,72 @@ public static String parseWithMetric(final String metric, return metric.substring(0, curly); } + /** + * Parses the metric and tags out of the given string. + * @param metric A string of the form "metric" or "metric{tag=value,...}" or + * now "metric{groupby=filter}{filter=filter}". + * @param filters A list of filters to write the results to. May not be null + * @return The name of the metric. + * @throws IllegalArgumentException if the metric is malformed or the filter + * list is null. + * @since 2.2 + */ + public static String parseWithMetricAndFilters(final String metric, + final List filters) { + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("Metric cannot be null or empty"); + } + if (filters == null) { + throw new IllegalArgumentException("Filters cannot be null"); + } + final int curly = metric.indexOf('{'); + if (curly < 0) { + return metric; + } + final int len = metric.length(); + if (metric.charAt(len - 1) != '}') { // "foo{" + throw new IllegalArgumentException("Missing '}' at the end of: " + metric); + } else if (curly == len - 2) { // "foo{}" + return metric.substring(0, len - 2); + } + final int close = metric.indexOf('}'); + final HashMap filter_map = new HashMap(); + if (close != metric.length() - 1) { // "foo{...}{tagk=filter}" + final int filter_bracket = metric.lastIndexOf('{'); + for (final String filter : splitString(metric.substring(filter_bracket + 1, + metric.length() - 1), ',')) { + if (filter.isEmpty()) { + break; + } + filter_map.clear(); + try { + parse(filter_map, filter); + TagVFilter.mapToFilters(filter_map, filters, false); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("When parsing filter '" + filter + + "': " + e.getMessage(), e); + } + } + } + + // substring the tags out of "foo{a=b,...,x=y}" and parse them. + for (final String tag : splitString(metric.substring(curly + 1, close), ',')) { + try { + if (tag.isEmpty() && close != metric.length() - 1){ + break; + } + filter_map.clear(); + parse(filter_map, tag); + TagVFilter.tagsToFilters(filter_map, filters); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("When parsing tag '" + tag + + "': " + e.getMessage(), e); + } + } + // Return the "foo" part of "foo{a=b,...,x=y}" + return metric.substring(0, curly); + } + /** * Parses an integer value as a long from the given character sequence. *

    diff --git a/test/core/TestTags.java b/test/core/TestTags.java index 973641dc60..3618c1ac7f 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -18,6 +18,11 @@ import java.util.List; import java.util.Map; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVLiteralOrFilter; +import net.opentsdb.query.filter.TagVLiteralOrFilter.TagVILiteralOrFilter; +import net.opentsdb.query.filter.TagVRegexFilter; +import net.opentsdb.query.filter.TagVWildcardFilter; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; @@ -43,6 +48,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -380,6 +386,121 @@ public void parseWithMetricOnlyEquals() { Tags.parseWithMetric("{=}", tags); } + @Test + public void parseWithMetricAndFilters() { + final List filters = new ArrayList(); + String metric = Tags.parseWithMetricAndFilters("sys.cpu.user", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(0, filters.size()); + + filters.clear(); + metric = Tags.parseWithMetricAndFilters("sys.cpu.user{host=web01}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(1, filters.size()); + assertEquals("host", filters.get(0).getTagk()); + assertTrue(filters.get(0).isGroupBy()); + assertTrue(filters.get(0) instanceof TagVLiteralOrFilter); + + filters.clear(); + metric = Tags.parseWithMetricAndFilters("sys.cpu.user{host=*}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(1, filters.size()); + assertEquals("host", filters.get(0).getTagk()); + assertTrue(filters.get(0).isGroupBy()); + assertTrue(filters.get(0) instanceof TagVWildcardFilter); + + filters.clear(); + metric = Tags.parseWithMetricAndFilters("sys.cpu.user{host=web01}{}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(1, filters.size()); + assertEquals("host", filters.get(0).getTagk()); + assertTrue(filters.get(0).isGroupBy()); + assertTrue(filters.get(0) instanceof TagVLiteralOrFilter); + + filters.clear(); + metric = Tags.parseWithMetricAndFilters( + "sys.cpu.user{host=*,owner=regexp(.*ob)}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(2, filters.size()); + for (final TagVFilter filter : filters) { + if (filter instanceof TagVWildcardFilter) { + assertEquals("host", filter.getTagk()); + } else if (filter instanceof TagVRegexFilter) { + assertEquals("owner", filter.getTagk()); + } + assertTrue(filter.isGroupBy()); + } + + filters.clear(); + metric = Tags.parseWithMetricAndFilters("sys.cpu.user{}{host=web01}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(1, filters.size()); + assertEquals("host", filters.get(0).getTagk()); + assertFalse(filters.get(0).isGroupBy()); + assertTrue(filters.get(0) instanceof TagVLiteralOrFilter); + + filters.clear(); + metric = Tags.parseWithMetricAndFilters( + "sys.cpu.user{}{host=iliteral_or(web01|Web02)}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(1, filters.size()); + assertEquals("host", filters.get(0).getTagk()); + assertFalse(filters.get(0).isGroupBy()); + assertTrue(filters.get(0) instanceof TagVILiteralOrFilter); + + filters.clear(); + metric = Tags.parseWithMetricAndFilters( + "sys.cpu.user{}{host=iliteral_or(web01|Web02),owner=*}", filters); + assertEquals("sys.cpu.user", metric); + assertEquals(2, filters.size()); + for (final TagVFilter filter : filters) { + if (filter instanceof TagVWildcardFilter) { + assertEquals("owner", filter.getTagk()); + } else if (filter instanceof TagVILiteralOrFilter) { + assertEquals("host", filter.getTagk()); + } + assertFalse(filter.isGroupBy()); + } + + filters.clear(); + metric = Tags.parseWithMetricAndFilters( + "sys.cpu.user{host=iliteral_or(web01|Web02)}{owner=*}", filters); + assertEquals("sys.cpu.user", metric); + System.out.println(filters); + assertEquals(2, filters.size()); + for (final TagVFilter filter : filters) { + if (filter instanceof TagVWildcardFilter) { + assertEquals("owner", filter.getTagk()); + assertFalse(filter.isGroupBy()); + } else if (filter instanceof TagVILiteralOrFilter) { + assertEquals("host", filter.getTagk()); + assertTrue(filter.isGroupBy()); + } + } + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricAndFiltersMissingTrailingCurly() { + final List filters = new ArrayList(); + Tags.parseWithMetricAndFilters("sys.cpu.user{}{host=web01", filters); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricAndFiltersNullString() { + final List filters = new ArrayList(); + Tags.parseWithMetricAndFilters(null, filters); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricAndFiltersEmptyString() { + final List filters = new ArrayList(); + Tags.parseWithMetricAndFilters("", filters); + } + + @Test (expected = IllegalArgumentException.class) + public void parseWithMetricAndFiltersNullFilters() { + Tags.parseWithMetricAndFilters("sys.cpu.user{}{host=web01}", null); + } @Test public void parseSuccessful() { final HashMap tags = new HashMap(2); From 0d10f05a03871cc520cc6a97bae0aa3142c91e9c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 15:48:36 -0700 Subject: [PATCH 177/826] Add a tsd.query.skip_unresolved_tagvs default to the config, set to false. Signed-off-by: Chris Larsen --- src/utils/Config.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/Config.java b/src/utils/Config.java index e0a389184d..df7297c65d 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -491,6 +491,7 @@ protected void setDefaults() { default_map.put("tsd.core.storage_exception_handler.enable", "false"); default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.query.filter.expansion_limit", "4096"); + default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); From 8c8d4b6e119ac42352eedbf6374139803efad639 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 15:51:56 -0700 Subject: [PATCH 178/826] Modify the QueryRpc class to parse filters from the URI Signed-off-by: Chris Larsen --- src/tsd/QueryRpc.java | 7 +- test/core/TestTSQuery.java | 4 +- test/tsd/TestQueryRpc.java | 133 ++++++++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 9a0c9db370..8e18757213 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -42,6 +42,7 @@ import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSUIDQuery; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.QueryStats; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; @@ -494,9 +495,9 @@ private void parseMTypeSubQuery(final String query_string, sub_query.setAggregator(parts[0]); i--; // Move to the last part (the metric name). - HashMap tags = new HashMap(); - sub_query.setMetric(Tags.parseWithMetric(parts[i], tags)); - sub_query.setTags(tags); + List filters = new ArrayList(); + sub_query.setMetric(Tags.parseWithMetricAndFilters(parts[i], filters)); + sub_query.setFilters(filters); // parse out the rate and downsampler for (int x = 1; x < parts.length - 1; x++) { diff --git a/test/core/TestTSQuery.java b/test/core/TestTSQuery.java index 1e905a7490..9c1fdf90dd 100644 --- a/test/core/TestTSQuery.java +++ b/test/core/TestTSQuery.java @@ -46,8 +46,8 @@ public void validate() { assertEquals(1356998400000L, q.startTime()); assertEquals(1356998460000L, q.endTime()); assertEquals("sys.cpu.0", q.getQueries().get(0).getMetric()); - assertEquals("*", q.getQueries().get(0).getTags().get("host")); - assertEquals("lga", q.getQueries().get(0).getTags().get("dc")); + assertEquals("wildcard(*)", q.getQueries().get(0).getTags().get("host")); + assertEquals("literal_or(lga)", q.getQueries().get(0).getTags().get("dc")); assertEquals(Aggregators.SUM, q.getQueries().get(0).aggregator()); assertEquals(Aggregators.AVG, q.getQueries().get(0).downsampler()); assertEquals(300000, q.getQueries().get(0).downsampleInterval()); diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index 8abb43c2fd..b7e8f72f8b 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -29,6 +29,9 @@ import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; +import net.opentsdb.query.filter.TagVLiteralOrFilter; +import net.opentsdb.query.filter.TagVRegexFilter; +import net.opentsdb.query.filter.TagVWildcardFilter; import net.opentsdb.storage.MockDataPoints; import net.opentsdb.utils.Config; import net.opentsdb.utils.DateTime; @@ -169,7 +172,135 @@ public void parseQueryMTypeWTag() throws Exception { TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); TSSubQuery sub = tsq.getQueries().get(0); assertNotNull(sub.getTags()); - assertEquals("web01", sub.getTags().get("host")); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + } + + @Test + public void parseQueryMTypeWGroupByRegex() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + + TagVRegexFilter.FILTER_NAME + "(something(foo|bar))}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVRegexFilter); + } + + @Test + public void parseQueryMTypeWGroupByWildcardExplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + + TagVWildcardFilter.FILTER_NAME + "(*quirm)}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + } + + @Test + public void parseQueryMTypeWGroupByWildcardImplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=*quirm}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + } + + @Test + public void parseQueryMTypeWWildcardFilterExplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{host=wildcard(*quirm)}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + } + + @Test + public void parseQueryMTypeWWildcardFilterImplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{host=*quirm}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(1, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + } + + @Test + public void parseQueryMTypeWGroupByAndWildcardFilterExplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{colo=lga}{host=wildcard(*quirm)}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + assertTrue(sub.getFilters().get(1) instanceof TagVLiteralOrFilter); + } + + @Test + public void parseQueryMTypeWGroupByAndWildcardFilterSameTagK() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=quirm|tsort}" + + "{host=wildcard(*quirm)}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + assertTrue(sub.getFilters().get(1) instanceof TagVLiteralOrFilter); + } + + @Test + public void parseQueryMTypeWGroupByFilterAndWildcardFilterSameTagK() + throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + + "{host=wildcard(*quirm)}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(2, sub.getFilters().size()); + assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); + assertTrue(sub.getFilters().get(1) instanceof TagVWildcardFilter); + } + + @Test (expected = IllegalArgumentException.class) + public void parseQueryMTypeWGroupByFilterMissingClose() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + + "{host=wildcard(*quirm)"); + parseQuery.invoke(rpc, tsdb, query); + } + + @Test (expected = IllegalArgumentException.class) + public void parseQueryMTypeWGroupByFilterMissingEquals() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + + "{hostwildcard(*quirm)}"); + parseQuery.invoke(rpc, tsdb, query); + } + + @Test (expected = IllegalArgumentException.class) + public void parseQueryMTypeWGroupByNoSuchFilter() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=nosuchfilter(*tsort)}" + + "{host=dummyfilter(*quirm)}"); + parseQuery.invoke(rpc, tsdb, query); + } + + @Test + public void parseQueryMTypeWEmptyFilterBrackets() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSSubQuery sub = tsq.getQueries().get(0); + sub.validateAndSetQuery(); + assertEquals(0, sub.getFilters().size()); } @Test From 9197b63ab711c5f1e3c7bfc869075ea001b629d4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 18:34:47 -0700 Subject: [PATCH 179/826] Add filter support in the TsdbQuery and salt scanner. If the filters consist of the existing group by wildcard * then we don't bother processing in the scanners. Likewise if the filters are literal ors and won't create a massive row key regex, we keep from using them in the scanner. For regex and other wildcards or if we would create a giant row key regex we have to resolve the row keys to strings and filter them post-facto. It works but is a bit slower. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 129 +++++- src/core/TsdbQuery.java | 643 +++++++++++++++------------- test/core/TestSaltScanner.java | 163 ++++++- test/core/TestTsdbQuery.java | 157 +++++-- test/core/TestTsdbQueryQueries.java | 47 ++ 5 files changed, 761 insertions(+), 378 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index d60253d9d6..bff70b2acb 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -15,12 +15,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import net.opentsdb.meta.Annotation; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.KeyValue; @@ -85,6 +89,9 @@ public class SaltScanner { * are done.*/ private long start_time; // milliseconds. + /** A list of filters to iterate over when processing rows */ + private final List filters; + /** A holder for storing the first exception thrown by a scanner if something * goes pear shaped. Make sure to synchronize on this object when checking * for null or assigning from a scanner's callback. */ @@ -97,12 +104,14 @@ public class SaltScanner { * @param metric The metric we're expecting to fetch * @param scanners A list of HBase scanners, one for each bucket * @param spans The span map to store results in + * @param filters A list of filters for processing * @throws IllegalArgumentException if any required data was missing or * we had invalid parameters. */ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, - final TreeMap spans) { + final TreeMap spans, + final List filters) { if (Const.SALT_WIDTH() < 1) { throw new IllegalArgumentException( "Salting is disabled. Use the regular scanner"); @@ -137,6 +146,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.spans = spans; this.metric = metric; this.tsdb = tsdb; + this.filters = filters; } /** @@ -247,6 +257,8 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); + private final Set skips = new HashSet(); + private final Set keepers = new HashSet(); public ScannerCB(final Scanner scanner) { this.scanner = scanner; @@ -290,6 +302,11 @@ public Object call(final ArrayList> rows) return null; } + // used for UID resolution if a filter is involved + final List> lookups = + filters != null && !filters.isEmpty() ? + new ArrayList>(rows.size()) : null; + for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (RowKey.rowKeyContainsMetric(metric, key) != 0) { @@ -301,29 +318,80 @@ public Object call(final ArrayList> rows) return null; } - List notes = annotations.get(key); - if (notes == null) { - notes = new ArrayList(); - annotations.put(key, notes); - } + // If any filters have made it this far then we need to resolve + // the row key UIDs to their names for string comparison. We'll + // try to avoid the resolution with some sets but we may dupe + // resolve a few times. + // TODO - more efficient resolution + // TODO - byte set instead of a string for the uid may be faster + if (filters != null && !filters.isEmpty()) { + lookups.clear(); + final String tsuid = + UniqueId.uidToString(UniqueId.getTSUIDFromKey(key, + TSDB.metrics_width(), Const.TIMESTAMP_BYTES)); + if (skips.contains(tsuid)) { + continue; + } + if (!keepers.contains(tsuid)) { + /** CB to called after all of the UIDs have been resolved */ + class MatchCB implements Callback> { + @Override + public Object call(final ArrayList matches) + throws Exception { + for (final boolean matched : matches) { + if (!matched) { + skips.add(tsuid); + return null; + } + } + // matched all, good data + keepers.add(tsuid); + processRow(key, row); + return null; + } + } - final KeyValue compacted; - try{ - compacted = tsdb.compact(row, notes); - } catch (final IllegalDataException idex) { - LOG.error("Caught IllegalDataException exception while parsing the " - + "row " + key + ", skipping it on scanner " + this, idex); - scanner.close(); - handleException(idex); - return null; - } - - if (compacted != null) { // Can be null if we ignored all KVs. - kvs.add(compacted); + /** Resolves all of the row key UIDs to their strings for filtering */ + class GetTagsCB implements + Callback>, Map> { + @Override + public Deferred> call( + final Map tags) throws Exception { + final List> matches = + new ArrayList>(filters.size()); + + for (final TagVFilter filter : filters) { + matches.add(filter.match(tags)); + } + + return Deferred.group(matches); + } + } + + lookups.add(Tags.getTagsAsync(tsdb, key) + .addCallbackDeferring(new GetTagsCB()) + .addBoth(new MatchCB())); + } else { + processRow(key, row); + } + } else { + processRow(key, row); } } - return scan(); + // either we need to wait on the UID resolutions or we can go ahead + // if we don't have filters. + if (lookups != null && lookups.size() > 0) { + class GroupCB implements Callback> { + @Override + public Object call(final ArrayList group) throws Exception { + return scan(); + } + } + return Deferred.group(lookups).addCallback(new GroupCB()); + } else { + return scan(); + } } catch (final RuntimeException e) { LOG.error("Unexpected exception on scanner " + this, e); scanner.close(); @@ -331,6 +399,27 @@ public Object call(final ArrayList> rows) return null; } } + + /** + * Finds or creates the span for this row, compacts it and stores it. + * @param key The row key to use for fetching the span + * @param row The row to add + */ + void processRow(final byte[] key, final ArrayList row) { + List notes = annotations.get(key); + if (notes == null) { + notes = new ArrayList(); + annotations.put(key, notes); + } + + final KeyValue compacted; + // let IllegalDataExceptions bubble up so the handler above can close + // the scanner + compacted = tsdb.compact(row, notes); + if (compacted != null) { // Can be null if we ignored all KVs. + kvs.add(compacted); + } + } } /** diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 7e0cd04875..e1aa36f46b 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -17,11 +17,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -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.TreeMap; +import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,8 +36,11 @@ import com.google.common.annotations.VisibleForTesting; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.Histogram; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; @@ -78,14 +83,6 @@ final class TsdbQuery implements Query { /** ID of the metric being looked up. */ private byte[] metric; - /** - * Tags of the metrics being looked up. - * Each tag is a byte array holding the ID of both the name and value - * of the tag. - * Invariant: an element cannot be both in this array and in group_bys. - */ - private ArrayList tags; - /** * Tags by which we must group the results. * Each element is a tag ID. @@ -94,13 +91,9 @@ final class TsdbQuery implements Query { private ArrayList group_bys; /** - * Values we may be grouping on. - * For certain elements in {@code group_bys}, we may have a specific list of - * values IDs we're looking for. Those IDs are stored in this map. The key - * is an element of {@code group_bys} (so a tag name ID) and the values are - * tag value IDs (at least two). + * Tag key and values to use in the row key filter, all pre-sorted */ - private ByteMap group_by_values; + private ByteMap row_key_literals; /** If true, use rate of change instead of actual values. */ private boolean rate; @@ -129,6 +122,9 @@ final class TsdbQuery implements Query { /** 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; + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -212,19 +208,37 @@ public void setTimeSeries(final String metric, final boolean rate, final RateOptions rate_options) throws NoSuchUniqueName { - final Map tags_copy = new HashMap(tags.size()); - tags_copy.putAll(tags); + if (filters == null) { + filters = new ArrayList(tags.size()); + } + TagVFilter.tagsToFilters(tags, filters); + try { - findGroupBys(tags_copy).join(); + for (final TagVFilter filter : this.filters) { + filter.resolveTagkName(tsdb).join(); + } } catch (final InterruptedException e) { LOG.warn("Interrupted", e); Thread.currentThread().interrupt(); + } catch (final NoSuchUniqueName e) { + throw e; } catch (final Exception e) { + if (e instanceof DeferredGroupException) { + // rollback to the actual case. The DGE missdirects + Throwable ex = e.getCause(); + while(ex != null && ex instanceof DeferredGroupException) { + ex = ex.getCause(); + } + if (ex != null) { + throw (RuntimeException)ex; + } + } LOG.error("Unexpected exception processing group bys", e); throw new RuntimeException(e); } + + findGroupBys(); this.metric = tsdb.metrics.getId(metric); - this.tags = Tags.resolveAll(tsdb, tags_copy); aggregator = function; this.rate = rate; this.rate_options = rate_options; @@ -292,6 +306,7 @@ public Deferred configureFromQuery(final TSQuery query, downsampler = sub_query.downsampler(); sample_interval_ms = sub_query.downsampleInterval(); fill_policy = sub_query.fillPolicy(); + filters = sub_query.getFilters(); // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { @@ -314,79 +329,30 @@ public Deferred configureFromQuery(final TSQuery query, } return Deferred.fromResult(null); } else { - // copy the tags to a new map as the groupby method will modify the map - // and doing so would cause the TSQuery to change it's hash code, making - // it impossible to remove from the query stats. - final Map tags_copy = - new HashMap(sub_query.getTags()); - - /** Adds the tagk and tagv in the array list in the proper order */ - class TagVCB implements Callback { - final byte[] tagk; - public TagVCB(final byte[] tagk) { - this.tagk = tagk; - } + /** Triggers the group by resolution if we had filters to resolve */ + class FilterCB implements Callback> { @Override - public Object call(final byte[] tagv) { - // multiple threads can call us back so make sure we lock the array - // to avoid concurrent modifications or add keys and values out of - // order - synchronized(tags) { - final byte[] pair = new byte[tagk.length + tagv.length]; - System.arraycopy(tagk, 0, pair, 0, tagk.length); - System.arraycopy(tagv, 0, pair, tagk.length, tagv.length); - tags.add(pair); - } - return null; - } - } - - /** Triggers the tagv resolution after resolving a tagk */ - class TagKCB implements Callback, byte[]> { - final String tagv; - public TagKCB(final String tagv) { - this.tagv = tagv; - } - @Override - public Deferred call(final byte[] tagk) { - return tsdb.tag_values.getIdAsync(tagv).addCallback(new TagVCB(tagk)); + public Object call(final ArrayList results) throws Exception { + findGroupBys(); + return Deferred.fromResult(null); } } - /** Resolves explicit tagk/tagv pairs after group bys */ - class GroupBy implements Callback>, - ArrayList> { - @Override - public Deferred> call(final ArrayList group) { - final List> tags = - new ArrayList>(tags_copy.size()); - TsdbQuery.this.tags = new ArrayList(tags.size()); - for (Map.Entry entry : tags_copy.entrySet()) { - tags.add(tsdb.tag_names.getIdAsync(entry.getKey()) - .addCallbackDeferring(new TagKCB(entry.getValue()))); - } - return Deferred.group(tags); - } - } - - /** Sort the tag array after resolution is complete */ - class SortTags implements Callback, ArrayList> { - @Override - public Deferred call(final ArrayList notused) - throws Exception { - Collections.sort(tags, Bytes.MEMCMP); - return null; - } - } - /** Resolve and group by tags after resolving the metric */ class MetricCB implements Callback { @Override public Object call(final byte[] uid) throws Exception { metric = uid; - return findGroupBys(tags_copy) - .addCallbackDeferring(new GroupBy()) - .addCallback(new SortTags()); + if (filters != null) { + final List> deferreds = + new ArrayList>(filters.size()); + for (final TagVFilter filter : filters) { + deferreds.add(filter.resolveTagkName(tsdb)); + } + return Deferred.group(deferreds).addCallback(new FilterCB()); + } else { + return Deferred.fromResult(null); + } } } @@ -423,144 +389,91 @@ public void downsample(final long interval, final Aggregator downsampler) { } /** - * Extracts all the tags we must use to group results. - *
      - *
    • If a tag has the form {@code name=*} then we'll create one - * group per value we find for that tag.
    • - *
    • If a tag has the form {@code name={v1,v2,..,vN}} then we'll - * create {@code N} groups.
    • - *
    - * In the both cases above, {@code name} will be stored in the - * {@code group_bys} attribute. In the second case specifically, - * the {@code N} values would be stored in {@code group_by_values}, - * the key in this map being {@code name}. - * @param tags The tags from which to extract the 'GROUP BY's. - * Each tag that represents a 'GROUP BY' will be removed from the map - * passed in argument. - * @return A deferred to wait on, the results are not important and should be - * discarded. + * Populates the {@link #group_bys} and {@link #row_key_literals}'s with + * values pulled from the filters. */ - private Deferred> findGroupBys(final Map tags) { - - /** - * Used to continue processing when we have a tag value that wasn't assigned - * a UID and the config explicitly allows unknown tags. - */ - class Errback implements Callback { - final boolean is_tagv; - public Errback(final boolean is_tagv) { - this.is_tagv = is_tagv; - } - - @Override - public byte[] call(final Exception e) throws Exception { - if (is_tagv && - tsdb.getConfig().getBoolean("tsd.query.skip_unresolved_tagvs")) { - LOG.warn("Query tag value not found: " + e.getMessage()); - return null; - } else { - throw e; - } - } + private void findGroupBys() { + if (filters == null || filters.isEmpty()) { + return; } - /** Adds the tagk to the group bys and passes along the UID */ - class ResolveTagKCB implements Callback { - @Override - public byte[] call(final byte[] uid) { - group_bys.add(uid); - return uid; - } - } - - /** Writes the resolved tagv to the proper group_by_values array */ - class ResoveTagVCB implements Callback { - final byte[] tagk; - final int index; - public ResoveTagVCB(final byte[] tagk, final int index) { - this.tagk = tagk; - this.index = index; - } - @Override - public byte[] call(final byte[] uid) { - final byte[][] value_ids = group_by_values.get(tagk); - System.arraycopy(uid, 0, value_ids[index], 0, tsdb.tag_values.width()); - return null; - } - } - - /** - * Only here to cast the {@code ArrayList} to a {@code byte[]} for - * typing purposes. - */ - class PipedGroupCB implements Callback> { - @Override - public byte[] call(final ArrayList tagvs) { - return null; - } - } + row_key_literals = new ByteMap(); - /** Resolves a piped list of tag values after resolving the tagk */ - class ResolvePipedGroupBy implements Callback, byte[]> { - final String[] values; - public ResolvePipedGroupBy(final String[] values) { - this.values = values; - } - @Override - public Deferred call(final byte[] uid) { - final byte[][] value_ids = new byte[values.length][tsdb.tag_values.width()]; - group_by_values.put(uid, value_ids); + Collections.sort(filters); + final Iterator current_iterator = filters.iterator(); + final Iterator look_ahead = filters.iterator(); + byte[] tagk = null; + TagVFilter next = look_ahead.hasNext() ? look_ahead.next() : null; + int row_key_literals_count = 0; + while (current_iterator.hasNext()) { + next = look_ahead.hasNext() ? look_ahead.next() : null; + int gbs = 0; + // sorted! + final ByteMap literals = new ByteMap(); + final List literal_filters = new ArrayList(); + TagVFilter current = null; + boolean not_key = false; + do { // yeah, I'm breakin out the do!!! + current = current_iterator.next(); + if (tagk == null) { + tagk = new byte[TSDB.tagk_width()]; + System.arraycopy(current.getTagkBytes(), 0, tagk, 0, TSDB.tagk_width()); + } - final List> tagvs = - new ArrayList>(values.length); - for (int j = 0; j < values.length; j++) { - tagvs.add(tsdb.tag_values.getIdAsync(values[j]) - .addCallback(new ResoveTagVCB(uid, j)) - .addErrback(new Errback(true))); + if (current.isGroupBy()) { + gbs++; } - return Deferred.group(tagvs).addCallback(new PipedGroupCB()); - } - } - - final List> deferreds = !tags.isEmpty() ? - new ArrayList>(tags.size()) : null; - - final Iterator> i = tags.entrySet().iterator(); - while (i.hasNext()) { - final Map.Entry tag = i.next(); - final String tagvalue = tag.getValue(); - if (tagvalue.equals("*") // 'GROUP BY' with any value. - || tagvalue.indexOf('|', 1) >= 0) { // Multiple possible values. + if (!current.getTagVUids().isEmpty()) { + for (final byte[] uid : current.getTagVUids()) { + literals.put(uid, null); + } + literal_filters.add(current); + } + if (current.isNotKeyFilter()) { + not_key = true; + } + + if (next != null && Bytes.memcmp(tagk, next.getTagkBytes()) != 0) { + break; + } + next = look_ahead.hasNext() ? look_ahead.next() : null; + } while (current_iterator.hasNext() && + Bytes.memcmp(tagk, current.getTagkBytes()) == 0); + + if (gbs > 0 && !not_key) { if (group_bys == null) { group_bys = new ArrayList(); } - final Deferred resolve_tagk = - tsdb.tag_names.getIdAsync(tag.getKey()) - .addCallback(new ResolveTagKCB()) - .addErrback(new Errback(false)); - deferreds.add(resolve_tagk); - i.remove(); - if (tagvalue.charAt(0) == '*') { - continue; // For a 'GROUP BY' with any value, we're done. - } - - // 'GROUP BY' with specific values. Need to split the values - // to group on and store their IDs in group_by_values. - final String[] values = Tags.splitString(tagvalue, '|'); - if (group_by_values == null) { - group_by_values = new ByteMap(); + group_bys.add(current.getTagkBytes()); + } + + if (not_key) { + // special value to notify the row key regex builder that we don't want + // rows with this tagk + row_key_literals.put(current.getTagkBytes(), new byte[0][]); + } else if (literals.size() > 0) { + // TODO - a good optimization would be to remove the filter from the + // list passed to the scanner since we'll have it in the regex. However + // that would then "OR" the literal filters + if (literals.size() + row_key_literals_count > + tsdb.getConfig().getInt("tsd.query.filter.expansion_limit")) { + LOG.debug("Skipping literals for " + current.getTagk() + + " as it exceedes the limit"); + } else { + final byte[][] values = new byte[literals.size()][]; + literals.keySet().toArray(values); + row_key_literals.put(current.getTagkBytes(), values); + row_key_literals_count += values.length; + + for (final TagVFilter filter : literal_filters) { + filter.setPostScan(false); + } } - resolve_tagk.addCallback(new ResolvePipedGroupBy(values)); + } else { + row_key_literals.put(current.getTagkBytes(), null); } } - - if (deferreds == null) { - return Deferred.fromResult(null); - } else { - return Deferred.group(deferreds); - } } - /** * Executes the query. * NOTE: Do not run the same query multiple times. Construct a new query with @@ -602,12 +515,28 @@ private Deferred> findSpans() throws HBaseException { new TreeMap(new SpanCmp( (short)(Const.SALT_WIDTH() + metric_width))); + // Copy only the filters that should trigger a tag resolution. If this list + // is empty due to literals or a wildcard star, then we'll save a TON of + // UID lookups + final List scanner_filters; + if (filters != null) { + scanner_filters = new ArrayList(filters.size()); + for (final TagVFilter filter : filters) { + if (filter.postScan()) { + scanner_filters.add(filter); + } + } + } else { + scanner_filters = null; + } + if (Const.SALT_WIDTH() > 0) { final List scanners = new ArrayList(Const.SALT_BUCKETS()); for (int i = 0; i < Const.SALT_BUCKETS(); i++) { scanners.add(getScanner(i)); } - return new SaltScanner(tsdb, metric, scanners, spans).scan(); + return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters) + .scan(); } final Scanner scanner = getScanner(); @@ -629,7 +558,8 @@ final class ScannerCB implements Callback skips = new HashSet(); + private final Set keepers = new HashSet(); /** * Starts the scanner and is called recursively to fetch the next set of * rows from the scanner. @@ -669,6 +599,11 @@ public Object call(final ArrayList> rows) throw new InterruptedException("Query timeout exceeded!"); } + // used for UID resolution if a filter is involved + final List> lookups = + filters != null && !filters.isEmpty() ? + new ArrayList>(rows.size()) : null; + for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (Bytes.memcmp(metric, key, 0, metric_width) != 0) { @@ -678,27 +613,107 @@ public Object call(final ArrayList> rows) + " our scanner (" + scanner + ")! " + row + " does not start" + " with " + Arrays.toString(metric)); } - Span datapoints = spans.get(key); - if (datapoints == null) { - datapoints = new Span(tsdb); - spans.put(key, datapoints); - } - final KeyValue compacted = - tsdb.compact(row, datapoints.getAnnotations()); - seenAnnotation |= !datapoints.getAnnotations().isEmpty(); - if (compacted != null) { // Can be null if we ignored all KVs. - datapoints.addRow(compacted); - nrows++; + + // If any filters have made it this far then we need to resolve + // the row key UIDs to their names for string comparison. We'll + // try to avoid the resolution with some sets but we may dupe + // resolve a few times. + // TODO - more efficient resolution + // TODO - byte set instead of a string for the uid may be faster + if (scanner_filters != null && !scanner_filters.isEmpty()) { + lookups.clear(); + final String tsuid = + UniqueId.uidToString(UniqueId.getTSUIDFromKey(key, + TSDB.metrics_width(), Const.TIMESTAMP_BYTES)); + if (skips.contains(tsuid)) { + continue; + } + if (!keepers.contains(tsuid)) { + /** CB to called after all of the UIDs have been resolved */ + class MatchCB implements Callback> { + @Override + public Object call(final ArrayList matches) + throws Exception { + for (final boolean matched : matches) { + if (!matched) { + skips.add(tsuid); + return null; + } + } + // matched all, good data + keepers.add(tsuid); + processRow(key, row); + return null; + } + } + + /** Resolves all of the row key UIDs to their strings for filtering */ + class GetTagsCB implements + Callback>, Map> { + @Override + public Deferred> call( + final Map tags) throws Exception { + final List> matches = + new ArrayList>(scanner_filters.size()); + + for (final TagVFilter filter : scanner_filters) { + matches.add(filter.match(tags)); + } + + return Deferred.group(matches); + } + } + + lookups.add(Tags.getTagsAsync(tsdb, key) + .addCallbackDeferring(new GetTagsCB()) + .addBoth(new MatchCB())); + } else { + processRow(key, row); + } + } else { + processRow(key, row); } } - return scan(); + // either we need to wait on the UID resolutions or we can go ahead + // if we don't have filters. + if (lookups != null && lookups.size() > 0) { + class GroupCB implements Callback> { + @Override + public Object call(final ArrayList group) throws Exception { + return scan(); + } + } + return Deferred.group(lookups).addCallback(new GroupCB()); + } else { + return scan(); + } } catch (Exception e) { scanner.close(); results.callback(e); return null; } } + + /** + * Finds or creates the span for this row, compacts it and stores it. + * @param key The row key to use for fetching the span + * @param row The row to add + */ + void processRow(final byte[] key, final ArrayList row) { + Span datapoints = spans.get(key); + if (datapoints == null) { + datapoints = new Span(tsdb); + spans.put(key, datapoints); + } + final KeyValue compacted = + tsdb.compact(row, datapoints.getAnnotations()); + seenAnnotation |= !datapoints.getAnnotations().isEmpty(); + if (compacted != null) { // Can be null if we ignored all KVs. + datapoints.addRow(compacted); + ++nrows; + } + } } new ScannerCB().scan(); @@ -857,7 +872,7 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { scanner.setStopKey(end_row); if (tsuids != null && !tsuids.isEmpty()) { createAndSetTSUIDFilter(scanner); - } else if (tags.size() > 0 || group_bys != null) { + } else if (filters.size() > 0) { createAndSetFilter(scanner); } scanner.setFamily(TSDB.FAMILY); @@ -961,54 +976,102 @@ private void createAndSetFilter(final Scanner scanner) { final StringBuilder buf = new StringBuilder( 15 // "^.{N}" + "(?:.{M})*" + "$" + ((13 + tagsize) // "(?:.{M})*\\Q" + tagsize bytes + "\\E" - * (tags.size() + (group_bys == null ? 0 : group_bys.size() * 3)))); + * ((row_key_literals == null ? 0 : row_key_literals.size()) + + (group_bys == null ? 0 : group_bys.size() * 3)))); // In order to avoid re-allocations, reserve a bit more w/ groups ^^^ // Alright, let's build this regexp. From the beginning... buf.append("(?s)" // Ensure we use the DOTALL flag. + "^.{") - // ... start by skipping the metric ID and timestamp. + // ... start by skipping the salt, metric ID and timestamp. .append(Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES) .append("}"); - final Iterator tags = this.tags.iterator(); - final Iterator group_bys = (this.group_bys == null - ? new ArrayList(0).iterator() - : this.group_bys.iterator()); - byte[] tag = tags.hasNext() ? tags.next() : null; - byte[] group_by = group_bys.hasNext() ? group_bys.next() : null; - // Tags and group_bys are already sorted. We need to put them in the - // regexp in order by ID, which means we just merge two sorted lists. - do { + + 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. - buf.append("(?:.{").append(tagsize).append("})*\\Q"); - if (isTagNext(name_width, tag, group_by)) { - addId(buf, tag); - tag = tags.hasNext() ? tags.next() : null; - } else { // Add a group_by. - addId(buf, group_by); - final byte[][] value_ids = (group_by_values == null - ? null - : group_by_values.get(group_by)); - if (value_ids == null) { // We don't want any specific ID... - buf.append(".{").append(value_width).append('}'); // Any value ID. - } else { // We want specific IDs. List them: /(AAA|BBB|CCC|..)/ - buf.append("(?:"); - for (final byte[] value_id : value_ids) { - buf.append("\\Q"); - addId(buf, value_id); - buf.append('|'); + buf.append("(?:.{").append(tagsize).append("})*"); + if (not_key) { + // start the lookahead as we have a key we expliclty do not want in the + // results + buf.append("(?!"); + } + buf.append("\\Q"); + + addId(buf, entry.getKey()); + 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; } - // Replace the pipe of the last iteration. - buf.setCharAt(buf.length() - 1, ')'); + buf.append("\\Q"); + addId(buf, value_id); + buf.append('|'); } - group_by = group_bys.hasNext() ? group_bys.next() : null; + // 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(")"); } - } while (tag != group_by); // Stop when they both become null. + } // Skip any number of tags before the end. buf.append("(?:.{").append(tagsize).append("})*$"); scanner.setKeyRegexp(buf.toString(), CHARSET); - } + if (LOG.isDebugEnabled()) { + logRegexScanner(buf.toString()); + } + } + /** + * Little helper to print out the regular expression by converting the UID + * bytes to an array. + * @param regexp The regex string to print to the debug log + * @since 2.2 + */ + void logRegexScanner(final String regexp) { + final StringBuilder buf = new StringBuilder(); + for (int i = 0; i < regexp.length(); i++) { + if (i > 0 && regexp.charAt(i - 1) == 'Q') { + if (regexp.charAt(i - 3) == '*') { + // tagk + byte[] tagk = new byte[TSDB.tagk_width()]; + for (int x = 0; x < TSDB.tagk_width(); x++) { + tagk[x] = (byte)regexp.charAt(i + x); + } + i += TSDB.tagk_width(); + buf.append(Arrays.toString(tagk)); + } else { + // tagv + byte[] tagv = new byte[TSDB.tagv_width()]; + for (int x = 0; x < TSDB.tagv_width(); x++) { + tagv[x] = (byte)regexp.charAt(i + x); + } + i += TSDB.tagv_width(); + buf.append(Arrays.toString(tagv)); + } + } else { + buf.append(regexp.charAt(i)); + } + } + LOG.debug("Scanner regex: " + buf.toString()); + } + /** * Sets the server-side regexp filter on the scanner. * This will compile a list of the tagk/v pairs for the TSUIDs to prevent @@ -1059,31 +1122,6 @@ private void createAndSetTSUIDFilter(final Scanner scanner) { buf.append("$"); scanner.setKeyRegexp(buf.toString(), CHARSET); } - - /** - * Helper comparison function to compare tag name IDs. - * @param name_width Number of bytes used by a tag name ID. - * @param tag A tag (array containing a tag name ID and a tag value ID). - * @param group_by A tag name ID. - * @return {@code true} number if {@code tag} should be used next (because - * it contains a smaller ID), {@code false} otherwise. - */ - private boolean isTagNext(final short name_width, - final byte[] tag, - final byte[] group_by) { - if (tag == null) { - return false; - } else if (group_by == null) { - return true; - } - final int cmp = Bytes.memcmp(tag, group_by, 0, name_width); - if (cmp == 0) { - throw new AssertionError("invariant violation: tag ID " - + Arrays.toString(group_by) + " is both in 'tags' and" - + " 'group_bys' in " + this); - } - return cmp < 0; - } /** * Appends the given ID to the given buffer, followed by "\\E". @@ -1117,9 +1155,9 @@ public String toString() { } } else { buf.append(", metric=").append(Arrays.toString(metric)); - buf.append(", tags=["); - for (final Iterator it = tags.iterator(); it.hasNext(); ) { - buf.append(Arrays.toString(it.next())); + buf.append(", filters=["); + for (final Iterator it = filters.iterator(); it.hasNext(); ) { + buf.append(it.next()); if (it.hasNext()) { buf.append(','); } @@ -1129,18 +1167,32 @@ public String toString() { .append(", group_bys=("); if (group_bys != null) { for (final byte[] tag_id : group_bys) { - buf.append(Arrays.toString(tag_id)); - if (group_by_values != null) { - final byte[][] value_ids = group_by_values.get(tag_id); + try { + buf.append(tsdb.tag_names.getName(tag_id)); + } catch (NoSuchUniqueId e) { + buf.append('<').append(e.getMessage()).append('>'); + } + buf.append(' ') + .append(Arrays.toString(tag_id)); + if (row_key_literals != null) { + final byte[][] value_ids = row_key_literals.get(tag_id); if (value_ids == null) { continue; } buf.append("={"); - for (int i = 0; i < value_ids.length; i++) { - buf.append(Arrays.toString(value_ids[i])); - if (i < value_ids.length - 1) { - buf.append(','); + for (final byte[] value_id : value_ids) { + try { + if (value_id != null) { + buf.append(tsdb.tag_values.getName(value_id)); + } else { + buf.append("null"); + } + } catch (NoSuchUniqueId e) { + buf.append('<').append(e.getMessage()).append('>'); } + buf.append(' ') + .append(Arrays.toString(value_id)) + .append(", "); } buf.append('}'); } @@ -1214,16 +1266,17 @@ static RateOptions getRateOptions(final TsdbQuery query) { return query.rate_options; } - static ArrayList getTags(final TsdbQuery query) { - return query.tags; + static List getFilters(final TsdbQuery query) { + return query.filters; } static ArrayList getGroupBys(final TsdbQuery query) { return query.group_bys; } - static ByteMap getGroupByValues(final TsdbQuery query) { - return query.group_by_values; + static ByteMap getRowKeyLiterals(final TsdbQuery query) { + return query.row_key_literals; } + } } diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java index d41df36f5b..6992fe6a97 100644 --- a/test/core/TestSaltScanner.java +++ b/test/core/TestSaltScanner.java @@ -17,8 +17,11 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.nio.charset.Charset; @@ -27,6 +30,7 @@ import java.util.List; import java.util.TreeMap; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.uid.UniqueId; import org.hbase.async.KeyValue; @@ -34,7 +38,6 @@ 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; @@ -66,6 +69,7 @@ public class TestSaltScanner extends BaseTsdbTest { private final static int NUM_BUCKETS = 2; private List scanners; private TreeMap spans; + private List filters; private List>> kvs_a; private List>> kvs_b; @@ -79,67 +83,70 @@ public void beforeLocal() { PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); + filters = new ArrayList(); + spans = new TreeMap(new RowKey.SaltCmp()); setupMockScanners(true); } @Test public void ctor() { - assertNotNull(new SaltScanner(tsdb, METRIC_BYTES, scanners, spans)); + assertNotNull(new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters)); } @Test (expected = IllegalArgumentException.class) public void ctorSaltDisabled() { PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); - new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorNullTSDB() { - new SaltScanner(null, METRIC_BYTES, scanners, spans); + new SaltScanner(null, METRIC_BYTES, scanners, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorNullMETRIC_BYTES() { - new SaltScanner(tsdb, null, scanners, spans); + new SaltScanner(tsdb, null, scanners, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorShortMETRIC_BYTES() { - new SaltScanner(tsdb, new byte[] { 0, 1 }, scanners, spans); + new SaltScanner(tsdb, new byte[] { 0, 1 }, scanners, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorNullScanners() { - new SaltScanner(tsdb, METRIC_BYTES, null, spans); + new SaltScanner(tsdb, METRIC_BYTES, null, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorNotEnoughScanners() { scanners.remove(1); - new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorTooManyScanners() { scanners.add(mock(Scanner.class)); - new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); } @Test (expected = IllegalArgumentException.class) public void ctorNullSpans() { - new SaltScanner(tsdb, METRIC_BYTES, scanners, null); + new SaltScanner(tsdb, METRIC_BYTES, scanners, null, filters); } @Test (expected = IllegalArgumentException.class) public void ctorSpansHaveData() { spans.put(new byte[] { 0, 0, 0, 1 }, new Span(tsdb)); - new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); } @Test public void scanNoData() throws Exception { - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); assertTrue(spans == scanner.scan().joinUninterruptibly()); assertTrue(spans.isEmpty()); } @@ -147,7 +154,82 @@ public void scanNoData() throws Exception { @Test public void scan() throws Exception { setupMockScanners(false); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); + assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertEquals(3, spans.size()); + + Span span = spans.get(KEY_A); + assertEquals(2, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(VALUE_LONG, span.longValue(1)); + assertEquals(1356998401000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(KEY_B); + assertEquals(1, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(KEY_C); + assertEquals(2, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(VALUE_LONG, span.longValue(1)); + assertEquals(1359680401000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + + verify(tag_values, never()).getNameAsync(TAGV_BYTES); + verify(tag_values, never()).getNameAsync(TAGV_B_BYTES); + } + + @Test + public void scanWithFilter() throws Exception { + setupMockScanners(false); + filters.add(TagVFilter.Builder().setType("regexp").setFilter("web.*") + .setTagk(TAGK_STRING).build()); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); + assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertEquals(3, spans.size()); + + Span span = spans.get(KEY_A); + assertEquals(2, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(VALUE_LONG, span.longValue(1)); + assertEquals(1356998401000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(KEY_B); + assertEquals(1, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(KEY_C); + assertEquals(2, span.size()); + assertEquals(VALUE_LONG, span.longValue(0)); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(VALUE_LONG, span.longValue(1)); + assertEquals(1359680401000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + + verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); + } + + @Test + public void scanWithTwoFilter() throws Exception { + setupMockScanners(false); + filters.add(TagVFilter.Builder().setType("regexp").setFilter("web.*") + .setTagk(TAGK_STRING).build()); + filters.add(TagVFilter.Builder().setType("wildcard").setFilter("web*") + .setTagk(TAGK_STRING).build()); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); assertTrue(spans == scanner.scan().joinUninterruptibly()); assertEquals(3, spans.size()); @@ -172,6 +254,40 @@ public void scan() throws Exception { assertEquals(VALUE_LONG, span.longValue(1)); assertEquals(1359680401000L, span.timestamp(1)); assertEquals(0, span.getAnnotations().size()); + + verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); + } + + @Test + public void scanWithFilterNoMatch() throws Exception { + setupMockScanners(false); + filters.add(TagVFilter.Builder().setType("regexp").setFilter("db.*") + .setTagk(TAGK_STRING).build()); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); + assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertEquals(0, spans.size()); + + verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); + } + + @Test + public void scanWithTwoFiltersNoMatch() throws Exception { + setupMockScanners(false); + filters.add(TagVFilter.Builder().setType("regexp").setFilter("web.*") + .setTagk(TAGK_STRING).build()); + filters.add(TagVFilter.Builder().setType("wildcard").setFilter("db*") + .setTagk(TAGK_STRING).build()); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); + assertTrue(spans == scanner.scan().joinUninterruptibly()); + assertEquals(0, spans.size()); + + verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); } @Test @@ -184,7 +300,8 @@ public void scanHBaseScannerFromDeferredA() throws Exception { .thenReturn(Deferred. >>fromError(e)); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); try { scanner.scan().joinUninterruptibly(); fail("Expected a runtime exception here"); @@ -204,7 +321,8 @@ public void scanHBaseScannerFromDeferredB() throws Exception { .thenReturn(Deferred. >>fromError(e)); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); try { scanner.scan().joinUninterruptibly(); fail("Expected a runtime exception here"); @@ -222,7 +340,8 @@ public void scanHBaseScannerThrownA() throws Exception { .thenReturn(Deferred.fromResult(kvs_a.get(0))) .thenThrow(e); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); try { scanner.scan().joinUninterruptibly(); fail("Expected a runtime exception here"); @@ -241,7 +360,8 @@ public void scanHBaseScannerThrownB() throws Exception { .thenReturn(Deferred.fromResult(kvs_b.get(1))) .thenThrow(e); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); try { scanner.scan().joinUninterruptibly(); fail("Expected a runtime exception here"); @@ -269,7 +389,8 @@ public void scanBadRowKey() throws Exception { .thenReturn(Deferred.fromResult(kvs_a.get(2))) .thenReturn(Deferred.>>fromResult(null)); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); scanner.scan().joinUninterruptibly(); } @@ -281,7 +402,8 @@ public void scanCompactionDataException() throws Exception { doThrow(new IllegalDataException("Boo!")).when( tsdb).compact(any(ArrayList.class), any(List.class)); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); scanner.scan().joinUninterruptibly(); } @@ -293,7 +415,8 @@ public void scanCompactionRuntimeException() throws Exception { doThrow(new RuntimeException("Boo!")).when( tsdb).compact(any(ArrayList.class), any(List.class)); - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans); + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); scanner.scan().joinUninterruptibly(); } diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 62b62cfc6e..b26f9f5e65 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -19,10 +19,12 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import net.opentsdb.core.TsdbQuery.ForTesting; -import net.opentsdb.storage.MockBase; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVWildcardFilter; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.DateTime; @@ -128,6 +130,14 @@ public void getEndTimeNotSet() throws Exception { public void setTimeSeries() throws Exception { query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); assertNotNull(query); + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(1, ForTesting.getFilters(query).size()); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); + assertEquals(1, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES).length); + assertArrayEquals(TAGV_BYTES, + ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)[0]); } @Test (expected = NullPointerException.class) @@ -197,11 +207,9 @@ public void configureFromQuery() throws Exception { query.configureFromQuery(ts_query, 0).joinUninterruptibly(); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); - assertEquals(1, ForTesting.getTags(query).size()); - assertArrayEquals(MockBase.concatByteArrays(TAGK_BYTES, TAGV_BYTES), - ForTesting.getTags(query).get(0)); - assertNull(ForTesting.getGroupBys(query)); - assertNull(ForTesting.getGroupByValues(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)); } @@ -217,11 +225,9 @@ public void configureFromQueryWithRate() throws Exception { query.configureFromQuery(ts_query, 0).joinUninterruptibly(); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); - assertEquals(1, ForTesting.getTags(query).size()); - assertArrayEquals(MockBase.concatByteArrays(TAGK_BYTES, TAGV_BYTES), - ForTesting.getTags(query).get(0)); - assertNull(ForTesting.getGroupBys(query)); - assertNull(ForTesting.getGroupByValues(query)); + assertEquals(1, ForTesting.getFilters(query).size()); + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getGroupBys(query).size()); assertTrue(rate_options == ForTesting.getRateOptions(query)); } @@ -229,55 +235,119 @@ public void configureFromQueryWithRate() throws Exception { public void configureFromQueryNoTags() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).setTags(null); + ts_query.getQueries().get(0).setTags(Collections.EMPTY_MAP); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); - assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(0, ForTesting.getFilters(query).size()); assertNull(ForTesting.getGroupBys(query)); - assertNull(ForTesting.getGroupByValues(query)); + assertNull(ForTesting.getRowKeyLiterals(query)); } @Test public void configureFromQueryGroupByAll() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(TAGK_STRING, "*"); + tags.clear(); + tags.put(TAGK_STRING, "*"); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); - assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); - assertNull(ForTesting.getGroupByValues(query)); + assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); + assertNull(ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)); } @Test public void configureFromQueryGroupByPipe() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(TAGK_STRING, - TAGV_STRING + "|" + TAGV_B_STRING); + tags.clear(); + tags.put(TAGK_STRING, TAGV_STRING + "|" + TAGV_B_STRING); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); - assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); - assertArrayEquals(TAGK_BYTES, - ForTesting.getGroupBys(query).get(0)); - assertEquals(1, ForTesting.getGroupByValues(query).size()); - final byte[][] tag_values = ForTesting.getGroupByValues(query) - .iterator().next().getValue(); - assertEquals(2, tag_values.length); - assertArrayEquals(TAGK_BYTES, tag_values[0]); - assertArrayEquals(new byte[] { 0, 0, 2 }, tag_values[1]); + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); + assertEquals(2, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES).length); + assertArrayEquals(TAGV_BYTES, + ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)[0]); + assertArrayEquals(TAGV_B_BYTES, + ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)[1]); + } + + @Test + public void configureFromQueryWithGroupByFilter() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + tags.clear(); + tags.put("host", TagVWildcardFilter.FILTER_NAME + "(*imes)"); + ts_query.getQueries().get(0).setTags(tags); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(1, ForTesting.getFilters(query).size()); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); + assertNull(ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)); + assertNotNull(ForTesting.getRateOptions(query)); + } + + @Test + public void configureFromQueryWithFilter() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "*imes")); + ts_query.getQueries().get(0).setFilters(filters); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(1, ForTesting.getFilters(query).size()); + assertNull(ForTesting.getGroupBys(query)); + assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); + assertNull(ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)); + assertNotNull(ForTesting.getRateOptions(query)); + } + + @Test + public void configureFromQueryWithGroupByAndRegularFilters() throws Exception { + setDataPointStorage(); + final TSQuery ts_query = getTSQuery(); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "*imes")); + filters.add(TagVFilter.Builder().setFilter("*").setTagk("host") + .setType("wildcard").setGroupBy(true).build()); + ts_query.getQueries().get(0).setFilters(filters); + ts_query.validateAndSetQuery(); + + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); + assertEquals(2, ForTesting.getFilters(query).size()); + assertEquals(1, ForTesting.getGroupBys(query).size()); + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); + assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); + assertNull(ForTesting.getRowKeyLiterals(query).get(TAGK_BYTES)); + assertNotNull(ForTesting.getRateOptions(query)); } @Test (expected = IllegalArgumentException.class) @@ -319,7 +389,9 @@ public void configureFromQueryNSUMetric() throws Exception { public void configureFromQueryNSUTagk() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(NSUN_TAGK, TAGV_STRING); + tags.clear(); + tags.put(NSUN_TAGK, TAGV_STRING); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); @@ -329,7 +401,9 @@ public void configureFromQueryNSUTagk() throws Exception { public void configureFromQueryNSUTagv() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(TAGK_STRING, NSUN_TAGV); + tags.clear(); + tags.put(TAGK_STRING, NSUN_TAGV); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); @@ -339,8 +413,9 @@ public void configureFromQueryNSUTagv() throws Exception { public void configureFromQueryGroupByPipeNSUTagk() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(NSUN_TAGK, - TAGV_STRING + "|" + TAGV_B_STRING); + tags.clear(); + tags.put(NSUN_TAGK, TAGV_STRING + "|" + TAGV_B_STRING); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); @@ -350,8 +425,9 @@ public void configureFromQueryGroupByPipeNSUTagk() throws Exception { public void configureFromQueryGroupByPipeNSUTagv() throws Exception { setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(TAGK_STRING, - TAGV_STRING + "|" + NSUN_TAGV); + tags.clear(); + tags.put(TAGK_STRING, TAGV_STRING + "|" + NSUN_TAGV); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); @@ -363,23 +439,18 @@ public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() config.overrideConfig("tsd.query.skip_unresolved_tagvs", "true"); setDataPointStorage(); final TSQuery ts_query = getTSQuery(); - ts_query.getQueries().get(0).getTags().put(TAGK_STRING, - TAGV_STRING + "|" + NSUN_TAGV); + tags.clear(); + tags.put(TAGK_STRING, TAGV_STRING + "|" + NSUN_TAGV); + ts_query.getQueries().get(0).setTags(tags); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); - assertEquals(0, ForTesting.getTags(query).size()); + assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); - assertEquals(1, ForTesting.getGroupByValues(query).size()); - final byte[][] tag_values = ForTesting.getGroupByValues(query) - .iterator().next().getValue(); - assertEquals(2, tag_values.length); - assertArrayEquals(TAGV_BYTES, tag_values[0]); - assertArrayEquals(new byte[] { 0, 0, 0 }, tag_values[1]); } /** @return a simple TSQuery object for testing */ diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 32fde1e46e..0f8c84551d 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -15,6 +15,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.lang.reflect.Field; @@ -63,6 +67,8 @@ public void runLongSingleTS() throws Exception { int value = 1; long timestamp = 1356998430000L; + verify(tag_values, times(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, never()).getNameAsync(TAGV_B_BYTES); for (DataPoint dp : dps[0]) { assertEquals(value, dp.longValue()); assertEquals(timestamp, dp.timestamp()); @@ -1399,4 +1405,45 @@ public void runInterpolationMsDownsampled() throws Exception { } assertEquals(151, dps[0].size()); } + + @Test + public void runRegexp() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + tags.clear(); + tags.put("host", "regexp(web01)"); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); + int value = 1; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + value++; + timestamp += 30000; + } + assertEquals(300, dps[0].aggregatedSize()); + } + + @Test + public void runRegexpNoMatch() throws Exception { + storeLongTimeSeriesSeconds(true, false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + tags.clear(); + tags.put("host", "regexp(dbsvr.*)"); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_BYTES); + verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); + assertEquals(0, dps.length); + } } From 24773ed3d05e28a2fc7f3680d06cb21f99368f54 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 6 Jun 2015 18:55:45 -0700 Subject: [PATCH 180/826] Add the /api/config/filter endpoint for displaying filter info Signed-off-by: Chris Larsen --- src/tsd/HttpJsonSerializer.java | 11 +++++++++++ src/tsd/HttpSerializer.java | 14 ++++++++++++++ src/tsd/RpcManager.java | 22 ++++++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 94d8d6a0f5..a7d548b4b0 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -1080,6 +1080,17 @@ public ChannelBuffer formatConfigV1(final Config config) { return serializeJSON(map); } + /** + * Format the loaded filter configurations + * @param config The filters to serialize + * @return A ChannelBuffer object to pass on to the caller + * @throws JSONException if serialization failed + */ + public ChannelBuffer formatFilterConfigV1( + final Map> config) { + return serializeJSON(config); + } + /** * Helper object for the format calls to wrap the JSON response in a JSONP * function if requested. Used for code dedupe. diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index ba391d6c2e..ee12052aaf 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -740,6 +740,20 @@ public ChannelBuffer formatConfigV1(final Config config) { " has not implemented formatConfigV1"); } + /** + * Format the loaded filter configurations + * @param config The filters to serialize + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + */ + public ChannelBuffer formatFilterConfigV1( + final Map> config) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatFilterConfigV1"); + } + /** * Formats a 404 error when an endpoint or file wasn't found *

    diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 603fd3e3f3..0f640e2970 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -43,6 +43,7 @@ import net.opentsdb.tools.BuildData; import net.opentsdb.core.Aggregators; import net.opentsdb.core.TSDB; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.Config; import net.opentsdb.utils.JSON; @@ -714,15 +715,32 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { "] is not permitted for this endpoint"); } - switch (query.apiVersion()) { + final String[] uri = query.explodeAPIPath(); + final String endpoint = uri.length > 1 ? uri[1].toLowerCase() : ""; + + if (endpoint.equals("filters")) { + switch (query.apiVersion()) { case 0: case 1: - query.sendReply(query.serializer().formatConfigV1(tsdb.getConfig())); + query.sendReply(query.serializer().formatFilterConfigV1( + TagVFilter.loadedFilters())); break; default: throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); + } + } else { + switch (query.apiVersion()) { + case 0: + case 1: + query.sendReply(query.serializer().formatConfigV1(tsdb.getConfig())); + break; + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + + query.apiVersion() + " is not implemented"); + } } } } From 45cb41ec4aa22a4290a57542520c03ce977e984d Mon Sep 17 00:00:00 2001 From: Michal Kimle Date: Thu, 18 Jun 2015 09:28:26 +0200 Subject: [PATCH 181/826] fixed file paths in Makefile --- Makefile.am | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile.am b/Makefile.am index 59f34534e8..ca5d938d53 100644 --- a/Makefile.am +++ b/Makefile.am @@ -73,12 +73,12 @@ tsdb_SRC := \ src/meta/TSMeta.java \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ - src/query/TagVFilter.java \ - src/query/TagVLiteralOrFilter.java \ - src/query/TagVNotKeyFilter.java \ - src/query/TagVNotLiteralOrFilter.java \ - src/query/TagVRegexFilter.java \ - src/query/TagVWildcardFilter.java \ + src/query/filter/TagVFilter.java \ + src/query/filter/TagVLiteralOrFilter.java \ + src/query/filter/TagVNotKeyFilter.java \ + src/query/filter/TagVNotLiteralOrFilter.java \ + src/query/filter/TagVRegexFilter.java \ + src/query/filter/TagVWildcardFilter.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ src/search/TimeSeriesLookup.java \ @@ -203,12 +203,12 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ - test/query/TestTagVFilter.java \ - test/query/TestTagVLiteralOrFilter.java \ - test/query/TestTagVNotKeyFilter.java \ - test/query/TestTagVNotLiteralOrFilter.java \ - test/query/TestTagVRegexFilter.java \ - test/query/TestTagVWildcardFilter.java \ + test/query/filter/TestTagVFilter.java \ + test/query/filter/TestTagVLiteralOrFilter.java \ + test/query/filter/TestTagVNotKeyFilter.java \ + test/query/filter/TestTagVNotLiteralOrFilter.java \ + test/query/filter/TestTagVRegexFilter.java \ + test/query/filter/TestTagVWildcardFilter.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ From 472533264c8ac2376485401fc17a57cf2c622be9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 25 Jun 2015 10:08:15 -0700 Subject: [PATCH 182/826] Fix paths to the filter .java files --- Makefile.am | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile.am b/Makefile.am index 59f34534e8..ca5d938d53 100644 --- a/Makefile.am +++ b/Makefile.am @@ -73,12 +73,12 @@ tsdb_SRC := \ src/meta/TSMeta.java \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ - src/query/TagVFilter.java \ - src/query/TagVLiteralOrFilter.java \ - src/query/TagVNotKeyFilter.java \ - src/query/TagVNotLiteralOrFilter.java \ - src/query/TagVRegexFilter.java \ - src/query/TagVWildcardFilter.java \ + src/query/filter/TagVFilter.java \ + src/query/filter/TagVLiteralOrFilter.java \ + src/query/filter/TagVNotKeyFilter.java \ + src/query/filter/TagVNotLiteralOrFilter.java \ + src/query/filter/TagVRegexFilter.java \ + src/query/filter/TagVWildcardFilter.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ src/search/TimeSeriesLookup.java \ @@ -203,12 +203,12 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ - test/query/TestTagVFilter.java \ - test/query/TestTagVLiteralOrFilter.java \ - test/query/TestTagVNotKeyFilter.java \ - test/query/TestTagVNotLiteralOrFilter.java \ - test/query/TestTagVRegexFilter.java \ - test/query/TestTagVWildcardFilter.java \ + test/query/filter/TestTagVFilter.java \ + test/query/filter/TestTagVLiteralOrFilter.java \ + test/query/filter/TestTagVNotKeyFilter.java \ + test/query/filter/TestTagVNotLiteralOrFilter.java \ + test/query/filter/TestTagVRegexFilter.java \ + test/query/filter/TestTagVWildcardFilter.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ From 1a563ae29849650615eee61d188b34e95cc1a527 Mon Sep 17 00:00:00 2001 From: Lex Herbert Date: Fri, 5 Jun 2015 14:10:19 -0700 Subject: [PATCH 183/826] Updates 'search/lookup' to obey limit This commit updates the 'search/lookup' HTTP API endpoint to obey the SearchQuery limit property. Now, a request can contain an optional 'limit' parameter which sets an upper bound on the number of results returned. Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 11 ++++++++++- src/tsd/SearchRpc.java | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index a95e3d791a..6ff3f89f2d 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -110,6 +110,7 @@ public TimeSeriesLookup(final TSDB tsdb, final SearchQuery query) { */ public List lookup() { LOG.info(query.toString()); + boolean limit_reached = false; final StringBuilder tagv_filter = new StringBuilder(); final Scanner scanner = getScanner(tagv_filter); final List tsuids = new ArrayList(); @@ -166,9 +167,17 @@ public List lookup() { } buf.setLength(0); // reset the buffer so we can re-use it } else { - tsuids.add(tsuid); + if(tsuids.size() < query.getLimit()) { + tsuids.add(tsuid); + } else { + limit_reached = true; + break; + } } } + if(limit_reached) { + break; + } } } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 3b40425bff..12431a0abe 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -109,6 +109,15 @@ private final SearchQuery parseQueryString(final HttpQuery query, } catch (IllegalArgumentException e) { throw new BadRequestException("Unable to parse query", e); } + if (query.hasQueryStringParam("limit")) { + final String limit = query.getQueryStringParam("limit"); + try { + search_query.setLimit(Integer.parseInt(limit)); + } catch (NumberFormatException e) { + throw new BadRequestException( + "Unable to convert 'limit' to a valid number"); + } + } return search_query; } From 496915f2ff0e7093b7cca5172090cd82dd19658d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 6 Aug 2015 21:28:12 -0700 Subject: [PATCH 184/826] Add a UT for the TimeSeriesLookup Limit fix from @lexh. Thanks! Signed-off-by: Chris Larsen --- test/search/TestTimeSeriesLookup.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index 347ae95ace..126c15b581 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -551,6 +551,22 @@ public void tagPairOnlyData() throws Exception { assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); } + @Test + public void limitVerification() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "web01")); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + query.setLimit(1); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + } + // TODO test the dump to stdout /** From d5f79860b16fddfb11ad67a0c54cf5dbe6663d2f Mon Sep 17 00:00:00 2001 From: Lex Herbert Date: Fri, 5 Jun 2015 14:10:19 -0700 Subject: [PATCH 185/826] Updates 'search/lookup' to obey limit This commit updates the 'search/lookup' HTTP API endpoint to obey the SearchQuery limit property. Now, a request can contain an optional 'limit' parameter which sets an upper bound on the number of results returned. Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 11 ++++++++++- src/tsd/SearchRpc.java | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index a95e3d791a..6ff3f89f2d 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -110,6 +110,7 @@ public TimeSeriesLookup(final TSDB tsdb, final SearchQuery query) { */ public List lookup() { LOG.info(query.toString()); + boolean limit_reached = false; final StringBuilder tagv_filter = new StringBuilder(); final Scanner scanner = getScanner(tagv_filter); final List tsuids = new ArrayList(); @@ -166,9 +167,17 @@ public List lookup() { } buf.setLength(0); // reset the buffer so we can re-use it } else { - tsuids.add(tsuid); + if(tsuids.size() < query.getLimit()) { + tsuids.add(tsuid); + } else { + limit_reached = true; + break; + } } } + if(limit_reached) { + break; + } } } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 3b40425bff..12431a0abe 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -109,6 +109,15 @@ private final SearchQuery parseQueryString(final HttpQuery query, } catch (IllegalArgumentException e) { throw new BadRequestException("Unable to parse query", e); } + if (query.hasQueryStringParam("limit")) { + final String limit = query.getQueryStringParam("limit"); + try { + search_query.setLimit(Integer.parseInt(limit)); + } catch (NumberFormatException e) { + throw new BadRequestException( + "Unable to convert 'limit' to a valid number"); + } + } return search_query; } From c775b5fb8a4e72288cdd9b8c76f026455d1cc4ce Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 6 Aug 2015 21:28:12 -0700 Subject: [PATCH 186/826] Add a UT for the TimeSeriesLookup Limit fix from @lexh. Thanks! Signed-off-by: Chris Larsen --- test/search/TestTimeSeriesLookup.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index e302f09cfb..73dc702c9c 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -542,6 +542,22 @@ public void tagPairOnlyData() throws Exception { assertArrayEquals(test_tsuids.get(3), tsuids.get(2)); } + @Test + public void limitVerification() throws Exception { + generateData(); + final List> tags = + new ArrayList>(1); + tags.add(new Pair("host", "web01")); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + query.setLimit(1); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + final List tsuids = lookup.lookup(); + assertNotNull(tsuids); + assertEquals(1, tsuids.size()); + assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); + } + // TODO test the dump to stdout /** From 010ed96a572f33b35b570cc4c4ce80a5a97b371a Mon Sep 17 00:00:00 2001 From: Cristian Sechel Date: Mon, 25 May 2015 14:09:44 +0300 Subject: [PATCH 187/826] Added a -P option to check_tsd: Only alarm if PERCENT of the data points violate the threshold. Signed-off-by: Chris Larsen --- tools/check_tsd | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index 522d8a5a4e..ee022782d7 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -68,9 +68,9 @@ def main(argv): parser.add_option('-I', '--ignore-recent', default=0, type='int', metavar='SECONDS', help='Ignore data points that are that' ' are that recent.') - parser.add_option('-P', '--bad-percent', default=None, type='float', - metavar='PCT', help='Do not alarm if less than PCT% bad values' - ' are found.') + parser.add_option('-P', '--percent-over', dest='percent_over', default=0, + metavar='PERCENT', type='float', help='Only alarm if PERCENT of the data' + ' points violate the threshold.') parser.add_option('-S', '--ssl', default=False, action='store_true', help='Make queries to OpenTSDB via SSL (https)') (options, args) = parser.parse_args(args=argv[1:]) @@ -93,6 +93,10 @@ def main(argv): ' critical threshold (-c).') elif options.ignore_recent < 0: parser.error('--ignore-recent must be positive.') + elif options.percent_over < 0 or options.percent_over > 100: + parser.error('--percent-over must be in the range 0..100.') + + options.percent_over /= 100.0 # Convert to range 0-1 if not options.critical: options.critical = options.warning @@ -176,7 +180,8 @@ def main(argv): badval = None # Value of the bad value we found, if any. npoints = 0 # How many values have we seen? nbad = 0 # How many bad values have we seen? - bad_pct = 0 # Percent of bad values we've found + ncrit = 0 # How many critical values have we seen? + nwarn = 0 # How many warning values have we seen? for datapoint in datapoints: datapoint = datapoint.split() ts = int(datapoint[1]) @@ -192,19 +197,25 @@ def main(argv): bad = False # Is the current value bad? # compare to warning/crit if comparator(val, options.critical): - rv = 2 bad = True - nbad += 1 + ncrit += 1 + nwarn += 1 elif rv < 2 and comparator(val, options.warning): - rv = 1 bad = True - nbad += 1 + nwarn += 1 if (bad and (badval is None # First bad value we find. or comparator(val, badval))): # Worse value. badval = val badts = ts - + if ncrit > 0 and (float(ncrit) / npoints > options.percent_over): + rv = 2 + nbad = ncrit + elif nwarn > 0 and (float(nwarn) / npoints > options.percent_over): + rv = 1 + nbad = nwarn + else: + rv=0 if options.verbose and len(datapoints) != npoints: print ('ignored %d/%d data points for being more than %ds old' % (len(datapoints) - npoints, len(datapoints), options.duration)) From 0d2fff30be3b13c52be66c368d9c9f7bc6459969 Mon Sep 17 00:00:00 2001 From: Bikrant Neupane Date: Thu, 20 Aug 2015 18:18:02 -0700 Subject: [PATCH 188/826] Added "&tz=" paramter to tsdb url This should make easy to understand graphs in different timezones - Note: tz values are not verifed. - TIMEZONE: Any valid Java timezone ID: PST, UTC, CST, Asia/Kathmandu etc --- src/tsd/client/QueryUi.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index ad09380bb5..98e8080bd5 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -147,6 +147,7 @@ public class QueryUi implements EntryPoint, HistoryListener { // Styling options. private final CheckBox smooth = new CheckBox(); private final ListBox styles = new ListBox(); + private String timezone = ""; /** * Handles every change to the query form and gets a new graph. @@ -787,6 +788,13 @@ private void refreshFromQueryString() { autoreload.setValue(qs.containsKey("autoreload"), true); maybeSetTextbox(qs, "autoreload", autoreoload_interval); + //get the tz param value + final ArrayList tzvalues = qs.get("tz"); + if (tzvalues == null) + timezone = ""; + else + timezone = tzvalues.get(0); + final ArrayList newmetrics = qs.get("m"); if (newmetrics == null) { // Clear all metric forms. final int toremove = metrics.getWidgetCount() - 1; @@ -893,6 +901,10 @@ private void refreshGraph() { // a special parameter that the server will delete from the query. url.append("&ignore=" + nrequests++); } + + if(timezone.length() > 1) + url.append("&tz=").append(timezone); + if (!addAllMetrics(url)) { return; } From c7173eb01eebd60f348d8072e05127fa57ad5859 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 14 Aug 2015 18:52:44 -0700 Subject: [PATCH 189/826] Fix a threading issue with the salt scanner thanks to @swaroopgr Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index bff70b2acb..175c7757aa 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import net.opentsdb.meta.Annotation; import net.opentsdb.query.filter.TagVFilter; @@ -83,7 +84,7 @@ public class SaltScanner { private final TSDB tsdb; /** A counter used to determine how many scanners are still running */ - private volatile int completed_tasks = 0; + private AtomicInteger completed_tasks = new AtomicInteger(); /** When the scanning started. We store the scan latency once all scanners * are done.*/ @@ -430,7 +431,7 @@ void processRow(final byte[] key, final ArrayList row) { private void validateAndTriggerCallback(final List kvs, final Map> annotations) { - final int tasks = ++completed_tasks; + final int tasks = completed_tasks.incrementAndGet(); if (kvs.size() > 0) { kv_map.put(tasks, kvs); } @@ -473,7 +474,7 @@ private void handleException(final Exception e) { } } - final int tasks = ++completed_tasks; + final int tasks = completed_tasks.incrementAndGet(); if (tasks >= Const.SALT_BUCKETS()) { results.callback(exception); } From 432d1ea48eaf753d0ac5aac6e9065b98105c0f83 Mon Sep 17 00:00:00 2001 From: Andre Pech Date: Mon, 4 May 2015 17:06:22 -0700 Subject: [PATCH 190/826] Change all URLs returned by tsd to be relative instead of absolute. This is necessary to root tsd under another path when it's sharing the domain with another application. Signed-off-by: Chris Larsen --- src/tsd/RpcManager.java | 2 +- src/tsd/client/QueryUi.java | 12 ++++++------ src/tsd/client/RemoteOracle.java | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 0f640e2970..96b98d6f46 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -567,7 +567,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) + ""); query.sendReply(HttpQuery.makePage( "", + + " src=s/queryui.nocache.js>", "TSD", "Time Series Database", buf.toString())); } } diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 98e8080bd5..4e2eccdd9d 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -113,10 +113,10 @@ public class QueryUi implements EntryPoint, HistoryListener { } // Some URLs we use to fetch data from the TSD. - private static final String AGGREGATORS_URL = "/aggregators"; - private static final String LOGS_URL = "/logs?json"; - private static final String STATS_URL = "/stats?json"; - private static final String VERSION_URL = "/version?json"; + private static final String AGGREGATORS_URL = "aggregators"; + private static final String LOGS_URL = "logs?json"; + private static final String STATS_URL = "stats?json"; + private static final String VERSION_URL = "version?json"; private static final DateTimeFormat FULLDATE = DateTimeFormat.getFormat("yyyy/MM/dd-HH:mm:ss"); @@ -876,7 +876,7 @@ private void refreshGraph() { } } final StringBuilder url = new StringBuilder(); - url.append("/q?start="); + url.append("q?start="); final String start_text = start_datebox.getTextBox().getText(); if (start_text.endsWith(" ago") || start_text.endsWith("-ago")) { url.append(start_text); @@ -956,7 +956,7 @@ public void got(final JSONValue json) { } else { clearError(); - String history = unencodedUri.substring(3) // Remove "/q?". + String history = unencodedUri.substring(2) // Remove "q?". .replaceFirst("ignore=[^&]*&", ""); // Unnecessary cruft. if (autoreload.getValue()) { history += "&autoreload=" + autoreoload_interval.getText(); diff --git a/src/tsd/client/RemoteOracle.java b/src/tsd/client/RemoteOracle.java index 971b42a489..2eb18ecae9 100644 --- a/src/tsd/client/RemoteOracle.java +++ b/src/tsd/client/RemoteOracle.java @@ -40,7 +40,7 @@ */ final class RemoteOracle extends SuggestOracle { - private static final String SUGGEST_URL = "/suggest?type="; // + type&q=foo + private static final String SUGGEST_URL = "suggest?type="; // + type&q=foo /** * Maps an oracle type to its suggestion cache. From 262b31bb81ff6829016fad61aa266d91202c53b1 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 14:32:33 -0700 Subject: [PATCH 191/826] Add TSDB.getUIDAsync() for async resolution Add Tags.resolveAllAsync() for the same Signed-off-by: Chris Larsen --- src/core/TSDB.java | 24 ++++++++++++++++++++++++ src/core/Tags.java | 15 +++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index a44f9ef094..cb3a190415 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -327,6 +327,30 @@ public byte[] getUID(final UniqueIdType type, final String name) { } } + /** + * Attempts to find the UID matching a given name + * @param type The type of UID + * @param name The name to search for + * @throws IllegalArgumentException if the type is not valid + * @throws NoSuchUniqueName if the name was not found + * @since 2.1 + */ + public Deferred getUIDAsync(final UniqueIdType type, final String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Missing UID name"); + } + switch (type) { + case METRIC: + return metrics.getIdAsync(name); + case TAGK: + return tag_names.getIdAsync(name); + case TAGV: + return tag_values.getIdAsync(name); + default: + throw new IllegalArgumentException("Unrecognized UID type"); + } + } + /** * Verifies that the data and UID tables exist in HBase and optionally the * tree and meta data tables if the user has enabled meta tracking or tree diff --git a/src/core/Tags.java b/src/core/Tags.java index 5422baeaab..500857f82d 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -436,6 +436,21 @@ public static ArrayList resolveAll(final TSDB tsdb, throw new RuntimeException("Should never happen!", e); } } + + /** + * Resolves a set of tag strings to their UIDs asynchronously + * @param tsdb the TSDB to use for access + * @param tags The tags to resolve + * @return A deferred with the list of UIDs in tagk1, tagv1, .. tagkn, tagvn + * order + * @throws NoSuchUniqueName if one of the elements in the map contained an + * unknown tag name or tag value. + * @since 2.1 + */ + public static Deferred> resolveAllAsync(final TSDB tsdb, + final Map tags) { + return resolveAllInternalAsync(tsdb, tags, false); + } /** * Resolves (and creates, if necessary) all the tags (name=value) into the a From 651add5eb61febf93b1d06e4c1071c7c46563558 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 15:56:24 -0700 Subject: [PATCH 192/826] Add an Internal.baseTime() overload to normalize an epoch timestamp. Signed-off-by: Chris Larsen --- src/core/Internal.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index 89961f045f..70a49480e2 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -95,7 +95,18 @@ public static String metricName(final TSDB tsdb, final byte[] id) { public static long baseTime(final TSDB tsdb, final byte[] row) { return Bytes.getUnsignedInt(row, tsdb.metrics.width()); } - + + /** @return the time normalized to an hour boundary in epoch seconds */ + public static long baseTime(final long timestamp) { + if ((timestamp & Const.SECOND_MASK) != 0) { + // drop the ms timestamp to seconds to calculate the base timestamp + return ((timestamp / 1000) - + ((timestamp / 1000) % Const.MAX_TIMESPAN)); + } else { + return (timestamp - (timestamp % Const.MAX_TIMESPAN)); + } + } + /** @see Tags#getTags */ public static Map getTags(final TSDB tsdb, final byte[] row) { return Tags.getTags(tsdb, row); From c39150b1b32eff3432f7f802ab1a7928f411176c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 16:41:14 -0700 Subject: [PATCH 193/826] Bring the DateTime.currentTimeMillis() up from 2.2 to 2.1 --- src/utils/DateTime.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index dca0de671b..6ee5c37e22 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -256,4 +256,15 @@ public static void setDefaultTimezone(final String tzname) { throw new IllegalArgumentException("Invalid timezone name: " + tzname); } } + + /** + * Pass through to {@link System.currentTimeMillis} for use in classes to + * make unit testing easier. Mocking System.class is a bad idea in general + * so placing this here and mocking DateTime.class is MUCH cleaner. + * @return The current epoch time in milliseconds + * @since 2.1 + */ + public static long currentTimeMillis() { + return System.currentTimeMillis(); + } } From df5e92360b5e2a9a41464b87e6b3e2a0ff80285a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 19:30:38 -0700 Subject: [PATCH 194/826] Cleanup and fix a bunch of bugs in the TSUIDQuery class. These should help out with #553 Signed-off-by: Chris Larsen --- src/meta/TSUIDQuery.java | 875 ++++++++++++++++++++++------------ test/meta/TestTSUIDQuery.java | 728 ++++++++++++++++++++++++++-- 2 files changed, 1257 insertions(+), 346 deletions(-) diff --git a/src/meta/TSUIDQuery.java b/src/meta/TSUIDQuery.java index b22031c5f7..57f02d2e3e 100644 --- a/src/meta/TSUIDQuery.java +++ b/src/meta/TSUIDQuery.java @@ -18,16 +18,17 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.Internal; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; -import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.DateTime; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.GetRequest; @@ -38,10 +39,10 @@ import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; -import com.stumbleupon.async.DeferredGroupException; /** - * Methods for querying the tsdb-meta table. This can be used to figure out what + * Methods for querying the tsdb-meta table or finding the last data point of + * a particular time series. This can be used to figure out what * time series are actually stored as well as fetch TSMeta objects or optimize * queries against the data table. * @since 2.1 @@ -55,26 +56,163 @@ public class TSUIDQuery { */ private static final Charset CHARSET = Charset.forName("ISO-8859-1"); - /** ID of the metric being looked up. */ - private byte[] metric; - - /** - * Tags of the metrics being looked up. - * Each tag is a byte array holding the ID of both the name and value - * of the tag. - * Invariant: an element cannot be both in this array and in group_bys. - */ - private ArrayList tags; - + /** The TSUID that can be set by the caller or after processing the metric */ + private byte[] tsuid; + + /** The metric set by the caller */ + private String metric; + + /** The metric UID after lookup */ + private byte[] metric_uid; + + /** The tags set by the caller */ + private Map tags; + + /** The tag UID list after lookup */ + private ArrayList tag_uids; + + /** Whether or not to resolve names for last data point queries */ + private boolean resolve_names; + + /** How far back, in hours, to scan for last data point queries */ + private int back_scan; + + /** The last timestamp scanned for last data point queries */ + private long last_timestamp; + /** The TSDB we belong to. */ private final TSDB tsdb; /** - * Constructor. + * Default CTor just sets the TSDB reference * @param tsdb The TSDB to use for storage access + * @throws IllegalArgumentException if the TSDB reference is null + * @deprecated Please use one of the other constructors. Will be removed in 2.3 */ public TSUIDQuery(final TSDB tsdb) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB reference cannot be null"); + } + this.tsdb =tsdb; + } + + /** + * CTor used for a TSUID based query when we know exactly what we want + * @param tsdb The TSDB to use for storage access + * @param tsuid A TSUID to use for querying + * @throws IllegalArgumentException if the TSUID is invalid + */ + public TSUIDQuery(final TSDB tsdb, final byte[] tsuid) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB reference cannot be null"); + } + if (tsuid == null || tsuid.length < + TSDB.metrics_width() + TSDB.tagk_width() + TSDB.tagv_width()) { + throw new IllegalArgumentException("TSUID must not be null and must " + + "have a metric and at least one tag pair"); + } + this.tsdb = tsdb; + this.tsuid = tsuid; + } + + /** + * CTor used for a metric style query + * @param tsdb The TSDB to use for storage access + * @param metric The metric to look up + * @param tags The tags to lookup. This may be an empty map if you're scanning + * meta. + * @throws IllegalArgumentException if the metric is null, empty or the tag + * map is null. + */ + public TSUIDQuery(final TSDB tsdb, final String metric, + final Map tags) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB reference cannot be null"); + } + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("Metric cannot be null or empty"); + } + if (tags == null) { + throw new IllegalArgumentException("Tag map cannot be null. Empty is ok"); + } this.tsdb = tsdb; + this.metric = metric; + this.tags = tags; + } + + /** + * Attempts to fetch the last data point for the given metric or TSUID. + * If back_scan == 0 and meta is enabled via + * "tsd.core.meta.enable_tsuid_tracking" or + * "tsd.core.meta.enable_tsuid_incrementing" then we will look up the metric + * or TSUID in the meta table first and use the counter there to get the + * last write time. + *

    + * However if backscan is set, then we'll start with the current time and + * iterate back "back_scan" number of hours until we find a value. + *

    + * @param resolve_names Whether or not to resolve the UIDs back to their + * names when we find a value. + * @param back_scan The number of hours back in time to scan + * @return A data point if found, null if not. Or an exception if something + * went pear shaped. + */ + public Deferred getLastPoint(final boolean resolve_names, + final int back_scan) { + if (back_scan < 0) { + throw new IllegalArgumentException( + "Backscan must be zero or a positive number"); + } + + this.resolve_names = resolve_names; + this.back_scan = back_scan; + + final boolean meta_enabled = tsdb.getConfig().enable_tsuid_tracking() || + tsdb.getConfig().enable_tsuid_incrementing(); + + class TSUIDCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] incoming_tsuid) + throws Exception { + if (tsuid == null && incoming_tsuid == null) { + return Deferred.fromError(new RuntimeException("Both incoming and " + + "supplied TSUIDs were null for " + TSUIDQuery.this)); + } else if (incoming_tsuid != null) { + setTSUID(incoming_tsuid); + } + if (back_scan < 1 && meta_enabled) { + final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); + get.family(TSMeta.FAMILY()); + get.qualifier(TSMeta.COUNTER_QUALIFIER()); + return tsdb.getClient().get(get).addCallbackDeferring(new MetaCB()); + } + + if (last_timestamp > 0) { + last_timestamp = Internal.baseTime(last_timestamp); + } else { + last_timestamp = Internal.baseTime(DateTime.currentTimeMillis()); + } + final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); + final GetRequest get = new GetRequest(tsdb.dataTable(), key); + get.family(TSDB.FAMILY()); + return tsdb.getClient().get(get).addCallbackDeferring(new LastPointCB()); + } + @Override + public String toString() { + return "TSUID callback"; + } + } + + if (tsuid == null) { + return tsuidFromMetric(tsdb, metric, tags) + .addCallbackDeferring(new TSUIDCB()); + } + try { + // damn typed exceptions.... + return new TSUIDCB().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } } /** @@ -83,186 +221,306 @@ public TSUIDQuery(final TSDB tsdb) { * @param tags A map of tag value pairs or simply an empty map * @throws NoSuchUniqueName if the metric or any of the tag names/values did * not exist + * @deprecated Please use one of the constructors instead. Will be removed in 2.3 */ - public void setQuery(final String metric, final HashMap tags) { - this.metric = tsdb.getUID(UniqueIdType.METRIC, metric); - this.tags = Tags.resolveAll(tsdb, tags); + public void setQuery(final String metric, final Map tags) { + this.metric = metric; + this.tags = tags; + metric_uid = tsdb.getUID(UniqueIdType.METRIC, metric); + tag_uids = Tags.resolveAll(tsdb, tags); } /** * Fetches a list of TSUIDs given the metric and optional tag pairs. The query * format is similar to TsdbQuery but doesn't support grouping operators for * tags. Only TSUIDs that had "ts_counter" qualifiers will be returned. + *

    + * NOTE: If you called {@link #setQuery(String, Map)} successfully this will + * immediately scan the meta table. But if you used the CTOR to set the + * metric and tags it will attempt to resolve those and may return an exception. * @return A map of TSUIDs to the last timestamp (in milliseconds) when the * "ts_counter" was updated. Note that the timestamp will be the time stored - * by HBase, not the actual timestamp of the data point + * by HBase, not the actual timestamp of the data point. If nothing was + * found, the map will be empty but not null. * @throws IllegalArgumentException if the metric was not set or the tag map * was null */ public Deferred> getLastWriteTimes() { - // we need at least a metric name and the tags can't be null. Empty tags are - // fine, but the map can't be null. - if (metric == null || metric.length < 0) { - throw new IllegalArgumentException("Missing metric UID"); - } - if (tags == null) { - throw new IllegalArgumentException("Tag map was null"); - } - - final Scanner scanner = getScanner(); - scanner.setQualifier(TSMeta.COUNTER_QUALIFIER()); - final Deferred> results = new Deferred>(); - final ByteMap tsuids = new ByteMap(); - - /** - * Scanner callback that will call itself while iterating through the - * tsdb-meta table - */ - final class ScannerCB implements Callback>> { - - /** - * Starts the scanner and is called recursively to fetch the next set of - * rows from the scanner. - * @return The map of spans if loaded successfully, null if no data was - * found - */ - public Object scan() { - return scanner.nextRows().addCallback(this); - } - - /** - * Loops through each row of the scanner results and parses out data - * points and optional meta data - * @return null if no rows were found, otherwise the TreeMap with spans - */ + class ResolutionCB implements Callback>, Object> { @Override - public Object call(final ArrayList> rows) - throws Exception { - try { - if (rows == null) { - results.callback(tsuids); + public Deferred> call(Object arg0) throws Exception { + final Scanner scanner = getScanner(); + scanner.setQualifier(TSMeta.COUNTER_QUALIFIER()); + final Deferred> results = new Deferred>(); + final ByteMap tsuids = new ByteMap(); + + final class ErrBack implements Callback { + @Override + public Object call(final Exception e) throws Exception { + results.callback(e); return null; } + @Override + public String toString() { + return "Error callback"; + } + } + + /** + * Scanner callback that will call itself while iterating through the + * tsdb-meta table + */ + final class ScannerCB implements Callback>> { - for (final ArrayList row : rows) { - final byte[] tsuid = row.get(0).key(); - tsuids.put(tsuid, row.get(0).timestamp()); + /** + * Starts the scanner and is called recursively to fetch the next set of + * rows from the scanner. + * @return The map of spans if loaded successfully, null if no data was + * found + */ + public Object scan() { + return scanner.nextRows().addCallback(this).addErrback(new ErrBack()); + } + + /** + * Loops through each row of the scanner results and parses out data + * points and optional meta data + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final ArrayList> rows) + throws Exception { + try { + if (rows == null) { + results.callback(tsuids); + return null; + } + + for (final ArrayList row : rows) { + final byte[] tsuid = row.get(0).key(); + tsuids.put(tsuid, row.get(0).timestamp()); + } + return scan(); + } catch (Exception e) { + results.callback(e); + return null; + } } - return scan(); - } catch (Exception e) { - results.callback(e); - return null; } + + new ScannerCB().scan(); + return results; + } + @Override + public String toString() { + return "Last counter time callback"; } } - new ScannerCB().scan(); - return results; + if (metric_uid == null) { + return resolveMetric().addCallbackDeferring(new ResolutionCB()); + } + try { + return new ResolutionCB().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } } /** * Returns all TSMeta objects stored for timeseries defined by this query. The * query is similar to TsdbQuery without any aggregations. Returns an empty * list, when no TSMetas are found. Only returns stored TSMetas. + *

    + * NOTE: If you called {@link #setQuery(String, Map)} successfully this will + * immediately scan the meta table. But if you used the CTOR to set the + * metric and tags it will attempt to resolve those and may return an exception. * @return A list of existing TSMetas for the timeseries covered by the query. * @throws IllegalArgumentException When either no metric was specified or the * tag map was null (Empty map is OK). */ public Deferred> getTSMetas() { - // we need at least a metric name and the tags can't be null. Empty tags are - // fine, but the map can't be null. - if (metric == null || metric.length < 0) { - throw new IllegalArgumentException("Missing metric UID"); - } - if (tags == null) { - throw new IllegalArgumentException("Tag map was null"); - } - - final Scanner scanner = getScanner(); - scanner.setQualifier(TSMeta.META_QUALIFIER()); - final Deferred> results = new Deferred>(); - final List tsmetas = new ArrayList(); - final List> tsmeta_group = new ArrayList>(); - - final class TSMetaGroupCB implements Callback> { - + class ResolutionCB implements Callback>, Object> { @Override - public List call(ArrayList ts) throws Exception { - for (TSMeta tsm: ts) { - if (tsm != null) { - tsmetas.add(tsm); + public Deferred> call(final Object done) throws Exception { + final Scanner scanner = getScanner(); + scanner.setQualifier(TSMeta.META_QUALIFIER()); + final Deferred> results = new Deferred>(); + final List tsmetas = new ArrayList(); + final List> tsmeta_group = new ArrayList>(); + + final class TSMetaGroupCB implements Callback> { + @Override + public List call(ArrayList ts) throws Exception { + for (TSMeta tsm: ts) { + if (tsm != null) { + tsmetas.add(tsm); + } + } + results.callback(tsmetas); + return null; + } + @Override + public String toString() { + return "TSMeta callback"; } } - results.callback(tsmetas); - return null; - } - - } - - /** - * Scanner callback that will call itself while iterating through the - * tsdb-meta table. - * - * Keeps track of a Set of Deferred TSMeta calls. When all rows are scanned, - * will wait for all TSMeta calls to be completed and then create the result - * list. - */ - final class ScannerCB implements Callback>> { - - /** - * Starts the scanner and is called recursively to fetch the next set of - * rows from the scanner. - * @return The map of spans if loaded successfully, null if no data was - * found - */ - public Object scan() { - return scanner.nextRows().addCallback(this); - } - - /** - * Loops through each row of the scanner results and parses out data - * points and optional meta data - * @return null if no rows were found, otherwise the TreeMap with spans - */ - @Override - public Object call(final ArrayList> rows) - throws Exception { - try { - if (rows == null) { - Deferred.group(tsmeta_group).addCallback(new TSMetaGroupCB()); + + final class ErrBack implements Callback { + @Override + public Object call(final Exception e) throws Exception { + results.callback(e); return null; } - for (final ArrayList row : rows) { - tsmeta_group.add(TSMeta.parseFromColumn(tsdb, row.get(0), true)); + @Override + public String toString() { + return "Error callback"; + } + } + + /** + * Scanner callback that will call itself while iterating through the + * tsdb-meta table. + * + * Keeps track of a Set of Deferred TSMeta calls. When all rows are scanned, + * will wait for all TSMeta calls to be completed and then create the result + * list. + */ + final class ScannerCB implements Callback>> { + + /** + * Starts the scanner and is called recursively to fetch the next set of + * rows from the scanner. + * @return The map of spans if loaded successfully, null if no data was + * found + */ + public Object scan() { + return scanner.nextRows().addCallback(this).addErrback(new ErrBack()); + } + + /** + * Loops through each row of the scanner results and parses out data + * points and optional meta data + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final ArrayList> rows) + throws Exception { + try { + if (rows == null) { + Deferred.group(tsmeta_group) + .addCallback(new TSMetaGroupCB()).addErrback(new ErrBack()); + return null; + } + for (final ArrayList row : rows) { + tsmeta_group.add(TSMeta.parseFromColumn(tsdb, row.get(0), true)); + } + return scan(); + } catch (Exception e) { + results.callback(e); + return null; + } } - return scan(); - } catch (Exception e) { - results.callback(e); - return null; } + + new ScannerCB().scan(); + return results; + } + @Override + public String toString() { + return "TSMeta scan callback"; } } - new ScannerCB().scan(); - return results; + if (metric_uid == null) { + return resolveMetric().addCallbackDeferring(new ResolutionCB()); + } + try { + return new ResolutionCB().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } } public String toString() { final StringBuilder buf = new StringBuilder(); - buf.append("TSUIDQuery(metric=") - .append(Arrays.toString(metric)); - try { - buf.append("), tags=").append(Tags.resolveIds(tsdb, tags)); - } catch (NoSuchUniqueId e) { - buf.append("), tags=<").append(e.getMessage()).append('>'); - } - buf.append("))"); + buf.append("TSUIDQuery(metric=").append(metric) + .append(", tags=").append(tags) + .append(", tsuid=") + .append(tsuid != null ? UniqueId.uidToString(tsuid) : "null") + .append(", last_timestamp=").append(last_timestamp) + .append(", back_scan=").append(back_scan) + .append(", resolve_names=").append(resolve_names) + .append(")"); return buf.toString(); } + /** + * Converts the given metric and tags to a TSUID by resolving the strings to + * their UIDs. Note that the resulting TSUID may not exist if the combination + * was not written to TSDB + * @param tsdb The TSDB to use for storage access + * @param metric The metric name to resolve + * @param tags The tags to resolve. May not be empty. + * @return A deferred containing the TSUID when ready or an error such as + * a NoSuchUniqueName exception if the metric didn't exist or a + * DeferredGroupException if one of the tag keys or values did not exist. + * @throws IllegalArgumentException if the metric or tags were null or + * empty. + */ + public static Deferred tsuidFromMetric(final TSDB tsdb, + final String metric, final Map tags) { + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("The metric cannot be empty"); + } + if (tags == null || tags.isEmpty()) { + throw new IllegalArgumentException("Tags cannot be null or empty " + + "when getting a TSUID"); + } + + final byte[] metric_uid = new byte[TSDB.metrics_width()]; + + class TagsCB implements Callback> { + @Override + public byte[] call(final ArrayList tag_list) throws Exception { + final byte[] tsuid = new byte[metric_uid.length + + ((TSDB.tagk_width() + TSDB.tagv_width()) + * tag_list.size())]; + int idx = 0; + System.arraycopy(metric_uid, 0, tsuid, 0, metric_uid.length); + idx += metric_uid.length; + for (final byte[] t : tag_list) { + System.arraycopy(t, 0, tsuid, idx, t.length); + idx += t.length; + } + return tsuid; + } + @Override + public String toString() { + return "Tag resolution callback"; + } + } + + class MetricCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] uid) + throws Exception { + System.arraycopy(uid, 0, metric_uid, 0, uid.length); + return Tags.resolveAllAsync(tsdb, tags).addCallback(new TagsCB()); + } + @Override + public String toString() { + return "Metric resolution callback"; + } + } + + return tsdb.getUIDAsync(UniqueIdType.METRIC, metric) + .addCallbackDeferring(new MetricCB()); + } + /** * Attempts to retrieve the last data point for the given TSUID. * This operates by checking the meta table for the {@link #COUNTER_QUALIFIER} @@ -287,186 +545,193 @@ public String toString() { * point was written * @return An {@link IncomingDataPoint} if data was found, null if not * @throws NoSuchUniqueId if one of the tag lookups failed + * @deprecated Please use {@link #getLastPoint} */ public static Deferred getLastPoint(final TSDB tsdb, final byte[] tsuid, final boolean resolve_names, final int max_lookups, final long last_timestamp) { - - final Deferred result = new Deferred(); - final long start_time; - final long time_limit; - if (max_lookups < 1) { - start_time = 0; - time_limit = 0; - } else { - start_time = System.currentTimeMillis() / 1000; - time_limit = start_time - (3600 * max_lookups); + final TSUIDQuery query = new TSUIDQuery(tsdb, tsuid); + query.last_timestamp = last_timestamp; + return query.getLastPoint(resolve_names, max_lookups); + } + + /** + * Resolve the UIDs to names. If the query was for a metric and tags then we + * can just use those. + * @param dp The data point to fill in values for + * @return A deferred with the data point or an exception if something went + * wrong. + */ + private Deferred resolveNames(final IncomingDataPoint dp) { + // If the caller gave us a metric and tags, save some time by NOT hitting + // our UID tables or storage. + if (metric != null) { + dp.setMetric(metric); + dp.setTags((HashMap)tags); + return Deferred.fromResult(dp); } - - final class ErrBack implements Callback { - public Object call(final Exception e) throws Exception { - Throwable ex = e; - while (ex.getClass().equals(DeferredGroupException.class)) { - if (ex.getCause() == null) { - LOG.warn("Unable to get to the root cause of the DGE"); - break; - } - ex = ex.getCause(); - } - if (ex instanceof RuntimeException) { - result.callback(ex); - } else { - result.callback(e); - } - return null; - } + + class TagsCB implements Callback> { + public IncomingDataPoint call(final HashMap tags) + throws Exception { + dp.setTags(tags); + return dp; + } + @Override + public String toString() { + return "Tags resolution CB"; + } } - /** - * Called after GetLastDataPointCB has completed. If nothing was found then - * we just return a null, otherwise we set the TSUID and optionally resolve - * the metric and tag names. - */ - final class ReturnCB implements Callback { - final long timestamp; - - public ReturnCB(final long timestamp) { - this.timestamp = timestamp; + class MetricCB implements Callback, String> { + public Deferred call(final String name) + throws Exception { + dp.setMetric(name); + final List tags = UniqueId.getTagPairsFromTSUID(tsuid); + return Tags.resolveIdsAsync(tsdb, tags).addCallback(new TagsCB()); } - - /** - * Callback implementation. If the time_limit was set and the result was - * null we call the local getPrevious() method to issue a Get on the - * previous row. - */ - public Object call(final IncomingDataPoint dp) - throws Exception { - if (dp == null) { - if (time_limit > 0) { - getPrevious(); - return null; - } - result.callback(null); - return null; - } - - dp.setTSUID(UniqueId.uidToString(tsuid)); - if (!resolve_names) { - result.callback(dp); - return null; - } - - class TagsCB implements Callback> { - public IncomingDataPoint call(final HashMap tags) - throws Exception { - dp.setTags(tags); - result.callback(dp); - return null; - } - } - - class MetricCB implements Callback { - public Object call(final String name) throws Exception { - dp.setMetric(name); - final List tags = UniqueId.getTagPairsFromTSUID(tsuid); - return Tags.resolveIdsAsync(tsdb, tags).addCallback(new TagsCB()); - } - } - - // start the resolve dance - final byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); - return tsdb.getUidName(UniqueIdType.METRIC, metric_uid) - .addCallback(new MetricCB()); + @Override + public String toString() { + return "Metric resolution CB"; } - - /** - * Issues a GetRequest on the previous hour's row of data if we haven't - * exceeded the limit - * @return Null if we've hit the time limit or another deferred to wait - * on - */ - private void getPrevious() { - if (timestamp <= time_limit) { - // we hit our limit and didn't find a valid data point - result.callback(null); - return; + } + + final byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + return tsdb.getUidName(UniqueIdType.METRIC, metric_uid) + .addCallbackDeferring(new MetricCB()); + } + + /** + * Handles getting the results of the first GetRequest and keeps iterating + * back in time until we find a point or run out of back scans. + */ + private class LastPointCB implements Callback, + ArrayList> { + int iteration = 0; + + @Override + public Deferred call(final ArrayList row) + throws Exception { + if (row == null || row.isEmpty()) { + if (iteration >= back_scan) { + if (LOG.isDebugEnabled()) { + LOG.debug("No data points found within the time span for TSUID query " + + TSUIDQuery.this); + } + return Deferred.fromResult(null); } - // look one row into the past - final long previous_time = timestamp - 3600; - - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, previous_time); + last_timestamp -= 3600; + ++iteration; + final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); final GetRequest get = new GetRequest(tsdb.dataTable(), key); get.family(TSDB.FAMILY()); - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(previous_time)) - .addErrback(new ErrBack()); + return tsdb.getClient().get(get).addCallbackDeferring(this); } + + final IncomingDataPoint dp = + new Internal.GetLastDataPointCB(tsdb).call(row); + dp.setTSUID(UniqueId.uidToString(tsuid)); + if (!resolve_names) { + return Deferred.fromResult(dp); + } + + return resolveNames(dp); } - - /** - * Callback from the GetRequest that simply determines if the row is empty - * or not - */ - final class ExistsCB implements Callback> { - public Object call(final ArrayList row) + @Override + public String toString() { + return "LastDataPoint callback"; + } + } + + /** + * Callback that receives the result of a single meta table lookup to see if + * the last write counter was there or not. The input should contain just + * the counter column + */ + private class MetaCB implements Callback, + ArrayList> { + @Override + public Deferred call(final ArrayList row) throws Exception { - if (row == null || row.isEmpty() || row.get(0).value() == null) { - result.callback(null); - return null; - } - - // we want the timestamp in seconds so we can figure out what row the - // data *should* be in (though it's not guaranteed) - final long last_write = row.get(0).timestamp(); - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_write); - final GetRequest get = new GetRequest(tsdb.dataTable(), key); - get.family(TSDB.FAMILY()); - - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(0)) - .addErrback(new ErrBack()); + if (row == null) { + return Deferred.fromResult(null); + } + last_timestamp = Internal.baseTime(row.get(0).timestamp()); + final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); + final GetRequest get = new GetRequest(tsdb.dataTable(), key); + get.family(TSDB.FAMILY()); + return tsdb.getClient().get(get).addCallbackDeferring(new LastPointCB()); + } + @Override + public String toString() { + return "Meta TSCounter lookup"; + } + } + + /** + * Resolves the metric and tags (if set) to their UIDs, setting the local + * arrays. + * @return A deferred to wait on or catch exceptions in + * @throws IllegalArgumentException if the metric is empty || null or the + * tag list is null. + */ + private Deferred resolveMetric() { + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("The metric cannot be empty"); + } + if (tags == null) { + throw new IllegalArgumentException("Tags cannot be null or empty " + + "when getting a TSUID"); + } + + class TagsCB implements Callback> { + @Override + public Object call(final ArrayList tag_list) throws Exception { + setTagUIDs(tag_list); return null; } + @Override + public String toString() { + return "Tag resolution callback"; + } } - // we need to determine a course of action. We can either: - // 1) Lookup the last time a data point was written for a TSUID - // 2) Immediately fetch data from a row if we were given a timestamp - // 3) Or start at NOW and iterate backwards until we find a point or hit - // the user supplied limit - if (time_limit == 0) { - if (last_timestamp < 1) { - final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); - get.family(TSMeta.FAMILY()); - get.qualifier(TSMeta.COUNTER_QUALIFIER()); - tsdb.getClient().get(get) - .addCallback(new ExistsCB()) - .addErrback(new ErrBack()); - } else { - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); - final GetRequest get = new GetRequest(tsdb.dataTable(), key); - get.family(TSDB.FAMILY()); - - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(0)) - .addErrback(new ErrBack()); + class MetricCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] uid) + throws Exception { + setMetricUID(uid); + if (tags.isEmpty()) { + setTagUIDs(new ArrayList(0)); + return null; + } + return Tags.resolveAllAsync(tsdb, tags).addCallback(new TagsCB()); + } + @Override + public String toString() { + return "Metric resolution callback"; } - } else { - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, start_time); - final GetRequest get = new GetRequest(tsdb.dataTable(), key); - get.family(TSDB.FAMILY()); - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(start_time)) - .addErrback(new ErrBack()); } - return result; - } + return tsdb.getUIDAsync(UniqueIdType.METRIC, metric) + .addCallbackDeferring(new MetricCB()); + } + + /** @param tsuid The TSUID to store */ + private void setTSUID(final byte[] tsuid) { + this.tsuid = tsuid; + } + + /** @param uid The metric UID to set */ + private void setMetricUID(final byte[] uid) { + metric_uid = uid; + } + + /** @param uids The tag UIDs to set */ + private void setTagUIDs(final ArrayList uids) { + tag_uids = uids; + } /** * Configures the scanner for a specific metric and optional tags @@ -474,11 +739,11 @@ public Object call(final ArrayList row) */ private Scanner getScanner() { final Scanner scanner = tsdb.getClient().newScanner(tsdb.metaTable()); - scanner.setStartKey(metric); + scanner.setStartKey(metric_uid); // increment the metric UID by one so we can scan all of the rows for the // given metric - final long stop = UniqueId.uidToLong(metric, TSDB.metrics_width()) + 1; + final long stop = UniqueId.uidToLong(metric_uid, TSDB.metrics_width()) + 1; scanner.setStopKey(UniqueId.longToUID(stop, TSDB.metrics_width())); scanner.setFamily(TSMeta.FAMILY()); @@ -501,7 +766,7 @@ private Scanner getScanner() { // ... start by skipping the metric ID. .append(TSDB.metrics_width()) .append("}"); - final Iterator tags = this.tags.iterator(); + final Iterator tags = this.tag_uids.iterator(); byte[] tag = tags.hasNext() ? tags.next() : null; // Tags and group_bys are already sorted. We need to put them in the // regexp in order by ID, which means we just merge two sorted lists. diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index 0b7511b8b5..c454298bd6 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -12,7 +12,12 @@ // see . package net.opentsdb.meta; +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.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -20,12 +25,16 @@ import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; +import java.util.Map; +import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; @@ -35,6 +44,7 @@ import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import org.hbase.async.Bytes.ByteMap; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,16 +54,20 @@ import org.powermock.modules.junit4.PowerMockRunner; import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, +@PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, - Scanner.class, TSMeta.class, AtomicIncrementRequest.class}) + Scanner.class, TSMeta.class, AtomicIncrementRequest.class, DateTime.class }) public final class TestTSUIDQuery { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private static final byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private static final byte[] TSUID = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }; + private static final byte[] QUAL = new byte[] { 0, 0 }; + private static final byte[] VAL = new byte[] { 0x2A }; private TSDB tsdb; private Config config; private HBaseClient client = mock(HBaseClient.class); @@ -62,9 +76,13 @@ public final class TestTSUIDQuery { private UniqueId tag_names = mock(UniqueId.class); private UniqueId tag_values = mock(UniqueId.class); private TSUIDQuery query; + private Map tags; @Before public void before() throws Exception { + tags = new HashMap(1); + tags.put("host", "web01"); + config = mock(Config.class); when(config.getString("tsd.storage.hbase.data_table")).thenReturn("tsdb"); when(config.getString("tsd.storage.hbase.uid_table")).thenReturn("tsdb-uid"); @@ -144,14 +162,14 @@ public void before() throws Exception { "1328140801,\"displayName\":\"Web server 2\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(TSUID, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, @@ -164,14 +182,14 @@ public void before() throws Exception { storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 2, 0, 0, 3, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), - ("{\"tsuid\":\"000002000002000003000001000001\",\"" + + ("{\"tsuid\":\"000002000001000001000002000003\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 2, 0, 0, 3, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); @@ -194,28 +212,46 @@ public void before() throws Exception { // mock UniqueId when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getIdAsync("sys.cpu.user")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")) .thenReturn(Deferred.fromResult("sys.cpu.user")); when(metrics.getId("sys.cpu.system")) .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getIdAsync("sys.cpu.system")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("sys.cpu.system", "metric"))); when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getIdAsync("sys.cpu.nice")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) .thenReturn(Deferred.fromResult("sys.cpu.nice")); + when(metrics.getNameAsync(new byte[] { 0, 0, 3 })) + .thenReturn(Deferred.fromError(new NoSuchUniqueId("metrics", + new byte[] { 0, 0, 3 }))); when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); when(tag_names.getIdAsync("host")).thenReturn( Deferred.fromResult(new byte[] { 0, 0, 1 })); when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("host")) .thenReturn(Deferred.fromResult("host")); when(tag_names.getOrCreateIdAsync("host")).thenReturn( Deferred.fromResult(new byte[] { 0, 0, 1 })); when(tag_names.getId("dc")) .thenThrow(new NoSuchUniqueName("dc", "metric")); + when(tag_names.getIdAsync("dc")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("dc", "metric"))); when(tag_names.getId("datacenter")).thenReturn(new byte[] { 0, 0, 2 }); when(tag_names.getIdAsync("datacenter")) .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); when(tag_names.getNameAsync(new byte[] { 0, 0, 2 })) .thenReturn(Deferred.fromResult("datacenter")); + when(tag_names.getNameAsync(new byte[] { 0, 0, 3 })) + .thenReturn(Deferred.fromError(new NoSuchUniqueId("tagk", + new byte[] { 0, 0, 3 }))); when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); when(tag_values.getIdAsync("web01")).thenReturn( @@ -233,51 +269,131 @@ public void before() throws Exception { Deferred.fromResult(new byte[] { 0, 0, 2 })); when(tag_values.getId("web03")) .thenThrow(new NoSuchUniqueName("web03", "metric")); + when(tag_values.getIdAsync("web03")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("web03", "metric"))); when(tag_values.getId("dc01")).thenReturn(new byte[] { 0, 0, 3 }); when(tag_values.getIdAsync("dc01")) .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); when(tag_values.getNameAsync(new byte[] { 0, 0, 3 })) .thenReturn(Deferred.fromResult("dc01")); + when(tag_values.getNameAsync(new byte[] { 0, 0, 4 })) + .thenReturn(Deferred.fromError(new NoSuchUniqueId("tagv", + new byte[] { 0, 0, 4 }))); when(metrics.width()).thenReturn((short)3); when(tag_names.width()).thenReturn((short)3); when(tag_values.width()).thenReturn((short)3); } - + @Test - public void setQuery() throws Exception { + public void ctorDefault() throws Exception { query = new TSUIDQuery(tsdb); - final HashMap tags = new HashMap(1); - tags.put("host", "web01"); - query.setQuery("sys.cpu.user", tags); + assertNotNull(query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSDB() throws Exception { + query = new TSUIDQuery(null); } @Test - public void setQueryEmtpyTags() throws Exception { - query = new TSUIDQuery(tsdb); - query.setQuery("sys.cpu.user", new HashMap(0)); + public void ctorTSUID() throws Exception { + query = new TSUIDQuery(tsdb, TSUID); + assertNotNull(query); } - @Test (expected = NoSuchUniqueName.class) - public void setQueryNSUMetric() throws Exception { - query = new TSUIDQuery(tsdb); - query.setQuery("sys.cpu.system", new HashMap(0)); + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSUID() throws Exception { + query = new TSUIDQuery(tsdb, null); } - @Test (expected = NoSuchUniqueName.class) - public void setQueryNSUTagk() throws Exception { + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSDBforTSUID() throws Exception { + query = new TSUIDQuery(null, TSUID); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyTSUID() throws Exception { + query = new TSUIDQuery(tsdb, new byte[] { }); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorShortTSUID() throws Exception { + query = new TSUIDQuery(tsdb, new byte[] { 0, 0, 1, 0, 0, 1 }); + } + + @Test + public void ctorMetric() throws Exception { + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + assertNotNull(query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricNullTSDB() throws Exception { + query = new TSUIDQuery(null, "sys.cpu.user", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricNullMetric() throws Exception { + query = new TSUIDQuery(tsdb, null, tags); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricEmptyMetric() throws Exception { + query = new TSUIDQuery(tsdb, "", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricNullTags() throws Exception { + query = new TSUIDQuery(tsdb, "sys.cpu.user", null); + } + + @Test + public void ctorMetricEmptyTags() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + assertNotNull(query); + } + + @Test + public void getLastWriteTimes() throws Exception { + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(1, tsuids.size()); + assertEquals(1388534400015L, (long)tsuids.get(TSUID)); + } + + @Test + public void getLastWriteTimesSetQuery() throws Exception { query = new TSUIDQuery(tsdb); - final HashMap tags = new HashMap(1); - tags.put("dc", "web01"); query.setQuery("sys.cpu.user", tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(1, tsuids.size()); + assertEquals(1388534400015L, (long)tsuids.get(TSUID)); } - @Test (expected = NoSuchUniqueName.class) - public void setQueryNSUTagv() throws Exception { + @Test + public void getLastWriteTimesEmptyTags() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(2, tsuids.size()); + assertEquals(1388534400015L, (long)tsuids.get(TSUID)); + assertEquals(1388534400017L, + (long)tsuids.get(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 })); + } + + @Test + public void getLastWriteTimesEmptyTagsSetQuery() throws Exception { + tags.clear(); query = new TSUIDQuery(tsdb); - final HashMap tags = new HashMap(1); - tags.put("host", "web03"); query.setQuery("sys.cpu.user", tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(2, tsuids.size()); + assertEquals(1388534400015L, (long)tsuids.get(TSUID)); + assertEquals(1388534400017L, + (long)tsuids.get(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 })); } @Test (expected = IllegalArgumentException.class) @@ -286,34 +402,88 @@ public void getLastWriteTimesQueryNotSet() throws Exception { query.getLastWriteTimes().joinUninterruptibly(); } + @Test + public void getLastWriteTimesNoMatch() throws Exception { + storage.flushStorage(); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertTrue(tsuids.isEmpty()); + } + + @Test (expected = NoSuchUniqueName.class) + public void getLastWriteTimesNSUNMetric() throws Exception { + query = new TSUIDQuery(tsdb, "sys.cpu.system", tags); + query.getLastWriteTimes().joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void getLastWriteTimesNSUNTagk() throws Exception { + tags.clear(); + tags.put("dc", "web01"); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastWriteTimes().joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void getLastWriteTimesNSUNTagv() throws Exception { + tags.put("host", "web03"); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastWriteTimes().joinUninterruptibly(); + } + @Test public void getTSMetasSingle() throws Exception { - query = new TSUIDQuery(tsdb); - HashMap tags = new HashMap(); - tags.put("host", "web01"); - query.setQuery("sys.cpu.user", tags); - List tsmetas = query.getTSMetas().joinUninterruptibly(); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); assertEquals(1, tsmetas.size()); + assertEquals("sys.cpu.user", tsmetas.get(0).getMetric().getName()); + assertEquals("host", tsmetas.get(0).getTags().get(0).getName()); + assertEquals("web01", tsmetas.get(0).getTags().get(1).getName()); } @Test - public void getTSMetasMulti() throws Exception { + public void getTSMetasSingleSetQuery() throws Exception { query = new TSUIDQuery(tsdb); - HashMap tags = new HashMap(); query.setQuery("sys.cpu.user", tags); - List tsmetas = query.getTSMetas().joinUninterruptibly(); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); + assertEquals(1, tsmetas.size()); + assertEquals("sys.cpu.user", tsmetas.get(0).getMetric().getName()); + assertEquals("host", tsmetas.get(0).getTags().get(0).getName()); + assertEquals("web01", tsmetas.get(0).getTags().get(1).getName()); + } + + @Test + public void getTSMetasMultipleResults() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); assertEquals(2, tsmetas.size()); + assertEquals("sys.cpu.user", tsmetas.get(0).getMetric().getName()); + assertEquals("host", tsmetas.get(0).getTags().get(0).getName()); + assertEquals("web01", tsmetas.get(0).getTags().get(1).getName()); + assertEquals("sys.cpu.user", tsmetas.get(1).getMetric().getName()); + assertEquals("host", tsmetas.get(1).getTags().get(0).getName()); + assertEquals("web02", tsmetas.get(1).getTags().get(1).getName()); } @Test public void getTSMetasMultipleTags() throws Exception { - query = new TSUIDQuery(tsdb); - HashMap tags = new HashMap(); - query.setQuery("sys.cpu.nice", tags); - tags.put("host", "web01"); tags.put("datacenter", "dc01"); - List tsmetas = query.getTSMetas().joinUninterruptibly(); + query = new TSUIDQuery(tsdb, "sys.cpu.nice", tags); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); assertEquals(1, tsmetas.size()); + assertEquals("sys.cpu.nice", tsmetas.get(0).getMetric().getName()); + assertEquals("host", tsmetas.get(0).getTags().get(0).getName()); + assertEquals("web01", tsmetas.get(0).getTags().get(1).getName()); + assertEquals("datacenter", tsmetas.get(0).getTags().get(2).getName()); + assertEquals("dc01", tsmetas.get(0).getTags().get(3).getName()); + } + + @Test (expected = DeferredGroupException.class) + public void getTSMetasNSUITagk() throws Exception { + tags.put("datacenter", "web03"); + query = new TSUIDQuery(tsdb, "sys.cpu.nice", tags); + query.getTSMetas().joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) @@ -322,4 +492,480 @@ public void getTSMetasNullMetric() throws Exception { query.getTSMetas().joinUninterruptibly(); } + @Test + public void tsuidFromMetric() throws Exception { + byte[] tsuid = TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.user", tags).join(); + assertArrayEquals(TSUID, tsuid); + } + + @Test + public void tsuidFromMetricTwoTags() throws Exception { + tags.put("datacenter", "dc01"); + byte[] tsuid = TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.user", tags).join(); + assertArrayEquals( + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, tsuid); + } + + @Test (expected = NoSuchUniqueName.class) + public void tsuidFromMetricNSUNMetric() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.system", tags).join(); + } + + @Test (expected = DeferredGroupException.class) + public void tsuidFromMetricNSUNTagk() throws Exception { + tags.clear(); + tags.put("dc", "web01"); + TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.user", tags).join(); + } + + @Test (expected = DeferredGroupException.class) + public void tsuidFromMetricNSUNTagv() throws Exception { + tags.put("host", "web03"); + TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.user", tags).join(); + } + + @Test (expected = NullPointerException.class) + public void tsuidFromMetricNullTSDB() throws Exception { + TSUIDQuery.tsuidFromMetric(null, "sys.cpu.user", tags).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricNullMetric() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, null, tags).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricEmptyMetric() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, "", tags).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricNullTags() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.user", null).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricEmptyTags() throws Exception { + tags.clear(); + TSUIDQuery.tsuidFromMetric(tsdb, "sys.cpu.user", tags).join(); + } + + @Test + public void getLastPointMetricZeroBackscanOnePoint() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricZeroBackscanMostRecent() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + tsdb.addPoint("sys.cpu.user", 1356998401L, 24, tags); + tsdb.addPoint("sys.cpu.user", 1356998402L, 1, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998402000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("1", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricZeroBackscanOutOfRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + assertNull(query.getLastPoint(false, 0).join()); + } + + @Test + public void getLastPointMetricOneBackscanInRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final IncomingDataPoint dp = query.getLastPoint(false, 1).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricOneBackscanOutOfRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357010600000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + assertNull(query.getLastPoint(false, 1).join()); + } + + @Test + public void getLastPointMetricManyBackscanInRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final IncomingDataPoint dp = query.getLastPoint(false, 1024).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricManyBackscanOutOfRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + assertNull(query.getLastPoint(false, 1022).join()); + } + + @Test (expected = IllegalArgumentException.class) + public void getLastPointMetricNegativeBackscan() throws Exception { + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastPoint(false, -1).join(); + } + + @Test + public void getLastPointMetricResolve() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + final IncomingDataPoint dp = query.getLastPoint(true, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertEquals("sys.cpu.user", dp.getMetric()); + assertSame(tags, dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test (expected = NoSuchUniqueName.class) + public void getLastPointMetricNSUNMetric() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.system", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastPoint(false, 0).join(); + } + + @Test (expected = NoSuchUniqueName.class) + public void getLastPointMetricNSUNTagk() throws Exception { + tags.clear(); + tags.put("dc", "web01"); + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastPoint(false, 0).join(); + } + + @Test (expected = NoSuchUniqueName.class) + public void getLastPointMetricNSUNTagv() throws Exception { + tags.put("host", "web03"); + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastPoint(false, 0).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void getLastPointMetricEmptyTags() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tags.clear(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, "sys.cpu.user", tags); + query.getLastPoint(false, 0).join(); + } + + @Test + public void getLastPointTSUIDZeroBackscanRecent() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDZeroBackscanRecentOutOfRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 0).join()); + } + + @Test + public void getLastPointTSUIDOneBackscanInRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 1).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDOneBackscanRecentOutOfRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357010600000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 1).join()); + } + + @Test + public void getLastPointTSUIDManyBackscanInRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 1024).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDManyBackscanRecentOutOfRange() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + tsdb.addPoint("sys.cpu.user", 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 1022).join()); + } + + // While these NSUI shouldn't happen, it's possible if someone deletes a metric + // or tag but not the actual data. + @Test + public void getLastPointTSUIDMetricNSUINotResolved() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000350E22700000001000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); + } + + @Test (expected = NoSuchUniqueId.class) + public void getLastPointTSUIDMetricNSUI() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000350E22700000001000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + query.getLastPoint(true, 0).join(); + } + + @Test + public void getLastPointTSUIDTagkNSUINotResolved() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000003000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 3, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); + } + + @Test (expected = DeferredGroupException.class) + public void getLastPointTSUIDTagkNSUI() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000003000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 3, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + query.getLastPoint(true, 0).join(); + } + + @Test + public void getLastPointTSUIDTagvNSUINotResolved() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000001000003"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 3 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); + } + + @Test (expected = DeferredGroupException.class) + public void getLastPoitTSUIDTagvNSUI() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000001000004"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 4 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + query.getLastPoint(true, 0).join(); + } + + @Test + public void getLastPointTSUIDMeta() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(false); + when(config.enable_realtime_ts()).thenReturn(false); + tsdb.addPoint("sys.cpu.user", 1388534400L, 42, tags); + + when(config.enable_tsuid_incrementing()).thenReturn(true); + when(config.enable_realtime_ts()).thenReturn(true); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1388534400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDMetaNoPoint() throws Exception { + when(config.enable_tsuid_incrementing()).thenReturn(true); + when(config.enable_realtime_ts()).thenReturn(true); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 0).join()); + } + } From 4b6a0900aca7824659653c48242778e2fa040e9e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 22:02:16 -0700 Subject: [PATCH 195/826] Shuffle some things around in the TestTSUIDQuery class so they can be shared amongst other test cases. Signed-off-by: Chris Larsen --- test/meta/TestTSUIDQuery.java | 388 +++++++++++++++++----------------- 1 file changed, 195 insertions(+), 193 deletions(-) diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index c454298bd6..11cce26dc4 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -22,7 +22,6 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; -import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -52,6 +51,7 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; @@ -70,11 +70,8 @@ public final class TestTSUIDQuery { private static final byte[] VAL = new byte[] { 0x2A }; private TSDB tsdb; private Config config; - private HBaseClient client = mock(HBaseClient.class); + private HBaseClient client; private MockBase storage; - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); private TSUIDQuery query; private Map tags; @@ -84,6 +81,7 @@ public void before() throws Exception { tags.put("host", "web01"); config = mock(Config.class); + client = mock(HBaseClient.class); when(config.getString("tsd.storage.hbase.data_table")).thenReturn("tsdb"); when(config.getString("tsd.storage.hbase.uid_table")).thenReturn("tsdb-uid"); when(config.getString("tsd.storage.hbase.meta_table")).thenReturn("tsdb-meta"); @@ -96,194 +94,7 @@ public void before() throws Exception { tsdb = new TSDB(config); storage = new MockBase(tsdb, client, true, true, true, true); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, - "metrics".getBytes(MockBase.ASCII()), - "sys.cpu.user".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, - "metric_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.user\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"System CPU\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, - "metrics".getBytes(MockBase.ASCII()), - "sys.cpu.nice".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, - "metric_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000002\",\"type\":\"METRIC\",\"name\":\"sys.cpu.nice\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"System CPU\"}") - .getBytes(MockBase.ASCII())); - - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, - "tagk".getBytes(MockBase.ASCII()), - "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, - "tagk_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000001\",\"type\":\"TAGK\",\"name\":\"host\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"Host server name\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, - "tagk".getBytes(MockBase.ASCII()), - "datacenter".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, - "tagk_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000002\",\"type\":\"TAGK\",\"name\":\"datacenter\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"Datecenter name\"}") - .getBytes(MockBase.ASCII())); - - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, - "tagv".getBytes(MockBase.ASCII()), - "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, - "tagv_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000001\",\"type\":\"TAGV\",\"name\":\"web01\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"Web server 1\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, - "tagv".getBytes(MockBase.ASCII()), - "web02".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, - "tagv_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000002\",\"type\":\"TAGV\",\"name\":\"web02\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"Web server 2\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, NAME_FAMILY, - "tagv".getBytes(MockBase.ASCII()), - "dc01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, NAME_FAMILY, - "tagv_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000003\",\"type\":\"TAGV\",\"name\":\"dc01\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"Web server 2\"}") - .getBytes(MockBase.ASCII())); - - storage.addColumn(TSUID, NAME_FAMILY, - "ts_meta".getBytes(MockBase.ASCII()), - ("{\"tsuid\":\"000001000001000001\",\"" + - "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + - "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + - "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(TSUID, NAME_FAMILY, - "ts_ctr".getBytes(MockBase.ASCII()), - Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, - "ts_meta".getBytes(MockBase.ASCII()), - ("{\"tsuid\":\"000001000001000002\",\"" + - "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + - "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + - "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, - "ts_ctr".getBytes(MockBase.ASCII()), - Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, - NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), - ("{\"tsuid\":\"000002000001000001000002000003\",\"" + - "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + - "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + - "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, - NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), - Bytes.fromLong(1L)); - - // replace the "real" field objects with mocks - Field cl = tsdb.getClass().getDeclaredField("client"); - cl.setAccessible(true); - cl.set(tsdb, client); - - Field met = tsdb.getClass().getDeclaredField("metrics"); - met.setAccessible(true); - met.set(tsdb, metrics); - - Field tagk = tsdb.getClass().getDeclaredField("tag_names"); - tagk.setAccessible(true); - tagk.set(tsdb, tag_names); - - Field tagv = tsdb.getClass().getDeclaredField("tag_values"); - tagv.setAccessible(true); - tagv.set(tsdb, tag_values); - - // mock UniqueId - when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getIdAsync("sys.cpu.user")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("sys.cpu.user")) - .thenReturn(Deferred.fromResult("sys.cpu.user")); - when(metrics.getId("sys.cpu.system")) - .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); - when(metrics.getIdAsync("sys.cpu.system")) - .thenReturn(Deferred.fromError( - new NoSuchUniqueName("sys.cpu.system", "metric"))); - when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); - when(metrics.getIdAsync("sys.cpu.nice")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("sys.cpu.nice")); - when(metrics.getNameAsync(new byte[] { 0, 0, 3 })) - .thenReturn(Deferred.fromError(new NoSuchUniqueId("metrics", - new byte[] { 0, 0, 3 }))); - - when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("host")) - .thenReturn(Deferred.fromResult("host")); - when(tag_names.getOrCreateIdAsync("host")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getId("dc")) - .thenThrow(new NoSuchUniqueName("dc", "metric")); - when(tag_names.getIdAsync("dc")) - .thenReturn(Deferred.fromError( - new NoSuchUniqueName("dc", "metric"))); - when(tag_names.getId("datacenter")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_names.getIdAsync("datacenter")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_names.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("datacenter")); - when(tag_names.getNameAsync(new byte[] { 0, 0, 3 })) - .thenReturn(Deferred.fromError(new NoSuchUniqueId("tagk", - new byte[] { 0, 0, 3 }))); - - when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 1 })) - .thenReturn(Deferred.fromResult("web01")); - when(tag_values.getOrCreateIdAsync("web01")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_values.getIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) - .thenReturn(Deferred.fromResult("web02")); - when(tag_values.getOrCreateIdAsync("web02")).thenReturn( - Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getId("web03")) - .thenThrow(new NoSuchUniqueName("web03", "metric")); - when(tag_values.getIdAsync("web03")) - .thenReturn(Deferred.fromError( - new NoSuchUniqueName("web03", "metric"))); - when(tag_values.getId("dc01")).thenReturn(new byte[] { 0, 0, 3 }); - when(tag_values.getIdAsync("dc01")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); - when(tag_values.getNameAsync(new byte[] { 0, 0, 3 })) - .thenReturn(Deferred.fromResult("dc01")); - when(tag_values.getNameAsync(new byte[] { 0, 0, 4 })) - .thenReturn(Deferred.fromError(new NoSuchUniqueId("tagv", - new byte[] { 0, 0, 4 }))); - - when(metrics.width()).thenReturn((short)3); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); + setupStorage(tsdb, storage); } @Test @@ -968,4 +779,195 @@ public void getLastPointTSUIDMetaNoPoint() throws Exception { assertNull(query.getLastPoint(false, 0).join()); } + /** + * Public for sharing with other UT classes + * @param tsdb The mock TSDB client + * @throws Exception If something went pear shaped + */ + public static void setupStorage(final TSDB tsdb, final MockBase storage) + throws Exception { + storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + "metrics".getBytes(MockBase.ASCII()), + "sys.cpu.user".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + "metric_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.user\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"System CPU\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + "metrics".getBytes(MockBase.ASCII()), + "sys.cpu.nice".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + "metric_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000002\",\"type\":\"METRIC\",\"name\":\"sys.cpu.nice\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"System CPU\"}") + .getBytes(MockBase.ASCII())); + + storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + "tagk".getBytes(MockBase.ASCII()), + "host".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + "tagk_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000001\",\"type\":\"TAGK\",\"name\":\"host\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"Host server name\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + "tagk".getBytes(MockBase.ASCII()), + "datacenter".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + "tagk_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000002\",\"type\":\"TAGK\",\"name\":\"datacenter\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"Datecenter name\"}") + .getBytes(MockBase.ASCII())); + + storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + "tagv".getBytes(MockBase.ASCII()), + "web01".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + "tagv_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000001\",\"type\":\"TAGV\",\"name\":\"web01\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"Web server 1\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + "tagv".getBytes(MockBase.ASCII()), + "web02".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + "tagv_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000002\",\"type\":\"TAGV\",\"name\":\"web02\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"Web server 2\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 3 }, NAME_FAMILY, + "tagv".getBytes(MockBase.ASCII()), + "dc01".getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 3 }, NAME_FAMILY, + "tagv_meta".getBytes(MockBase.ASCII()), + ("{\"uid\":\"000003\",\"type\":\"TAGV\",\"name\":\"dc01\"," + + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + + "1328140801,\"displayName\":\"Web server 2\"}") + .getBytes(MockBase.ASCII())); + + storage.addColumn(TSUID, NAME_FAMILY, + "ts_meta".getBytes(MockBase.ASCII()), + ("{\"tsuid\":\"000001000001000001\",\"" + + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(TSUID, NAME_FAMILY, + "ts_ctr".getBytes(MockBase.ASCII()), + Bytes.fromLong(1L)); + storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, + "ts_meta".getBytes(MockBase.ASCII()), + ("{\"tsuid\":\"000001000001000002\",\"" + + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, + "ts_ctr".getBytes(MockBase.ASCII()), + Bytes.fromLong(1L)); + storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, + NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), + ("{\"tsuid\":\"000002000001000001000002000003\",\"" + + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") + .getBytes(MockBase.ASCII())); + storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 3 }, + NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), + Bytes.fromLong(1L)); + + final UniqueId metrics = mock(UniqueId.class); + final UniqueId tag_names = mock(UniqueId.class); + final UniqueId tag_values = mock(UniqueId.class); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + // mock UniqueId + when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getIdAsync("sys.cpu.user")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })) + .thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(metrics.getId("sys.cpu.system")) + .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getIdAsync("sys.cpu.system")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("sys.cpu.system", "metric"))); + when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getIdAsync("sys.cpu.nice")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) + .thenReturn(Deferred.fromResult("sys.cpu.nice")); + when(metrics.getNameAsync(new byte[] { 0, 0, 3 })) + .thenReturn(Deferred.fromError(new NoSuchUniqueId("metrics", + new byte[] { 0, 0, 3 }))); + + when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getIdAsync("host")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })) + .thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("host")) + .thenReturn(Deferred.fromResult("host")); + when(tag_names.getOrCreateIdAsync("host")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_names.getId("dc")) + .thenThrow(new NoSuchUniqueName("dc", "tagk")); + when(tag_names.getIdAsync("dc")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("dc", "tagk"))); + when(tag_names.getId("datacenter")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_names.getIdAsync("datacenter")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(tag_names.getNameAsync(new byte[] { 0, 0, 2 })) + .thenReturn(Deferred.fromResult("datacenter")); + when(tag_names.getNameAsync(new byte[] { 0, 0, 3 })) + .thenReturn(Deferred.fromError(new NoSuchUniqueId("tagk", + new byte[] { 0, 0, 3 }))); + + when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getIdAsync("web01")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_values.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("web01")); + when(tag_values.getOrCreateIdAsync("web01")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getIdAsync("web02")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) + .thenReturn(Deferred.fromResult("web02")); + when(tag_values.getOrCreateIdAsync("web02")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(tag_values.getId("web03")) + .thenThrow(new NoSuchUniqueName("web03", "tagv")); + when(tag_values.getIdAsync("web03")) + .thenReturn(Deferred.fromError( + new NoSuchUniqueName("web03", "tagv"))); + when(tag_values.getId("dc01")).thenReturn(new byte[] { 0, 0, 3 }); + when(tag_values.getIdAsync("dc01")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); + when(tag_values.getNameAsync(new byte[] { 0, 0, 3 })) + .thenReturn(Deferred.fromResult("dc01")); + when(tag_values.getNameAsync(new byte[] { 0, 0, 4 })) + .thenReturn(Deferred.fromError(new NoSuchUniqueId("tagv", + new byte[] { 0, 0, 4 }))); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + } } From e2dc213251b5f518a90bc66f6c92f84e6b801402 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 22:02:56 -0700 Subject: [PATCH 196/826] Fix up the Last Data Point Query RPC class. Add a ton of tests around said class so it seems to be working nicely now. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/tsd/QueryRpc.java | 115 +-- src/tsd/UniqueIdRpc.java | 3 +- test/tsd/TestQueryRpc.java | 6 +- test/tsd/TestQueryRpcLastDataPoint.java | 985 ++++++++++++++++++++++++ 5 files changed, 1056 insertions(+), 54 deletions(-) create mode 100644 test/tsd/TestQueryRpcLastDataPoint.java diff --git a/Makefile.am b/Makefile.am index 9cbde8a8bb..6da1af624e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -185,6 +185,7 @@ test_SRC := \ test/tsd/TestHttpQuery.java \ test/tsd/TestPutRpc.java \ test/tsd/TestQueryRpc.java \ + test/tsd/TestQueryRpcLastDataPoint.java \ test/tsd/TestRpcHandler.java \ test/tsd/TestRpcPlugin.java \ test/tsd/TestRTPublisher.java \ diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 3c19897025..0c6558e4f9 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -179,7 +179,7 @@ public Object call(final ArrayList query_results) } /** - * + * Processes a last data point query * @param tsdb The TSDB to which we belong * @param query The HTTP query to parse/respond */ @@ -206,12 +206,10 @@ private void handleLastDataPointQuery(final TSDB tsdb, final HttpQuery query) { "Missing sub queries"); } - // list of getLastPoint calls - final ArrayList> calls = - new ArrayList>(); - // list of calls to TSUIDQuery for scanning the tsdb-meta table - final ArrayList> tsuid_query_wait = - new ArrayList>(); + // a list of deferreds to wait on + final ArrayList> calls = new ArrayList>(); + // final results for serialization + final List results = new ArrayList(); /** * Used to catch exceptions @@ -231,7 +229,29 @@ public Object call(final Exception e) throws Exception { } else { throw e; } - } + } + @Override + public String toString() { + return "Error back"; + } + } + + final class FetchCB implements Callback> { + @Override + public Object call(final ArrayList dps) throws Exception { + synchronized(results) { + for (final IncomingDataPoint dp : dps) { + if (dp != null) { + results.add(dp); + } + } + } + return null; + } + @Override + public String toString() { + return "Fetched data points CB"; + } } /** @@ -244,73 +264,70 @@ public Object call(final ByteMap tsuids) throws Exception { if (tsuids == null || tsuids.isEmpty()) { return null; } - + final ArrayList> deferreds = + new ArrayList>(tsuids.size()); for (Map.Entry entry : tsuids.entrySet()) { - calls.add(TSUIDQuery.getLastPoint(tsdb, entry.getKey(), + deferreds.add(TSUIDQuery.getLastPoint(tsdb, entry.getKey(), data_query.getResolveNames(), data_query.getBackScan(), entry.getValue())); } + calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); return null; } - } - - /** - * Callback used to force the thread to wait for the TSUIDQueries to complete - */ - final class TSUIDQueryWaitCB implements Callback> { - public Object call(ArrayList arg0) throws Exception { - return null; + @Override + public String toString() { + return "TSMeta scan CB"; } } - + /** * Used to wait on the list of data point deferreds. Once they're all done * this will return the results to the call via the serializer */ - final class FinalCB implements Callback> { - @SuppressWarnings("unchecked") - public Object call(final ArrayList data_points) - throws Exception { - if (data_points == null) { - query.sendReply(query.serializer() - .formatLastPointQueryV1(Collections.EMPTY_LIST)); - } else { - query.sendReply(query.serializer() - .formatLastPointQueryV1(data_points)); - } + final class FinalCB implements Callback> { + public Object call(final ArrayList done) throws Exception { + query.sendReply(query.serializer().formatLastPointQueryV1(results)); return null; } + @Override + public String toString() { + return "Final CB"; + } } + try { // start executing the queries - for (LastPointSubQuery sub_query : data_query.getQueries()) { + for (final LastPointSubQuery sub_query : data_query.getQueries()) { + final ArrayList> deferreds = + new ArrayList>(); // TSUID queries take precedence so if there are any TSUIDs listed, // process the TSUIDs and ignore the metric/tags if (sub_query.getTSUIDs() != null && !sub_query.getTSUIDs().isEmpty()) { - for (String tsuid : sub_query.getTSUIDs()) { - calls.add(TSUIDQuery.getLastPoint(tsdb, UniqueId.stringToUid(tsuid), - data_query.getResolveNames(), data_query.getBackScan(), 0)); + for (final String tsuid : sub_query.getTSUIDs()) { + final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb, + UniqueId.stringToUid(tsuid)); + deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(), + data_query.getBackScan())); } } else { - final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb); @SuppressWarnings("unchecked") - final HashMap tags = - (HashMap) (sub_query.getTags() != null ? - sub_query.getTags() : Collections.EMPTY_MAP); - tsuid_query.setQuery(sub_query.getMetric(), tags); - tsuid_query_wait.add( - tsuid_query.getLastWriteTimes().addCallback(new TSUIDQueryCB())); + final TSUIDQuery tsuid_query = + new TSUIDQuery(tsdb, sub_query.getMetric(), + sub_query.getTags() != null ? + sub_query.getTags() : Collections.EMPTY_MAP); + if (data_query.getBackScan() > 0) { + deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(), + data_query.getBackScan())); + } else { + calls.add(tsuid_query.getLastWriteTimes().addCallback(new TSUIDQueryCB())); + } + } + + if (deferreds.size() > 0) { + calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); } } - if (!tsuid_query_wait.isEmpty()) { - // wait on the time series queries first. If you don't, they may try - // to add deferreds to the calls list - Deferred.group(tsuid_query_wait) - .addCallback(new TSUIDQueryWaitCB()) - .addErrback(new ErrBack()) - .joinUninterruptibly(); - } Deferred.group(calls) .addCallback(new FinalCB()) .addErrback(new ErrBack()) diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index a3b3c62642..4c5487abad 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -291,9 +291,8 @@ private void handleTSMeta(final TSDB tsdb, final HttpQuery query) { } catch (IllegalArgumentException e) { throw new BadRequestException(e); } - final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb); + final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb, metric, tags); try { - tsuid_query.setQuery(metric, tags); final List tsmetas = tsuid_query.getTSMetas() .joinUninterruptibly(); query.sendReply(query.serializer().formatTSMetaListV1(tsmetas)); diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index d07627788a..0fdd2aac6e 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -20,8 +20,6 @@ import org.powermock.api.mockito.PowerMockito; import org.mockito.Matchers; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; import java.util.ArrayList; import net.opentsdb.core.DataPoints; import net.opentsdb.core.Query; @@ -29,7 +27,7 @@ import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; 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; @@ -38,6 +36,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import net.opentsdb.uid.NoSuchUniqueName; import com.stumbleupon.async.Deferred; + /** * Unit tests for the Query RPC class that handles parsing user queries for * timeseries data and returning that data @@ -322,4 +321,5 @@ public void postQueryNoMetricBadRequest() throws Exception { } //TODO(cl) add unit tests for the rate options parsing + } \ No newline at end of file diff --git a/test/tsd/TestQueryRpcLastDataPoint.java b/test/tsd/TestQueryRpcLastDataPoint.java new file mode 100644 index 0000000000..2ee6c02e3d --- /dev/null +++ b/test/tsd/TestQueryRpcLastDataPoint.java @@ -0,0 +1,985 @@ +// 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.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +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 com.stumbleupon.async.Deferred; + +@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 TestQueryRpcLastDataPoint { + private Config config; + private TSDB tsdb; + private HBaseClient client; + private QueryRpc rpc; + private MockBase storage; + private Map tags; + + @Before + public void before() throws Exception { + tags = new HashMap(1); + tags.put("host", "web01"); + rpc = new QueryRpc(); + config = mock(Config.class); + when(config.getString("tsd.storage.hbase.data_table")).thenReturn("tsdb"); + when(config.getString("tsd.storage.hbase.uid_table")).thenReturn("tsdb-uid"); + when(config.getString("tsd.storage.hbase.meta_table")).thenReturn("tsdb-meta"); + when(config.getString("tsd.storage.hbase.tree_table")).thenReturn("tsdb-tree"); + when(config.getString("tsd.http.show_stack_trace")).thenReturn("true"); + when(config.enable_tsuid_incrementing()).thenReturn(true); + when(config.enable_realtime_ts()).thenReturn(true); + client = mock(HBaseClient.class); + + PowerMockito.whenNew(HBaseClient.class) + .withArguments(anyString(), anyString()).thenReturn(client); + tsdb = new TSDB(config); + Whitebox.setInternalState(tsdb, "client", client); + + 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.system{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("00000150E22700000003000001"), + new byte[] { 0, 0 }, new byte[] { 0x2A }); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000003000001&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("00000150E22700000001000004"), + new byte[] { 0, 0 }, new byte[] { 0x2A }); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/last?tsuids=000001000001000004&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")); + } +} From f11b6ead0e4075b7fc308f082b135aa1786b1c68 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 14:32:33 -0700 Subject: [PATCH 197/826] Add Tags.resolveAllAsync() for resolution asynchronously Signed-off-by: Chris Larsen --- src/core/TSDB.java | 10 +++++----- src/core/Tags.java | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index c99d548c69..8b2f451e5d 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -373,12 +373,12 @@ public byte[] getUID(final UniqueIdType type, final String name) { } /** - * Attempts to find the UID matching a given name asynchronously + * Attempts to find the UID matching a given name * @param type The type of UID * @param name The name to search for * @throws IllegalArgumentException if the type is not valid * @throws NoSuchUniqueName if the name was not found - * @since 2.2 + * @since 2.1 */ public Deferred getUIDAsync(final UniqueIdType type, final String name) { if (name == null || name.isEmpty()) { @@ -386,11 +386,11 @@ public Deferred getUIDAsync(final UniqueIdType type, final String name) } switch (type) { case METRIC: - return this.metrics.getIdAsync(name); + return metrics.getIdAsync(name); case TAGK: - return this.tag_names.getIdAsync(name); + return tag_names.getIdAsync(name); case TAGV: - return this.tag_values.getIdAsync(name); + return tag_values.getIdAsync(name); default: throw new IllegalArgumentException("Unrecognized UID type"); } diff --git a/src/core/Tags.java b/src/core/Tags.java index a2a84657a1..83ec687824 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -531,6 +531,21 @@ public static ArrayList resolveAll(final TSDB tsdb, throw new RuntimeException("Should never happen!", e); } } + + /** + * Resolves a set of tag strings to their UIDs asynchronously + * @param tsdb the TSDB to use for access + * @param tags The tags to resolve + * @return A deferred with the list of UIDs in tagk1, tagv1, .. tagkn, tagvn + * order + * @throws NoSuchUniqueName if one of the elements in the map contained an + * unknown tag name or tag value. + * @since 2.1 + */ + public static Deferred> resolveAllAsync(final TSDB tsdb, + final Map tags) { + return resolveAllInternalAsync(tsdb, tags, false); + } /** * Resolves (and creates, if necessary) all the tags (name=value) into the a From 51cf5bf7cbad689e998e4400a4f0c6e0ba6288f9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 15:56:24 -0700 Subject: [PATCH 198/826] Add an Internal.baseTime() overload to normalize an epoch timestamp. Signed-off-by: Chris Larsen --- src/core/Internal.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index 54e2f7306d..e8d7867874 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -111,7 +111,18 @@ public static String metricName(final TSDB tsdb, final byte[] id) { public static long baseTime(final TSDB tsdb, final byte[] row) { return Bytes.getUnsignedInt(row, Const.SALT_WIDTH() + TSDB.metrics_width()); } - + + /** @return the time normalized to an hour boundary in epoch seconds */ + public static long baseTime(final long timestamp) { + if ((timestamp & Const.SECOND_MASK) != 0) { + // drop the ms timestamp to seconds to calculate the base timestamp + return ((timestamp / 1000) - + ((timestamp / 1000) % Const.MAX_TIMESPAN)); + } else { + return (timestamp - (timestamp % Const.MAX_TIMESPAN)); + } + } + /** @see Tags#getTags */ public static Map getTags(final TSDB tsdb, final byte[] row) { return Tags.getTags(tsdb, row); From f58861e7597d7141e6b01e61a8f406acfdcccd6e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 16:41:14 -0700 Subject: [PATCH 199/826] Bring the DateTime.currentTimeMillis() up from 2.2 to 2.1 Signed-off-by: Chris Larsen --- src/utils/DateTime.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 277cca46cb..97b052c0bb 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -262,7 +262,7 @@ public static void setDefaultTimezone(final String tzname) { * make unit testing easier. Mocking System.class is a bad idea in general * so placing this here and mocking DateTime.class is MUCH cleaner. * @return The current epoch time in milliseconds - * @since 2.2 + * @since 2.1 */ public static long currentTimeMillis() { return System.currentTimeMillis(); From 7c4320307451ae29537a55ad94b363d70e630fa8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 30 Aug 2015 12:05:37 -0700 Subject: [PATCH 200/826] Some tweaks to the base test class as deferreds need Answers instead of simple returns to function properly. Signed-off-by: Chris Larsen --- test/core/BaseTsdbTest.java | 82 ++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 9bf987b7ea..3d60552aa4 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -34,6 +34,8 @@ import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; 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.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -121,20 +123,44 @@ public void before() throws Exception { void setupMetricMaps() { when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); when(metrics.getIdAsync(METRIC_STRING)) - .thenReturn(Deferred.fromResult(METRIC_BYTES)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(METRIC_BYTES); + } + }); when(metrics.getOrCreateId(METRIC_STRING)) .thenReturn(METRIC_BYTES); when(metrics.getId(METRIC_B_STRING)).thenReturn(METRIC_B_BYTES); when(metrics.getIdAsync(METRIC_B_STRING)) - .thenReturn(Deferred.fromResult(METRIC_B_BYTES)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(METRIC_B_BYTES); + } + }); when(metrics.getOrCreateId(METRIC_B_STRING)) .thenReturn(METRIC_B_BYTES); when(metrics.getNameAsync(METRIC_BYTES)) - .thenReturn(Deferred.fromResult(METRIC_STRING)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(METRIC_STRING); + } + }); when(metrics.getNameAsync(METRIC_B_BYTES)) - .thenReturn(Deferred.fromResult(METRIC_B_STRING)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(METRIC_B_STRING); + } + }); when(metrics.getNameAsync(NSUI_METRIC)) .thenThrow(new NoSuchUniqueId("metrics", NSUI_METRIC)); @@ -150,21 +176,45 @@ void setupTagkMaps() { when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); when(tag_names.getIdAsync(TAGK_STRING)) - .thenReturn(Deferred.fromResult(TAGK_BYTES)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGK_BYTES); + } + }); when(tag_names.getOrCreateIdAsync(TAGK_STRING)) .thenReturn(Deferred.fromResult(TAGK_BYTES)); when(tag_names.getId(TAGK_B_STRING)).thenReturn(TAGK_B_BYTES); when(tag_names.getOrCreateId(TAGK_B_STRING)).thenReturn(TAGK_B_BYTES); when(tag_names.getIdAsync(TAGK_B_STRING)) - .thenReturn(Deferred.fromResult(TAGK_B_BYTES)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGK_B_BYTES); + } + }); when(tag_names.getOrCreateIdAsync(TAGK_B_STRING)) .thenReturn(Deferred.fromResult(TAGK_B_BYTES)); when(tag_names.getNameAsync(TAGK_BYTES)) - .thenReturn(Deferred.fromResult(TAGK_STRING)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGK_STRING); + } + }); when(tag_names.getNameAsync(TAGK_B_BYTES)) - .thenReturn(Deferred.fromResult(TAGK_B_STRING)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGK_B_STRING); + } + }); when(tag_names.getNameAsync(NSUI_TAGK)) .thenThrow(new NoSuchUniqueId("tagk", NSUI_TAGK)); @@ -180,14 +230,26 @@ void setupTagvMaps() { when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); when(tag_values.getIdAsync(TAGV_STRING)) - .thenReturn(Deferred.fromResult(TAGV_BYTES)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGV_BYTES); + } + }); when(tag_values.getOrCreateIdAsync(TAGV_STRING)) .thenReturn(Deferred.fromResult(TAGV_BYTES)); when(tag_values.getId(TAGV_B_STRING)).thenReturn(TAGV_B_BYTES); when(tag_values.getOrCreateId(TAGV_B_STRING)).thenReturn(TAGV_B_BYTES); when(tag_values.getIdAsync(TAGV_B_STRING)) - .thenReturn(Deferred.fromResult(TAGV_B_BYTES)); + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGV_B_BYTES); + } + }); when(tag_values.getOrCreateIdAsync(TAGV_B_STRING)) .thenReturn(Deferred.fromResult(TAGV_B_BYTES)); From 9cf072be37419a066a8759238c6f3d33c2b5c729 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 30 Aug 2015 12:12:38 -0700 Subject: [PATCH 201/826] Fix up the LastDataPoint querys and bring in the UTs from 2.1 Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/meta/TSUIDQuery.java | 877 +++++++++++++-------- src/tsd/QueryRpc.java | 115 +-- src/tsd/UniqueIdRpc.java | 5 +- test/meta/TestTSUIDQuery.java | 819 +++++++++++++++++--- test/tsd/TestQueryRpcLastDataPoint.java | 961 ++++++++++++++++++++++++ 6 files changed, 2318 insertions(+), 460 deletions(-) create mode 100644 test/tsd/TestQueryRpcLastDataPoint.java diff --git a/Makefile.am b/Makefile.am index ca5d938d53..514d02b60e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -235,6 +235,7 @@ test_SRC := \ test/tsd/TestHttpRpcPluginQuery.java \ test/tsd/TestPutRpc.java \ test/tsd/TestQueryRpc.java \ + test/tsd/TestQueryRpcLastDataPoint.java \ test/tsd/TestRpcHandler.java \ test/tsd/TestRpcPlugin.java \ test/tsd/TestRpcManager.java \ diff --git a/src/meta/TSUIDQuery.java b/src/meta/TSUIDQuery.java index b22031c5f7..95d51f93f7 100644 --- a/src/meta/TSUIDQuery.java +++ b/src/meta/TSUIDQuery.java @@ -18,16 +18,17 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.Internal; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; -import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.DateTime; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.GetRequest; @@ -38,10 +39,10 @@ import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; -import com.stumbleupon.async.DeferredGroupException; /** - * Methods for querying the tsdb-meta table. This can be used to figure out what + * Methods for querying the tsdb-meta table or finding the last data point of + * a particular time series. This can be used to figure out what * time series are actually stored as well as fetch TSMeta objects or optimize * queries against the data table. * @since 2.1 @@ -55,26 +56,163 @@ public class TSUIDQuery { */ private static final Charset CHARSET = Charset.forName("ISO-8859-1"); - /** ID of the metric being looked up. */ - private byte[] metric; - - /** - * Tags of the metrics being looked up. - * Each tag is a byte array holding the ID of both the name and value - * of the tag. - * Invariant: an element cannot be both in this array and in group_bys. - */ - private ArrayList tags; - + /** The TSUID that can be set by the caller or after processing the metric */ + private byte[] tsuid; + + /** The metric set by the caller */ + private String metric; + + /** The metric UID after lookup */ + private byte[] metric_uid; + + /** The tags set by the caller */ + private Map tags; + + /** The tag UID list after lookup */ + private ArrayList tag_uids; + + /** Whether or not to resolve names for last data point queries */ + private boolean resolve_names; + + /** How far back, in hours, to scan for last data point queries */ + private int back_scan; + + /** The last timestamp scanned for last data point queries */ + private long last_timestamp; + /** The TSDB we belong to. */ private final TSDB tsdb; /** - * Constructor. + * Default CTor just sets the TSDB reference * @param tsdb The TSDB to use for storage access + * @throws IllegalArgumentException if the TSDB reference is null + * @deprecated Please use one of the other constructors. Will be removed in 2.3 */ public TSUIDQuery(final TSDB tsdb) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB reference cannot be null"); + } + this.tsdb =tsdb; + } + + /** + * CTor used for a TSUID based query when we know exactly what we want + * @param tsdb The TSDB to use for storage access + * @param tsuid A TSUID to use for querying + * @throws IllegalArgumentException if the TSUID is invalid + */ + public TSUIDQuery(final TSDB tsdb, final byte[] tsuid) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB reference cannot be null"); + } + if (tsuid == null || tsuid.length < + TSDB.metrics_width() + TSDB.tagk_width() + TSDB.tagv_width()) { + throw new IllegalArgumentException("TSUID must not be null and must " + + "have a metric and at least one tag pair"); + } + this.tsdb = tsdb; + this.tsuid = tsuid; + } + + /** + * CTor used for a metric style query + * @param tsdb The TSDB to use for storage access + * @param metric The metric to look up + * @param tags The tags to lookup. This may be an empty map if you're scanning + * meta. + * @throws IllegalArgumentException if the metric is null, empty or the tag + * map is null. + */ + public TSUIDQuery(final TSDB tsdb, final String metric, + final Map tags) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB reference cannot be null"); + } + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("Metric cannot be null or empty"); + } + if (tags == null) { + throw new IllegalArgumentException("Tag map cannot be null. Empty is ok"); + } this.tsdb = tsdb; + this.metric = metric; + this.tags = tags; + } + + /** + * Attempts to fetch the last data point for the given metric or TSUID. + * If back_scan == 0 and meta is enabled via + * "tsd.core.meta.enable_tsuid_tracking" or + * "tsd.core.meta.enable_tsuid_incrementing" then we will look up the metric + * or TSUID in the meta table first and use the counter there to get the + * last write time. + *

    + * However if backscan is set, then we'll start with the current time and + * iterate back "back_scan" number of hours until we find a value. + *

    + * @param resolve_names Whether or not to resolve the UIDs back to their + * names when we find a value. + * @param back_scan The number of hours back in time to scan + * @return A data point if found, null if not. Or an exception if something + * went pear shaped. + */ + public Deferred getLastPoint(final boolean resolve_names, + final int back_scan) { + if (back_scan < 0) { + throw new IllegalArgumentException( + "Backscan must be zero or a positive number"); + } + + this.resolve_names = resolve_names; + this.back_scan = back_scan; + + final boolean meta_enabled = tsdb.getConfig().enable_tsuid_tracking() || + tsdb.getConfig().enable_tsuid_incrementing(); + + class TSUIDCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] incoming_tsuid) + throws Exception { + if (tsuid == null && incoming_tsuid == null) { + return Deferred.fromError(new RuntimeException("Both incoming and " + + "supplied TSUIDs were null for " + TSUIDQuery.this)); + } else if (incoming_tsuid != null) { + setTSUID(incoming_tsuid); + } + if (back_scan < 1 && meta_enabled) { + final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); + get.family(TSMeta.FAMILY()); + get.qualifier(TSMeta.COUNTER_QUALIFIER()); + return tsdb.getClient().get(get).addCallbackDeferring(new MetaCB()); + } + + if (last_timestamp > 0) { + last_timestamp = Internal.baseTime(last_timestamp); + } else { + last_timestamp = Internal.baseTime(DateTime.currentTimeMillis()); + } + final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); + final GetRequest get = new GetRequest(tsdb.dataTable(), key); + get.family(TSDB.FAMILY()); + return tsdb.getClient().get(get).addCallbackDeferring(new LastPointCB()); + } + @Override + public String toString() { + return "TSUID callback"; + } + } + + if (tsuid == null) { + return tsuidFromMetric(tsdb, metric, tags) + .addCallbackDeferring(new TSUIDCB()); + } + try { + // damn typed exceptions.... + return new TSUIDCB().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } } /** @@ -83,186 +221,306 @@ public TSUIDQuery(final TSDB tsdb) { * @param tags A map of tag value pairs or simply an empty map * @throws NoSuchUniqueName if the metric or any of the tag names/values did * not exist + * @deprecated Please use one of the constructors instead. Will be removed in 2.3 */ - public void setQuery(final String metric, final HashMap tags) { - this.metric = tsdb.getUID(UniqueIdType.METRIC, metric); - this.tags = Tags.resolveAll(tsdb, tags); + public void setQuery(final String metric, final Map tags) { + this.metric = metric; + this.tags = tags; + metric_uid = tsdb.getUID(UniqueIdType.METRIC, metric); + tag_uids = Tags.resolveAll(tsdb, tags); } /** * Fetches a list of TSUIDs given the metric and optional tag pairs. The query * format is similar to TsdbQuery but doesn't support grouping operators for * tags. Only TSUIDs that had "ts_counter" qualifiers will be returned. + *

    + * NOTE: If you called {@link #setQuery(String, Map)} successfully this will + * immediately scan the meta table. But if you used the CTOR to set the + * metric and tags it will attempt to resolve those and may return an exception. * @return A map of TSUIDs to the last timestamp (in milliseconds) when the * "ts_counter" was updated. Note that the timestamp will be the time stored - * by HBase, not the actual timestamp of the data point + * by HBase, not the actual timestamp of the data point. If nothing was + * found, the map will be empty but not null. * @throws IllegalArgumentException if the metric was not set or the tag map * was null */ public Deferred> getLastWriteTimes() { - // we need at least a metric name and the tags can't be null. Empty tags are - // fine, but the map can't be null. - if (metric == null || metric.length < 0) { - throw new IllegalArgumentException("Missing metric UID"); - } - if (tags == null) { - throw new IllegalArgumentException("Tag map was null"); - } - - final Scanner scanner = getScanner(); - scanner.setQualifier(TSMeta.COUNTER_QUALIFIER()); - final Deferred> results = new Deferred>(); - final ByteMap tsuids = new ByteMap(); - - /** - * Scanner callback that will call itself while iterating through the - * tsdb-meta table - */ - final class ScannerCB implements Callback>> { - - /** - * Starts the scanner and is called recursively to fetch the next set of - * rows from the scanner. - * @return The map of spans if loaded successfully, null if no data was - * found - */ - public Object scan() { - return scanner.nextRows().addCallback(this); - } - - /** - * Loops through each row of the scanner results and parses out data - * points and optional meta data - * @return null if no rows were found, otherwise the TreeMap with spans - */ + class ResolutionCB implements Callback>, Object> { @Override - public Object call(final ArrayList> rows) - throws Exception { - try { - if (rows == null) { - results.callback(tsuids); + public Deferred> call(Object arg0) throws Exception { + final Scanner scanner = getScanner(); + scanner.setQualifier(TSMeta.COUNTER_QUALIFIER()); + final Deferred> results = new Deferred>(); + final ByteMap tsuids = new ByteMap(); + + final class ErrBack implements Callback { + @Override + public Object call(final Exception e) throws Exception { + results.callback(e); return null; } + @Override + public String toString() { + return "Error callback"; + } + } + + /** + * Scanner callback that will call itself while iterating through the + * tsdb-meta table + */ + final class ScannerCB implements Callback>> { - for (final ArrayList row : rows) { - final byte[] tsuid = row.get(0).key(); - tsuids.put(tsuid, row.get(0).timestamp()); + /** + * Starts the scanner and is called recursively to fetch the next set of + * rows from the scanner. + * @return The map of spans if loaded successfully, null if no data was + * found + */ + public Object scan() { + return scanner.nextRows().addCallback(this).addErrback(new ErrBack()); + } + + /** + * Loops through each row of the scanner results and parses out data + * points and optional meta data + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final ArrayList> rows) + throws Exception { + try { + if (rows == null) { + results.callback(tsuids); + return null; + } + + for (final ArrayList row : rows) { + final byte[] tsuid = row.get(0).key(); + tsuids.put(tsuid, row.get(0).timestamp()); + } + return scan(); + } catch (Exception e) { + results.callback(e); + return null; + } } - return scan(); - } catch (Exception e) { - results.callback(e); - return null; } + + new ScannerCB().scan(); + return results; + } + @Override + public String toString() { + return "Last counter time callback"; } } - new ScannerCB().scan(); - return results; + if (metric_uid == null) { + return resolveMetric().addCallbackDeferring(new ResolutionCB()); + } + try { + return new ResolutionCB().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } } /** * Returns all TSMeta objects stored for timeseries defined by this query. The * query is similar to TsdbQuery without any aggregations. Returns an empty * list, when no TSMetas are found. Only returns stored TSMetas. + *

    + * NOTE: If you called {@link #setQuery(String, Map)} successfully this will + * immediately scan the meta table. But if you used the CTOR to set the + * metric and tags it will attempt to resolve those and may return an exception. * @return A list of existing TSMetas for the timeseries covered by the query. * @throws IllegalArgumentException When either no metric was specified or the * tag map was null (Empty map is OK). */ public Deferred> getTSMetas() { - // we need at least a metric name and the tags can't be null. Empty tags are - // fine, but the map can't be null. - if (metric == null || metric.length < 0) { - throw new IllegalArgumentException("Missing metric UID"); - } - if (tags == null) { - throw new IllegalArgumentException("Tag map was null"); - } - - final Scanner scanner = getScanner(); - scanner.setQualifier(TSMeta.META_QUALIFIER()); - final Deferred> results = new Deferred>(); - final List tsmetas = new ArrayList(); - final List> tsmeta_group = new ArrayList>(); - - final class TSMetaGroupCB implements Callback> { - + class ResolutionCB implements Callback>, Object> { @Override - public List call(ArrayList ts) throws Exception { - for (TSMeta tsm: ts) { - if (tsm != null) { - tsmetas.add(tsm); + public Deferred> call(final Object done) throws Exception { + final Scanner scanner = getScanner(); + scanner.setQualifier(TSMeta.META_QUALIFIER()); + final Deferred> results = new Deferred>(); + final List tsmetas = new ArrayList(); + final List> tsmeta_group = new ArrayList>(); + + final class TSMetaGroupCB implements Callback> { + @Override + public List call(ArrayList ts) throws Exception { + for (TSMeta tsm: ts) { + if (tsm != null) { + tsmetas.add(tsm); + } + } + results.callback(tsmetas); + return null; + } + @Override + public String toString() { + return "TSMeta callback"; } } - results.callback(tsmetas); - return null; - } - - } - - /** - * Scanner callback that will call itself while iterating through the - * tsdb-meta table. - * - * Keeps track of a Set of Deferred TSMeta calls. When all rows are scanned, - * will wait for all TSMeta calls to be completed and then create the result - * list. - */ - final class ScannerCB implements Callback>> { - - /** - * Starts the scanner and is called recursively to fetch the next set of - * rows from the scanner. - * @return The map of spans if loaded successfully, null if no data was - * found - */ - public Object scan() { - return scanner.nextRows().addCallback(this); - } - - /** - * Loops through each row of the scanner results and parses out data - * points and optional meta data - * @return null if no rows were found, otherwise the TreeMap with spans - */ - @Override - public Object call(final ArrayList> rows) - throws Exception { - try { - if (rows == null) { - Deferred.group(tsmeta_group).addCallback(new TSMetaGroupCB()); + + final class ErrBack implements Callback { + @Override + public Object call(final Exception e) throws Exception { + results.callback(e); return null; } - for (final ArrayList row : rows) { - tsmeta_group.add(TSMeta.parseFromColumn(tsdb, row.get(0), true)); + @Override + public String toString() { + return "Error callback"; + } + } + + /** + * Scanner callback that will call itself while iterating through the + * tsdb-meta table. + * + * Keeps track of a Set of Deferred TSMeta calls. When all rows are scanned, + * will wait for all TSMeta calls to be completed and then create the result + * list. + */ + final class ScannerCB implements Callback>> { + + /** + * Starts the scanner and is called recursively to fetch the next set of + * rows from the scanner. + * @return The map of spans if loaded successfully, null if no data was + * found + */ + public Object scan() { + return scanner.nextRows().addCallback(this).addErrback(new ErrBack()); + } + + /** + * Loops through each row of the scanner results and parses out data + * points and optional meta data + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final ArrayList> rows) + throws Exception { + try { + if (rows == null) { + Deferred.group(tsmeta_group) + .addCallback(new TSMetaGroupCB()).addErrback(new ErrBack()); + return null; + } + for (final ArrayList row : rows) { + tsmeta_group.add(TSMeta.parseFromColumn(tsdb, row.get(0), true)); + } + return scan(); + } catch (Exception e) { + results.callback(e); + return null; + } } - return scan(); - } catch (Exception e) { - results.callback(e); - return null; } + + new ScannerCB().scan(); + return results; + } + @Override + public String toString() { + return "TSMeta scan callback"; } } - new ScannerCB().scan(); - return results; + if (metric_uid == null) { + return resolveMetric().addCallbackDeferring(new ResolutionCB()); + } + try { + return new ResolutionCB().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } } public String toString() { final StringBuilder buf = new StringBuilder(); - buf.append("TSUIDQuery(metric=") - .append(Arrays.toString(metric)); - try { - buf.append("), tags=").append(Tags.resolveIds(tsdb, tags)); - } catch (NoSuchUniqueId e) { - buf.append("), tags=<").append(e.getMessage()).append('>'); - } - buf.append("))"); + buf.append("TSUIDQuery(metric=").append(metric) + .append(", tags=").append(tags) + .append(", tsuid=") + .append(tsuid != null ? UniqueId.uidToString(tsuid) : "null") + .append(", last_timestamp=").append(last_timestamp) + .append(", back_scan=").append(back_scan) + .append(", resolve_names=").append(resolve_names) + .append(")"); return buf.toString(); } + /** + * Converts the given metric and tags to a TSUID by resolving the strings to + * their UIDs. Note that the resulting TSUID may not exist if the combination + * was not written to TSDB + * @param tsdb The TSDB to use for storage access + * @param metric The metric name to resolve + * @param tags The tags to resolve. May not be empty. + * @return A deferred containing the TSUID when ready or an error such as + * a NoSuchUniqueName exception if the metric didn't exist or a + * DeferredGroupException if one of the tag keys or values did not exist. + * @throws IllegalArgumentException if the metric or tags were null or + * empty. + */ + public static Deferred tsuidFromMetric(final TSDB tsdb, + final String metric, final Map tags) { + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("The metric cannot be empty"); + } + if (tags == null || tags.isEmpty()) { + throw new IllegalArgumentException("Tags cannot be null or empty " + + "when getting a TSUID"); + } + + final byte[] metric_uid = new byte[TSDB.metrics_width()]; + + class TagsCB implements Callback> { + @Override + public byte[] call(final ArrayList tag_list) throws Exception { + final byte[] tsuid = new byte[metric_uid.length + + ((TSDB.tagk_width() + TSDB.tagv_width()) + * tag_list.size())]; + int idx = 0; + System.arraycopy(metric_uid, 0, tsuid, 0, metric_uid.length); + idx += metric_uid.length; + for (final byte[] t : tag_list) { + System.arraycopy(t, 0, tsuid, idx, t.length); + idx += t.length; + } + return tsuid; + } + @Override + public String toString() { + return "Tag resolution callback"; + } + } + + class MetricCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] uid) + throws Exception { + System.arraycopy(uid, 0, metric_uid, 0, uid.length); + return Tags.resolveAllAsync(tsdb, tags).addCallback(new TagsCB()); + } + @Override + public String toString() { + return "Metric resolution callback"; + } + } + + return tsdb.getUIDAsync(UniqueIdType.METRIC, metric) + .addCallbackDeferring(new MetricCB()); + } + /** * Attempts to retrieve the last data point for the given TSUID. * This operates by checking the meta table for the {@link #COUNTER_QUALIFIER} @@ -287,186 +545,193 @@ public String toString() { * point was written * @return An {@link IncomingDataPoint} if data was found, null if not * @throws NoSuchUniqueId if one of the tag lookups failed + * @deprecated Please use {@link #getLastPoint} */ public static Deferred getLastPoint(final TSDB tsdb, final byte[] tsuid, final boolean resolve_names, final int max_lookups, final long last_timestamp) { - - final Deferred result = new Deferred(); - final long start_time; - final long time_limit; - if (max_lookups < 1) { - start_time = 0; - time_limit = 0; - } else { - start_time = System.currentTimeMillis() / 1000; - time_limit = start_time - (3600 * max_lookups); + final TSUIDQuery query = new TSUIDQuery(tsdb, tsuid); + query.last_timestamp = last_timestamp; + return query.getLastPoint(resolve_names, max_lookups); + } + + /** + * Resolve the UIDs to names. If the query was for a metric and tags then we + * can just use those. + * @param dp The data point to fill in values for + * @return A deferred with the data point or an exception if something went + * wrong. + */ + private Deferred resolveNames(final IncomingDataPoint dp) { + // If the caller gave us a metric and tags, save some time by NOT hitting + // our UID tables or storage. + if (metric != null) { + dp.setMetric(metric); + dp.setTags((HashMap)tags); + return Deferred.fromResult(dp); } - - final class ErrBack implements Callback { - public Object call(final Exception e) throws Exception { - Throwable ex = e; - while (ex.getClass().equals(DeferredGroupException.class)) { - if (ex.getCause() == null) { - LOG.warn("Unable to get to the root cause of the DGE"); - break; - } - ex = ex.getCause(); - } - if (ex instanceof RuntimeException) { - result.callback(ex); - } else { - result.callback(e); - } - return null; - } + + class TagsCB implements Callback> { + public IncomingDataPoint call(final HashMap tags) + throws Exception { + dp.setTags(tags); + return dp; + } + @Override + public String toString() { + return "Tags resolution CB"; + } } - /** - * Called after GetLastDataPointCB has completed. If nothing was found then - * we just return a null, otherwise we set the TSUID and optionally resolve - * the metric and tag names. - */ - final class ReturnCB implements Callback { - final long timestamp; - - public ReturnCB(final long timestamp) { - this.timestamp = timestamp; + class MetricCB implements Callback, String> { + public Deferred call(final String name) + throws Exception { + dp.setMetric(name); + final List tags = UniqueId.getTagPairsFromTSUID(tsuid); + return Tags.resolveIdsAsync(tsdb, tags).addCallback(new TagsCB()); } - - /** - * Callback implementation. If the time_limit was set and the result was - * null we call the local getPrevious() method to issue a Get on the - * previous row. - */ - public Object call(final IncomingDataPoint dp) - throws Exception { - if (dp == null) { - if (time_limit > 0) { - getPrevious(); - return null; - } - result.callback(null); - return null; - } - - dp.setTSUID(UniqueId.uidToString(tsuid)); - if (!resolve_names) { - result.callback(dp); - return null; - } - - class TagsCB implements Callback> { - public IncomingDataPoint call(final HashMap tags) - throws Exception { - dp.setTags(tags); - result.callback(dp); - return null; - } - } - - class MetricCB implements Callback { - public Object call(final String name) throws Exception { - dp.setMetric(name); - final List tags = UniqueId.getTagPairsFromTSUID(tsuid); - return Tags.resolveIdsAsync(tsdb, tags).addCallback(new TagsCB()); - } - } - - // start the resolve dance - final byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); - return tsdb.getUidName(UniqueIdType.METRIC, metric_uid) - .addCallback(new MetricCB()); + @Override + public String toString() { + return "Metric resolution CB"; } - - /** - * Issues a GetRequest on the previous hour's row of data if we haven't - * exceeded the limit - * @return Null if we've hit the time limit or another deferred to wait - * on - */ - private void getPrevious() { - if (timestamp <= time_limit) { - // we hit our limit and didn't find a valid data point - result.callback(null); - return; + } + + final byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + return tsdb.getUidName(UniqueIdType.METRIC, metric_uid) + .addCallbackDeferring(new MetricCB()); + } + + /** + * Handles getting the results of the first GetRequest and keeps iterating + * back in time until we find a point or run out of back scans. + */ + private class LastPointCB implements Callback, + ArrayList> { + int iteration = 0; + + @Override + public Deferred call(final ArrayList row) + throws Exception { + if (row == null || row.isEmpty()) { + if (iteration >= back_scan) { + if (LOG.isDebugEnabled()) { + LOG.debug("No data points found within the time span for TSUID query " + + TSUIDQuery.this); + } + return Deferred.fromResult(null); } - // look one row into the past - final long previous_time = timestamp - 3600; - - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, previous_time); + last_timestamp -= 3600; + ++iteration; + final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); final GetRequest get = new GetRequest(tsdb.dataTable(), key); get.family(TSDB.FAMILY()); - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(previous_time)) - .addErrback(new ErrBack()); + return tsdb.getClient().get(get).addCallbackDeferring(this); } + + final IncomingDataPoint dp = + new Internal.GetLastDataPointCB(tsdb).call(row); + dp.setTSUID(UniqueId.uidToString(tsuid)); + if (!resolve_names) { + return Deferred.fromResult(dp); + } + + return resolveNames(dp); } - - /** - * Callback from the GetRequest that simply determines if the row is empty - * or not - */ - final class ExistsCB implements Callback> { - public Object call(final ArrayList row) + @Override + public String toString() { + return "LastDataPoint callback"; + } + } + + /** + * Callback that receives the result of a single meta table lookup to see if + * the last write counter was there or not. The input should contain just + * the counter column + */ + private class MetaCB implements Callback, + ArrayList> { + @Override + public Deferred call(final ArrayList row) throws Exception { - if (row == null || row.isEmpty() || row.get(0).value() == null) { - result.callback(null); - return null; - } - - // we want the timestamp in seconds so we can figure out what row the - // data *should* be in (though it's not guaranteed) - final long last_write = row.get(0).timestamp(); - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_write); - final GetRequest get = new GetRequest(tsdb.dataTable(), key); - get.family(TSDB.FAMILY()); - - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(0)) - .addErrback(new ErrBack()); + if (row == null) { + return Deferred.fromResult(null); + } + last_timestamp = Internal.baseTime(row.get(0).timestamp()); + final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); + final GetRequest get = new GetRequest(tsdb.dataTable(), key); + get.family(TSDB.FAMILY()); + return tsdb.getClient().get(get).addCallbackDeferring(new LastPointCB()); + } + @Override + public String toString() { + return "Meta TSCounter lookup"; + } + } + + /** + * Resolves the metric and tags (if set) to their UIDs, setting the local + * arrays. + * @return A deferred to wait on or catch exceptions in + * @throws IllegalArgumentException if the metric is empty || null or the + * tag list is null. + */ + private Deferred resolveMetric() { + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("The metric cannot be empty"); + } + if (tags == null) { + throw new IllegalArgumentException("Tags cannot be null or empty " + + "when getting a TSUID"); + } + + class TagsCB implements Callback> { + @Override + public Object call(final ArrayList tag_list) throws Exception { + setTagUIDs(tag_list); return null; } + @Override + public String toString() { + return "Tag resolution callback"; + } } - // we need to determine a course of action. We can either: - // 1) Lookup the last time a data point was written for a TSUID - // 2) Immediately fetch data from a row if we were given a timestamp - // 3) Or start at NOW and iterate backwards until we find a point or hit - // the user supplied limit - if (time_limit == 0) { - if (last_timestamp < 1) { - final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); - get.family(TSMeta.FAMILY()); - get.qualifier(TSMeta.COUNTER_QUALIFIER()); - tsdb.getClient().get(get) - .addCallback(new ExistsCB()) - .addErrback(new ErrBack()); - } else { - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp); - final GetRequest get = new GetRequest(tsdb.dataTable(), key); - get.family(TSDB.FAMILY()); - - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(0)) - .addErrback(new ErrBack()); + class MetricCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] uid) + throws Exception { + setMetricUID(uid); + if (tags.isEmpty()) { + setTagUIDs(new ArrayList(0)); + return null; + } + return Tags.resolveAllAsync(tsdb, tags).addCallback(new TagsCB()); + } + @Override + public String toString() { + return "Metric resolution callback"; } - } else { - final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, start_time); - final GetRequest get = new GetRequest(tsdb.dataTable(), key); - get.family(TSDB.FAMILY()); - tsdb.getClient().get(get).addCallback( - new Internal.GetLastDataPointCB(tsdb)) - .addCallback(new ReturnCB(start_time)) - .addErrback(new ErrBack()); } - return result; - } + return tsdb.getUIDAsync(UniqueIdType.METRIC, metric) + .addCallbackDeferring(new MetricCB()); + } + + /** @param tsuid The TSUID to store */ + private void setTSUID(final byte[] tsuid) { + this.tsuid = tsuid; + } + + /** @param uid The metric UID to set */ + private void setMetricUID(final byte[] uid) { + metric_uid = uid; + } + + /** @param uids The tag UIDs to set */ + private void setTagUIDs(final ArrayList uids) { + tag_uids = uids; + } /** * Configures the scanner for a specific metric and optional tags @@ -474,11 +739,11 @@ public Object call(final ArrayList row) */ private Scanner getScanner() { final Scanner scanner = tsdb.getClient().newScanner(tsdb.metaTable()); - scanner.setStartKey(metric); + scanner.setStartKey(metric_uid); // increment the metric UID by one so we can scan all of the rows for the // given metric - final long stop = UniqueId.uidToLong(metric, TSDB.metrics_width()) + 1; + final long stop = UniqueId.uidToLong(metric_uid, TSDB.metrics_width()) + 1; scanner.setStopKey(UniqueId.longToUID(stop, TSDB.metrics_width())); scanner.setFamily(TSMeta.FAMILY()); @@ -501,7 +766,7 @@ private Scanner getScanner() { // ... start by skipping the metric ID. .append(TSDB.metrics_width()) .append("}"); - final Iterator tags = this.tags.iterator(); + final Iterator tags = this.tag_uids.iterator(); byte[] tag = tags.hasNext() ? tags.next() : null; // Tags and group_bys are already sorted. We need to put them in the // regexp in order by ID, which means we just merge two sorted lists. @@ -518,4 +783,4 @@ private Scanner getScanner() { return scanner; } -} +} \ No newline at end of file diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 8e18757213..8d48a73234 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -245,7 +245,7 @@ public Object call(final List annotations) throws Exception { } /** - * Returns the last data point for each sub query if found. + * Processes a last data point query * @param tsdb The TSDB to which we belong * @param query The HTTP query to parse/respond */ @@ -272,12 +272,10 @@ private void handleLastDataPointQuery(final TSDB tsdb, final HttpQuery query) { "Missing sub queries"); } - // list of getLastPoint calls - final ArrayList> calls = - new ArrayList>(); - // list of calls to TSUIDQuery for scanning the tsdb-meta table - final ArrayList> tsuid_query_wait = - new ArrayList>(); + // a list of deferreds to wait on + final ArrayList> calls = new ArrayList>(); + // final results for serialization + final List results = new ArrayList(); /** * Used to catch exceptions @@ -297,7 +295,29 @@ public Object call(final Exception e) throws Exception { } else { throw e; } - } + } + @Override + public String toString() { + return "Error back"; + } + } + + final class FetchCB implements Callback> { + @Override + public Object call(final ArrayList dps) throws Exception { + synchronized(results) { + for (final IncomingDataPoint dp : dps) { + if (dp != null) { + results.add(dp); + } + } + } + return null; + } + @Override + public String toString() { + return "Fetched data points CB"; + } } /** @@ -310,73 +330,70 @@ public Object call(final ByteMap tsuids) throws Exception { if (tsuids == null || tsuids.isEmpty()) { return null; } - + final ArrayList> deferreds = + new ArrayList>(tsuids.size()); for (Map.Entry entry : tsuids.entrySet()) { - calls.add(TSUIDQuery.getLastPoint(tsdb, entry.getKey(), + deferreds.add(TSUIDQuery.getLastPoint(tsdb, entry.getKey(), data_query.getResolveNames(), data_query.getBackScan(), entry.getValue())); } + calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); return null; } - } - - /** - * Callback used to force the thread to wait for the TSUIDQueries to complete - */ - final class TSUIDQueryWaitCB implements Callback> { - public Object call(ArrayList arg0) throws Exception { - return null; + @Override + public String toString() { + return "TSMeta scan CB"; } } - + /** * Used to wait on the list of data point deferreds. Once they're all done * this will return the results to the call via the serializer */ - final class FinalCB implements Callback> { - @SuppressWarnings("unchecked") - public Object call(final ArrayList data_points) - throws Exception { - if (data_points == null) { - query.sendReply(query.serializer() - .formatLastPointQueryV1(Collections.EMPTY_LIST)); - } else { - query.sendReply(query.serializer() - .formatLastPointQueryV1(data_points)); - } + final class FinalCB implements Callback> { + public Object call(final ArrayList done) throws Exception { + query.sendReply(query.serializer().formatLastPointQueryV1(results)); return null; } + @Override + public String toString() { + return "Final CB"; + } } + try { // start executing the queries - for (LastPointSubQuery sub_query : data_query.getQueries()) { + for (final LastPointSubQuery sub_query : data_query.getQueries()) { + final ArrayList> deferreds = + new ArrayList>(); // TSUID queries take precedence so if there are any TSUIDs listed, // process the TSUIDs and ignore the metric/tags if (sub_query.getTSUIDs() != null && !sub_query.getTSUIDs().isEmpty()) { - for (String tsuid : sub_query.getTSUIDs()) { - calls.add(TSUIDQuery.getLastPoint(tsdb, UniqueId.stringToUid(tsuid), - data_query.getResolveNames(), data_query.getBackScan(), 0)); + for (final String tsuid : sub_query.getTSUIDs()) { + final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb, + UniqueId.stringToUid(tsuid)); + deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(), + data_query.getBackScan())); } } else { - final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb); @SuppressWarnings("unchecked") - final HashMap tags = - (HashMap) (sub_query.getTags() != null ? - sub_query.getTags() : Collections.EMPTY_MAP); - tsuid_query.setQuery(sub_query.getMetric(), tags); - tsuid_query_wait.add( - tsuid_query.getLastWriteTimes().addCallback(new TSUIDQueryCB())); + final TSUIDQuery tsuid_query = + new TSUIDQuery(tsdb, sub_query.getMetric(), + sub_query.getTags() != null ? + sub_query.getTags() : Collections.EMPTY_MAP); + if (data_query.getBackScan() > 0) { + deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(), + data_query.getBackScan())); + } else { + calls.add(tsuid_query.getLastWriteTimes().addCallback(new TSUIDQueryCB())); + } + } + + if (deferreds.size() > 0) { + calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); } } - if (!tsuid_query_wait.isEmpty()) { - // wait on the time series queries first. If you don't, they may try - // to add deferreds to the calls list - Deferred.group(tsuid_query_wait) - .addCallback(new TSUIDQueryWaitCB()) - .addErrback(new ErrBack()) - .joinUninterruptibly(); - } Deferred.group(calls) .addCallback(new FinalCB()) .addErrback(new ErrBack()) diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index a3b3c62642..1747e3f188 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -291,11 +291,10 @@ private void handleTSMeta(final TSDB tsdb, final HttpQuery query) { } catch (IllegalArgumentException e) { throw new BadRequestException(e); } - final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb); + final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb, metric, tags); try { - tsuid_query.setQuery(metric, tags); final List tsmetas = tsuid_query.getTSMetas() - .joinUninterruptibly(); + .joinUninterruptibly(); query.sendReply(query.serializer().formatTSMetaListV1(tsmetas)); } catch (NoSuchUniqueName e) { throw new BadRequestException(HttpResponseStatus.NOT_FOUND, diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index 174352f3f4..9a7a5cc628 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -12,17 +12,24 @@ // see . package net.opentsdb.meta; +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 java.util.HashMap; import java.util.List; import net.opentsdb.core.BaseTsdbTest; +import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; @@ -32,12 +39,17 @@ import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import org.hbase.async.Bytes.ByteMap; 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 org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.DeferredGroupException; @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", @@ -45,15 +57,706 @@ @RunWith(PowerMockRunner.class) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, - Scanner.class, TSMeta.class, AtomicIncrementRequest.class}) + Scanner.class, TSMeta.class, AtomicIncrementRequest.class, DateTime.class }) public final class TestTSUIDQuery extends BaseTsdbTest { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private static final byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private static final byte[] TSUID = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }; + private static final byte[] QUAL = new byte[] { 0, 0 }; + private static final byte[] VAL = new byte[] { 0x2A }; private TSUIDQuery query; @Before public void beforeLocal() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); + setupStorage(tsdb, storage); + } + + @Test + public void ctorDefault() throws Exception { + query = new TSUIDQuery(tsdb); + assertNotNull(query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSDB() throws Exception { + query = new TSUIDQuery(null); + } + + @Test + public void ctorTSUID() throws Exception { + query = new TSUIDQuery(tsdb, TSUID); + assertNotNull(query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSUID() throws Exception { + query = new TSUIDQuery(tsdb, null); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullTSDBforTSUID() throws Exception { + query = new TSUIDQuery(null, TSUID); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyTSUID() throws Exception { + query = new TSUIDQuery(tsdb, new byte[] { }); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorShortTSUID() throws Exception { + query = new TSUIDQuery(tsdb, new byte[] { 0, 0, 1, 0, 0, 1 }); + } + + @Test + public void ctorMetric() throws Exception { + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + assertNotNull(query); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricNullTSDB() throws Exception { + query = new TSUIDQuery(null, METRIC_STRING, tags); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricNullMetric() throws Exception { + query = new TSUIDQuery(tsdb, null, tags); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricEmptyMetric() throws Exception { + query = new TSUIDQuery(tsdb, "", tags); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorMetricNullTags() throws Exception { + query = new TSUIDQuery(tsdb, METRIC_STRING, null); + } + + @Test + public void ctorMetricEmptyTags() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + assertNotNull(query); + } + + @Test + public void getLastWriteTimes() throws Exception { + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(1, tsuids.size()); + assertEquals(1388534400013L, (long)tsuids.get(TSUID)); + } + + @Test + public void getLastWriteTimesSetQuery() throws Exception { + query = new TSUIDQuery(tsdb); + query.setQuery(METRIC_STRING, tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(1, tsuids.size()); + assertEquals(1388534400013L, (long)tsuids.get(TSUID)); + } + + @Test + public void getLastWriteTimesEmptyTags() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(2, tsuids.size()); + assertEquals(1388534400013L, (long)tsuids.get(TSUID)); + assertEquals(1388534400015L, + (long)tsuids.get(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 })); + } + + @Test + public void getLastWriteTimesEmptyTagsSetQuery() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb); + query.setQuery(METRIC_STRING, tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertEquals(2, tsuids.size()); + assertEquals(1388534400013L, (long)tsuids.get(TSUID)); + assertEquals(1388534400015L, + (long)tsuids.get(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 })); + } + + @Test (expected = IllegalArgumentException.class) + public void getLastWriteTimesQueryNotSet() throws Exception { + query = new TSUIDQuery(tsdb); + query.getLastWriteTimes().joinUninterruptibly(); + } + + @Test + public void getLastWriteTimesNoMatch() throws Exception { + storage.flushStorage(); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final ByteMap tsuids = query.getLastWriteTimes().joinUninterruptibly(); + assertTrue(tsuids.isEmpty()); + } + + @Test (expected = NoSuchUniqueName.class) + public void getLastWriteTimesNSUNMetric() throws Exception { + query = new TSUIDQuery(tsdb, NSUN_METRIC, tags); + query.getLastWriteTimes().joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void getLastWriteTimesNSUNTagk() throws Exception { + tags.clear(); + tags.put("dc", TAGV_STRING); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastWriteTimes().joinUninterruptibly(); + } + + @Test (expected = DeferredGroupException.class) + public void getLastWriteTimesNSUNTagv() throws Exception { + tags.put(TAGK_STRING, "web03"); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastWriteTimes().joinUninterruptibly(); + } + + @Test + public void getTSMetasSingle() throws Exception { + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); + assertEquals(1, tsmetas.size()); + assertEquals(METRIC_STRING, tsmetas.get(0).getMetric().getName()); + assertEquals(TAGK_STRING, tsmetas.get(0).getTags().get(0).getName()); + assertEquals(TAGV_STRING, tsmetas.get(0).getTags().get(1).getName()); + } + + @Test + public void getTSMetasSingleSetQuery() throws Exception { + query = new TSUIDQuery(tsdb); + query.setQuery(METRIC_STRING, tags); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); + assertEquals(1, tsmetas.size()); + assertEquals(METRIC_STRING, tsmetas.get(0).getMetric().getName()); + assertEquals(TAGK_STRING, tsmetas.get(0).getTags().get(0).getName()); + assertEquals(TAGV_STRING, tsmetas.get(0).getTags().get(1).getName()); + } + + @Test + public void getTSMetasMultipleResults() throws Exception { + tags.clear(); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final List tsmetas = query.getTSMetas().joinUninterruptibly(); + assertEquals(2, tsmetas.size()); + assertEquals(METRIC_STRING, tsmetas.get(0).getMetric().getName()); + assertEquals(TAGK_STRING, tsmetas.get(0).getTags().get(0).getName()); + assertEquals(TAGV_STRING, tsmetas.get(0).getTags().get(1).getName()); + assertEquals(METRIC_STRING, tsmetas.get(1).getMetric().getName()); + assertEquals(TAGK_STRING, tsmetas.get(1).getTags().get(0).getName()); + assertEquals(TAGV_B_STRING, tsmetas.get(1).getTags().get(1).getName()); + } + + @Test + public void getTSMetasMultipleTags() throws Exception { + tags.put(TAGK_B_STRING, TAGV_B_STRING); + query = new TSUIDQuery(tsdb, METRIC_B_STRING, tags); + + final List tsmetas = query.getTSMetas().joinUninterruptibly(); + assertEquals(1, tsmetas.size()); + assertEquals(METRIC_B_STRING, tsmetas.get(0).getMetric().getName()); + assertEquals(TAGK_STRING, tsmetas.get(0).getTags().get(0).getName()); + assertEquals(TAGV_STRING, tsmetas.get(0).getTags().get(1).getName()); + assertEquals(TAGK_B_STRING, tsmetas.get(0).getTags().get(2).getName()); + assertEquals(TAGV_B_STRING, tsmetas.get(0).getTags().get(3).getName()); + } + + @Test (expected = DeferredGroupException.class) + public void getTSMetasNSUITagk() throws Exception { + tags.put(NSUN_TAGK, TAGV_B_STRING); + query = new TSUIDQuery(tsdb, METRIC_B_STRING, tags); + query.getTSMetas().joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void getTSMetasNullMetric() throws Exception { + query = new TSUIDQuery(tsdb); + query.getTSMetas().joinUninterruptibly(); + } + + @Test + public void tsuidFromMetric() throws Exception { + byte[] tsuid = TSUIDQuery.tsuidFromMetric(tsdb, METRIC_STRING, tags).join(); + assertArrayEquals(TSUID, tsuid); + } + + @Test + public void tsuidFromMetricTwoTags() throws Exception { + tags.put(TAGK_B_STRING, TAGV_B_STRING); + byte[] tsuid = TSUIDQuery.tsuidFromMetric(tsdb, METRIC_STRING, tags).join(); + assertArrayEquals( + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, tsuid); + } + + @Test (expected = NoSuchUniqueName.class) + public void tsuidFromMetricNSUNMetric() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, NSUN_METRIC, tags).join(); + } + + @Test (expected = DeferredGroupException.class) + public void tsuidFromMetricNSUNTagk() throws Exception { + tags.clear(); + tags.put("dc", TAGV_STRING); + TSUIDQuery.tsuidFromMetric(tsdb, METRIC_STRING, tags).join(); + } + + @Test (expected = DeferredGroupException.class) + public void tsuidFromMetricNSUNTagv() throws Exception { + tags.put(TAGK_STRING, "web03"); + TSUIDQuery.tsuidFromMetric(tsdb, METRIC_STRING, tags).join(); + } + + @Test (expected = NullPointerException.class) + public void tsuidFromMetricNullTSDB() throws Exception { + TSUIDQuery.tsuidFromMetric(null, METRIC_STRING, tags).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricNullMetric() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, null, tags).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricEmptyMetric() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, "", tags).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricNullTags() throws Exception { + TSUIDQuery.tsuidFromMetric(tsdb, METRIC_STRING, null).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void tsuidFromMetricEmptyTags() throws Exception { + tags.clear(); + TSUIDQuery.tsuidFromMetric(tsdb, METRIC_STRING, tags).join(); + } + + @Test + public void getLastPointMetricZeroBackscanOnePoint() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricZeroBackscanMostRecent() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + tsdb.addPoint(METRIC_STRING, 1356998401L, 24, tags); + tsdb.addPoint(METRIC_STRING, 1356998402L, 1, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998402000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("1", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricZeroBackscanOutOfRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + assertNull(query.getLastPoint(false, 0).join()); + } + + @Test + public void getLastPointMetricOneBackscanInRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final IncomingDataPoint dp = query.getLastPoint(false, 1).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricOneBackscanOutOfRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357010600000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + assertNull(query.getLastPoint(false, 1).join()); + } + + @Test + public void getLastPointMetricManyBackscanInRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final IncomingDataPoint dp = query.getLastPoint(false, 1024).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointMetricManyBackscanOutOfRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + assertNull(query.getLastPoint(false, 1022).join()); + } + + @Test (expected = IllegalArgumentException.class) + public void getLastPointMetricNegativeBackscan() throws Exception { + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastPoint(false, -1).join(); + } + + @Test + public void getLastPointMetricResolve() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + final IncomingDataPoint dp = query.getLastPoint(true, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertEquals(METRIC_STRING, dp.getMetric()); + assertSame(tags, dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test (expected = NoSuchUniqueName.class) + public void getLastPointMetricNSUNMetric() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(NSUN_METRIC, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastPoint(false, 0).join(); + } + + @Test (expected = DeferredGroupException.class) + public void getLastPointMetricNSUNTagk() throws Exception { + tags.clear(); + tags.put(NSUN_TAGK, TAGV_STRING); + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastPoint(false, 0).join(); + } + + @Test (expected = DeferredGroupException.class) + public void getLastPointMetricNSUNTagv() throws Exception { + tags.put(TAGK_STRING, NSUN_TAGV); + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastPoint(false, 0).join(); + } + + @Test (expected = IllegalArgumentException.class) + public void getLastPointMetricEmptyTags() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tags.clear(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, METRIC_STRING, tags); + query.getLastPoint(false, 0).join(); + } + + @Test + public void getLastPointTSUIDZeroBackscanRecent() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDZeroBackscanRecentOutOfRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 0).join()); + } + + @Test + public void getLastPointTSUIDOneBackscanInRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357002000000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 1).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDOneBackscanRecentOutOfRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357010600000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 1).join()); + } + + @Test + public void getLastPointTSUIDManyBackscanInRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 1024).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDManyBackscanRecentOutOfRange() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1360681200000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 1022).join()); + } + + // While these NSUI shouldn't happen, it's possible if someone deletes a metric + // or tag but not the actual data. + @Test + public void getLastPointTSUIDMetricNSUINotResolved() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000350E22700000001000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); + } + + @Test (expected = NoSuchUniqueId.class) + public void getLastPointTSUIDMetricNSUI() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000350E22700000001000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + query.getLastPoint(true, 0).join(); + } + + @Test + public void getLastPointTSUIDTagkNSUINotResolved() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000003000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 3, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); + } + + @Test (expected = NoSuchUniqueId.class) + public void getLastPointTSUIDTagkNSUI() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000004000001"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 4, 0, 0, 1 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + query.getLastPoint(true, 0).join(); + } + + @Test + public void getLastPointTSUIDTagvNSUINotResolved() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000001000003"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 3 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1356998400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); + } + + @Test (expected = NoSuchUniqueId.class) + public void getLastPoitTSUIDTagvNSUI() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + storage.flushStorage(); + storage.addColumn(MockBase.stringToBytes("00000150E22700000001000003"), + QUAL, VAL); + final byte[] tsuid = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 3 }; + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, tsuid); + query.getLastPoint(true, 0).join(); + } + + @Test + public void getLastPointTSUIDMeta() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); + Whitebox.setInternalState(config, "enable_realtime_ts", false); + tsdb.addPoint(METRIC_STRING, 1388534400L, 42, tags); + Whitebox.setInternalState(config, "enable_tsuid_incrementing", true); + Whitebox.setInternalState(config, "enable_realtime_ts", true); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, TSUID); + final IncomingDataPoint dp = query.getLastPoint(false, 0).join(); + assertEquals(1388534400000L, dp.getTimestamp()); + assertNull(dp.getMetric()); + assertNull(dp.getTags()); + assertEquals("42", dp.getValue()); + assertEquals(UniqueId.uidToString(TSUID), dp.getTSUID()); + } + + @Test + public void getLastPointTSUIDMetaNoPoint() throws Exception { + Whitebox.setInternalState(config, "enable_tsuid_incrementing", true); + Whitebox.setInternalState(config, "enable_realtime_ts", true); + + PowerMockito.mockStatic(DateTime.class); + PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); + query = new TSUIDQuery(tsdb, TSUID); + assertNull(query.getLastPoint(false, 0).join()); + } + + /** + * Public for sharing with other UT classes + * @param tsdb The mock TSDB client + * @throws Exception If something went pear shaped + */ + public static void setupStorage(final TSDB tsdb, final MockBase storage) + throws Exception { storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), METRIC_STRING.getBytes(MockBase.ASCII())); @@ -65,10 +768,10 @@ public void beforeLocal() throws Exception { .getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), - "sys.cpu.nice".getBytes(MockBase.ASCII())); + METRIC_B_STRING.getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000002\",\"type\":\"METRIC\",\"name\":\"sys.cpu.nice\"," + + ("{\"uid\":\"000002\",\"type\":\"METRIC\",\"name\":\"sys.cpu.system\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"System CPU\"}") .getBytes(MockBase.ASCII())); @@ -84,10 +787,10 @@ public void beforeLocal() throws Exception { .getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), - "datacenter".getBytes(MockBase.ASCII())); + TAGK_B_STRING.getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000002\",\"type\":\"TAGK\",\"name\":\"datacenter\"," + + ("{\"uid\":\"000002\",\"type\":\"TAGK\",\"name\":\"owner\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Datecenter name\"}") .getBytes(MockBase.ASCII())); @@ -103,31 +806,22 @@ public void beforeLocal() throws Exception { .getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), - "web02".getBytes(MockBase.ASCII())); + TAGV_B_STRING.getBytes(MockBase.ASCII())); storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"TAGV\",\"name\":\"web02\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Web server 2\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, NAME_FAMILY, - "tagv".getBytes(MockBase.ASCII()), - "dc01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, NAME_FAMILY, - "tagv_meta".getBytes(MockBase.ASCII()), - ("{\"uid\":\"000003\",\"type\":\"TAGV\",\"name\":\"dc01\"," + - "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + - "1328140801,\"displayName\":\"Web server 2\"}") - .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(TSUID, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, @@ -140,94 +834,15 @@ public void beforeLocal() throws Exception { storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 2, 0, 0, 3, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), - ("{\"tsuid\":\"000002000002000003000001000001\",\"" + + ("{\"tsuid\":\"000002000001000001000003000002\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 2, 0, 0, 3, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), - Bytes.fromLong(1L)); - } - - @Test - public void setQuery() throws Exception { - query = new TSUIDQuery(tsdb); - final HashMap tags = new HashMap(1); - tags.put(TAGK_STRING, TAGV_STRING); - query.setQuery(METRIC_STRING, tags); - } - - @Test - public void setQueryEmtpyTags() throws Exception { - query = new TSUIDQuery(tsdb); - query.setQuery(METRIC_STRING, new HashMap(0)); - } - - @Test (expected = NoSuchUniqueName.class) - public void setQueryNSUMetric() throws Exception { - query = new TSUIDQuery(tsdb); - query.setQuery(NSUN_METRIC, new HashMap(0)); - } - - @Test (expected = NoSuchUniqueName.class) - public void setQueryNSUTagk() throws Exception { - query = new TSUIDQuery(tsdb); - final HashMap tags = new HashMap(1); - tags.put(NSUN_TAGK, TAGV_STRING); - query.setQuery(METRIC_STRING, tags); - } - - @Test (expected = NoSuchUniqueName.class) - public void setQueryNSUTagv() throws Exception { - query = new TSUIDQuery(tsdb); - final HashMap tags = new HashMap(1); - tags.put(TAGK_STRING, "web03"); - query.setQuery(METRIC_STRING, tags); - } - - @Test (expected = IllegalArgumentException.class) - public void getLastWriteTimesQueryNotSet() throws Exception { - query = new TSUIDQuery(tsdb); - query.getLastWriteTimes().joinUninterruptibly(); - } - - @Test - public void getTSMetasSingle() throws Exception { - query = new TSUIDQuery(tsdb); - HashMap tags = new HashMap(); - tags.put(TAGK_STRING, TAGV_STRING); - query.setQuery(METRIC_STRING, tags); - List tsmetas = query.getTSMetas().joinUninterruptibly(); - assertEquals(1, tsmetas.size()); - } - - @Test - public void getTSMetasMulti() throws Exception { - query = new TSUIDQuery(tsdb); - HashMap tags = new HashMap(); - query.setQuery(METRIC_STRING, tags); - List tsmetas = query.getTSMetas().joinUninterruptibly(); - assertEquals(2, tsmetas.size()); - } - - @Test - public void getTSMetasMultipleTags() throws Exception { - query = new TSUIDQuery(tsdb); - HashMap tags = new HashMap(); - query.setQuery(METRIC_STRING, tags); - tags.put(TAGK_STRING, TAGV_STRING); - tags.put(TAGK_B_STRING, TAGV_B_STRING); - List tsmetas = query.getTSMetas().joinUninterruptibly(); - assertEquals(2, tsmetas.size()); - } - - @Test (expected = IllegalArgumentException.class) - public void getTSMetasNullMetric() throws Exception { - query = new TSUIDQuery(tsdb); - query.getTSMetas().joinUninterruptibly(); + Bytes.fromLong(1L)); } - } diff --git a/test/tsd/TestQueryRpcLastDataPoint.java b/test/tsd/TestQueryRpcLastDataPoint.java new file mode 100644 index 0000000000..1494d95806 --- /dev/null +++ b/test/tsd/TestQueryRpcLastDataPoint.java @@ -0,0 +1,961 @@ +// 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.assertTrue; +import static org.junit.Assert.fail; + +import java.nio.charset.Charset; + +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 com.stumbleupon.async.Deferred; + +@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 TestQueryRpcLastDataPoint 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); + 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 From 67822d01a9cfe0099117269fc5063c8b8d611ffe Mon Sep 17 00:00:00 2001 From: Slawek Ligus Date: Mon, 18 May 2015 21:48:06 -0700 Subject: [PATCH 202/826] Relax the pgrep regex to correctly find and kill the java process. --- build-aux/rpm/init.d/opentsdb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/rpm/init.d/opentsdb b/build-aux/rpm/init.d/opentsdb index d721d483ba..5f4ee1d8d5 100644 --- a/build-aux/rpm/init.d/opentsdb +++ b/build-aux/rpm/init.d/opentsdb @@ -139,7 +139,7 @@ rh_status_q() { } findproc() { - pgrep -f "^java .* net.opentsdb.tools.TSDMain .*${NAME}" + pgrep -f "java .* net.opentsdb.tools.TSDMain .*${NAME}" } case "$1" in From c687dd9abb96bd74b3aafd23914191deaef2a64b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Aug 2015 12:39:18 -0700 Subject: [PATCH 203/826] Disable the not tag key filter until we find a better hbase filter or add some more validation logic on our end Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 15 ++------------- src/query/filter/TagVFilter.java | 15 ++------------- src/query/filter/TagVNotKeyFilter.java | 4 ++-- test/query/filter/TestTagVNotKeyFilter.java | 2 +- 4 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index e1aa36f46b..621c43aee7 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -412,7 +412,6 @@ private void findGroupBys() { final ByteMap literals = new ByteMap(); final List literal_filters = new ArrayList(); TagVFilter current = null; - boolean not_key = false; do { // yeah, I'm breakin out the do!!! current = current_iterator.next(); if (tagk == null) { @@ -429,9 +428,6 @@ private void findGroupBys() { } literal_filters.add(current); } - if (current.isNotKeyFilter()) { - not_key = true; - } if (next != null && Bytes.memcmp(tagk, next.getTagkBytes()) != 0) { break; @@ -440,21 +436,14 @@ private void findGroupBys() { } while (current_iterator.hasNext() && Bytes.memcmp(tagk, current.getTagkBytes()) == 0); - if (gbs > 0 && !not_key) { + if (gbs > 0) { if (group_bys == null) { group_bys = new ArrayList(); } group_bys.add(current.getTagkBytes()); } - if (not_key) { - // special value to notify the row key regex builder that we don't want - // rows with this tagk - row_key_literals.put(current.getTagkBytes(), new byte[0][]); - } else if (literals.size() > 0) { - // TODO - a good optimization would be to remove the filter from the - // list passed to the scanner since we'll have it in the regex. However - // that would then "OR" the literal filters + if (literals.size() > 0) { if (literals.size() + row_key_literals_count > tsdb.getConfig().getInt("tsd.query.filter.expansion_limit")) { LOG.debug("Skipping literals for " + current.getTagk() + diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java index 2797f229fd..0c5e797b22 100644 --- a/src/query/filter/TagVFilter.java +++ b/src/query/filter/TagVFilter.java @@ -97,9 +97,11 @@ public abstract class TagVFilter implements Comparable { tagv_filter_map.put(TagVWildcardFilter.TagVIWildcardFilter.FILTER_NAME, new Pair, Constructor>(TagVWildcardFilter.TagVIWildcardFilter.class, TagVWildcardFilter.TagVIWildcardFilter.class.getDeclaredConstructor(String.class, String.class))); + /* TODO - this requires either a better HBase filter or more logic on our side tagv_filter_map.put(TagVNotKeyFilter.FILTER_NAME, new Pair, Constructor>(TagVNotKeyFilter.class, TagVNotKeyFilter.class.getDeclaredConstructor(String.class, String.class))); + */ } catch (SecurityException e) { throw new RuntimeException("Failed to load a tag value filter", e); } catch (NoSuchMethodException e) { @@ -123,11 +125,6 @@ public abstract class TagVFilter implements Comparable { @JsonProperty protected boolean group_by; - /** Flag the implementation can set to tell the scanner to pick up rows that - * DON'T have the given tagk (regardless of value) - */ - protected boolean not_key; - /** A flag to indicate whether or not we need to execute a post-scan lookup */ protected boolean post_scan = true; @@ -529,9 +526,6 @@ public String getName() { /** @return Whether or not this filter should be executed against scan results */ public boolean postScan() { - if (not_key) { - return false; - } return post_scan; } @@ -541,11 +535,6 @@ public void setPostScan(final boolean post_scan) { this.post_scan = post_scan; } - @JsonIgnore - public boolean isNotKeyFilter() { - return not_key; - } - @Override public int compareTo(final TagVFilter filter) { return Bytes.memcmpMaybeNull(tagk_bytes, filter.tagk_bytes); diff --git a/src/query/filter/TagVNotKeyFilter.java b/src/query/filter/TagVNotKeyFilter.java index 9166bc64c0..fa2980082b 100644 --- a/src/query/filter/TagVNotKeyFilter.java +++ b/src/query/filter/TagVNotKeyFilter.java @@ -15,9 +15,9 @@ public TagVNotKeyFilter(final String tagk, final String filter) { throw new IllegalArgumentException("The filter must be empty for the " + FILTER_NAME + " filter"); } - not_key = true; + post_scan = true; } - + @Override public Deferred match(Map tags) { if (tags.containsKey(tagk)) { diff --git a/test/query/filter/TestTagVNotKeyFilter.java b/test/query/filter/TestTagVNotKeyFilter.java index ea9631bde8..75d34670cd 100644 --- a/test/query/filter/TestTagVNotKeyFilter.java +++ b/test/query/filter/TestTagVNotKeyFilter.java @@ -37,7 +37,7 @@ public void matchDoesNotHaveKey() throws Exception { @Test public void ctorNullFilter() throws Exception { TagVFilter filter = new TagVNotKeyFilter(TAGK, null); - assertTrue(filter.isNotKeyFilter()); + assertTrue(filter.postScan()); } @Test (expected = IllegalArgumentException.class) From c53ad3fb7ed24c32902f9fcc507f45c1593e7734 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 30 Aug 2015 13:07:17 -0700 Subject: [PATCH 204/826] Add a config to determine whether or not we allow duplicate, simultaneous queries from the same endpoint. Disabled by default. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 4 ++++ src/stats/QueryStats.java | 36 ++++++++++++++++++++++++++++-------- src/utils/Config.java | 1 + 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 8b2f451e5d..7e2138cbd1 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -53,6 +53,7 @@ import net.opentsdb.search.SearchPlugin; import net.opentsdb.search.SearchQuery; import net.opentsdb.stats.Histogram; +import net.opentsdb.stats.QueryStats; import net.opentsdb.stats.StatsCollector; /** @@ -169,6 +170,9 @@ public TSDB(final HBaseClient client, final Config config) { tag_values.setTSDB(this); } + QueryStats.setEnableDuplicates( + config.getBoolean("tsd.query.allow_simultaneous_duplicates")); + if (config.getBoolean("tsd.core.preload_uid_cache")) { final ByteMap uid_cache_map = new ByteMap(); uid_cache_map.put(METRICS_QUAL.getBytes(CHARSET), metrics); diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 42b4045afd..7f1892f86f 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -52,6 +52,10 @@ public class QueryStats { /** Determines how many query stats to keep in the cache */ private static int COMPLETED_QUERY_CACHE_SIZE = 256; + /** Whether or not to allow duplicate queries from the same endpoint to + * run simultaneously. */ + private static boolean ENABLE_DUPLICATES = false; + /** Stores queries currently executing. If a thread doesn't call into * markComplete then it's possible for this map to fill up. * Hash is the remote + query */ @@ -118,15 +122,25 @@ public QueryStats(final String remote_address, final TSQuery query) { this.query = query; executed = 1; query_start = DateTime.currentTimeMillis(); - LOG.debug("New query for remote " + remote_address + " with hash " + - hashCode() + " on thread " + Thread.currentThread().getId()); + if (LOG.isDebugEnabled()) { + LOG.debug("New query for remote " + remote_address + " with hash " + + hashCode() + " on thread " + Thread.currentThread().getId()); + } if (running_queries.putIfAbsent(this.hashCode(), this) != null) { - throw new QueryException("Query is already executing for endpoint: " + + if (ENABLE_DUPLICATES) { + LOG.warn("Query " + query + " is already executing for endpoint: " + remote_address); + } else { + throw new QueryException("Query is already executing for endpoint: " + + remote_address); + } + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Successfully put new query for remote " + remote_address + + " with hash " + hashCode() + " on thread " + + Thread.currentThread().getId() + " w q " + query.toString()); + } } - LOG.debug("Successfully put new query for remote " + remote_address + - " with hash " + hashCode() + " on thread " + - Thread.currentThread().getId() + " w q " + query.toString()); } /** @@ -190,8 +204,9 @@ public void markComplete(final HttpResponseStatus response, time_total = DateTime.currentTimeMillis() - query_start; synchronized (running_queries) { if (!running_queries.containsKey(this.hashCode())) { - //throw new IllegalDataException("Query was already marked as complete"); - LOG.error("Query was already marked as complete: " + this); + if (!ENABLE_DUPLICATES) { + LOG.warn("Query was already marked as complete: " + this); + } return; } running_queries.remove(this.hashCode()); @@ -360,4 +375,9 @@ public HttpResponseStatus getStatus() { public Throwable getException() { return exception; } + + /** @param whether or not to allow duplicate queries to run */ + public static void setEnableDuplicates(final boolean enable_dupes) { + ENABLE_DUPLICATES = enable_dupes; + } } diff --git a/src/utils/Config.java b/src/utils/Config.java index df7297c65d..2aecc71d0f 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -492,6 +492,7 @@ protected void setDefaults() { default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); + default_map.put("tsd.query.allow_simultaneous_duplicates", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); From f7c6f2eb2e382727691f3bffb0d4d2cb06f537d7 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 30 Aug 2015 14:42:15 -0700 Subject: [PATCH 205/826] Allow overriding the UID widths and salt settings via the config file. We may live to regret this but users were asking for it so that they wouldn't have to recompile or worry about code changes. Signed-off-by: Chris Larsen --- src/core/Const.java | 43 ++++++++++++++++++++++++++++++---- src/core/TSDB.java | 25 +++++++++++++++++--- test/core/TestTSDB.java | 52 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/core/Const.java b/src/core/Const.java index b487e6ea87..7aa503a744 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -88,22 +88,55 @@ public final class Const { * queries as the salt calculation will differ. Scanning queries will be OK * though. */ - private static final int SALT_BUCKETS = 20; + private static int SALT_BUCKETS = 20; public static int SALT_BUCKETS() { return SALT_BUCKETS; } + /** + * -------------- WARNING ---------------- + * Package private method to override the bucket size. + * ONLY change this value in your configs if you are starting out with a brand + * new install or set of tables. Users wanted this, lets hope they don't + * regret it. + * @param buckets The number of buckets to use. + * @throws IllegalArgumentException if the bucket size is less than 1. You + * *could* have one bucket if you plan to change it later, but *shrug* + */ + static void setSaltBuckets(final int buckets) { + if (buckets < 1) { + throw new IllegalArgumentException("Salt buckets must be greater than 0"); + } + SALT_BUCKETS = buckets; + } + /** * Width of the salt in bytes. - * Its width should be proportional to MAX_SALT data type. + * Its width should be proportional to SALT_BUCKETS data type. * When set to 0, salting is disabled. - * if SALT_WIDTH = 1, the MAX_SALT should be byte - * if SALT_WIDTH = 2, the MAX_SALT can be byte or short + * if SALT_WIDTH = 1, the SALT_BUCKETS should be byte + * if SALT_WIDTH = 2, the SALT_BUCKETS can be byte or short * WARNING: Do NOT change this after you start writing data or you will not * be able to query for anything. */ - private static final int SALT_WIDTH = 0; + private static int SALT_WIDTH = 0; public static int SALT_WIDTH() { return SALT_WIDTH; } + + /** + * -------------- WARNING ---------------- + * Package private method to override the salt byte width. + * ONLY change this value in your configs if you are starting out with a brand + * new install or set of tables. Users wanted this, lets hope they don't + * regret it. + * @param buckets The number of bytes of salt to use + * @throws IllegalArgumentException if width < 0 or > 8 + */ + static void setSaltWidth(final int width) { + if (width < 0 || width > 8) { + throw new IllegalArgumentException("Salt width must be between 0 and 8"); + } + SALT_WIDTH = width; + } } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 7e2138cbd1..1c1dba2ae9 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -70,11 +70,11 @@ public final class TSDB { /** Charset used to convert Strings to byte arrays and back. */ private static final Charset CHARSET = Charset.forName("ISO-8859-1"); private static final String METRICS_QUAL = "metrics"; - private static final short METRICS_WIDTH = 3; + private static short METRICS_WIDTH = 3; private static final String TAG_NAME_QUAL = "tagk"; - private static final short TAG_NAME_WIDTH = 3; + private static short TAG_NAME_WIDTH = 3; private static final String TAG_VALUE_QUAL = "tagv"; - private static final short TAG_VALUE_WIDTH = 3; + private static short TAG_VALUE_WIDTH = 3; /** Client for the HBase cluster to use. */ final HBaseClient client; @@ -144,6 +144,25 @@ public TSDB(final HBaseClient client, final Config config) { this.client = client; } + // SALT AND UID WIDTHS + // Users really wanted this to be set via config instead of having to + // compile. Hopefully they know NOT to change these after writing data. + if (config.hasProperty("tsd.storage.uid.width.metric")) { + METRICS_WIDTH = config.getShort("tsd.storage.uid.width.metric"); + } + if (config.hasProperty("tsd.storage.uid.width.tagk")) { + TAG_NAME_WIDTH = config.getShort("tsd.storage.uid.width.tagk"); + } + if (config.hasProperty("tsd.storage.uid.width.tagv")) { + TAG_VALUE_WIDTH = config.getShort("tsd.storage.uid.width.tagv"); + } + if (config.hasProperty("tsd.storage.salt.buckets")) { + Const.setSaltBuckets(config.getInt("tsd.storage.salt.buckets")); + } + if (config.hasProperty("tsd.storage.salt.width")) { + Const.setSaltWidth(config.getInt("tsd.storage.salt.width")); + } + table = config.getString("tsd.storage.hbase.data_table").getBytes(CHARSET); uidtable = config.getString("tsd.storage.hbase.uid_table").getBytes(CHARSET); treetable = config.getString("tsd.storage.hbase.tree_table").getBytes(CHARSET); diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 036a456dcb..ec8f3a738c 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -60,6 +60,58 @@ public void beforeLocal() throws Exception { config.setFixDuplicates(true); // TODO(jat): test both ways } + @Test + public void ctorNullClient() throws Exception { + assertNotNull(new TSDB(null, config)); + } + + @Test (expected = NullPointerException.class) + public void ctorNullConfig() throws Exception { + new TSDB(client, null); + } + + @Test + public void ctorOverrideUIDWidths() throws Exception { + // assert defaults + assertEquals(3, TSDB.metrics_width()); + assertEquals(3, TSDB.tagk_width()); + assertEquals(3, TSDB.tagv_width()); + + config.overrideConfig("tsd.storage.uid.width.metric", "1"); + config.overrideConfig("tsd.storage.uid.width.tagk", "4"); + config.overrideConfig("tsd.storage.uid.width.tagv", "5"); + final TSDB tsdb = new TSDB(client, config); + assertEquals(1, TSDB.metrics_width()); + assertEquals(4, TSDB.tagk_width()); + assertEquals(5, TSDB.tagv_width()); + assertEquals(1, tsdb.metrics.width()); + assertEquals(4, tsdb.tag_names.width()); + assertEquals(5, tsdb.tag_values.width()); + + // IMPORTANT Restore + config.overrideConfig("tsd.storage.uid.width.metric", "3"); + config.overrideConfig("tsd.storage.uid.width.tagk", "3"); + config.overrideConfig("tsd.storage.uid.width.tagv", "3"); + new TSDB(client, config); + } + + @Test + public void ctorOverrideSalt() throws Exception { + assertEquals(20, Const.SALT_BUCKETS()); + assertEquals(0, Const.SALT_WIDTH()); + + config.overrideConfig("tsd.storage.salt.buckets", "15"); + config.overrideConfig("tsd.storage.salt.width", "2"); + new TSDB(client, config); + assertEquals(15, Const.SALT_BUCKETS()); + assertEquals(2, Const.SALT_WIDTH()); + + // IMPORTANT Restore + config.overrideConfig("tsd.storage.salt.buckets", "20"); + config.overrideConfig("tsd.storage.salt.width", "0"); + new TSDB(client, config); + } + @Test public void initializePluginsDefaults() { // no configured plugin path, plugins disabled, no exceptions From d589d52616df2f8926e16b1e188e4ed16d506d3e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 2 Sep 2015 15:00:14 -0700 Subject: [PATCH 206/826] Add a Threads class for thread naming and timer creation. Also name all threads created in TSDMain Signed-off-by: Chris Larsen --- src/tools/TSDMain.java | 18 ++++++-- src/utils/Threads.java | 97 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/utils/Threads.java diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index f467e24e8f..a836b3a565 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -16,14 +16,19 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.socket.ServerSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioServerBossPool; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioWorkerPool; import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; +import org.jboss.netty.util.ThreadNameDeterminer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import net.opentsdb.tools.BuildData; import net.opentsdb.core.TSDB; import net.opentsdb.core.Const; @@ -31,6 +36,7 @@ import net.opentsdb.tsd.RpcManager; import net.opentsdb.utils.Config; import net.opentsdb.utils.FileSystem; +import net.opentsdb.utils.Threads; import net.opentsdb.graph.Plot; /** * Main class of the TSD, the Time Series Daemon. @@ -130,12 +136,16 @@ public static void main(String[] args) throws IOException { usage(argp, "Invalid worker thread count", 1); } } - factory = new NioServerSocketChannelFactory( - Executors.newCachedThreadPool(), Executors.newCachedThreadPool(), - workers); + final Executor executor = Executors.newCachedThreadPool(); + final NioServerBossPool boss_pool = + new NioServerBossPool(executor, 1, new Threads.BossThreadNamer()); + final NioWorkerPool worker_pool = new NioWorkerPool(executor, + workers, new Threads.WorkerThreadNamer()); + factory = new NioServerSocketChannelFactory(boss_pool, worker_pool); } else { factory = new OioServerSocketChannelFactory( - Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); + Executors.newCachedThreadPool(), Executors.newCachedThreadPool(), + new Threads.PrependThreadNamer()); } try { diff --git a/src/utils/Threads.java b/src/utils/Threads.java new file mode 100644 index 0000000000..a3757c7aca --- /dev/null +++ b/src/utils/Threads.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.utils; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jboss.netty.util.HashedWheelTimer; +import org.jboss.netty.util.ThreadNameDeterminer; + +/** + * Utilities dealing with threads, timers and the like. + */ +public class Threads { + /** Used to count HashedWheelTimers */ + final static AtomicInteger TIMER_ID = new AtomicInteger(); + + /** Helps give useful names to the Netty threads */ + public static class BossThreadNamer implements ThreadNameDeterminer { + final static AtomicInteger tid = new AtomicInteger(); + @Override + public String determineThreadName(String currentThreadName, + String proposedThreadName) throws Exception { + return "OpenTSDB I/O Boss #" + tid.incrementAndGet(); + } + } + + /** Helps give useful names to the Netty threads */ + public static class WorkerThreadNamer implements ThreadNameDeterminer { + final static AtomicInteger tid = new AtomicInteger(); + @Override + public String determineThreadName(String currentThreadName, + String proposedThreadName) throws Exception { + return "OpenTSDB I/O Worker #" + tid.incrementAndGet(); + } + } + + /** Simple prepends "OpenTSDB" to all threads */ + public static class PrependThreadNamer implements ThreadNameDeterminer { + @Override + public String determineThreadName(String currentThreadName, String proposedThreadName) + throws Exception { + return "OpenTSDB " + proposedThreadName; + } + } + + /** + * Returns a new HashedWheelTimer with a name and default ticks + * @param name The name to add to the thread name + * @return A timer + */ + public static HashedWheelTimer newTimer(final String name) { + return newTimer(100, name); + } + + /** + * Returns a new HashedWheelTimer with a name and default ticks + * @param ticks How many ticks per second to sleep between executions, in ms + * @param name The name to add to the thread name + * @return A timer + */ + public static HashedWheelTimer newTimer(final int ticks, final String name) { + return newTimer(ticks, 512, name); + } + + /** + * Returns a new HashedWheelTimer with a name and default ticks + * @param ticks How many ticks per second to sleep between executions, in ms + * @param ticks_per_wheel The size of the wheel + * @param name The name to add to the thread name + * @return A timer + */ + public static HashedWheelTimer newTimer(final int ticks, + final int ticks_per_wheel, final String name) { + class TimerThreadNamer implements ThreadNameDeterminer { + @Override + public String determineThreadName(String currentThreadName, + String proposedThreadName) throws Exception { + return "OpenTSDB Timer " + name + " #" + TIMER_ID.incrementAndGet(); + } + } + return new HashedWheelTimer(Executors.defaultThreadFactory(), + new TimerThreadNamer(), ticks, MILLISECONDS, ticks_per_wheel); + } +} From 16bb18ec47b3025d7967f5278e5737cad8569cfb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 4 Sep 2015 11:49:04 -0700 Subject: [PATCH 207/826] Update AsyncHBase version Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 new file mode 100644 index 0000000000..00f09b9593 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 @@ -0,0 +1 @@ +af3e0778bf5b94f1302bc8a01504680a diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index a173db7fd3..cd2cd0df04 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.0-20150517.200244-1 +ASYNCHBASE_VERSION := 1.7.0-20150904.040751-2 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.0-SNAPSHOT/ From a5d28202500ed9c9fe1f0227a7f60130616f3c6a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 4 Sep 2015 13:08:43 -0700 Subject: [PATCH 208/826] Add missing Threads class to the make file Signed-off-by: Chris Larsen --- Makefile.am | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 514d02b60e..f7307fe915 100644 --- a/Makefile.am +++ b/Makefile.am @@ -147,7 +147,8 @@ tsdb_SRC := \ src/utils/JSON.java \ src/utils/JSONException.java \ src/utils/Pair.java \ - src/utils/PluginLoader.java + src/utils/PluginLoader.java \ + src/utils/Threads.java tsdb_DEPS = \ $(ASYNCHBASE) \ From 8d5d1795defdd2173604c1e9ae0fe5370f69c45b Mon Sep 17 00:00:00 2001 From: Matt Schallert Date: Wed, 26 Aug 2015 13:55:55 -0400 Subject: [PATCH 209/826] [check_tsd] Explicitly check for w/c is None In its current form, if you want to warn between 0 and `x` and go critical after `x` (i.e. `-w 0 -c 10`) the script will set `options.warning` to `x` as `not 0` or `not 0.0` will evaluate to true. This changes it so that `None` is explicitly checked for, thus if you pass `-w 0 -c 10` then `options.warning` will remain `0` and `options.critical` will still be 10 Signed-off-by: Chris Larsen --- tools/check_tsd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index ee022782d7..101b5d5cec 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -98,9 +98,9 @@ def main(argv): options.percent_over /= 100.0 # Convert to range 0-1 - if not options.critical: + if options.critical is None: options.critical = options.warning - elif not options.warning: + elif options.warning is None: options.warning = options.critical # argument construction From f69750a24ae81c386f9f300c85298453e8c6c077 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 4 Sep 2015 15:36:52 -0700 Subject: [PATCH 210/826] Add the tsd.storage.hbase.prefetch_meta flag to allow for prefetching all region information for the tsdb and uid tables. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 28 ++++++++++++++++++++++++++++ src/tools/TSDMain.java | 7 ++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 1c1dba2ae9..1210605c35 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1175,6 +1175,34 @@ public Object call(final Exception e) throws Exception { } } + /** + * Blocks while pre-fetching meta data from the data and uid tables + * so that performance improves, particularly with a large number of + * regions and region servers. + * @since 2.2 + */ + public void preFetchHBaseMeta() { + LOG.info("Pre-fetching meta data for all tables"); + final long start = System.currentTimeMillis(); + final ArrayList> deferreds = new ArrayList>(); + deferreds.add(client.prefetchMeta(table)); + deferreds.add(client.prefetchMeta(uidtable)); + + // TODO(cl) - meta, tree, etc + + try { + Deferred.group(deferreds).join(); + LOG.info("Fetched meta data for tables in " + + (System.currentTimeMillis() - start) + "ms"); + } catch (InterruptedException e) { + LOG.error("Interrupted", e); + Thread.currentThread().interrupt(); + return; + } catch (Exception e) { + LOG.error("Failed to prefetch meta for our tables", e); + } + } + // ------------------ // // Compaction helpers // // ------------------ // diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index a836b3a565..da02826a5f 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.tools; -import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -25,7 +24,6 @@ import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.channel.socket.nio.NioWorkerPool; import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; -import org.jboss.netty.util.ThreadNameDeterminer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +35,7 @@ import net.opentsdb.utils.Config; import net.opentsdb.utils.FileSystem; import net.opentsdb.utils.Threads; -import net.opentsdb.graph.Plot; + /** * Main class of the TSD, the Time Series Daemon. */ @@ -151,6 +149,9 @@ public static void main(String[] args) throws IOException { try { tsdb = new TSDB(config); tsdb.initializePlugins(true); + if (config.getBoolean("tsd.storage.hbase.prefetch_meta")) { + tsdb.preFetchHBaseMeta(); + } // Make sure we don't even start if we can't find our tables. tsdb.checkNecessaryTablesExist().joinUninterruptibly(); From 9342348244a3d2aaefcc325f083fd36ed639af2b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 4 Sep 2015 15:31:48 -0700 Subject: [PATCH 211/826] Add the /api/stats/region_clients endpoint with data per region client connection. Add some overall region client stats to the main stats page. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 4 ++++ src/tsd/HttpJsonSerializer.java | 11 +++++++++++ src/tsd/HttpSerializer.java | 14 +++++++++++++ src/tsd/StatsRpc.java | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 1210605c35..9811867b18 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -548,6 +548,10 @@ public void collectStats(final StatsCollector collector) { collector.record("hbase.nsre", stats.noSuchRegionExceptions()); collector.record("hbase.nsre.rpcs_delayed", stats.numRpcDelayedDueToNSRE()); + collector.record("hbase.region_clients.open", + stats.regionClients()); + collector.record("hbase.region_clients.idle_closed", + stats.idleConnectionsClosed()); compactionq.collectStats(collector); // Collect Stats from Plugins diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index a7d548b4b0..57fcf1b567 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -1031,6 +1031,17 @@ public ChannelBuffer formatThreadStatsV1(final List> stats) return serializeJSON(stats); } + /** + * format a list of region client statistics + * @param stats The list of region client stats to format + * @return A ChannelBuffer object to pass on to the caller + * @throws JSONException if serialization failed + * @since 2.2 + */ + public ChannelBuffer formatRegionStatsV1(final List> stats) { + return serializeJSON(stats); + } + /** * Format a list of JVM statistics * @param stats The JVM stats map to format diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index ee12052aaf..8f16ea9590 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -685,6 +685,20 @@ public ChannelBuffer formatThreadStatsV1(final List> stats) " has not implemented formatThreadStatsV1"); } + /** + * format a list of region client statistics + * @param stats The list of region client stats to format + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.2 + */ + public ChannelBuffer formatRegionStatsV1(final List> stats) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatRegionStatsV1"); + } + /** * Format a list of JVM statistics * @param map The JVM stats list to format diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index 536363108f..0e65d17a47 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -30,6 +30,7 @@ import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.JSON; +import org.hbase.async.RegionClientStats; import org.jboss.netty.channel.Channel; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -92,6 +93,9 @@ public void execute(final TSDB tsdb, final HttpQuery query) { } else if ("query".equals(endpoint)) { printQueryStats(query); return; + } else if ("region_clients".equals(endpoint)) { + printRegionClientStats(tsdb, query); + return; } } catch (IllegalArgumentException e) { // this is thrown if the url doesn't start with /api. To maintain backwards @@ -141,6 +145,37 @@ private void doCollectStats(final TSDB tsdb, final StatsCollector collector, tsdb.collectStats(collector); } + /** + * Display stats for each region client + * @param tsdb The TSDB to use for fetching stats + * @param query The query to respond to + */ + private void printRegionClientStats(final TSDB tsdb, final HttpQuery query) { + final List region_stats = tsdb.getClient().regionStats(); + final List> stats = + new ArrayList>(region_stats.size()); + for (final RegionClientStats rcs : region_stats) { + final Map stat_map = new HashMap(8); + stat_map.put("rpcsSent", rcs.rpcsSent()); + stat_map.put("rpcsInFlight", rcs.inflightRPCs()); + stat_map.put("pendingRPCs", rcs.pendingRPCs()); + stat_map.put("pendingBatchedRPCs", rcs.pendingBatchedRPCs()); + stat_map.put("dead", rcs.isDead()); + stat_map.put("rpcid", rcs.rpcID()); + stat_map.put("endpoint", rcs.remoteEndpoint()); + stat_map.put("rpcsTimedout", rcs.rpcsTimedout()); + stat_map.put("rpcResponsesTimedout", rcs.rpcResponsesTimedout()); + stat_map.put("rpcResponsesUnknown", rcs.rpcResponsesUnknown()); + stat_map.put("inflightBreached", rcs.inflightBreached()); + stat_map.put("pendingBreached", rcs.pendingBreached()); + stat_map.put("writesBlocked", rcs.writesBlocked()); + + stats.add(stat_map); + } + query.sendReply(query.serializer().formatRegionStatsV1(stats)); + } + + /** * Grabs a snapshot of all JVM thread states and formats it in a manner to * be displayed via API. From eacb601c4fa820129b3f9b30b4c8814131b70802 Mon Sep 17 00:00:00 2001 From: Guenther Schmuelling Date: Fri, 4 Sep 2015 16:00:12 -0700 Subject: [PATCH 212/826] Log /api/put calls at debug Signed-off-by: Chris Larsen --- src/tsd/AbstractHttpQuery.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 31eacd90b4..ba6b736124 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -286,6 +286,14 @@ public String getContent() { */ public void done() { final int processing_time = processingTimeMillis(); + final String url = request.getUri(); + final String msg = String.format("HTTP %s done in %d ms", url, processing_time); + if (url.startsWith("/api/put") && LOG.isDebugEnabled()) { + // NOTE: Suppresses too many log lines from /api/put. + LOG.debug(msg); + } else { + logInfo(msg); + } logInfo("HTTP " + request.getUri() + " done in " + processing_time + "ms"); } From 97d395076e58f8760c7910ad6186a9e3f8ffa1cf Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 4 Sep 2015 17:09:11 -0700 Subject: [PATCH 213/826] Add missing default for the prefetch setting Signed-off-by: Chris Larsen --- src/utils/Config.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/Config.java b/src/utils/Config.java index 2aecc71d0f..a140f72df9 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -506,6 +506,7 @@ protected void setDefaults() { 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.enable_compaction", "true"); From 756dbda1a19ac7e0ff03a8534c74ed3bec3d9fa8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 9 Sep 2015 12:50:41 -0700 Subject: [PATCH 214/826] Allow for writes through the HTTP API to be synchronous and include a timeout. Thanks to @waisbrot and @jesse5e for their contributions. This is a merging of the two ideas so that we allow the timeouts but also avoid blocking the threads. Also add a FakeTimer for unit testing, pulled from AsyncHBase. Signed-off-by: Chris Larsen --- src/tsd/PutDataPointRpc.java | 243 ++++++++++++++++++++---- test/core/BaseTsdbTest.java | 62 +++++++ test/tsd/TestPutRpc.java | 349 +++++++++++++++++++++++++++++++++++ 3 files changed, 619 insertions(+), 35 deletions(-) diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 6a539510e9..71cd8ecc5d 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -16,14 +16,19 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.TimeoutException; import org.jboss.netty.channel.Channel; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.util.Timeout; +import org.jboss.netty.util.TimerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,13 +41,16 @@ /** Implements the "put" telnet-style command. */ final class PutDataPointRpc implements TelnetRpc, HttpRpc { private static final Logger LOG = LoggerFactory.getLogger(PutDataPointRpc.class); + private static final ArrayList EMPTY_DEFERREDS = + new ArrayList(0); 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(); private static final AtomicLong writes_blocked = new AtomicLong(); - + private static final AtomicLong writes_timedout = new AtomicLong(); + public Deferred execute(final TSDB tsdb, final Channel chan, final String[] cmd) { requests.incrementAndGet(); @@ -118,27 +126,49 @@ public void execute(final TSDB tsdb, final HttpQuery query) final boolean show_details = query.hasQueryStringParam("details"); final boolean show_summary = query.hasQueryStringParam("summary"); + final boolean synchronous = query.hasQueryStringParam("sync"); + final int sync_timeout = query.hasQueryStringParam("sync_timeout") ? + Integer.parseInt(query.getQueryStringParam("sync_timeout")) : 0; + // this is used to coordinate timeouts + final AtomicBoolean sending_response = new AtomicBoolean(); + sending_response.set(false); + final ArrayList> details = show_details ? new ArrayList>() : null; - long success = 0; - long total = 0; - + int queued = 0; + final List> deferreds = synchronous ? + new ArrayList>(dps.size()) : null; for (final IncomingDataPoint dp : dps) { /** Handles passing a data point to the storage exception handler if * we were unable to store it for any reason */ - final class PutErrback implements Callback { - public Object call(final Exception arg) { + final class PutErrback implements Callback { + public Boolean call(final Exception arg) { handleStorageException(tsdb, dp, arg); hbase_errors.incrementAndGet(); - return null; + + if (show_details) { + details.add(getHttpDetails("Storage exception: " + + arg.getMessage(), dp)); + } + return false; + } + public String toString() { + return "HTTP Put Exception CB"; + } + } + + /** Simply marks the put as successful */ + final class SuccessCB implements Callback { + @Override + public Boolean call(final Object obj) { + return true; } public String toString() { - return "HTTP Put exception"; + return "HTTP Put success CB"; } } - total++; try { if (dp.getMetric() == null || dp.getMetric().isEmpty()) { if (show_details) { @@ -172,16 +202,19 @@ public String toString() { illegal_arguments.incrementAndGet(); continue; } + final Deferred deferred; if (Tags.looksLikeInteger(dp.getValue())) { - tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Tags.parseLong(dp.getValue()), dp.getTags()) - .addErrback(new PutErrback()); + deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), + Tags.parseLong(dp.getValue()), dp.getTags()); } else { - tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Float.parseFloat(dp.getValue()), dp.getTags()) - .addErrback(new PutErrback()); + deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), + Float.parseFloat(dp.getValue()), dp.getTags()); } - success++; + if (synchronous) { + deferreds.add(deferred.addCallback(new SuccessCB())); + } + deferred.addErrback(new PutErrback()); + ++queued; } catch (NumberFormatException x) { if (show_details) { details.add(this.getHttpDetails("Unable to parse value to a number", @@ -204,30 +237,170 @@ public String toString() { } } - 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 put request"); - } else { - query.sendReply(HttpResponseStatus.NO_CONTENT, "".getBytes()); + /** A timer task that will respond to the user with the number of timeouts + * for synchronous writes. */ + class PutTimeout implements TimerTask { + final int queued; + public PutTimeout(final int queued) { + this.queued = queued; } - } else { - final HashMap summary = new HashMap(); - summary.put("success", success); - summary.put("failed", failures); - if (show_details) { - summary.put("errors", details); + @Override + public void run(final Timeout timeout) throws Exception { + if (sending_response.get()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Put data point call " + query + + " already responded successfully"); + } + return; + } else { + sending_response.set(true); + } + + // figure out how many writes are outstanding + int good_writes = 0; + int failed_writes = 0; + int timeouts = 0; + for (int i = 0; i < deferreds.size(); i++) { + try { + if (deferreds.get(i).join(1)) { + ++good_writes; + } else { + ++failed_writes; + } + } catch (TimeoutException te) { + if (show_details) { + details.add(getHttpDetails("Write timedout", dps.get(i))); + } + ++timeouts; + } + } + writes_timedout.addAndGet(timeouts); + final int failures = dps.size() - queued; + if (!show_summary && !show_details) { + throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, + "The put call has timedout with " + good_writes + + " successful writes, " + failed_writes + " failed writes and " + + timeouts + " timed out writes.", + "Please see the TSD logs or append \"details\" to the put request"); + } else { + final HashMap summary = new HashMap(); + summary.put("success", good_writes); + summary.put("failed", failures + failed_writes); + summary.put("timeouts", timeouts); + if (show_details) { + summary.put("errors", details); + } + + query.sendReply(HttpResponseStatus.BAD_REQUEST, + query.serializer().formatPutV1(summary)); + } + } + } + + // now after everything has been sent we can schedule a timeout if so + // the caller asked for a synchronous write. + final Timeout timeout = sync_timeout > 0 ? + tsdb.getTimer().newTimeout(new PutTimeout(queued), sync_timeout, + TimeUnit.MILLISECONDS) : null; + + /** Serializes the response to the client */ + class GroupCB implements Callback> { + final int queued; + public GroupCB(final int queued) { + this.queued = queued; } - if (failures > 0) { - query.sendReply(HttpResponseStatus.BAD_REQUEST, - query.serializer().formatPutV1(summary)); - } else { - query.sendReply(query.serializer().formatPutV1(summary)); + @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)); + } + } + + return null; + } + @Override + public String toString() { + return "put data point serialization callback"; + } + } + + /** Catches any unexpected exceptions thrown in the callback chain */ + class ErrCB implements Callback { + @Override + public Object call(final Exception e) throws Exception { + 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(); + } + } + LOG.error("Unexpected exception", e); + throw new RuntimeException("Unexpected exception", e); + } + @Override + public String toString() { + return "put data point error callback"; } } + + if (synchronous) { + Deferred.groupInOrder(deferreds).addCallback(new GroupCB(queued)) + .addErrback(new ErrCB()); + } else { + new GroupCB(queued).call(EMPTY_DEFERREDS); + } } /** diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 3d60552aa4..d51efd5601 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; import net.opentsdb.meta.Annotation; import net.opentsdb.storage.MockBase; @@ -32,6 +34,8 @@ import org.hbase.async.HBaseClient; import org.hbase.async.Scanner; import org.jboss.netty.util.HashedWheelTimer; +import org.jboss.netty.util.Timeout; +import org.jboss.netty.util.TimerTask; import org.junit.Before; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; @@ -521,4 +525,62 @@ protected void storeAnnotation(final long timestamp) throws Exception { note.syncToStorage(tsdb, false).joinUninterruptibly(); } + /** + * A fake {@link org.jboss.netty.util.Timer} implementation. + * Instead of executing the task it will store that task in a internal state + * and provides a function to start the execution of the stored task. + * This implementation thus allows the flexibility of simulating the + * things that will be going on during the time out period of a TimerTask. + * This was mainly return to simulate the timeout period for + * alreadyNSREdRegion test, where the region will be in the NSREd mode only + * during this timeout period, which was difficult to simulate using the + * above {@link FakeTimer} implementation, as we don't get back the control + * during the timeout period + * + * Here it will hold at most two Tasks. We have two tasks here because when + * one is being executed, it may call for newTimeOut for another task. + */ + public static final class FakeTaskTimer extends HashedWheelTimer { + + public TimerTask newPausedTask = null; + public TimerTask pausedTask = null; + public Timeout timeout = null; + + @Override + public synchronized Timeout newTimeout(final TimerTask task, + final long delay, + final TimeUnit unit) { + if (pausedTask == null) { + pausedTask = task; + } else if (newPausedTask == null) { + newPausedTask = task; + } else { + throw new IllegalStateException("Cannot Pause Two Timer Tasks"); + } + timeout = mock(Timeout.class); + return timeout; + } + + @Override + public Set stop() { + return null; + } + + public boolean continuePausedTask() { + if (pausedTask == null) { + return false; + } + try { + if (newPausedTask != null) { + throw new IllegalStateException("Cannot be in this state"); + } + pausedTask.run(null); // Argument never used in this code base + pausedTask = newPausedTask; + newPausedTask = null; + return true; + } catch (Exception e) { + throw new RuntimeException("Timer task failed: " + pausedTask, e); + } + } + } } \ No newline at end of file diff --git a/test/tsd/TestPutRpc.java b/test/tsd/TestPutRpc.java index 31655d2caf..7dab72e5a7 100644 --- a/test/tsd/TestPutRpc.java +++ b/test/tsd/TestPutRpc.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -30,6 +31,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; import net.opentsdb.uid.NoSuchUniqueName; @@ -68,6 +70,7 @@ public final class TestPutRpc { private AtomicLong unknown_metrics = new AtomicLong(); private AtomicLong writes_blocked = new AtomicLong(); private StorageExceptionHandler handler; + private FakeTaskTimer timer; @Before public void before() throws Exception { @@ -90,8 +93,13 @@ public void before() throws Exception { .thenReturn(Deferred.fromResult(new Object())); when(tsdb.addPoint("sys.cpu.system", 1365465600, 24, TAGS)) .thenReturn(Deferred.fromResult(new Object())); + // errors when(tsdb.addPoint("doesnotexist", 1365465600, 42, TAGS)) .thenThrow(new NoSuchUniqueName("metric", "doesnotexist")); + when(tsdb.addPoint("sys.cpu.system", 1365465600, 1, TAGS)) + .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + when(tsdb.addPoint("sys.cpu.system", 1365465600, 2, TAGS)) + .thenReturn(new Deferred()); requests = Whitebox.getInternalState(PutDataPointRpc.class, "requests"); requests.set(0); @@ -106,8 +114,11 @@ public void before() throws Exception { writes_blocked = Whitebox.getInternalState(PutDataPointRpc.class, "writes_blocked"); writes_blocked.set(0); + timer = new FakeTaskTimer(); + handler = mock(StorageExceptionHandler.class); when(tsdb.getStorageExceptionHandler()).thenReturn(handler); + when(tsdb.getTimer()).thenReturn(timer); } @Test @@ -1105,4 +1116,342 @@ public void emptyTags() throws Exception { verify(tsdb, never()).getStorageExceptionHandler(); } + @Test + public void syncOKNoDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOKSummary() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true&summary", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":0")); + assertTrue(response.contains("\"success\":2")); + assertFalse(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOKSummaryDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/put?sync=true&summary&details", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + + assertTrue(response.contains("\"failed\":0")); + assertTrue(response.contains("\"success\":2")); + assertTrue(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOKDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true&details", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":0")); + assertTrue(response.contains("\"success\":2")); + assertTrue(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOneFailed() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOneFailedSummary() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&summary", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":1")); + assertTrue(response.contains("\"success\":1")); + assertFalse(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOneFailedDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&details", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":1")); + assertTrue(response.contains("\"success\":1")); + assertTrue(response.contains("\"errors\":[{")); + assertTrue(response.contains("Wotcher!")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncTwoFailedDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&details", + "[{\"metric\":\"doesnotexist\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + System.out.println(response); + assertTrue(response.contains("\"success\":0")); + assertTrue(response.contains("\"failed\":2")); + assertTrue(response.contains("\"errors\":[{")); + assertTrue(response.contains("Wotcher!")); + assertTrue(response.contains("Unknown metric")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, never()).getTimer(); + } + + @Test + public void syncOKTimeoutNoDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&sync_timeout=30000", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, times(1)).cancel(); + } + + @Test + public void syncOKTimeoutSummary() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/put?sync&sync_timeout=30000&summary", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":0")); + assertTrue(response.contains("\"success\":2")); + assertTrue(response.contains("\"timeouts\":0")); + assertFalse(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, never()).getStorageExceptionHandler(); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, times(1)).cancel(); + } + + @Test + public void syncTimeoutOneFailed() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&sync_timeout=30000", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, times(1)).cancel(); + } + + @Test + public void syncTimeoutOneFailedSummary() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&summary&sync_timeout=30000", + "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":1")); + assertTrue(response.contains("\"success\":1")); + assertTrue(response.contains("\"timeouts\":0")); + assertFalse(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, times(1)).cancel(); + } + + @Test + public void syncTimeoutTwoFailedDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/put?sync&details&sync_timeout=30000", + "[{\"metric\":\"doesnotexist\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + System.out.println(response); + assertTrue(response.contains("\"success\":0")); + assertTrue(response.contains("\"failed\":2")); + assertTrue(response.contains("\"timeouts\":0")); + assertTrue(response.contains("\"errors\":[{")); + assertTrue(response.contains("Wotcher!")); + assertTrue(response.contains("Unknown metric")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, times(1)).cancel(); + } + + @Test + public void syncTimeoutOneFailedTimedoutSummary() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/put?sync&summary&sync_timeout=30000", + "[{\"metric\":\"sys.cpu.system\",\"timestamp\":1365465600,\"value\"" + + ":2,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, never()).cancel(); + + timer.continuePausedTask(); + + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"failed\":1")); + assertTrue(response.contains("\"success\":0")); + assertTrue(response.contains("\"timeouts\":1")); + assertFalse(response.contains("\"errors\":[]")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(timer.timeout, never()).cancel(); + } + + @Test + public void syncTimeoutOneFailedTimedoutDetails() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/put?sync&details&sync_timeout=30000", + "[{\"metric\":\"sys.cpu.system\",\"timestamp\":1365465600,\"value\"" + + ":2,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"host\":\"web01\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(); + put.execute(tsdb, query); + verify(tsdb, times(1)).getTimer(); + verify(timer.timeout, never()).cancel(); + + timer.continuePausedTask(); + + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + System.out.println(response); + assertTrue(response.contains("\"failed\":1")); + assertTrue(response.contains("\"success\":0")); + assertTrue(response.contains("\"timeouts\":1")); + assertTrue(response.contains("\"errors\":[{")); + assertTrue(response.contains("Write timedout")); + assertTrue(response.contains("Wotcher!")); + assertEquals(1, requests.get()); + assertEquals(0, invalid_values.get()); + verify(tsdb, times(1)).getStorageExceptionHandler(); + verify(timer.timeout, never()).cancel(); + } } From 6b31325130ee0077ae6da2afcbf7c80f7ddc92c8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 4 Sep 2015 18:04:48 -0700 Subject: [PATCH 215/826] Store a timer in the main TSDB class and re-arrange the shutdown code to deal with it and the storage exception handler plugin properly. We'll use it for query timeouts. Pass said timer to the Netty pipeline. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 66 +++++++++++++++++++++++++++++++----- src/tsd/PipelineFactory.java | 23 +++---------- src/utils/Threads.java | 2 ++ 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 9811867b18..9867980423 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -36,6 +37,9 @@ import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; +import org.jboss.netty.util.HashedWheelTimer; +import org.jboss.netty.util.Timeout; +import org.jboss.netty.util.Timer; import net.opentsdb.tree.TreeBuilder; import net.opentsdb.tsd.RTPublisher; @@ -46,6 +50,7 @@ import net.opentsdb.utils.Config; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.PluginLoader; +import net.opentsdb.utils.Threads; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; @@ -98,6 +103,9 @@ public final class TSDB { /** Configuration object for all TSDB components */ final Config config; + /** Timer used for various tasks such as idle timeouts or query timeouts */ + private final HashedWheelTimer timer; + /** * Row keys that need to be compacted. * Whenever we write a new data point to a row, we add the row key to this @@ -177,7 +185,7 @@ public TSDB(final HBaseClient client, final Config config) { tag_names = new UniqueId(this.client, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH); tag_values = new UniqueId(this.client, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH); compactionq = new CompactionQueue(this); - + if (config.hasProperty("tsd.core.timezone")) { DateTime.setDefaultTimezone(config.getString("tsd.core.timezone")); } @@ -189,6 +197,8 @@ public TSDB(final HBaseClient client, final Config config) { tag_values.setTSDB(this); } + timer = Threads.newTimer("TSDB Timer"); + QueryStats.setEnableDuplicates( config.getBoolean("tsd.query.allow_simultaneous_duplicates")); @@ -874,9 +884,44 @@ public Deferred shutdown() { final ArrayList> deferreds = new ArrayList>(); - final class HClientShutdown implements Callback> { - public Object call(final ArrayList args) { - return client.shutdown(); + final class FinalShutdown implements Callback { + @Override + public Object call(Object result) throws Exception { + if (result instanceof Exception) { + LOG.error("A previous shutdown failed", (Exception)result); + } + final Set timeouts = timer.stop(); + // TODO - at some point we should clean these up. + if (timeouts.size() > 0) { + LOG.warn("There were " + timeouts.size() + " timer tasks queued"); + } + LOG.info("Completed shutting down the TSDB"); + return Deferred.fromResult(null); + } + } + + final class SEHShutdown implements Callback { + @Override + public Object call(Object result) throws Exception { + if (result instanceof Exception) { + LOG.error("Shutdown of the HBase client failed", (Exception)result); + } + LOG.info("Shutting down storage exception handler plugin: " + + storage_exception_handler.getClass().getCanonicalName()); + return storage_exception_handler.shutdown().addBoth(new FinalShutdown()); + } + @Override + public String toString() { + return "SEHShutdown"; + } + } + + final class HClientShutdown implements Callback, ArrayList> { + public Deferred call(final ArrayList args) { + if (storage_exception_handler != null) { + return client.shutdown().addBoth(new SEHShutdown()); + } + return client.shutdown().addBoth(new FinalShutdown()); } public String toString() { return "shutdown HBase client"; @@ -896,7 +941,7 @@ public Object call(final Exception e) { } else { LOG.error("Failed to shutdown the TSD", e); } - return client.shutdown(); + return new HClientShutdown().call(null); } public String toString() { return "shutdown HBase client after error"; @@ -931,9 +976,9 @@ public Object call(ArrayList compactions) throws Exception { // wait for plugins to shutdown before we close the client return deferreds.size() > 0 - ? Deferred.group(deferreds).addCallbacks(new HClientShutdown(), - new ShutdownErrback()) - : client.shutdown(); + ? Deferred.group(deferreds).addCallbackDeferring(new HClientShutdown()) + .addErrback(new ShutdownErrback()) + : new HClientShutdown().call(null); } /** @@ -1207,6 +1252,11 @@ public void preFetchHBaseMeta() { } } + /** @return the timer used for various house keeping functions */ + public Timer getTimer() { + return timer; + } + // ------------------ // // Compaction helpers // // ------------------ // diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index b238d1397c..85f66a7fbd 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -49,7 +49,7 @@ public final class PipelineFactory implements ChannelPipelineFactory { // PipelineFactory is needed. private final ConnectionManager connmgr = new ConnectionManager(); private final DetectHttpOrRpc HTTP_OR_RPC = new DetectHttpOrRpc(); - private final Timer timer = new HashedWheelTimer(new PipelineThreadFactory()); + private final Timer timer; private final ChannelHandler timeoutHandler; /** Stateless handler for RPCs. */ @@ -85,7 +85,8 @@ public PipelineFactory(final TSDB tsdb) { public PipelineFactory(final TSDB tsdb, final RpcManager manager) { this.tsdb = tsdb; this.socketTimeout = tsdb.getConfig().getInt("tsd.core.socket.timeout"); - this.timeoutHandler = new IdleStateHandler(this.timer, 0, 0, this.socketTimeout); + timer = tsdb.getTimer(); + this.timeoutHandler = new IdleStateHandler(timer, 0, 0, this.socketTimeout); this.rpchandler = new RpcHandler(tsdb, manager); try { HttpQuery.initializeSerializerMaps(tsdb); @@ -150,22 +151,6 @@ protected Object decode(final ChannelHandlerContext ctx, } } - - /** - * A class to generate a daemon thread for the idle connection timer. - */ - class PipelineThreadFactory implements ThreadFactory { - @Override - public Thread newThread(final Runnable r) { - final Thread t = new Thread(r, "PipelineFactoryTimer"); - t.setDaemon(true); - return t; - } - - @Override - public String toString() { - return "Pipeline timer thread factory"; - } - } + } \ No newline at end of file diff --git a/src/utils/Threads.java b/src/utils/Threads.java index a3757c7aca..93a0e02547 100644 --- a/src/utils/Threads.java +++ b/src/utils/Threads.java @@ -94,4 +94,6 @@ public String determineThreadName(String currentThreadName, return new HashedWheelTimer(Executors.defaultThreadFactory(), new TimerThreadNamer(), ticks, MILLISECONDS, ticks_per_wheel); } + + } From e8c39d8becbd93a21b819ced0023b3884157cdf3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 11 Aug 2015 15:56:41 -0700 Subject: [PATCH 216/826] Modify the MockBase implementation to support multiple tables. And tweak all of the pertinent UTs Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 2 +- src/search/TimeSeriesLookup.java | 3 + test/meta/TestTSMeta.java | 48 +- test/meta/TestTSUIDQuery.java | 53 +- test/meta/TestUIDMeta.java | 11 +- test/search/TestTimeSeriesLookup.java | 10 +- test/storage/MockBase.java | 1051 +++++++++++++++++++------ test/tools/TestUID.java | 411 +++++----- test/tree/TestBranch.java | 69 +- test/tree/TestLeaf.java | 24 +- test/tree/TestTree.java | 93 ++- test/tree/TestTreeBuilder.java | 135 ++-- test/tree/TestTreeRule.java | 24 +- test/tsd/TestAnnotationRpc.java | 1 + test/tsd/TestTreeRpc.java | 132 ++-- test/tsd/TestUniqueIdRpc.java | 95 ++- 16 files changed, 1396 insertions(+), 766 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 9abc89e463..765ac0eb16 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -77,7 +77,7 @@ public final class TSMeta { private static final Charset CHARSET = Charset.forName("ISO-8859-1"); /** The single column family used by this class. */ - private static final byte[] FAMILY = "name".getBytes(CHARSET); + public static final byte[] FAMILY = "name".getBytes(CHARSET); /** The cell qualifier to use for timeseries meta */ private static final byte[] META_QUALIFIER = "ts_meta".getBytes(CHARSET); diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index 6ff3f89f2d..5683f352df 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -23,6 +23,7 @@ import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.meta.TSMeta; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; @@ -200,6 +201,8 @@ public List lookup() { private Scanner getScanner(final StringBuilder tagv_filter) { final Scanner scanner = tsdb.getClient().newScanner( query.useMeta() ? tsdb.metaTable() : tsdb.dataTable()); + scanner.setFamily( + query.useMeta() ? TSMeta.FAMILY : TSDB.FAMILY()); // if a metric is given, we need to resolve it's UID and set the start key // to the UID and the stop key to the next row by incrementing the UID. diff --git a/test/meta/TestTSMeta.java b/test/meta/TestTSMeta.java index 72693bb99a..49c6e2f390 100644 --- a/test/meta/TestTSMeta.java +++ b/test/meta/TestTSMeta.java @@ -23,6 +23,9 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; +import java.util.ArrayList; +import java.util.List; + import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; @@ -56,7 +59,9 @@ GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, Scanner.class, UIDMeta.class, TSMeta.class, AtomicIncrementRequest.class}) public final class TestTSMeta { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] META_TABLE = "tsdb-meta".getBytes(MockBase.ASCII()); + private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); private TSDB tsdb; private Config config; private HBaseClient client = mock(HBaseClient.class); @@ -75,12 +80,15 @@ public void before() throws Exception { tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); + final List families = new ArrayList(); + families.add(TSMeta.FAMILY); + storage.addTable(META_TABLE, families); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.0\"," + @@ -88,11 +96,11 @@ public void before() throws Exception { "1328140801,\"displayName\":\"System CPU\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGK\",\"name\":\"host\"," + @@ -100,11 +108,11 @@ public void before() throws Exception { "1328140801,\"displayName\":\"Host server name\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGV\",\"name\":\"web01\"," + @@ -112,7 +120,7 @@ public void before() throws Exception { "1328140801,\"displayName\":\"Web server 1\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"" + @@ -120,13 +128,13 @@ public void before() throws Exception { "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSMeta.FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, + TSMeta.FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000002\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + @@ -189,7 +197,7 @@ public void getTSMetaDoesNotExist() throws Exception { @Test (expected = NoSuchUniqueId.class) public void getTSMetaNSUMetric() throws Throwable { - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000002000001000001\",\"" + @@ -206,7 +214,7 @@ public void getTSMetaNSUMetric() throws Throwable { @Test (expected = NoSuchUniqueId.class) public void getTSMetaNSUTagk() throws Throwable { - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 2, 0, 0, 1 }, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 2, 0, 0, 1 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000002000001\",\"" + @@ -223,7 +231,7 @@ public void getTSMetaNSUTagk() throws Throwable { @Test (expected = NoSuchUniqueId.class) public void getTSMetaNSUTagv() throws Throwable { - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000002\",\"" + @@ -282,7 +290,7 @@ public void syncToStorageNullTSUID() throws Exception { @Test (expected = IllegalArgumentException.class) public void syncToStorageDoesNotExist() throws Exception { - storage.flushRow(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); meta.syncToStorage(tsdb, false).joinUninterruptibly(); } @@ -315,7 +323,7 @@ public void metaExistsInStorage() throws Exception { @Test public void metaExistsInStorageNot() throws Exception { - storage.flushRow(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); assertFalse(TSMeta.metaExistsInStorage(tsdb, "000001000001000001") .joinUninterruptibly()); } @@ -328,7 +336,7 @@ public void counterExistsInStorage() throws Exception { @Test public void counterExistsInStorageNot() throws Exception { - storage.flushRow(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); assertFalse(TSMeta.counterExistsInStorage(tsdb, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }).joinUninterruptibly()); } @@ -374,7 +382,7 @@ public void COUNTER_QUALIFIER() throws Exception { public void parseFromColumn() throws Exception { final KeyValue column = mock(KeyValue.class); when(column.key()).thenReturn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); - when(column.value()).thenReturn(storage.getColumn( + when(column.value()).thenReturn(storage.getColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()))); @@ -389,7 +397,7 @@ public void parseFromColumn() throws Exception { public void parseFromColumnWithUIDMeta() throws Exception { final KeyValue column = mock(KeyValue.class); when(column.key()).thenReturn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); - when(column.value()).thenReturn(storage.getColumn( + when(column.value()).thenReturn(storage.getColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()))); diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index 9a7a5cc628..39923cc412 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import java.util.ArrayList; import java.util.List; import net.opentsdb.core.BaseTsdbTest; @@ -59,12 +60,14 @@ GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, Scanner.class, TSMeta.class, AtomicIncrementRequest.class, DateTime.class }) public final class TestTSUIDQuery extends BaseTsdbTest { - private static final byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] META_TABLE = "tsdb-meta".getBytes(MockBase.ASCII()); + private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); private static final byte[] TSUID = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }; private static final byte[] QUAL = new byte[] { 0, 0 }; private static final byte[] VAL = new byte[] { 0x2A }; - private TSUIDQuery query; + private TSUIDQuery query; @Before public void beforeLocal() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); @@ -749,7 +752,7 @@ public void getLastPointTSUIDMetaNoPoint() throws Exception { query = new TSUIDQuery(tsdb, TSUID); assertNull(query.getLastPoint(false, 0).join()); } - + /** * Public for sharing with other UT classes * @param tsdb The mock TSDB client @@ -757,91 +760,99 @@ public void getLastPointTSUIDMetaNoPoint() throws Exception { */ public static void setupStorage(final TSDB tsdb, final MockBase storage) throws Exception { - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + final List families = new ArrayList(); + families.add(TSMeta.FAMILY); + storage.addTable(META_TABLE, families); + + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), METRIC_STRING.getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.user\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"System CPU\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), METRIC_B_STRING.getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"METRIC\",\"name\":\"sys.cpu.system\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"System CPU\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), TAGK_STRING.getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGK\",\"name\":\"host\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Host server name\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), TAGK_B_STRING.getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"TAGK\",\"name\":\"owner\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Datecenter name\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), TAGV_STRING.getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGV\",\"name\":\"web01\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Web server 1\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), TAGV_B_STRING.getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"TAGV\",\"name\":\"web02\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Web server 2\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(TSUID, NAME_FAMILY, + storage.addColumn(META_TABLE, TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(TSUID, NAME_FAMILY, + storage.addColumn(META_TABLE, TSUID, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000002\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000002000001000001000003000002\",\"" + "description\":\"Description\",\"notes\":\"Notes\",\"created\":1328140800," + "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 2 }, NAME_FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); } diff --git a/test/meta/TestUIDMeta.java b/test/meta/TestUIDMeta.java index c628a5872b..c53338706c 100644 --- a/test/meta/TestUIDMeta.java +++ b/test/meta/TestUIDMeta.java @@ -48,7 +48,8 @@ GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, Scanner.class, UIDMeta.class}) public final class TestUIDMeta { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); private TSDB tsdb; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; @@ -61,17 +62,17 @@ public void before() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 3 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.2".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.0\"," + @@ -250,7 +251,7 @@ public void storeNew() throws Exception { meta = new UIDMeta(UniqueIdType.METRIC, new byte[] { 0, 0, 1 }, "sys.cpu.1"); meta.setDisplayName("System CPU"); meta.storeNew(tsdb).joinUninterruptibly(); - meta = JSON.parseToObject(storage.getColumn(new byte[] { 0, 0, 1 }, + meta = JSON.parseToObject(storage.getColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII())), UIDMeta.class); assertEquals("System CPU", meta.getDisplayName()); diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index 126c15b581..af0b73fd17 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -185,6 +185,7 @@ public void metricOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchMetricMeta() throws Exception { + storage = new MockBase(tsdb, client, true, true, true, true); final SearchQuery query = new SearchQuery("sys.cpu.system"); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); lookup.lookup(); @@ -293,6 +294,7 @@ public void tagkOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchTagkMeta() throws Exception { + storage = new MockBase(tsdb, client, true, true, true, true); final List> tags = new ArrayList>(1); tags.add(new Pair("dc", null)); @@ -386,6 +388,7 @@ public void tagvOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchTagvMeta() throws Exception { + storage = new MockBase(tsdb, client, true, true, true, true); final List> tags = new ArrayList>(1); tags.add(new Pair(null, "web03")); @@ -574,11 +577,14 @@ public void limitVerification() throws Exception { */ private void generateMeta() { storage = new MockBase(tsdb, client, true, true, true, true); - storage.setFamily("t".getBytes(MockBase.ASCII())); + final List families = new ArrayList(1); + families.add(TSMeta.FAMILY); + storage.addTable("tsdb-meta".getBytes(), families); final byte[] val = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }; for (final byte[] tsuid : test_tsuids) { - storage.addColumn(tsuid, TSMeta.COUNTER_QUALIFIER(), val); + storage.addColumn("tsdb-meta".getBytes(), tsuid, TSMeta.FAMILY, + TSMeta.COUNTER_QUALIFIER(), val); } } diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index d2243ff259..73099e38b5 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -22,8 +22,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; @@ -32,6 +34,7 @@ import javax.xml.bind.DatatypeConverter; import net.opentsdb.core.TSDB; +import net.opentsdb.utils.Pair; import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; @@ -54,8 +57,13 @@ * Mock HBase implementation useful in testing calls to and from storage with * actual pretend data. The underlying data store is an incredibly ugly nesting * of ByteMaps from AsyncHbase so it stores and orders byte arrays similar to - * HBase. A MockBase instance represents a SINGLE table in HBase but it provides - * support for column families and timestamped entries. + * HBase. It supports tables and column families along with timestamps but + * doesn't deal with TTLs or other features. + *

    + * By default we configure the "'tsdb', {NAME => 't'}" and + * "'tsdb-uid', {NAME => 'id'}, {NAME => 'name'}" tables. If you need more, just + * add em. + * *

    * It's not a perfect mock but is useful for the majority of unit tests. Gets, * puts, cas, deletes and scans are currently supported. See notes for each @@ -85,15 +93,26 @@ public final class MockBase { private static final Charset ASCII = Charset.forName("ISO-8859-1"); private TSDB tsdb; - // KEY Column Family Qualifier Timestamp Value - private ByteMap>>> - storage = new ByteMap>>>(); + /** Gross huh? >>>> + * Why is CF before row? Because we want to throw exceptions if a CF hasn't + * been "configured" + */ + private ByteMap>>>> + storage = new ByteMap>>>>(); private HashSet scanners = new HashSet(2); + + /** The default family for shortcuts */ private byte[] default_family; + /** The default table for shortcuts */ + private byte[] default_table; + /** Incremented every time a new value is stored (without a timestamp) */ private long current_timestamp = 1388534400000L; + /** A list of exceptions that can be thrown when working with a row key */ + private ByteMap> exceptions; + /** * Setups up mock intercepts for all of the calls. Depending on the given * flags, some mocks may not be enabled, allowing local unit tests to setup @@ -114,8 +133,10 @@ public MockBase( final boolean default_delete, final boolean default_scan) { this.tsdb = tsdb; - - default_family = "t".getBytes(ASCII); // set a default + + default_family = "t".getBytes(ASCII); + default_table = "tsdb".getBytes(ASCII); + setupDefaultTables(); // replace the "real" field objects with mocks Whitebox.setInternalState(tsdb, "client", client); @@ -146,7 +167,8 @@ public MockBase( @Override public Scanner answer(InvocationOnMock arg0) throws Throwable { final Scanner scanner = mock(Scanner.class); - scanners.add(new MockScanner(scanner)); + final byte[] table = (byte[])arg0.getArguments()[0]; + scanners.add(new MockScanner(scanner, table)); return scanner; } @@ -161,9 +183,42 @@ public Scanner answer(InvocationOnMock arg0) throws Throwable { when(client.append((AppendRequest)any())).thenAnswer(new MockAppend()); } - /** @param family Sets the family for calls that need it */ + /** + * Add a table with families to the data store. If the table or family + * exists, it's a no-op. Give real values as we don't check `em. + * @param table The table to add + * @param families A list of one or more famlies to add to the table + */ + public void addTable(final byte[] table, final List families) { + ByteMap>>> map = storage.get(table); + if (map == null) { + map = new ByteMap>>>(); + storage.put(table, map); + } + for (final byte[] family : families) { + if (!map.containsKey(family)) { + map.put(family, new ByteMap>>()); + } + } + } + + /** + * Pops the table out of the map + * @param table The table to pop + * @return True if the table was there, false if it wasn't. + */ + public boolean deleteTable(final byte[] table) { + return storage.remove(table) != null; + } + + /** @param family Sets the default family for calls that need it */ public void setFamily(final byte[] family) { - this.default_family = family; + default_family = family; + } + + /** @param table Sets the default table for calls that need it */ + public void setDefaultTable(final byte[] table) { + default_table = table; } /** @param timestamp The timestamp to use for further storage increments */ @@ -179,20 +234,23 @@ public long getCurrentTimestamp() { /** * Add a column to the hash table using the default column family. * The proper row will be created if it doesn't exist. If the column already - * exists, the original value will be overwritten with the new data + * exists, the original value will be overwritten with the new data. + * Uses the default table and family * @param key The row key * @param qualifier The qualifier * @param value The value to store */ public void addColumn(final byte[] key, final byte[] qualifier, final byte[] value) { - addColumn(key, default_family, qualifier, value, current_timestamp++); + addColumn(default_table, key, default_family, qualifier, value, + current_timestamp++); } /** * Add a column to the hash table * The proper row will be created if it doesn't exist. If the column already - * exists, the original value will be overwritten with the new data + * exists, the original value will be overwritten with the new data. + * Uses the default table. * @param key The row key * @param family The column family to store the value in * @param qualifier The qualifier @@ -200,97 +258,215 @@ public void addColumn(final byte[] key, final byte[] qualifier, */ public void addColumn(final byte[] key, final byte[] family, final byte[] qualifier, final byte[] value) { - addColumn(key, family, qualifier, value, current_timestamp++); + addColumn(default_table, key, family, qualifier, value, current_timestamp++); + } + + /** + * Add a column to the hash table + * The proper row will be created if it doesn't exist. If the column already + * exists, the original value will be overwritten with the new data + * @param table The table + * @param key The row key + * @param family The column family to store the value in + * @param qualifier The qualifier + * @param value The value to store + */ + public void addColumn(final byte[] table, final byte[] key, final byte[] family, + final byte[] qualifier, final byte[] value) { + addColumn(table, key, family, qualifier, value, current_timestamp++); } /** * Add a column to the hash table * The proper row will be created if it doesn't exist. If the column already * exists, the original value will be overwritten with the new data + * @param table The table * @param key The row key * @param family The column family to store the value in * @param qualifier The qualifier * @param value The value to store * @param timestamp The timestamp to store */ - public void addColumn(final byte[] key, final byte[] family, + public void addColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier, final byte[] value, final long timestamp) { // AsyncHBase will throw an NPE if the user tries to write a NULL value // so we better do the same. An empty value is ok though, i.e. new byte[] {} if (value == null) { throw new NullPointerException(); } - - ByteMap>> row = storage.get(key); - if (row == null) { - row = new ByteMap>>(); - storage.put(key, row); + final ByteMap>>> map = storage.get(table); + if (map == null) { + throw new RuntimeException( + "No such table " + Bytes.pretty(table)); } - - ByteMap> cf = row.get(family); + final ByteMap>> cf = map.get(family); if (cf == null) { - cf = new ByteMap>(); - row.put(family, cf); + throw new RuntimeException( + "No such CF " + Bytes.pretty(family)); + } + + ByteMap> row = cf.get(key); + if (row == null) { + row = new ByteMap>(); + cf.put(key, row); } - TreeMap column = cf.get(qualifier); + + TreeMap column = row.get(qualifier); if (column == null) { // remember, most recent at the top! column = new TreeMap(Collections.reverseOrder()); - cf.put(qualifier, column); + row.put(qualifier, column); } column.put(timestamp, value); } - /** @return TTotal number of rows in the hash table */ + /** + * Stores an exception so that any operation on the given key will cause it + * to be thrown. + * @param key The key to go pear shaped on + * @param exception The exception to throw + */ + public void throwException(final byte[] key, final RuntimeException exception) { + throwException(key, exception, true); + } + + /** + * Stores an exception so that any operation on the given key will cause it + * to be thrown. + * @param key The key to go pear shaped on + * @param exception The exception to throw + * @param as_result Whether or not to return the exception in the deferred + * result or throw it outright. + */ + public void throwException(final byte[] key, final RuntimeException exception, + final boolean as_result) { + if (exceptions == null) { + exceptions = new ByteMap>(); + } + exceptions.put(key, new Pair(exception, as_result)); + } + + /** Removes all exceptions from the exception list */ + public void clearExceptions() { + exceptions.clear(); + } + + /** @return Total number of unique rows in the default table. Returns 0 if the + * default table does not exist */ public int numRows() { - return storage.size(); + return numRows(default_table); + } + + /** + * Total number of rows in the given table. Returns 0 if the table does not exit. + * @param table The table to scan + * @return The number of rows + */ + public int numRows(final byte[] table) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { + return 0; + } + final ByteMap unique_rows = new ByteMap(); + for (final ByteMap>> cf : map.values()) { + for (final byte[] key : cf.keySet()) { + unique_rows.put(key, null); + } + } + return unique_rows.size(); } /** - * Return the total number of column families for the row + * Return the total number of column families for the row in the default table * @param key The row to search for - * @return -1 if the row did not exist, otherwise the number of column families. + * @return -1 if the table or row did not exist, otherwise the number of + * column families. */ public int numColumnFamilies(final byte[] key) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + return numColumnFamilies(default_table, key); + } + + /** + * Return the number of column families for the given row key in the given table. + * @param table The table to iterate over + * @param key The row to search for + * @return -1 if the table or row did not exist, otherwise the number of + * column families. + */ + public int numColumnFamilies(final byte[] table, final byte[] key) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { return -1; } - return row.size(); + int sum = 0; + for (final ByteMap>> cf : map.values()) { + if (cf.containsKey(key)) { + ++sum; + } + } + return sum == 0 ? -1 : sum; } /** - * Total number of columns in the given row across all column families + * Total number of columns in the given row across all column families in the + * default table * @param key The row to search for * @return -1 if the row did not exist, otherwise the number of columns. */ public long numColumns(final byte[] key) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + return numColumns(default_table, key); + } + + /** + * Total number of columns in the given row across all column families in the + * default table + * @param table The table to iterate over + * @param key The row to search for + * @return -1 if the row did not exist, otherwise the number of columns. + */ + public long numColumns(final byte[] table, final byte[] key) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { return -1; } long size = 0; - for (Map.Entry>> entry : row) { - size += entry.getValue().size(); + for (final ByteMap>> cf : map.values()) { + final ByteMap> row = cf.get(key); + if (row != null) { + size += row.size(); + } } - return size; + return size == 0 ? -1 : size; } /** - * Return the total number of columns for a specific row and family + * Return the total number of columns for a specific row and family in the + * default table * @param key The row to search for * @param family The column family to search for * @return -1 if the row did not exist, otherwise the number of columns. */ public int numColumnsInFamily(final byte[] key, final byte[] family) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + return numColumnsInFamily(default_table, key, family); + } + + /** + * Return the total number of columns for a specific row and family + * @param key The row to search for + * @param family The column family to search for + * @return -1 if the row did not exist, otherwise the number of columns. + */ + public int numColumnsInFamily(final byte[] table, final byte[] key, + final byte[] family) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { return -1; } - final ByteMap> cf = row.get(family); + final ByteMap>> cf = map.get(family); if (cf == null) { return -1; } @@ -299,16 +475,17 @@ public int numColumnsInFamily(final byte[] key, final byte[] family) { /** * Retrieve the most recent contents of a single column with the default family + * and in the default table * @param key The row key of the column * @param qualifier The column qualifier * @return The byte array of data or null if not found */ public byte[] getColumn(final byte[] key, final byte[] qualifier) { - return getColumn(key, default_family, qualifier); + return getColumn(default_table, key, default_family, qualifier); } /** - * Retrieve the most recent contents of a single column + * Retrieve the most recent contents of a single column with the default table * @param key The row key of the column * @param family The column family * @param qualifier The column qualifier @@ -316,16 +493,33 @@ public byte[] getColumn(final byte[] key, final byte[] qualifier) { */ public byte[] getColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + return getColumn(default_table, key, family, qualifier); + } + + /** + * Retrieve the most recent contents of a single column + * @param table The table to fetch from + * @param key The row key of the column + * @param family The column family + * @param qualifier The column qualifier + * @return The byte array of data or null if not found + */ + public byte[] getColumn(final byte[] table, final byte[] key, + final byte[] family, final byte[] qualifier) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { return null; } - final ByteMap> cf = row.get(family); + final ByteMap>> cf = map.get(family); if (cf == null) { return null; } - final TreeMap column = cf.get(qualifier); + final ByteMap> row = cf.get(key); + if (row == null) { + return null; + } + final TreeMap column = row.get(qualifier); if (column == null) { return null; } @@ -334,70 +528,108 @@ public byte[] getColumn(final byte[] key, final byte[] family, /** * Retrieve the full map of timestamps and values of a single column with - * the default family + * the default family and default table * @param key The row key of the column * @param qualifier The column qualifier * @return The byte array of data or null if not found */ public TreeMap getFullColumn(final byte[] key, final byte[] qualifier) { - return getFullColumn(key, default_family, qualifier); + return getFullColumn(default_table, key, default_family, qualifier); } /** * Retrieve the full map of timestamps and values of a single column + * @param table The table to fetch from * @param key The row key of the column * @param family The column family * @param qualifier The column qualifier * @return The tree map of timestamps and values or null if not found */ - public TreeMap getFullColumn(final byte[] key, + public TreeMap getFullColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { return null; } - final ByteMap> cf = row.get(family); + final ByteMap>> cf = map.get(family); if (cf == null) { return null; } - final TreeMap column = cf.get(qualifier); - if (column == null) { + final ByteMap> row = cf.get(key); + if (row == null) { return null; } - return column; + return row.get(qualifier); } /** * Returns the most recent value from all columns for a given column family + * in the default table * @param key The row key * @param family The column family ID * @return A map of columns if the CF was found, null if no such CF */ public ByteMap getColumnFamily(final byte[] key, final byte[] family) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + return getColumnFamily(default_table, key , family); + } + + /** + * Returns the most recent value from all columns for a given column family + * @param table The table to fetch from + * @param key The row key + * @param family The column family ID + * @return A map of columns if the CF was found, null if no such CF + */ + public ByteMap getColumnFamily(final byte[] table, final byte[] key, + final byte[] family) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { return null; } - final ByteMap> cf = row.get(family); + final ByteMap>> cf = map.get(family); if (cf == null) { return null; } + final ByteMap> row = cf.get(key); + if (row == null) { + return null; + } // convert to a byte map final ByteMap columns = new ByteMap(); - for (Map.Entry> entry : cf.entrySet()) { + for (Entry> entry : row.entrySet()) { // the map should never be null columns.put(entry.getKey(), entry.getValue().firstEntry().getValue()); } return columns; } - /** @return the list of keys stored in the table */ + /** @return the list of keys stored in the default table for all CFs */ public Set getKeys() { - return storage.keySet(); + return getKeys(default_table); + } + + /** + * Return the list of unique keys in the given table for all CFs + * @param table The table to pull from + * @return A list of keys. May be null if the table doesn't exist + */ + public Set getKeys(final byte[] table) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { + return null; + } + final ByteMap unique_rows = new ByteMap(); + for (final ByteMap>> cf : map.values()) { + for (final byte[] key : cf.keySet()) { + unique_rows.put(key, null); + } + } + return unique_rows.keySet(); } /** @@ -409,18 +641,28 @@ public TSDB getTSDB() { } /** - * Runs through all rows in the table and compacts them by making a call to - * the {@link TSDB.compact} method. It will delete any columns that were - * compacted and leave others untouched, just as the normal method does. - * Note, assumes only one column family + * Runs through all rows in the "tsdb" table and compacts them by making a + * call to the {@link TSDB.compact} method. It will delete any columns + * that were compacted and leave others untouched, just as the normal + * method does. + * And only iterates over the 't' family. * @throws Exception if Whitebox couldn't access the compact method */ public void tsdbCompactAllRows() throws Exception { - for (Map.Entry>>> entry : - storage.entrySet()) { + final ByteMap>>> map = + storage.get("tsdb".getBytes(ASCII)); + if (map == null) { + return; + } + final ByteMap>> cf = map.get("t".getBytes(ASCII)); + if (cf == null) { + return; + } + + for (Entry>> entry : cf.entrySet()) { final byte[] key = entry.getKey(); - final ByteMap> row = entry.getValue().firstEntry().getValue(); + final ByteMap> row = entry.getValue(); ArrayList kvs = new ArrayList(row.size()); final Set deletes = new HashSet(); for (Map.Entry> column : row.entrySet()) { @@ -445,49 +687,111 @@ public void tsdbCompactAllRows() throws Exception { } /** - * Clears the entire hash table. Use it if your unit test needs to start fresh + * Clears out all rows from storage but doesn't delete the tables or families. */ public void flushStorage() { - storage.clear(); + for (final ByteMap>>> table : + storage.values()) { + for (final ByteMap>> cf : table.values()) { + cf.clear(); + } + } + } + + /** + * Clears out all rows for a given table + * @param table The table to empty out + */ + public void flushStorage(final byte[] table) { + final ByteMap>>> map = storage.get(table); + if (map == null) { + return; + } + for (final ByteMap>> cf : map.values()) { + cf.clear(); + } } /** - * Removes the entire row from the hash table + * Removes the entire row from the default table for all column families * @param key The row to remove */ public void flushRow(final byte[] key) { - storage.remove(key); + flushRow(default_table, key); + } + + /** + * Removes the entire row from the table for all column families + * @param table The table to purge + * @param key The row to remove + */ + public void flushRow(final byte[] table, final byte[] key) { + final ByteMap>>> map = storage.get(table); + if (map == null) { + return; + } + for (final ByteMap>> cf : map.values()) { + cf.remove(key); + } } /** - * Removes the entire column family from the hash table for ALL rows + * Removes all rows from the default table for the given column family * @param family The family to remove */ public void flushFamily(final byte[] family) { - for (Map.Entry>>> row : - storage.entrySet()) { - row.getValue().remove(family); + flushFamily(default_table, family); + } + + /** + * Removes all rows from the default table for the given column family + * @param table The table to purge from + * @param family The family to remove + */ + public void flushFamily(final byte[] table, final byte[] family) { + final ByteMap>>> map = storage.get(table); + if (map == null) { + return; + } + final ByteMap>> cf = map.get(family); + if (cf != null) { + cf.clear(); } } /** - * Removes the given column from the hash map + * Removes the given column from the default table * @param key Row key * @param family Column family * @param qualifier Column qualifier */ public void flushColumn(final byte[] key, final byte[] family, final byte[] qualifier) { - final ByteMap>> row = - storage.get(key); - if (row == null) { + flushColumn(default_table, key, family, qualifier); + } + + /** + * Removes the given column from the table + * @param table The table to purge from + * @param key Row key + * @param family Column family + * @param qualifier Column qualifier + */ + public void flushColumn(final byte[] table, final byte[] key, + final byte[] family, final byte[] qualifier) { + final ByteMap>>> map = storage.get(table); + if (map == null) { return; } - final ByteMap> cf = row.get(family); + final ByteMap>> cf = map.get(family); if (cf == null) { return; } - cf.remove(qualifier); + final ByteMap> row = cf.get(key); + if (row == null) { + return; + } + row.remove(qualifier); } /** @@ -508,26 +812,28 @@ public void dumpToSystemOut(final boolean ascii) { return; } - for (Map.Entry>>> row : + for (Entry>>>> table : storage.entrySet()) { - System.out.println("[Row] " + (ascii ? new String(row.getKey(), ASCII) : - bytesToString(row.getKey()))); + System.out.println("[Table] " + new String(table.getKey(), ASCII)); - for (Map.Entry>> cf : - row.getValue().entrySet()) { - - final String family = ascii ? new String(cf.getKey(), ASCII) : - bytesToString(cf.getKey()); - System.out.println(" [CF] " + family); - - for (Map.Entry> column : cf.getValue().entrySet()) { - System.out.println(" [Qual] " + (ascii ? - "\"" + new String(column.getKey(), ASCII) + "\"" - : bytesToString(column.getKey()))); - for (Map.Entry cell : column.getValue().entrySet()) { - System.out.println(" [TS] " + cell.getKey() + " [Value] " + - (ascii ? new String(cell.getValue(), ASCII) - : bytesToString(cell.getValue()))); + for (Entry>>> cf : + table.getValue().entrySet()) { + System.out.println(" [CF] " + new String(cf.getKey(), ASCII)); + + for (Entry>> row : + cf.getValue().entrySet()) { + System.out.println(" [Row] " + (ascii ? + new String(row.getKey(), ASCII) : bytesToString(row.getKey()))); + + for (Map.Entry> column : row.getValue().entrySet()) { + System.out.println(" [Qual] " + (ascii ? + "\"" + new String(column.getKey(), ASCII) + "\"" + : bytesToString(column.getKey()))); + for (Map.Entry cell : column.getValue().entrySet()) { + System.out.println(" [TS] " + cell.getKey() + " [Value] " + + (ascii ? new String(cell.getValue(), ASCII) + : bytesToString(cell.getValue()))); + } } } } @@ -580,6 +886,22 @@ public static byte[] concatByteArrays(final byte[]... arrays) { return result; } + /** Creates the TSDB and UID tables */ + private void setupDefaultTables() { + final ByteMap>>> tsdb = + new ByteMap>>>(); + tsdb.put("t".getBytes(ASCII), new ByteMap>>()); + storage.put("tsdb".getBytes(ASCII), tsdb); + + final ByteMap>>> tsdb_uid = + new ByteMap>>>(); + tsdb_uid.put("name".getBytes(ASCII), + new ByteMap>>()); + tsdb_uid.put("id".getBytes(ASCII), + new ByteMap>>()); + storage.put("tsdb-uid".getBytes(ASCII), tsdb_uid); + } + /** * Gets one or more columns from a row. If the row does not exist, a null is * returned. If no qualifiers are given, the entire row is returned. @@ -592,52 +914,59 @@ public Deferred> answer(InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final GetRequest get = (GetRequest)args[0]; - final ByteMap>> row = - storage.get(get.key()); - - if (row == null) { - return Deferred.fromResult((ArrayList)null); - } - - final byte[] family = get.family(); - if (family != null && family.length > 0) { - if (!row.containsKey(family)) { - return Deferred.fromResult((ArrayList)null); + if (exceptions != null) { + final Pair ex = exceptions.get(get.key()); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } } } + final ByteMap>>> map = + storage.get(get.table()); + if (map == null) { + return Deferred.fromError(new RuntimeException( + "No such table " + Bytes.pretty(get.table()))); + } + // compile a set of qualifiers to use as a filter if necessary - ByteMap qualifiers = new ByteMap(); + final ByteMap qualifiers = new ByteMap(); if (get.qualifiers() != null && get.qualifiers().length > 0) { for (byte[] q : get.qualifiers()) { qualifiers.put(q, null); } } - final ArrayList kvs = new ArrayList(row.size()); - for (Map.Entry>> cf : - row.entrySet()) { + final ArrayList kvs = new ArrayList(); + for (final Entry>>> cf : + map.entrySet()) { + if (get.family() != null && Bytes.memcmp(get.family(), cf.getKey()) != 0) { + continue; + } - // column family filter - if (family != null && family.length > 0 && - !Bytes.equals(family, cf.getKey())) { + final ByteMap> row = cf.getValue().get(get.key()); + if (row == null) { continue; } - for (Map.Entry> column : - cf.getValue().entrySet()) { - // qualifier filter + for (Entry> column : row.entrySet()) { if (!qualifiers.isEmpty() && !qualifiers.containsKey(column.getKey())) { continue; } // TODO - if we want to support multiple values, iterate over the // tree map. Otherwise Get returns just the latest value. - kvs.add(new KeyValue(get.key(), default_family, column.getKey(), + kvs.add(new KeyValue(get.key(), cf.getKey(), column.getKey(), column.getValue().firstKey(), column.getValue().firstEntry().getValue())); } } + if (kvs.isEmpty()) { + return Deferred.fromResult(null); + } return Deferred.fromResult(kvs); } } @@ -652,25 +981,42 @@ public Deferred answer(final InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final PutRequest put = (PutRequest)args[0]; - - ByteMap>> row = - storage.get(put.key()); - if (row == null) { - row = new ByteMap>>(); - storage.put(put.key(), row); + + if (exceptions != null) { + final Pair ex = exceptions.get(put.key()); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } + } + } + + final ByteMap>>> map = + storage.get(put.table()); + if (map == null) { + return Deferred.fromError(new RuntimeException( + "No such table " + Bytes.pretty(put.table()))); } - ByteMap> cf = row.get(put.family()); + final ByteMap>> cf = map.get(put.family()); if (cf == null) { - cf = new ByteMap>(); - row.put(put.family(), cf); + return Deferred.fromError(new RuntimeException( + "No such CF " + Bytes.pretty(put.table()))); } + ByteMap> row = cf.get(put.key()); + if (row == null) { + row = new ByteMap>(); + cf.put(put.key(), row); + } + for (int i = 0; i < put.qualifiers().length; i++) { - TreeMap column = cf.get(put.qualifiers()[i]); + TreeMap column = row.get(put.qualifiers()[i]); if (column == null) { column = new TreeMap(Collections.reverseOrder()); - cf.put(put.qualifiers()[i], column); + row.put(put.qualifiers()[i], column); } column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : @@ -692,50 +1038,71 @@ public Deferred answer(final InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final AppendRequest append = (AppendRequest)args[0]; - ByteMap>> row = storage.get(append.key()); - if (row == null) { - row = new ByteMap>>(); - storage.put(append.key(), row); + if (exceptions != null) { + final Pair ex = exceptions.get(append.key()); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } + } } - ByteMap> cf = row.get(append.family()); + final ByteMap>>> map = + storage.get(append.table()); + if (map == null) { + return Deferred.fromError(new RuntimeException( + "No such table " + Bytes.pretty(append.table()))); + } + + final ByteMap>> cf = map.get(append.family()); if (cf == null) { - cf = new ByteMap>(); - row.put(append.family(), cf); + return Deferred.fromError(new RuntimeException( + "No such CF " + Bytes.pretty(append.table()))); } - TreeMap column = cf.get(append.qualifier()); - if (column == null) { - column = new TreeMap(); - cf.put(append.qualifier(), column); + ByteMap> row = cf.get(append.key()); + if (row == null) { + row = new ByteMap>(); + cf.put(append.key(), row); } - final byte[] values; - long column_timestamp = 0; - if (append.timestamp() != Long.MAX_VALUE) { - values = column.get(append.timestamp()); - column_timestamp = append.timestamp(); - } else { - if (column.isEmpty()) { - values = null; + for (int i = 0; i < append.qualifiers().length; i++) { + TreeMap column = row.get(append.qualifiers()[i]); + if (column == null) { + column = new TreeMap(Collections.reverseOrder()); + row.put(append.qualifiers()[i], column); + } + + final byte[] values; + long column_timestamp = 0; + if (append.timestamp() != Long.MAX_VALUE) { + values = column.get(append.timestamp()); + column_timestamp = append.timestamp(); } else { - values = column.firstEntry().getValue(); - column_timestamp = column.firstKey(); + if (column.isEmpty()) { + values = null; + } else { + values = column.firstEntry().getValue(); + column_timestamp = column.firstKey(); + } + } + if (column_timestamp == 0) { + column_timestamp = current_timestamp++; } - } - if (column_timestamp == 0) { - column_timestamp = current_timestamp++; - } - final int current_len = values != null ? values.length : 0; - final byte[] append_value = new byte[current_len + append.value().length]; - if (current_len > 0) { - System.arraycopy(values, 0, append_value, 0, values.length); + final int current_len = values != null ? values.length : 0; + final byte[] append_value = new byte[current_len + append.values()[i].length]; + if (current_len > 0) { + System.arraycopy(values, 0, append_value, 0, values.length); + } + + System.arraycopy(append.value(), 0, append_value, current_len, + append.values()[i].length); + column.put(column_timestamp, append_value); } - System.arraycopy(append.value(), 0, append_value, current_len, - append.value().length); - column.put(column_timestamp, append_value); return Deferred.fromResult(true); } } @@ -758,30 +1125,42 @@ public Deferred answer(final InvocationOnMock invocation) final PutRequest put = (PutRequest)args[0]; final byte[] expected = (byte[])args[1]; - ByteMap>> row = - storage.get(put.key()); - if (row == null) { - if (expected != null && expected.length > 0) { - return Deferred.fromResult(false); + if (exceptions != null) { + final Pair ex = exceptions.get(put.key()); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } } - - row = new ByteMap>>(); - storage.put(put.key(), row); } - ByteMap> cf = row.get(put.family()); + final ByteMap>>> map = + storage.get(put.table()); + if (map == null) { + return Deferred.fromError(new RuntimeException( + "No such table " + Bytes.pretty(put.table()))); + } + + final ByteMap>> cf = map.get(put.family()); if (cf == null) { + return Deferred.fromError(new RuntimeException( + "No such CF " + Bytes.pretty(put.table()))); + } + + ByteMap> row = cf.get(put.key()); + if (row == null) { if (expected != null && expected.length > 0) { return Deferred.fromResult(false); } - - cf = new ByteMap>(); - row.put(put.family(), cf); + row = new ByteMap>(); + cf.put(put.key(), row); } // CAS can only operate on one cell, so if the put request has more than // one, we ignore any but the first - TreeMap column = cf.get(put.qualifiers()[0]); + TreeMap column = row.get(put.qualifiers()[0]); if (column == null && (expected != null && expected.length > 0)) { return Deferred.fromResult(false); } @@ -804,7 +1183,7 @@ public Deferred answer(final InvocationOnMock invocation) // passed CAS! if (column == null) { column = new TreeMap(Collections.reverseOrder()); - cf.put(put.qualifiers()[0], column); + row.put(put.qualifiers()[0], column); } column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : current_timestamp++, put.value()); @@ -825,28 +1204,44 @@ public Deferred answer(InvocationOnMock invocation) final Object[] args = invocation.getArguments(); final DeleteRequest delete = (DeleteRequest)args[0]; - ByteMap>> row = - storage.get(delete.key()); - if (row == null) { - return Deferred.fromResult(null); + if (exceptions != null) { + final Pair ex = exceptions.get(delete.key()); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } + } } - // if no qualifiers or family, then delete the row + final ByteMap>>> map = + storage.get(delete.table()); + if (map == null) { + return Deferred.fromError(new RuntimeException( + "No such table " + Bytes.pretty(delete.table()))); + } + + // if no qualifiers or family, then delete the row from all families if ((delete.qualifiers() == null || delete.qualifiers().length < 1 || delete.qualifiers()[0].length < 1) && (delete.family() == null || delete.family().length < 1)) { - storage.remove(delete.key()); + for (final Entry>>> cf : + map.entrySet()) { + cf.getValue().remove(delete.key()); + } return Deferred.fromResult(new Object()); } final byte[] family = delete.family(); if (family != null && family.length > 0) { - if (!row.containsKey(family)) { - return Deferred.fromResult(null); + if (!map.containsKey(family)) { + return Deferred.fromError(new RuntimeException( + "No such CF " + Bytes.pretty(family))); } } - // compile a set of qualifiers to use as a filter if necessary + // compile a set of qualifiers ByteMap qualifiers = new ByteMap(); if (delete.qualifiers() != null || delete.qualifiers().length > 0) { for (byte[] q : delete.qualifiers()) { @@ -854,19 +1249,20 @@ public Deferred answer(InvocationOnMock invocation) } } + // TODO - validate the assumption that a delete with a row key and qual + // but without a family would delete the columns in ALL families + // if the request only has a column family and no qualifiers, we delete - // the entire family + // the row from the entire family if (family != null && qualifiers.isEmpty()) { - row.remove(family); - if (row.isEmpty()) { - storage.remove(delete.key()); - } + final ByteMap>> cf = map.get(delete.family()); + // cf != null validated above + cf.remove(delete.key()); return Deferred.fromResult(new Object()); } - List cf_removals = new ArrayList(row.entrySet().size()); - for (Map.Entry>> cf : - row.entrySet()) { + for (final Entry>>> cf : + map.entrySet()) { // column family filter if (family != null && family.length > 0 && @@ -874,8 +1270,13 @@ public Deferred answer(InvocationOnMock invocation) continue; } + ByteMap> row = cf.getValue().get(delete.key()); + if (row == null) { + continue; + } + for (byte[] qualifier : qualifiers.keySet()) { - final TreeMap column = cf.getValue().get(qualifier); + final TreeMap column = row.get(qualifier); if (column == null) { continue; } @@ -885,7 +1286,7 @@ public Deferred answer(InvocationOnMock invocation) if (column != null) { column.remove(delete.timestamp()); if (column.isEmpty()) { - cf.getValue().remove(qualifier); + row.remove(qualifier); } } } else { @@ -901,24 +1302,15 @@ public Deferred answer(InvocationOnMock invocation) column.remove(ts); } if (column.isEmpty()) { - cf.getValue().remove(qualifier); + row.remove(qualifier); } } } - if (cf.getValue().isEmpty()) { - cf_removals.add(cf.getKey()); + if (row.isEmpty()) { + cf.getValue().remove(delete.key()); } } - - for (byte[] cf : cf_removals) { - row.remove(cf); - } - - if (row.isEmpty()) { - storage.remove(delete.key()); - } - return Deferred.fromResult(new Object()); } @@ -942,15 +1334,26 @@ public Deferred answer(InvocationOnMock invocation) */ private class MockScanner implements Answer>>> { - + + private final byte[] table; private byte[] start = null; private byte[] stop = null; private HashSet scnr_qualifiers = null; private byte[] family = null; private String regex = null; - private boolean called; + private int max_num_rows = Scanner.DEFAULT_MAX_NUM_ROWS; + private ByteMap>>>> + cursors; + private ByteMap>>> cf_rows; + private byte[] last_row; - public MockScanner(final Scanner mock_scanner) { + /** + * Default ctor + * @param mock_scanner The scanner we're using + * @param table The table (confirmed to exist) + */ + public MockScanner(final Scanner mock_scanner, final byte[] table) { + this.table = table; // capture the scanner fields when set doAnswer(new Answer() { @@ -1029,13 +1432,43 @@ public Object answer(InvocationOnMock invocation) throws Throwable { public Deferred>> answer( final InvocationOnMock invocation) throws Throwable { - // It's critical to see if this scanner has been processed before, - // otherwise the code under test will likely wind up in an infinite loop. - // If the scanner has been seen before, we return null. - if (called) { + if (cursors == null) { + final ByteMap>>> map = + storage.get(table); + if (map == null) { + return Deferred.fromError( new RuntimeException( + "No such table " + Bytes.pretty(table))); + } + + cursors = new ByteMap>>>>(); + cf_rows = new ByteMap>>>(); + + if (family == null || family.length < 1) { + for (final Entry>>> cf : map) { + final Iterator>>> + cursor = cf.getValue().iterator(); + cursors.put(cf.getKey(), cursor); + cf_rows.put(cf.getKey(), null); + } + } else { + final ByteMap>> cf = map.get(family); + if (cf == null) { + return Deferred.fromError(new RuntimeException( + "No such CF " + Bytes.pretty(family))); + } + final Iterator>>> + cursor = cf.iterator(); + cursors.put(family, cursor); + cf_rows.put(family, null); + } + } + + // If we're out of rows to scan, then you HAVE to return null as the + // HBase client does. + if (!hasNext()) { return Deferred.fromResult(null); } - called = true; Pattern pattern = null; if (regex != null && !regex.isEmpty()) { @@ -1047,14 +1480,15 @@ public Deferred>> answer( } // return all matches - ArrayList> results = + final ArrayList> results = new ArrayList>(); - for (Map.Entry>>> row : - storage.entrySet()) { - + int rows_read = 0; + while (hasNext()) { + advance(); + // if it's before the start row, after the end row or doesn't // match the given regex, continue on to the next row - if (start != null && Bytes.memcmp(row.getKey(), start) < 0) { + if (start != null && Bytes.memcmp(last_row, start) < 0) { continue; } // asynchbase Scanner's logic: @@ -1063,48 +1497,61 @@ public Deferred>> answer( // include the key in scan result // - if stop key is empty, scan till the end if (stop != null && stop.length > 0 && - Bytes.memcmp(row.getKey(), stop) >= 0 && + Bytes.memcmp(last_row, stop) >= 0 && Bytes.memcmp(start, stop) != 0) { continue; } if (pattern != null) { - final String from_bytes = new String(row.getKey(), MockBase.ASCII); + final String from_bytes = new String(last_row, MockBase.ASCII); if (!pattern.matcher(from_bytes).find()) { continue; } } + + // throws AFTER we match on a row key + if (exceptions != null) { + final Pair ex = exceptions.get(last_row); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } + } + } - // loop on the column families - final ArrayList kvs = - new ArrayList(row.getValue().size()); - for (Map.Entry>> cf : - row.getValue().entrySet()) { - - // column family filter - if (family != null && family.length > 0 && - !Bytes.equals(family, cf.getKey())) { + // loop over the column family rows to see if they match + final ArrayList kvs = new ArrayList(); + for (final Entry>>> row : + cf_rows.entrySet()) { + if (row.getValue() == null || + Bytes.memcmp(last_row, row.getValue().getKey()) != 0) { continue; } - - for (Map.Entry> column : - cf.getValue().entrySet()) { - + + for (final Entry> column : + row.getValue().getValue().entrySet()) { // if the qualifier isn't in the set, continue if (scnr_qualifiers != null && !scnr_qualifiers.contains(bytesToString(column.getKey()))) { continue; } - kvs.add(new KeyValue(row.getKey(), cf.getKey(), column.getKey(), - column.getValue().firstKey(), + kvs.add(new KeyValue(row.getValue().getKey(), row.getKey(), + column.getKey(), column.getValue().firstKey(), column.getValue().firstEntry().getValue())); } - } if (!kvs.isEmpty()) { results.add(kvs); } + rows_read++; + + if (rows_read >= max_num_rows) { + Thread.sleep(10); // this is here for time based unit tests + break; + } } if (results.isEmpty()) { @@ -1112,6 +1559,71 @@ public Deferred>> answer( } return Deferred.fromResult(results); } + + /** @return Returns true if any of the CF iterators have another value */ + private boolean hasNext() { + for (final Iterator>>> cursor : + cursors.values()) { + if (cursor.hasNext()) { + return true; + } + } + return false; + } + + /** Insanely inefficient and ugly way of advancing the cursors */ + private void advance() { + // first time to get the ceiling + if (last_row == null) { + for (final Entry>>>> iterator : + cursors.entrySet()) { + final Entry>> row = + iterator.getValue().hasNext() ? iterator.getValue().next() : null; + cf_rows.put(iterator.getKey(), row); + if (last_row == null) { + last_row = row.getKey(); + } else { + if (Bytes.memcmp(last_row, row.getKey()) < 0) { + last_row = row.getKey(); + } + } + } + return; + } + + for (final Entry>>> cf : + cf_rows.entrySet()) { + final Entry>> row = cf.getValue(); + if (row == null) { + continue; + } + + if (Bytes.memcmp(last_row, row.getKey()) == 0) { + if (!cursors.get(cf.getKey()).hasNext()) { + cf_rows.put(cf.getKey(), null); // EX? + } else { + cf_rows.put(cf.getKey(), cursors.get(cf.getKey()).next()); + } + } + } + + last_row = null; + for (final Entry>> row : + cf_rows.values()) { + if (row == null) { + continue; + } + + if (last_row == null) { + last_row = row.getKey(); + } else { + if (Bytes.memcmp(last_row, row.getKey()) < 0) { + last_row = row.getKey(); + } + } + } + } } /** @@ -1126,23 +1638,41 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final AtomicIncrementRequest air = (AtomicIncrementRequest)args[0]; final long amount = air.getAmount(); - ByteMap>> row = - storage.get(air.key()); - if (row == null) { - row = new ByteMap>>(); - storage.put(air.key(), row); + + if (exceptions != null) { + final Pair ex = exceptions.get(air.key()); + if (ex != null) { + if (ex.getValue()) { + return Deferred.fromError(ex.getKey()); + } else { + throw ex.getKey(); + } + } + } + + final ByteMap>>> map = + storage.get(air.table()); + if (map == null) { + return Deferred.fromError(new RuntimeException( + "No such table " + Bytes.pretty(air.table()))); } - ByteMap> cf = row.get(air.family()); + final ByteMap>> cf = map.get(air.family()); if (cf == null) { - cf = new ByteMap>(); - row.put(air.family(), cf); + return Deferred.fromError(new RuntimeException( + "No such CF " + Bytes.pretty(air.table()))); } - TreeMap column = cf.get(air.qualifier()); + ByteMap> row = cf.get(air.key()); + if (row == null) { + row = new ByteMap>(); + cf.put(air.key(), row); + } + + TreeMap column = row.get(air.qualifier()); if (column == null) { column = new TreeMap(Collections.reverseOrder()); - cf.put(air.qualifier(), column); + row.put(air.qualifier(), column); column.put(current_timestamp++, Bytes.fromLong(amount)); return Deferred.fromResult(amount); } @@ -1154,4 +1684,5 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { } } + } diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index 2da55c3303..ab271dc131 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -55,7 +55,7 @@ public class TestUID { private byte[] METRICS = "metrics".getBytes(MockBase.ASCII()); private byte[] TAGK = "tagk".getBytes(MockBase.ASCII()); private byte[] TAGV = "tagv".getBytes(MockBase.ASCII()); - + private byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); private final static Method fsck; static { try { @@ -112,14 +112,14 @@ public void before() throws Exception { public void fsckNoData() throws Exception { storage.flushStorage(); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckNoErrors() throws Exception { int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -131,18 +131,18 @@ public void fsckNoErrors() throws Exception { @Test public void fsckMetricsUIDHigh() throws Exception { // currently a warning, not an error - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(42L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(42L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkUIDHigh() throws Exception { // currently a warning, not an error - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(42L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(42L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -150,9 +150,9 @@ public void fsckTagkUIDHigh() throws Exception { public void fsckTagvUIDHigh() throws Exception { // currently a warning, not an error - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(42L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(42L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -163,60 +163,60 @@ public void fsckTagvUIDHigh() throws Exception { @Test public void fsckMetricsUIDLow() throws Exception { // currently a warning, not an error - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(1L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXMetricsUIDLow() throws Exception { // currently a warning, not an error - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(0L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(0L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkUIDLow() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(1L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXTagkUIDLow() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(1L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagvUIDLow() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(1L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXTagvUIDLow() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(1L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(1L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -227,25 +227,25 @@ public void fsckFIXTagvUIDLow() throws Exception { */ @Test public void fsckMetricsUIDWrongLength() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromInt(3)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromInt(3)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckTagkUIDWrongLength() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromInt(3)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromInt(3)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckTagvUIDWrongLength() throws Exception { - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromInt(3)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromInt(3)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @@ -259,64 +259,64 @@ public void fsckTagvUIDWrongLength() throws Exception { */ @Test public void fsckMetricsMissingReverse() throws Exception { - storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, METRICS); + storage.flushColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXMetricsMissingReverse() throws Exception { - storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, METRICS); + storage.flushColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); assertArrayEquals("foo".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkMissingReverse() throws Exception { - storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGK); + storage.flushColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXTagkMissingReverse() throws Exception { - storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGK); + storage.flushColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); assertArrayEquals("host".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, TAGK)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, TAGK)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagvMissingReverse() throws Exception { - storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGV); + storage.flushColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXTagvMissingReverse() throws Exception { - storage.flushColumn(new byte[] {0, 0, 1}, NAME_FAMILY, TAGV); + storage.flushColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); assertArrayEquals("web01".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, TAGV)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, TAGV)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -337,88 +337,88 @@ public void fsckFIXTagvMissingReverse() throws Exception { */ @Test public void fsckMetricsInconsistentForward() throws Exception { - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXMetricsInconsistentForward() throws Exception { - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("fsck.foo.wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); - assertNull(storage.getColumn("foo".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); + assertNull(storage.getColumn(UID_TABLE, "foo".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS)); - assertNull(storage.getColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + assertNull(storage.getColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkInconsistentForward() throws Exception { - storage.addColumn("some.other.value".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "some.other.value".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXTagkInconsistentForward() throws Exception { - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("fsck.host.wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, TAGK)); - assertNull(storage.getColumn("host".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, TAGK)); + assertNull(storage.getColumn(UID_TABLE, "host".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK)); - assertNull(storage.getColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + assertNull(storage.getColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagvInconsistentForward() throws Exception { - storage.addColumn("some.other.value".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "some.other.value".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXTagvInconsistentForward() throws Exception { - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("fsck.web01.wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, TAGV)); - assertNull(storage.getColumn("web01".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, TAGV)); + assertNull(storage.getColumn(UID_TABLE, "web01".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV)); - assertNull(storage.getColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + assertNull(storage.getColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -436,70 +436,70 @@ public void fsckFIXTagvInconsistentForward() throws Exception { */ @Test public void fsckMetricsDuplicateForward() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXMetricsDuplicateForward() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("bar".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkDuplicateForward() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXTagkDuplicateForward() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("dc".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, TAGK)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 2}, NAME_FAMILY, TAGK)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagvDuplicateForward() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXTagvDuplicateForward() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("web02".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, TAGV)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 2}, NAME_FAMILY, TAGV)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -517,7 +517,7 @@ public void fsckMetricsMissingForward() throws Exception { storage.flushColumn("bar".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -527,7 +527,7 @@ public void fsckFIXMetricsMissingForward() throws Exception { storage.flushColumn("bar".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(0, errors); assertNull(storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); } @@ -537,7 +537,7 @@ public void fsckTagkMissingForward() throws Exception { // currently a warning, not an error storage.flushColumn("host".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -546,7 +546,7 @@ public void fsckFIXTagkMissingForward() throws Exception { // currently a warning, not an error storage.flushColumn("host".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(0, errors); assertNull(storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, TAGK)); } @@ -556,7 +556,7 @@ public void fsckTagvMissingForward() throws Exception { // currently a warning, not an error storage.flushColumn("web01".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -565,7 +565,7 @@ public void fsckFIXTagvMissingForward() throws Exception { // currently a warning, not an error storage.flushColumn("web01".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(0, errors); assertNull(storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, TAGV)); } @@ -581,73 +581,73 @@ public void fsckFIXTagvMissingForward() throws Exception { */ @Test public void fsckMetricsInconsistentReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXMetricsInconsistentReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); assertNull(storage.getColumn(new byte [] {0, 0, 3}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkInconsistentReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXTagkInconsistentReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); assertNull(storage.getColumn(new byte [] {0, 0, 3}, NAME_FAMILY, TAGK)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagvInconsistentReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(1, errors); } @Test public void fsckFIXTagvInconsistentReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(1, errors); assertNull(storage.getColumn(new byte [] {0, 0, 3}, NAME_FAMILY, TAGV)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -664,91 +664,91 @@ public void fsckFIXTagvInconsistentReverse() throws Exception { */ @Test public void fsckMetricsDuplicateReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 4}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(4L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(4L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXMetricsDuplicateReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 4}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(4L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(4L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertNull(storage.getColumn(new byte [] {0, 0, 3}, NAME_FAMILY, METRICS)); assertArrayEquals("wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 4}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 4}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagkDuplicateReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 4}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(4L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(4L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXTagkDuplicateReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGK, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 4}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(4L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(4L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertNull(storage.getColumn(new byte [] {0, 0, 3}, NAME_FAMILY, TAGK)); assertArrayEquals("wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 4}, NAME_FAMILY, TAGK)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 4}, NAME_FAMILY, TAGK)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @Test public void fsckTagvDuplicateReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 4}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(4L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(4L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(2, errors); } @Test public void fsckFIXTagvDuplicateReverse() throws Exception { - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, TAGV, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 4}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(4L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(4L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertNull(storage.getColumn(new byte [] {0, 0, 3}, NAME_FAMILY, TAGV)); assertArrayEquals("wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 4}, NAME_FAMILY, TAGV)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 4}, NAME_FAMILY, TAGV)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -771,36 +771,36 @@ public void fsckFIXTagvDuplicateReverse() throws Exception { */ @Test public void fsckMetricsInconsistentFwdAndDupeRev() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(5, errors); } @Test public void fsckFIXMetricsInconsistentFwdAndDupeRev() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(4, errors); assertArrayEquals("fsck.foo.wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); assertNull(storage.getColumn("foo".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS)); assertNull(storage.getColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS)); assertArrayEquals("bar".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -822,40 +822,40 @@ public void fsckFIXMetricsInconsistentFwdAndDupeRev() throws Exception { */ @Test public void fsckMetricsInconsistentFwdAndInconsistentRev() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(6, errors); } @Test public void fsckFIXMetricsInconsistentFwdAndInconsistentRev() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(4, errors); // diff than above since we remove some forwards early assertArrayEquals("fsck.foo.wtf".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 1}, NAME_FAMILY, METRICS)); assertNull(storage.getColumn("foo".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS)); assertNull(storage.getColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS)); assertArrayEquals("bar".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(0, errors); } @@ -872,34 +872,34 @@ public void fsckFIXMetricsInconsistentFwdAndInconsistentRev() throws Exception { */ @Test public void fsckMetricsInconsistentFwdNoDupes() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 3}); - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), false, false); + UID_TABLE, false, false); assertEquals(3, errors); } @Test public void fsckFixMetricsInconsistentFwdNoDupes() throws Exception { - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn("wtf".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "wtf".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 3}); - storage.addColumn(new byte[] {0, 0, 3}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 3}, NAME_FAMILY, METRICS, "wtf".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(3L)); int errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(2, errors); assertArrayEquals("bar".getBytes(MockBase.ASCII()), - storage.getColumn(new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); + storage.getColumn(UID_TABLE, new byte [] {0, 0, 2}, NAME_FAMILY, METRICS)); errors = (Integer)fsck.invoke(null, client, - "tsdb".getBytes(MockBase.ASCII()), true, false); + UID_TABLE, true, false); assertEquals(0, errors); } @@ -908,37 +908,40 @@ public void fsckFixMetricsInconsistentFwdNoDupes() throws Exception { */ private void setupMockBase() { storage = new MockBase(tsdb, client, true, true, true, true); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, METRICS, Bytes.fromLong(2L)); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGK, Bytes.fromLong(2L)); - storage.addColumn(new byte[] { 0 }, ID_FAMILY, TAGV, Bytes.fromLong(2L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, METRICS, + Bytes.fromLong(2L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGK, + Bytes.fromLong(2L)); + storage.addColumn(UID_TABLE, new byte[] { 0 }, ID_FAMILY, TAGV, + Bytes.fromLong(2L)); // forward mappings - storage.addColumn("foo".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "foo".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 1}); - storage.addColumn("host".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "host".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 1}); - storage.addColumn("web01".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "web01".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 1}); - storage.addColumn("bar".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "bar".getBytes(MockBase.ASCII()), ID_FAMILY, METRICS, new byte[] {0, 0, 2}); - storage.addColumn("dc".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "dc".getBytes(MockBase.ASCII()), ID_FAMILY, TAGK, new byte[] {0, 0, 2}); - storage.addColumn("web02".getBytes(MockBase.ASCII()), ID_FAMILY, + storage.addColumn(UID_TABLE, "web02".getBytes(MockBase.ASCII()), ID_FAMILY, TAGV, new byte[] {0, 0, 2}); // reverse mappings - storage.addColumn(new byte[] {0, 0, 1}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, METRICS, "foo".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] {0, 0, 1}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, TAGK, "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] {0, 0, 1}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 1}, NAME_FAMILY, TAGV, "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, METRICS, "bar".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, TAGK, "dc".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] {0, 0, 2}, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] {0, 0, 2}, NAME_FAMILY, TAGV, "web02".getBytes(MockBase.ASCII())); } diff --git a/test/tree/TestBranch.java b/test/tree/TestBranch.java index 4aff7fc0d5..7c228df094 100644 --- a/test/tree/TestBranch.java +++ b/test/tree/TestBranch.java @@ -22,6 +22,8 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -49,7 +51,9 @@ @PrepareForTest({ TSDB.class, HBaseClient.class, GetRequest.class, PutRequest.class, KeyValue.class, Scanner.class, DeleteRequest.class }) public final class TestBranch { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] TREE_TABLE = "tsdb-tree".getBytes(MockBase.ASCII()); + private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); private MockBase storage; private Tree tree = TestTree.buildTestTree(); final static private Method toStorageJson; @@ -266,28 +270,28 @@ public void compileBranchIdInvalidId() { public void fetchBranch() throws Exception { setupStorage(); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.1".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "owner".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "ops".getBytes(MockBase.ASCII())); @@ -306,15 +310,15 @@ public void fetchBranch() throws Exception { public void fetchBranchNSU() throws Exception { setupStorage(); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web01".getBytes(MockBase.ASCII())); @@ -361,11 +365,11 @@ public void storeBranch() throws Exception { setupStorage(); final Branch branch = buildTestBranch(tree); branch.storeBranch(storage.getTSDB(), tree, true); - assertEquals(3, storage.numRows()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); - final Branch parsed = JSON.parseToObject(storage.getColumn( - new byte[] { 0, 1 }, "branch".getBytes(MockBase.ASCII())), - Branch.class); + assertEquals(3, storage.numRows(TREE_TABLE)); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + final Branch parsed = JSON.parseToObject(storage.getColumn(TREE_TABLE, + new byte[] { 0, 1 }, Tree.TREE_FAMILY(), + "branch".getBytes(MockBase.ASCII())), Branch.class); parsed.setTreeId(1); assertEquals("ROOT", parsed.getDisplayName()); } @@ -403,12 +407,12 @@ public void storeBranchExistingLeaf() throws Exception { qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); branch.storeBranch(storage.getTSDB(), tree, true); - assertEquals(3, storage.numRows()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(3, storage.numRows(TREE_TABLE)); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); assertNull(tree.getCollisions()); - final Branch parsed = JSON.parseToObject(storage.getColumn( - new byte[] { 0, 1 }, "branch".getBytes(MockBase.ASCII())), - Branch.class); + final Branch parsed = JSON.parseToObject(storage.getColumn(TREE_TABLE, + new byte[] { 0, 1 }, Tree.TREE_FAMILY(), + "branch".getBytes(MockBase.ASCII())), Branch.class); parsed.setTreeId(1); assertEquals("ROOT", parsed.getDisplayName()); } @@ -419,16 +423,16 @@ public void storeBranchCollision() throws Exception { final Branch branch = buildTestBranch(tree); Leaf leaf = new Leaf("Alarms", "0101"); byte[] qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); branch.storeBranch(storage.getTSDB(), tree, true); - assertEquals(3, storage.numRows()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(3, storage.numRows(TREE_TABLE)); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); assertEquals(1, tree.getCollisions().size()); - final Branch parsed = JSON.parseToObject(storage.getColumn( - new byte[] { 0, 1 }, "branch".getBytes(MockBase.ASCII())), - Branch.class); + final Branch parsed = JSON.parseToObject(storage.getColumn(TREE_TABLE, + new byte[] { 0, 1 }, Tree.TREE_FAMILY(), + "branch".getBytes(MockBase.ASCII())), Branch.class); parsed.setTreeId(1); assertEquals("ROOT", parsed.getDisplayName()); } @@ -565,6 +569,9 @@ private void setupStorage() throws Exception { final Config config = new Config(false); storage = new MockBase(new TSDB(client, config), client, true, true, true, true); + final List families = new ArrayList(); + families.add(Tree.TREE_FAMILY()); + storage.addTable(TREE_TABLE, families); Branch branch = new Branch(1); TreeMap path = new TreeMap(); @@ -573,18 +580,18 @@ private void setupStorage() throws Exception { path.put(2, "cpu"); branch.prependParentPath(path); branch.setDisplayName("cpu"); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])toStorageJson.invoke(branch)); Leaf leaf = new Leaf("user", "000001000001000001"); byte[] qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); leaf = new Leaf("nice", "000002000002000002"); qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); // child branch @@ -592,13 +599,13 @@ private void setupStorage() throws Exception { path.put(3, "mboard"); branch.prependParentPath(path); branch.setDisplayName("mboard"); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])toStorageJson.invoke(branch)); leaf = new Leaf("Asus", "000003000003000003"); qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); } } diff --git a/test/tree/TestLeaf.java b/test/tree/TestLeaf.java index e9b32ede0c..cb245a7d6e 100644 --- a/test/tree/TestLeaf.java +++ b/test/tree/TestLeaf.java @@ -19,6 +19,9 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; +import java.util.ArrayList; +import java.util.List; + import net.opentsdb.core.TSDB; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; @@ -48,7 +51,9 @@ GetRequest.class, PutRequest.class, DeleteRequest.class, KeyValue.class, Scanner.class }) public final class TestLeaf { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] TREE_TABLE = "tsdb-tree".getBytes(MockBase.ASCII()); + private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); private TSDB tsdb; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; @@ -59,18 +64,21 @@ public void before() throws Exception { tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); + final List families = new ArrayList(); + families.add(Tree.TREE_FAMILY()); + storage.addTable(TREE_TABLE, families); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 1 }, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, new byte[] { 0, 1 }, Tree.TREE_FAMILY(), new Leaf("0", "000001000001000001").columnQualifier(), ("{\"displayName\":\"0\",\"tsuid\":\"000001000001000001\"}") .getBytes(MockBase.ASCII())); @@ -150,7 +158,7 @@ public void storeLeaf() throws Exception { final Tree tree = TestTree.buildTestTree(); assertTrue(leaf.storeLeaf(tsdb, new byte[] { 0, 1 }, tree) .joinUninterruptibly()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test @@ -159,7 +167,7 @@ public void storeLeafExistingSame() throws Exception { final Tree tree = TestTree.buildTestTree(); assertTrue(leaf.storeLeaf(tsdb, new byte[] { 0, 1 }, tree) .joinUninterruptibly()); - assertEquals(1, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test @@ -168,7 +176,7 @@ public void storeLeafCollision() throws Exception { final Tree tree = TestTree.buildTestTree(); assertFalse(leaf.storeLeaf(tsdb, new byte[] { 0, 1 }, tree) .joinUninterruptibly()); - assertEquals(1, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); assertEquals(1, tree.getCollisions().size()); } diff --git a/test/tree/TestTree.java b/test/tree/TestTree.java index a7bdeccce2..048a713945 100644 --- a/test/tree/TestTree.java +++ b/test/tree/TestTree.java @@ -56,6 +56,7 @@ @PrepareForTest({TSDB.class, HBaseClient.class, GetRequest.class, PutRequest.class, KeyValue.class, Scanner.class, DeleteRequest.class}) public final class TestTree { + private final static byte[] TREE_TABLE = "tsdb-tree".getBytes(); private MockBase storage; private TSDB tsdb; private HBaseClient client = mock(HBaseClient.class); @@ -252,8 +253,8 @@ public void flushCollisions() throws Exception { tree.addCollision("010203", "AABBCCDD"); assertNotNull(tree.flushCollisions(storage.getTSDB()) .joinUninterruptibly()); - assertEquals(4, storage.numRows()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1, 1 })); + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1, 1 })); } @Test @@ -263,8 +264,8 @@ public void flushCollisionsDisabled() throws Exception { tree.addCollision("010203", "AABBCCDD"); assertNotNull(tree.flushCollisions(storage.getTSDB()) .joinUninterruptibly()); - assertEquals(4, storage.numRows()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1, 1 })); + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1, 1 })); } @Test @@ -274,8 +275,8 @@ public void flushCollisionsWCollisionExisting() throws Exception { tree.addCollision("010101", "AAAAAA"); assertNotNull(tree.flushCollisions(storage.getTSDB()) .joinUninterruptibly()); - assertEquals(4, storage.numRows()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1, 1 })); + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1, 1 })); } @Test @@ -286,8 +287,8 @@ public void flushNotMatched() throws Exception { tree.addNotMatched("010203", "Failed rule 2:2"); assertNotNull(tree.flushNotMatched(storage.getTSDB()) .joinUninterruptibly()); - assertEquals(4, storage.numRows()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1, 2 })); + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1, 2 })); } @Test @@ -297,8 +298,8 @@ public void flushNotMatchedDisabled() throws Exception { tree.addNotMatched("010203", "Failed rule 2:2"); assertNotNull(tree.flushNotMatched(storage.getTSDB()) .joinUninterruptibly()); - assertEquals(4, storage.numRows()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1, 2 })); + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1, 2 })); } @Test @@ -308,8 +309,8 @@ public void flushNotMatchedWNotMatchedExisting() throws Exception { tree.addNotMatched("010101", "Failed rule 4:4"); assertNotNull(tree.flushNotMatched(storage.getTSDB()) .joinUninterruptibly()); - assertEquals(4, storage.numRows()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1, 2 })); + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1, 2 })); } @Test @@ -349,8 +350,8 @@ public void createNewTree() throws Exception { final int tree_id = tree.createNewTree(storage.getTSDB()) .joinUninterruptibly(); assertEquals(3, tree_id); - assertEquals(5, storage.numRows()); - assertEquals(1, storage.numColumns(new byte[] { 0, 3 })); + assertEquals(5, storage.numRows(TREE_TABLE)); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 3 })); } @Test @@ -362,8 +363,8 @@ public void createNewFirstTree() throws Exception { final int tree_id = tree.createNewTree(storage.getTSDB()) .joinUninterruptibly(); assertEquals(1, tree_id); - assertEquals(1, storage.numRows()); - assertEquals(1, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(1, storage.numRows(TREE_TABLE)); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test (expected = IllegalArgumentException.class) @@ -448,7 +449,7 @@ public void fetchAllCollisions() throws Exception { @Test public void fetchAllCollisionsNone() throws Exception { setupStorage(true, true); - storage.flushRow(new byte[] { 0, 1, 1 }); + storage.flushRow(TREE_TABLE, new byte[] { 0, 1, 1 }); Map collisions = Tree.fetchCollisions(storage.getTSDB(), 1, null).joinUninterruptibly(); assertNotNull(collisions); @@ -506,7 +507,7 @@ public void fetchAllNotMatched() throws Exception { @Test public void fetchAllNotMatchedNone() throws Exception { setupStorage(true, true); - storage.flushRow(new byte[] { 0, 1, 2 }); + storage.flushRow(TREE_TABLE, new byte[] { 0, 1, 2 }); Map not_matched = Tree.fetchNotMatched(storage.getTSDB(), 1, null).joinUninterruptibly(); assertNotNull(not_matched); @@ -553,14 +554,14 @@ public void fetchNotMatchedID655536() throws Exception { public void deleteTree() throws Exception { setupStorage(true, true); - assertEquals(4, storage.numRows()); + assertEquals(4, storage.numRows(TREE_TABLE)); assertNotNull(Tree.deleteTree(storage.getTSDB(), 1, true) .joinUninterruptibly()); byte[] remainingKey = new byte[] {0, 2}; - assertEquals(1, storage.numRows()); - assertNotNull(storage.getColumn( - remainingKey, "tree".getBytes(MockBase.ASCII()))); + assertEquals(1, storage.numRows(TREE_TABLE)); + assertNotNull(storage.getColumn(TREE_TABLE, remainingKey, Tree.TREE_FAMILY(), + "tree".getBytes(MockBase.ASCII()))); } @Test @@ -739,24 +740,28 @@ public static Tree buildTestTree() { private void setupStorage(final boolean default_get, final boolean default_put) throws Exception { storage = new MockBase(tsdb, client, default_get, default_put, true, true); + final List families = new ArrayList(1); + families.add(Tree.TREE_FAMILY()); + storage.addTable(TREE_TABLE, families); byte[] key = new byte[] { 0, 1 }; // set pre-test values - storage.addColumn(key, "tree".getBytes(MockBase.ASCII()), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree".getBytes(MockBase.ASCII()), (byte[])TreetoStorageJson.invoke(buildTestTree())); TreeRule rule = new TreeRule(1); rule.setField("host"); rule.setType(TreeRuleType.TAGK); - storage.addColumn(key, "tree_rule:0:0".getBytes(MockBase.ASCII()), - JSON.serializeToBytes(rule)); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree_rule:0:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); rule = new TreeRule(1); rule.setField(""); rule.setLevel(1); rule.setType(TreeRuleType.METRIC); - storage.addColumn(key, "tree_rule:1:0".getBytes(MockBase.ASCII()), - JSON.serializeToBytes(rule)); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree_rule:1:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); Branch root = new Branch(1); root.setDisplayName("ROOT"); @@ -766,8 +771,8 @@ private void setupStorage(final boolean default_get, // TODO - static Method branch_json = Branch.class.getDeclaredMethod("toStorageJson"); branch_json.setAccessible(true); - storage.addColumn(key, "branch".getBytes(MockBase.ASCII()), - (byte[])branch_json.invoke(root)); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "branch".getBytes(MockBase.ASCII()), (byte[])branch_json.invoke(root)); // tree 2 key = new byte[] { 0, 2 }; @@ -776,29 +781,30 @@ private void setupStorage(final boolean default_get, tree2.setTreeId(2); tree2.setName("2nd Tree"); tree2.setDescription("Other Tree"); - storage.addColumn(key, "tree".getBytes(MockBase.ASCII()), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree".getBytes(MockBase.ASCII()), (byte[])TreetoStorageJson.invoke(tree2)); rule = new TreeRule(2); rule.setField("host"); rule.setType(TreeRuleType.TAGK); - storage.addColumn(key, "tree_rule:0:0".getBytes(MockBase.ASCII()), - JSON.serializeToBytes(rule)); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree_rule:0:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); rule = new TreeRule(2); rule.setField(""); rule.setLevel(1); rule.setType(TreeRuleType.METRIC); - storage.addColumn(key, "tree_rule:1:0".getBytes(MockBase.ASCII()), - JSON.serializeToBytes(rule)); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree_rule:1:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); root = new Branch(2); root.setDisplayName("ROOT"); root_path = new TreeMap(); root_path.put(0, "ROOT"); root.prependParentPath(root_path); - storage.addColumn(key, "branch".getBytes(MockBase.ASCII()), - (byte[])branch_json.invoke(root)); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "branch".getBytes(MockBase.ASCII()), (byte[])branch_json.invoke(root)); // sprinkle in some collisions and no matches for fun // collisions @@ -811,7 +817,8 @@ private void setupStorage(final boolean default_get, byte[] tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.COLLISION_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, qualifier, "AAAAAA".getBytes(MockBase.ASCII())); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, + "AAAAAA".getBytes(MockBase.ASCII())); tsuid = "020202"; qualifier = new byte[Tree.COLLISION_PREFIX().length + @@ -821,7 +828,8 @@ private void setupStorage(final boolean default_get, tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.COLLISION_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, qualifier, "BBBBBB".getBytes(MockBase.ASCII())); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, + "BBBBBB".getBytes(MockBase.ASCII())); // not matched key = new byte[] { 0, 1, 2 }; @@ -833,8 +841,8 @@ private void setupStorage(final boolean default_get, tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.NOT_MATCHED_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, qualifier, "Failed rule 0:0" - .getBytes(MockBase.ASCII())); + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, + "Failed rule 0:0".getBytes(MockBase.ASCII())); tsuid = "020202"; qualifier = new byte[Tree.NOT_MATCHED_PREFIX().length + @@ -844,8 +852,7 @@ private void setupStorage(final boolean default_get, tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.NOT_MATCHED_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, qualifier, "Failed rule 1:1" - .getBytes(MockBase.ASCII())); - + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, + "Failed rule 1:1".getBytes(MockBase.ASCII())); } } diff --git a/test/tree/TestTreeBuilder.java b/test/tree/TestTreeBuilder.java index ff9ba81659..edb5b111bb 100644 --- a/test/tree/TestTreeBuilder.java +++ b/test/tree/TestTreeBuilder.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.TreeMap; import net.opentsdb.core.TSDB; @@ -58,6 +59,8 @@ HBaseClient.class, Scanner.class, GetRequest.class, KeyValue.class, DeleteRequest.class, Tree.class}) public final class TestTreeBuilder { + private final static byte[] TREE_TABLE = "tsdb-tree".getBytes(MockBase.ASCII()); + private final static byte[] BRANCH = "branch".getBytes(MockBase.ASCII()); private TSDB tsdb; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; @@ -93,6 +96,10 @@ public void before() throws Exception { tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); + final List families = new ArrayList(); + families.add(Tree.TREE_FAMILY()); + storage.addTable(TREE_TABLE, families); + treebuilder = new TreeBuilder(storage.getTSDB(), tree); PowerMockito.spy(Tree.class); PowerMockito.doReturn(Deferred.fromResult(tree)).when(Tree.class, @@ -120,25 +127,25 @@ public void before() throws Exception { root.setDisplayName("ROOT"); root_path.put(0, "ROOT"); root.prependParentPath(root_path); - storage.addColumn(root.compileBranchId(), - "branch".getBytes(MockBase.ASCII()), - (byte[])toStorageJson.invoke(root)); + storage.addColumn(TREE_TABLE, root.compileBranchId(), Tree.TREE_FAMILY(), + BRANCH, (byte[])toStorageJson.invoke(root)); } @Test public void processTimeseriesMetaDefaults() throws Exception { treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns(Branch.stringToId( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId( + storage.getColumn(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + Tree.TREE_FAMILY(), + BRANCH), Branch.class); assertNotNull(branch); assertEquals("0", branch.getDisplayName()); - final Leaf leaf = JSON.parseToObject(storage.getColumn(Branch.stringToId( - "00010001A2460001CB54247F72020001BECD000181A800000030"), + final Leaf leaf = JSON.parseToObject(storage.getColumn(TREE_TABLE, Branch.stringToId( + "00010001A2460001CB54247F72020001BECD000181A800000030"), Tree.TREE_FAMILY(), new Leaf("user", "").columnQualifier()), Leaf.class); assertNotNull(leaf); assertEquals("user", leaf.getDisplayName()); @@ -148,8 +155,8 @@ public void processTimeseriesMetaDefaults() throws Exception { public void processTimeseriesMetaNewRoot() throws Exception { storage.flushStorage(); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(1, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test @@ -191,8 +198,8 @@ public void processTimeseriesMetaMiddleNonMatchedRules() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(5, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(5, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId("0001247F72020001BECD000181A800000030"))); } @@ -236,8 +243,8 @@ public void processTimeseriesMetaEndNonMatchedRules() throws Exception { treebuilder.setTree(tree); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -277,8 +284,8 @@ public void processTimeseriesMetaNullMetaOddNumTags() throws Exception { tags_field.set(meta, tags); tags_field.setAccessible(false); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(5, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(5, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010036EBCB0001BECD000181A800000030"))); } @@ -286,15 +293,15 @@ public void processTimeseriesMetaNullMetaOddNumTags() throws Exception { @Test public void processTimeseriesMetaTesting() throws Exception { treebuilder.processTimeseriesMeta(meta, true).joinUninterruptibly(); - assertEquals(1, storage.numRows()); + assertEquals(1, storage.numRows(TREE_TABLE)); } @Test public void processTimeseriesMetaStrict() throws Exception { tree.setStrictMatch(true); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -307,15 +314,15 @@ public void processTimeseriesMetaStrictNoMatch() throws Exception { name.setAccessible(false); tree.setStrictMatch(true); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(1, storage.numRows()); + assertEquals(1, storage.numRows(TREE_TABLE)); } @Test public void processTimeseriesMetaNoSplit() throws Exception { tree.getRules().get(3).get(0).setSeparator(""); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(5, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(5, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001A2460001CB54247F7202CBBF5B09"))); } @@ -323,8 +330,8 @@ public void processTimeseriesMetaNoSplit() throws Exception { public void processTimeseriesMetBadSeparator() throws Exception { tree.getRules().get(3).get(0).setSeparator("."); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(4, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(4, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001A2460001CB54247F7202"))); } @@ -332,8 +339,8 @@ public void processTimeseriesMetBadSeparator() throws Exception { public void processTimeseriesMetaInvalidRegexIdx() throws Exception { tree.getRules().get(1).get(1).setRegexGroupIdx(42); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(6, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(6, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001A246247F72020001BECD000181A800000030"))); } @@ -352,8 +359,8 @@ public void processTimeseriesMetaMetricCustom() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "0001AE805CA50001CB54247F72020001BECD000181A800000030"))); } @@ -390,8 +397,8 @@ public void processTimeseriesMetaMetricCustomEmptyValue() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -412,8 +419,8 @@ public void processTimeseriesMetaTagkCustom() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "0001AE805CA50001CB54247F72020001BECD000181A800000030"))); } @@ -452,8 +459,8 @@ public void processTimeseriesMetaTagkCustomEmptyValue() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -474,8 +481,8 @@ public void processTimeseriesMetaTagkCustomNoField() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -496,8 +503,8 @@ public void processTimeseriesMetaTagvCustom() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "0001AE805CA50001CB54247F72020001BECD000181A800000030"))); } @@ -536,8 +543,8 @@ public void processTimeseriesMetaTagvCustomEmptyValue() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -558,8 +565,8 @@ public void processTimeseriesMetaTagvCustomNoField() throws Exception { tree.addRule(rule); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); - assertEquals(2, storage.numColumns( + assertEquals(7, storage.numRows(TREE_TABLE)); + assertEquals(2, storage.numColumns(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72020001BECD000181A800000030"))); } @@ -568,10 +575,10 @@ public void processTimeseriesMetaTagvCustomNoField() throws Exception { public void processTimeseriesMetaFormatOvalue() throws Exception { tree.getRules().get(1).get(1).setDisplayFormat("OV: {ovalue}"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); + assertEquals(7, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId("00010001A24637E140D5"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + storage.getColumn(TREE_TABLE, Branch.stringToId("00010001A24637E140D5"), + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("OV: web-01.lga.mysite.com", branch.getDisplayName()); } @@ -579,10 +586,10 @@ public void processTimeseriesMetaFormatOvalue() throws Exception { public void processTimeseriesMetaFormatValue() throws Exception { tree.getRules().get(1).get(1).setDisplayFormat("V: {value}"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); + assertEquals(7, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId("00010001A24696026FD8"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + storage.getColumn(TREE_TABLE, Branch.stringToId("00010001A24696026FD8"), + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("V: web", branch.getDisplayName()); } @@ -590,10 +597,10 @@ public void processTimeseriesMetaFormatValue() throws Exception { public void processTimeseriesMetaFormatTSUID() throws Exception { tree.getRules().get(1).get(1).setDisplayFormat("TSUID: {tsuid}"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); + assertEquals(7, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId("00010001A246E0A07086"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + storage.getColumn(TREE_TABLE, Branch.stringToId("00010001A246E0A07086"), + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("TSUID: " + tsuid, branch.getDisplayName()); } @@ -601,10 +608,10 @@ public void processTimeseriesMetaFormatTSUID() throws Exception { public void processTimeseriesMetaFormatTagName() throws Exception { tree.getRules().get(1).get(1).setDisplayFormat("TAGNAME: {tag_name}"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); + assertEquals(7, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId("00010001A2467BFCCB13"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + storage.getColumn(TREE_TABLE, Branch.stringToId("00010001A2467BFCCB13"), + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("TAGNAME: host", branch.getDisplayName()); } @@ -613,10 +620,10 @@ public void processTimeseriesMetaFormatMulti() throws Exception { tree.getRules().get(1).get(1).setDisplayFormat( "{ovalue}:{value}:{tag_name}:{tsuid}"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(7, storage.numRows()); + assertEquals(7, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId("00010001A246E4592083"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + storage.getColumn(TREE_TABLE, Branch.stringToId("00010001A246E4592083"), + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("web-01.lga.mysite.com:web:host:0102030405", branch.getDisplayName()); } @@ -625,11 +632,11 @@ public void processTimeseriesMetaFormatMulti() throws Exception { public void processTimeseriesMetaFormatBadType() throws Exception { tree.getRules().get(3).get(0).setDisplayFormat("Wrong: {tag_name}"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(5, storage.numRows()); + assertEquals(5, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId( + storage.getColumn(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F7202C3165573"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("Wrong: ", branch.getDisplayName()); } @@ -637,11 +644,11 @@ public void processTimeseriesMetaFormatBadType() throws Exception { public void processTimeseriesMetaFormatOverride() throws Exception { tree.getRules().get(3).get(0).setDisplayFormat("OVERRIDE"); treebuilder.processTimeseriesMeta(meta, false).joinUninterruptibly(); - assertEquals(5, storage.numRows()); + assertEquals(5, storage.numRows(TREE_TABLE)); final Branch branch = JSON.parseToObject( - storage.getColumn(Branch.stringToId( + storage.getColumn(TREE_TABLE, Branch.stringToId( "00010001A2460001CB54247F72024E3D0BCC"), - "branch".getBytes(MockBase.ASCII())), Branch.class); + Tree.TREE_FAMILY(), BRANCH), Branch.class); assertEquals("OVERRIDE", branch.getDisplayName()); } } diff --git a/test/tree/TestTreeRule.java b/test/tree/TestTreeRule.java index bceabe7ecc..3c3010e405 100644 --- a/test/tree/TestTreeRule.java +++ b/test/tree/TestTreeRule.java @@ -18,6 +18,8 @@ import static org.junit.Assert.assertTrue; import static org.powermock.api.mockito.PowerMockito.mock; +import java.util.ArrayList; +import java.util.List; import java.util.regex.PatternSyntaxException; import net.opentsdb.core.TSDB; @@ -48,6 +50,7 @@ PutRequest.class, KeyValue.class, Scanner.class, DeleteRequest.class, Tree.class}) public final class TestTreeRule { + private final static byte[] TREE_TABLE = "tsdb-tree".getBytes(MockBase.ASCII()); private TSDB tsdb; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; @@ -239,7 +242,7 @@ public void storeRule() throws Exception { rule.setType(TreeRuleType.METRIC); rule.setNotes("Just some notes"); assertTrue(rule.syncToStorage(storage.getTSDB(), false).joinUninterruptibly()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test @@ -250,9 +253,9 @@ public void storeRuleMege() throws Exception { rule.setOrder(1); rule.setNotes("Just some notes"); assertTrue(rule.syncToStorage(storage.getTSDB(), false).joinUninterruptibly()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); final TreeRule stored = JSON.parseToObject( - storage.getColumn(new byte[] { 0, 1 }, + storage.getColumn(TREE_TABLE, new byte[] { 0, 1 }, Tree.TREE_FAMILY(), "tree_rule:2:1".getBytes(MockBase.ASCII())), TreeRule.class); assertEquals("Host owner", stored.getDescription()); assertEquals("Just some notes", stored.getNotes()); @@ -386,14 +389,14 @@ public void storeRuleInvalidRegexIdx() throws Exception { public void deleteRule() throws Exception { setupStorage(); assertNotNull(TreeRule.deleteRule(storage.getTSDB(), 1, 2, 1)); - assertEquals(1, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test public void deleteAllRules() throws Exception { setupStorage(); TreeRule.deleteAllRules(storage.getTSDB(), 1); - assertEquals(1, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test @@ -413,7 +416,10 @@ public void getQualifier() throws Exception { */ private void setupStorage() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); - + final List families = new ArrayList(); + families.add(Tree.TREE_FAMILY()); + storage.addTable(TREE_TABLE, families); + final TreeRule stored_rule = new TreeRule(1); stored_rule.setLevel(2); stored_rule.setOrder(1); @@ -424,11 +430,11 @@ private void setupStorage() throws Exception { stored_rule.setNotes("Owner of the host machine"); // pretend there's a tree definition in the storage row - storage.addColumn(new byte[] { 0, 1 }, "tree".getBytes(MockBase.ASCII()), - new byte[] { 1 }); + storage.addColumn(TREE_TABLE, new byte[] { 0, 1 }, Tree.TREE_FAMILY(), + "tree".getBytes(MockBase.ASCII()), new byte[] { 1 }); // add a rule to the row - storage.addColumn(new byte[] { 0, 1 }, + storage.addColumn(TREE_TABLE, new byte[] { 0, 1 }, Tree.TREE_FAMILY(), "tree_rule:2:1".getBytes(MockBase.ASCII()), JSON.serializeToBytes(stored_rule)); } diff --git a/test/tsd/TestAnnotationRpc.java b/test/tsd/TestAnnotationRpc.java index 2335ce1fe6..596f7acbe9 100644 --- a/test/tsd/TestAnnotationRpc.java +++ b/test/tsd/TestAnnotationRpc.java @@ -116,6 +116,7 @@ public void badMethod() throws Exception { @Test public void get() throws Exception { + storage.dumpToSystemOut(); HttpQuery query = NettyMocks.getQuery(tsdb, "/api/annotation?tsuid=000001000001000001&start_time=1388450562"); rpc.execute(tsdb, query); diff --git a/test/tsd/TestTreeRpc.java b/test/tsd/TestTreeRpc.java index 864afecdad..4ec9fb7c28 100644 --- a/test/tsd/TestTreeRpc.java +++ b/test/tsd/TestTreeRpc.java @@ -18,6 +18,8 @@ import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.TreeMap; import net.opentsdb.core.TSDB; @@ -60,7 +62,8 @@ @PrepareForTest({ TSDB.class, HBaseClient.class, GetRequest.class, Tree.class, PutRequest.class, KeyValue.class, Scanner.class, DeleteRequest.class }) public final class TestTreeRpc { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] TREE_TABLE = "tsdb-tree".getBytes(); private TSDB tsdb; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; @@ -121,6 +124,9 @@ public void before() throws Exception { final Config config = new Config(false); tsdb = new TSDB(client, config); storage = new MockBase(tsdb, client, true, true, true, true); + final List families = new ArrayList(1); + families.add(Tree.TREE_FAMILY()); + storage.addTable(TREE_TABLE, families); } @Test @@ -188,7 +194,7 @@ public void handleTreeQSCreate() throws Exception { "/api/tree?name=NewTree&method_override=post"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals(1, storage.numColumns(new byte[] { 0, 3 })); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 3 })); } @Test (expected = BadRequestException.class) @@ -216,7 +222,7 @@ public void handleTreePOSTCreate() throws Exception { "/api/tree", "{\"name\":\"New Tree\"}"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals(1, storage.numColumns(new byte[] { 0, 3 })); + assertEquals(1, storage.numColumns(TREE_TABLE, new byte[] { 0, 3 })); } @Test @@ -311,14 +317,14 @@ public void handleTreeQSDeleteDefault() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/tree?treeid=1&method_override=delete"); // make sure the root is there BEFORE we delete - assertEquals(4, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(4, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); // make sure the definition is still there but the root is gone - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); - assertEquals(-1, storage.numColumns( + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8"))); - assertEquals(-1, storage.numColumns( + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8BF992A99"))); } @@ -328,14 +334,14 @@ public void handleTreeQSDeleteDefinition() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/tree?treeid=1&method_override=delete&definition=true"); // make sure the root is there BEFORE we delete - assertEquals(4, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(4, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); // make sure the definition has been deleted too - assertEquals(-1, storage.numColumns(new byte[] { 0, 1 })); - assertEquals(-1, storage.numColumns( + assertEquals(-1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8"))); - assertEquals(-1, storage.numColumns( + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8BF992A99"))); } @@ -345,14 +351,14 @@ public void handleTreePOSTDeleteDefault() throws Exception { HttpQuery query = NettyMocks.deleteQuery(tsdb, "/api/tree", "{\"treeId\":1}"); // make sure the root is there BEFORE we delete - assertEquals(4, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(4, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); // make sure the definition is still there but the root is gone - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); - assertEquals(-1, storage.numColumns( + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8"))); - assertEquals(-1, storage.numColumns( + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8BF992A99"))); } @@ -362,14 +368,14 @@ public void handleTreePOSTDeleteDefinition() throws Exception { HttpQuery query = NettyMocks.deleteQuery(tsdb, "/api/tree", "{\"treeId\":1,\"definition\":true}"); // make sure the root is there BEFORE we delete - assertEquals(4, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(4, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); // make sure the definition has been deleted too - assertEquals(-1, storage.numColumns(new byte[] { 0, 1 })); - assertEquals(-1, storage.numColumns( + assertEquals(-1, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8"))); - assertEquals(-1, storage.numColumns( + assertEquals(-1, storage.numColumns(TREE_TABLE, Branch.stringToId("00010001BECD000181A8BF992A99"))); } @@ -624,7 +630,7 @@ public void handleRuleQSDelete() throws Exception { "/api/tree/rule?treeid=1&level=1&order=0&method_override=delete"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test (expected = BadRequestException.class) @@ -642,7 +648,7 @@ public void handleRuleDELETE() throws Exception { "/api/tree/rule", "{\"treeId\":1,\"level\":1,\"order\":0}"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(3, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(3, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test (expected = BadRequestException.class) @@ -671,8 +677,9 @@ public void handleRulesPOST() throws Exception { "\"tagk\",\"field\":\"host\"}]"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(5, storage.numColumns(new byte[] { 0, 1 })); - final String rule = new String(storage.getColumn(new byte[] { 0, 1 }, + assertEquals(5, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + final String rule = new String(storage.getColumn(TREE_TABLE, + new byte[] { 0, 1 }, Tree.TREE_FAMILY(), "tree_rule:0:0".getBytes(MockBase.ASCII())), MockBase.ASCII()); assertTrue(rule.contains("\"type\":\"METRIC\"")); assertTrue(rule.contains("description\":\"Host Name\"")); @@ -696,8 +703,9 @@ public void handleRulesPUT() throws Exception { "\"tagk\",\"field\":\"host\"}]"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(5, storage.numColumns(new byte[] { 0, 1 })); - final String rule = new String(storage.getColumn(new byte[] { 0, 1 }, + assertEquals(5, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); + final String rule = new String(storage.getColumn(TREE_TABLE, + new byte[] { 0, 1 }, Tree.TREE_FAMILY(), "tree_rule:0:0".getBytes(MockBase.ASCII())), MockBase.ASCII()); assertTrue(rule.contains("\"type\":\"METRIC\"")); assertFalse(rule.contains("\"description\":\"Host Name\"")); @@ -721,7 +729,7 @@ public void handleRulesDeleteQS() throws Exception { "/api/tree/rules?treeid=1&method_override=delete"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test @@ -731,7 +739,7 @@ public void handleRulesDelete() throws Exception { "/api/tree/rules?treeid=1", ""); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(2, storage.numColumns(new byte[] { 0, 1 })); + assertEquals(2, storage.numColumns(TREE_TABLE, new byte[] { 0, 1 })); } @Test (expected = BadRequestException.class) @@ -851,7 +859,7 @@ public void handleTestNSU() throws Exception { setupStorage(); setupBranch(); setupTSMeta(); - storage.flushRow(new byte[] { 0, 0, 2 }); + storage.flushRow("tsdb-uid".getBytes(), new byte[] { 0, 0, 2 }); HttpQuery query = NettyMocks.getQuery(tsdb, "/api/tree/test?treeid=1&tsuids=000001000001000001000002000002"); rpc.execute(tsdb, query); @@ -1139,7 +1147,7 @@ public void handleNotMatchedBadMethod() throws Exception { * child branch, leaves and some collisions and no matches. These are used for * most of the tests so they're all here. */ - private void setupStorage() throws Exception { + private void setupStorage() throws Exception { Tree tree = TestTree.buildTestTree(); // store root @@ -1148,13 +1156,14 @@ private void setupStorage() throws Exception { root.setDisplayName("ROOT"); root_path.put(0, "ROOT"); root.prependParentPath(root_path); - storage.addColumn(root.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, root.compileBranchId(), Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])branchToStorageJson.invoke(root)); // store the first tree byte[] key = new byte[] { 0, 1 }; - storage.addColumn(key, Tree.TREE_FAMILY(), "tree".getBytes(MockBase.ASCII()), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree".getBytes(MockBase.ASCII()), (byte[])TreetoStorageJson.invoke(TestTree.buildTestTree())); TreeRule rule = new TreeRule(1); @@ -1162,7 +1171,7 @@ private void setupStorage() throws Exception { rule.setDescription("Hostname rule"); rule.setType(TreeRuleType.TAGK); rule.setDescription("Host Name"); - storage.addColumn(key, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), "tree_rule:0:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); @@ -1171,7 +1180,7 @@ private void setupStorage() throws Exception { rule.setLevel(1); rule.setNotes("Metric rule"); rule.setType(TreeRuleType.METRIC); - storage.addColumn(key, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), "tree_rule:1:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); @@ -1180,7 +1189,7 @@ private void setupStorage() throws Exception { root_path = new TreeMap(); root_path.put(0, "ROOT"); root.prependParentPath(root_path); - storage.addColumn(key, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])branchToStorageJson.invoke(root)); @@ -1191,13 +1200,14 @@ private void setupStorage() throws Exception { tree2.setTreeId(2); tree2.setName("2nd Tree"); tree2.setDescription("Other Tree"); - storage.addColumn(key, Tree.TREE_FAMILY(), "tree".getBytes(MockBase.ASCII()), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), + "tree".getBytes(MockBase.ASCII()), (byte[])TreetoStorageJson.invoke(tree2)); rule = new TreeRule(2); rule.setField("host"); rule.setType(TreeRuleType.TAGK); - storage.addColumn(key, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), "tree_rule:0:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); @@ -1205,7 +1215,7 @@ private void setupStorage() throws Exception { rule.setField(""); rule.setLevel(1); rule.setType(TreeRuleType.METRIC); - storage.addColumn(key, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), "tree_rule:1:0".getBytes(MockBase.ASCII()), JSON.serializeToBytes(rule)); @@ -1214,7 +1224,7 @@ private void setupStorage() throws Exception { root_path = new TreeMap(); root_path.put(0, "ROOT"); root.prependParentPath(root_path); - storage.addColumn(key, Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])branchToStorageJson.invoke(root)); @@ -1229,7 +1239,7 @@ private void setupStorage() throws Exception { byte[] tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.COLLISION_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, Tree.TREE_FAMILY(), qualifier, + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, "AAAAAA".getBytes(MockBase.ASCII())); tsuid = "020202"; @@ -1240,7 +1250,7 @@ private void setupStorage() throws Exception { tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.COLLISION_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, Tree.TREE_FAMILY(), qualifier, + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, "BBBBBB".getBytes(MockBase.ASCII())); // not matched @@ -1253,7 +1263,7 @@ private void setupStorage() throws Exception { tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.NOT_MATCHED_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, Tree.TREE_FAMILY(), qualifier, + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, "Failed rule 0:0".getBytes(MockBase.ASCII())); tsuid = "020202"; @@ -1264,7 +1274,7 @@ private void setupStorage() throws Exception { tsuid_bytes = UniqueId.stringToUid(tsuid); System.arraycopy(tsuid_bytes, 0, qualifier, Tree.NOT_MATCHED_PREFIX().length, tsuid_bytes.length); - storage.addColumn(key, Tree.TREE_FAMILY(), qualifier, + storage.addColumn(TREE_TABLE, key, Tree.TREE_FAMILY(), qualifier, "Failed rule 1:1".getBytes(MockBase.ASCII())); // drop some branches in for tree 1 @@ -1275,18 +1285,18 @@ private void setupStorage() throws Exception { path.put(2, "cpu"); branch.prependParentPath(path); branch.setDisplayName("cpu"); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])branchToStorageJson.invoke(branch)); Leaf leaf = new Leaf("user", "000001000001000001"); qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); leaf = new Leaf("nice", "000002000002000002"); qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); // child branch @@ -1294,13 +1304,13 @@ private void setupStorage() throws Exception { path.put(3, "mboard"); branch.prependParentPath(path); branch.setDisplayName("mboard"); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), "branch".getBytes(MockBase.ASCII()), (byte[])branchToStorageJson.invoke(branch)); leaf = new Leaf("Asus", "000003000003000003"); qualifier = leaf.columnQualifier(); - storage.addColumn(branch.compileBranchId(), Tree.TREE_FAMILY(), + storage.addColumn(TREE_TABLE, branch.compileBranchId(), Tree.TREE_FAMILY(), qualifier, (byte[])LeaftoStorageJson.invoke(leaf)); } @@ -1310,13 +1320,13 @@ private void setupStorage() throws Exception { * find their name maps. */ private void setupBranch() { - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn("tsdb-uid".getBytes(), new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn("tsdb-uid".getBytes(), new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn("tsdb-uid".getBytes(), new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web01".getBytes(MockBase.ASCII())); } @@ -1327,41 +1337,47 @@ private void setupBranch() { * parsed through the tree. */ private void setupTSMeta() throws Exception { + final byte[] meta_table = "tsdb-meta".getBytes(); + final byte[] uid_table = "tsdb-uid".getBytes(); + final List families = new ArrayList(1); + families.add(TSMeta.FAMILY); + storage.addTable(meta_table, families); final TSMeta meta = new TSMeta("000001000001000001000002000002"); - storage.addColumn(UniqueId.stringToUid("000001000001000001000002000002"), + storage.addColumn(meta_table, + UniqueId.stringToUid("000001000001000001000002000002"), NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), (byte[])TSMetagetStorageJSON.invoke(meta)); final UIDMeta metric = new UIDMeta(UniqueIdType.METRIC, new byte[] { 0, 0, 1 }, "sys.cpu.0"); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), (byte[])UIDMetagetStorageJSON.invoke(metric)); final UIDMeta tagk1 = new UIDMeta(UniqueIdType.TAGK, new byte[] { 0, 0, 1 }, "host"); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), (byte[])UIDMetagetStorageJSON.invoke(tagk1)); final UIDMeta tagv1 = new UIDMeta(UniqueIdType.TAGV, new byte[] { 0, 0, 1 }, "web-01.lga.mysite.com"); - storage.addColumn(new byte[] { 0, 0, 1 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), (byte[])UIDMetagetStorageJSON.invoke(tagv1)); final UIDMeta tagk2 = new UIDMeta(UniqueIdType.TAGK, new byte[] { 0, 0, 2 }, "type"); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), (byte[])UIDMetagetStorageJSON.invoke(tagk2)); final UIDMeta tagv2 = new UIDMeta(UniqueIdType.TAGV, new byte[] { 0, 0, 2 }, "user"); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), (byte[])UIDMetagetStorageJSON.invoke(tagv2)); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "type".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, NAME_FAMILY, + storage.addColumn(uid_table, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "user".getBytes(MockBase.ASCII())); } diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index f306391689..1e7c5240b6 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -14,12 +14,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Field; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; @@ -39,7 +40,6 @@ 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; @@ -54,7 +54,9 @@ HBaseClient.class, RowLock.class, UniqueIdRpc.class, KeyValue.class, GetRequest.class, Scanner.class, UniqueId.class}) public final class TestUniqueIdRpc { - private static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); + private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); + private final static byte[] META_TABLE = "tsdb-meta".getBytes(); private TSDB tsdb = null; private HBaseClient client = mock(HBaseClient.class); private UniqueId metrics = mock(UniqueId.class); @@ -915,17 +917,17 @@ private void setupUID() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 3 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.2".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.0\"," + @@ -955,139 +957,152 @@ private void setupTSUID() throws Exception { tagv.set(tsdb, tag_values); storage = new MockBase(tsdb, client, true, true, true, true); - storage.setFamily(NAME_FAMILY); + final List families = new ArrayList(1); + families.add(TSMeta.FAMILY); + storage.addTable(META_TABLE, families); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.0".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"METRIC\",\"name\":\"sys.cpu.0\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"System CPU\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "metrics".getBytes(MockBase.ASCII()), "sys.cpu.2".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "metric_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"METRIC\",\"name\":\"sys.cpu.2\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"System CPU\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "host".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGK\",\"name\":\"host\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Host server name\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk".getBytes(MockBase.ASCII()), "datacenter".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagk_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"TAGK\",\"name\":\"datacenter\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Host server name\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 1 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000001\",\"type\":\"TAGV\",\"name\":\"web01\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Web server 1\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 3 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "web02".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 3 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 3 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000003\",\"type\":\"TAGV\",\"name\":\"web02\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Web server 1\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv".getBytes(MockBase.ASCII()), "dc01".getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2 }, + storage.addColumn(UID_TABLE, new byte[] { 0, 0, 2 }, NAME_FAMILY, "tagv_meta".getBytes(MockBase.ASCII()), ("{\"uid\":\"000002\",\"type\":\"TAGV\",\"name\":\"dc01\"," + "\"description\":\"Description\",\"notes\":\"MyNotes\",\"created\":" + "1328140801,\"displayName\":\"Web server 1\"}").getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSMeta.FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"displayName\":\"Display\"," + "\"description\":\"Description\",\"notes\":\"Notes\",\"created" + "\":1366671600,\"custom\":null,\"units\":\"\",\"dataType\":" + "\"Data\",\"retention\":42,\"max\":1.0,\"min\":\"NaN\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSMeta.FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 2 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 2 }, + TSMeta.FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000002000001000001000002000002\",\"displayName\":\"Display\"," + "\"description\":\"Description\",\"notes\":\"Notes\",\"created" + "\":1366671600,\"custom\":null,\"units\":\"\",\"dataType\":" + "\"Data\",\"retention\":42,\"max\":1.0,\"min\":\"NaN\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 2 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 2, 0, 0, 2 }, + TSMeta.FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 2, 0, 0, 2 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 2, 0, 0, 2 }, + TSMeta.FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000002000001000003000002000002\",\"displayName\":\"Display\"," + "\"description\":\"Description\",\"notes\":\"Notes\",\"created" + "\":1366671600,\"custom\":null,\"units\":\"\",\"dataType\":" + "\"Data\",\"retention\":42,\"max\":1.0,\"min\":\"NaN\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 2, 0, 0, 2 }, - NAME_FAMILY, + storage.addColumn(META_TABLE, + new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 2, 0, 0, 2 }, + TSMeta.FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); when(metrics.getId("sys.cpu.0")).thenReturn(new byte[] { 0, 0, 1 }); - when(metrics.getIdAsync("sys.cpu.0")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(metrics.getIdAsync("sys.cpu.0")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) .thenReturn(Deferred.fromResult("sys.cpu.0")); when(metrics.getId("sys.cpu.2")).thenReturn(new byte[] { 0, 0, 2 }); - when(metrics.getIdAsync("sys.cpu.2")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(metrics.getIdAsync("sys.cpu.2")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 2 })); when(metrics.getNameAsync(new byte[] { 0, 0, 2 })) .thenReturn(Deferred.fromResult("sys.cpu.2")); when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getIdAsync("host")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_names.getIdAsync("host")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); when(tag_names.getNameAsync(new byte[] { 0, 0, 1 })) .thenReturn(Deferred.fromResult("host")); when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getIdAsync("web01")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_values.getIdAsync("web01")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 1 })); when(tag_values.getNameAsync(new byte[] { 0, 0, 1 })) .thenReturn(Deferred.fromResult("web01")); when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 3 }); - when(tag_values.getIdAsync("web02")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); + when(tag_values.getIdAsync("web02")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 3 })); when(tag_values.getNameAsync(new byte[] { 0, 0, 3 })) .thenReturn(Deferred.fromResult("web02")); when(tag_names.getId("datacenter")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_names.getIdAsync("datacenter")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(tag_names.getIdAsync("datacenter")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 2 })); when(tag_names.getNameAsync(new byte[] { 0, 0, 2 })) .thenReturn(Deferred.fromResult("datacenter")); when(tag_values.getId("dc01")).thenReturn(new byte[] { 0, 0, 2 }); - when(tag_values.getIdAsync("dc01")).thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); + when(tag_values.getIdAsync("dc01")).thenReturn( + Deferred.fromResult(new byte[] { 0, 0, 2 })); when(tag_values.getNameAsync(new byte[] { 0, 0, 2 })) .thenReturn(Deferred.fromResult("dc01")); From 3ead45a1d2c282863fe9cd6c69f48be95a66459d Mon Sep 17 00:00:00 2001 From: Jason Harvey Date: Thu, 10 Sep 2015 11:08:03 -0800 Subject: [PATCH 217/826] Update AsyncHBase version. --- third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 new file mode 100644 index 0000000000..9d3a066783 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 @@ -0,0 +1 @@ +84b8410ba9003ecadbeececb02943ee1 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index cd2cd0df04..d04740e54c 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.0-20150904.040751-2 +ASYNCHBASE_VERSION := 1.7.0-20150910.030815-3 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.0-SNAPSHOT/ From d119af614a6849672f963f16a4098000a8b9067e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 9 Sep 2015 22:52:50 -0700 Subject: [PATCH 218/826] Add the UniqueId.deleteAsync() method as desired by a number of users. This will remove ONLY the UID mappings (and UIDMeta) from the UID table, not associated data. A future PR will have a flag to let queries handle the NoSuchUniqueId exceptions. Signed-off-by: Chris Larsen --- src/uid/UniqueId.java | 100 ++++++++++++++- test/uid/TestUniqueId.java | 244 ++++++++++++++++++++++++++++--------- 2 files changed, 288 insertions(+), 56 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 780645b61f..e94c988648 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -43,7 +43,6 @@ import net.opentsdb.core.Internal; import net.opentsdb.core.TSDB; import net.opentsdb.meta.UIDMeta; -import net.opentsdb.stats.StatsCollector; /** * Represents a table of Unique IDs, manages the lookup and creation of IDs. @@ -940,6 +939,105 @@ public void rename(final String oldname, final String newname) { // Success! } + /** + * Attempts to remove the mappings for the given string from the UID table + * as well as the cache. If used, the caller should remove the entry from all + * TSD caches as well. + *

    + * WARNING: This is a best attempt only method in that we'll lookup the UID + * for the given string, then issue two delete requests, one for each mapping. + * If either mapping fails then the cache can be re-populated later on with + * stale data. In that case, please run the FSCK utility. + *

    + * WARNING 2: This method will NOT delete time series data or TSMeta data + * associated with the UIDs. It only removes them from the UID table. Deleting + * a metric is generally safe as you won't query over it in the future. But + * deleting tag keys or values can cause queries to fail if they find data + * without a corresponding name. + * + * @param name The name of the UID to delete + * @return A deferred to wait on for completion. The result will be null if + * successful, an exception otherwise. + * @throws NoSuchUniqueName if the UID string did not exist in storage + * @throws IllegalStateException if the TSDB wasn't set for this UID object + * @since 2.2 + */ + public Deferred deleteAsync(final String name) { + if (tsdb == null) { + throw new IllegalStateException("The TSDB is null for this UID object."); + } + final byte[] uid = new byte[id_width]; + final ArrayList> deferreds = + new ArrayList>(2); + + /** Catches errors and still cleans out the cache */ + class ErrCB implements Callback { + @Override + public Object call(final Exception ex) throws Exception { + name_cache.remove(name); + id_cache.remove(fromBytes(uid)); + LOG.error("Failed to delete " + fromBytes(kind) + " UID " + name + + " but still cleared the cache", ex); + return ex; + } + } + + /** Used to wait on the group of delete requests */ + class GroupCB implements Callback, ArrayList> { + @Override + public Deferred call(final ArrayList response) + throws Exception { + name_cache.remove(name); + id_cache.remove(fromBytes(uid)); + LOG.info("Successfully deleted " + fromBytes(kind) + " UID " + name); + return Deferred.fromResult(null); + } + } + + /** Called after fetching the UID from storage */ + class LookupCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] stored_uid) throws Exception { + if (stored_uid == null) { + return Deferred.fromError(new NoSuchUniqueName(kind(), name)); + } + System.arraycopy(stored_uid, 0, uid, 0, id_width); + final DeleteRequest forward = + new DeleteRequest(table, toBytes(name), ID_FAMILY, kind); + deferreds.add(tsdb.getClient().delete(forward)); + + final DeleteRequest reverse = + new DeleteRequest(table, uid, NAME_FAMILY, kind); + deferreds.add(tsdb.getClient().delete(reverse)); + + final DeleteRequest meta = new DeleteRequest(table, uid, NAME_FAMILY, + toBytes((type.toString().toLowerCase() + "_meta"))); + deferreds.add(tsdb.getClient().delete(meta)); + return Deferred.group(deferreds).addCallbackDeferring(new GroupCB()); + } + } + + final byte[] cached_uid = name_cache.get(name); + if (cached_uid == null) { + return getIdFromHBase(name).addCallbackDeferring(new LookupCB()) + .addErrback(new ErrCB()); + } + System.arraycopy(cached_uid, 0, uid, 0, id_width); + final DeleteRequest forward = + new DeleteRequest(table, toBytes(name), ID_FAMILY, kind); + deferreds.add(tsdb.getClient().delete(forward)); + + final DeleteRequest reverse = + new DeleteRequest(table, uid, NAME_FAMILY, kind); + deferreds.add(tsdb.getClient().delete(reverse)); + + final DeleteRequest meta = new DeleteRequest(table, uid, NAME_FAMILY, + toBytes((type.toString().toLowerCase() + "_meta"))); + deferreds.add(tsdb.getClient().delete(meta)); + return Deferred.group(deferreds).addCallbackDeferring(new GroupCB()) + .addErrback(new ErrCB()); + } + /** The start row to scan on empty search strings. `!' = first ASCII char. */ private static final byte[] START_ROW = new byte[] { '!' }; diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index 682dcac70a..a35aeeae3c 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -22,6 +22,7 @@ import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; +import net.opentsdb.storage.MockBase; import net.opentsdb.utils.Config; import org.hbase.async.AtomicIncrementRequest; @@ -69,25 +70,32 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({ HBaseClient.class, TSDB.class, Config.class, - RandomUniqueId.class, Const.class }) +@PrepareForTest({ HBaseClient.class, TSDB.class, Config.class, Scanner.class, + RandomUniqueId.class, Const.class, Deferred.class }) public final class TestUniqueId { - - private HBaseClient client = mock(HBaseClient.class); - private static final byte[] table = { 't', 'a', 'b', 'l', 'e' }; + private static final byte[] table = { 't', 's', 'd', 'b', '-', 'u', 'i', 'd' }; private static final byte[] ID = { 'i', 'd' }; - private UniqueId uid; - private static final String kind = "metric"; - private static final byte[] kind_array = { 'm', 'e', 't', 'r', 'i', 'c' }; + private static final byte[] NAME = { 'n', 'a', 'm', 'e' }; + private static final String METRIC = "metric"; + private static final byte[] METRIC_ARRAY = { 'm', 'e', 't', 'r', 'i', 'c' }; + private static final String TAGK = "tagk"; + private static final byte[] TAGK_ARRAY = { 't', 'a', 'g', 'k' }; + private static final String TAGV = "tagv"; + private static final byte[] TAGV_ARRAY = { 't', 'a', 'g', 'v' }; + private static final byte[] UID = new byte[] { 0, 0, 1 }; + private TSDB tsdb = mock(TSDB.class); + private HBaseClient client = mock(HBaseClient.class); + private UniqueId uid; + private MockBase storage; @Test(expected=IllegalArgumentException.class) public void testCtorZeroWidth() { - uid = new UniqueId(client, table, kind, 0); + uid = new UniqueId(client, table, METRIC, 0); } @Test(expected=IllegalArgumentException.class) public void testCtorNegativeWidth() { - uid = new UniqueId(client, table, kind, -1); + uid = new UniqueId(client, table, METRIC, -1); } @Test(expected=IllegalArgumentException.class) @@ -97,29 +105,29 @@ public void testCtorEmptyKind() { @Test(expected=IllegalArgumentException.class) public void testCtorLargeWidth() { - uid = new UniqueId(client, table, kind, 9); + uid = new UniqueId(client, table, METRIC, 9); } @Test public void kindEqual() { - uid = new UniqueId(client, table, kind, 3); - assertEquals(kind, uid.kind()); + uid = new UniqueId(client, table, METRIC, 3); + assertEquals(METRIC, uid.kind()); } @Test public void widthEqual() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); assertEquals(3, uid.width()); } @Test public void getNameSuccessfulHBaseLookup() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 0, 'a', 0x42 }; final byte[] byte_name = { 'f', 'o', 'o' }; ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(id, ID, kind_array, byte_name)); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); when(client.get(anyGet())) .thenReturn(Deferred.fromResult(kvs)); @@ -137,14 +145,14 @@ public void getNameSuccessfulHBaseLookup() { @Test public void getNameWithErrorDuringHBaseLookup() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 0, 'a', 0x42 }; final byte[] byte_name = { 'f', 'o', 'o' }; HBaseException hbe = mock(HBaseException.class); ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(id, ID, kind_array, byte_name)); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); when(client.get(anyGet())) .thenThrow(hbe) .thenReturn(Deferred.fromResult(kvs)); @@ -169,7 +177,7 @@ public void getNameWithErrorDuringHBaseLookup() { @Test(expected=NoSuchUniqueId.class) public void getNameForNonexistentId() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); when(client.get(anyGet())) .thenReturn(Deferred.fromResult(new ArrayList(0))); @@ -179,19 +187,19 @@ public void getNameForNonexistentId() { @Test(expected=IllegalArgumentException.class) public void getNameWithInvalidId() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); uid.getName(new byte[] { 1 }); } @Test public void getIdSuccessfulHBaseLookup() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 0, 'a', 0x42 }; final byte[] byte_name = { 'f', 'o', 'o' }; ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(byte_name, ID, kind_array, id)); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); when(client.get(anyGet())) .thenReturn(Deferred.fromResult(kvs)); @@ -212,12 +220,12 @@ public void getIdSuccessfulHBaseLookup() { // The table contains IDs encoded on 2 bytes but the instance wants 3. @Test(expected=IllegalStateException.class) public void getIdMisconfiguredWidth() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 'a', 0x42 }; final byte[] byte_name = { 'f', 'o', 'o' }; ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(byte_name, ID, kind_array, id)); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); when(client.get(anyGet())) .thenReturn(Deferred.fromResult(kvs)); @@ -226,7 +234,7 @@ public void getIdMisconfiguredWidth() { @Test(expected=NoSuchUniqueName.class) public void getIdForNonexistentName() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); when(client.get(anyGet())) // null => ID doesn't exist. .thenReturn(Deferred.>fromResult(null)); @@ -237,12 +245,12 @@ public void getIdForNonexistentName() { @Test public void getOrCreateIdWithExistingId() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 0, 'a', 0x42 }; final byte[] byte_name = { 'f', 'o', 'o' }; ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(byte_name, ID, kind_array, id)); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); when(client.get(anyGet())) .thenReturn(Deferred.fromResult(kvs)); @@ -259,7 +267,7 @@ public void getOrCreateIdWithExistingId() { @Test // Test the creation of an ID with no problem. public void getOrCreateIdAssignIdWithSuccess() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 0, 0, 5 }; final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); @@ -289,13 +297,12 @@ public void getOrCreateIdAssignIdWithSuccess() { // Reverse + forward mappings. verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); } - - @PrepareForTest({HBaseClient.class, UniqueId.class}) + @Test // Test the creation of an ID when unable to increment MAXID public void getOrCreateIdUnableToIncrementMaxId() throws Exception { PowerMockito.mockStatic(Thread.class); - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); when(client.get(anyGet())) // null => ID doesn't exist. .thenReturn(Deferred.>fromResult(null)); @@ -315,8 +322,7 @@ public void getOrCreateIdUnableToIncrementMaxId() throws Exception { } @Test // Test the creation of an ID with a race condition. - @PrepareForTest({HBaseClient.class, Deferred.class}) - public void getOrCreateIdAssignIdWithRaceCondition() { + public void getOrCreateIdAssignIdWithRaceCondition() { // Simulate a race between client A and client B. // A does a Get and sees that there's no ID for this name. // B does a Get and sees that there's no ID too, and B actually goes @@ -324,17 +330,17 @@ public void getOrCreateIdAssignIdWithRaceCondition() { // Then A attempts to go through the process and should discover that the // ID has already been assigned. - uid = new UniqueId(client, table, kind, 3); // Used by client A. + uid = new UniqueId(client, table, METRIC, 3); // Used by client A. HBaseClient client_b = mock(HBaseClient.class); // For client B. - final UniqueId uid_b = new UniqueId(client_b, table, kind, 3); + final UniqueId uid_b = new UniqueId(client_b, table, METRIC, 3); final byte[] id = { 0, 0, 5 }; final byte[] byte_name = { 'f', 'o', 'o' }; final ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(byte_name, ID, kind_array, id)); - - @SuppressWarnings("unchecked") - final Deferred> d = PowerMockito.spy(new Deferred>()); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); + + final Deferred> d = + PowerMockito.spy(new Deferred>()); when(client.get(anyGet())) .thenReturn(d) .thenReturn(Deferred.fromResult(kvs)); @@ -395,7 +401,7 @@ public byte[] answer(final InvocationOnMock unused_invocation) throws Exception @Test // Test the creation of an ID when all possible IDs are already in use public void getOrCreateIdWithOverflow() { - uid = new UniqueId(client, table, kind, 1); // IDs are only on 1 byte. + uid = new UniqueId(client, table, METRIC, 1); // IDs are only on 1 byte. when(client.get(anyGet())) // null => ID doesn't exist. .thenReturn(Deferred.>fromResult(null)); @@ -419,7 +425,7 @@ public void getOrCreateIdWithOverflow() { @Test // ICV throws an exception, we can't get an ID. public void getOrCreateIdWithICVFailure() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); final TSDB tsdb = mock(TSDB.class); @@ -451,7 +457,7 @@ public void getOrCreateIdWithICVFailure() { @Test // Test that the reverse mapping is created before the forward one. public void getOrCreateIdPutsReverseMappingFirst() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); final TSDB tsdb = mock(TSDB.class); @@ -483,7 +489,7 @@ public void getOrCreateIdPutsReverseMappingFirst() { @Test public void getOrCreateIdRandom() { PowerMockito.mockStatic(RandomUniqueId.class); - uid = new UniqueId(client, table, kind, 3, true); + uid = new UniqueId(client, table, METRIC, 3, true); final long id = 42L; final byte[] id_array = { 0, 0, 0x2A }; @@ -509,7 +515,7 @@ public void getOrCreateIdRandom() { @Test public void getOrCreateIdRandomCollision() { PowerMockito.mockStatic(RandomUniqueId.class); - uid = new UniqueId(client, table, kind, 3, true); + uid = new UniqueId(client, table, METRIC, 3, true); final long id = 42L; final byte[] id_array = { 0, 0, 0x2A }; @@ -538,7 +544,7 @@ public void getOrCreateIdRandomCollision() { @Test public void getOrCreateIdRandomCollisionTooManyAttempts() { PowerMockito.mockStatic(RandomUniqueId.class); - uid = new UniqueId(client, table, kind, 3, true); + uid = new UniqueId(client, table, METRIC, 3, true); final long id = 42L; when(RandomUniqueId.getRandomUID()).thenReturn(24L).thenReturn(id); @@ -577,13 +583,13 @@ public void getOrCreateIdRandomCollisionTooManyAttempts() { @Test public void getOrCreateIdRandomWithRaceCondition() { PowerMockito.mockStatic(RandomUniqueId.class); - uid = new UniqueId(client, table, kind, 3, true); + uid = new UniqueId(client, table, METRIC, 3, true); final long id = 24L; final byte[] id_array = { 0, 0, 0x2A }; final byte[] byte_name = { 'f', 'o', 'o' }; ArrayList kvs = new ArrayList(1); - kvs.add(new KeyValue(byte_name, ID, kind_array, id_array)); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id_array)); when(RandomUniqueId.getRandomUID()).thenReturn(id); @@ -605,10 +611,9 @@ public void getOrCreateIdRandomWithRaceCondition() { verify(client, times(2)).get(any(GetRequest.class)); } - @PrepareForTest({HBaseClient.class, Scanner.class}) @Test public void suggestWithNoMatch() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final Scanner fake_scanner = mock(Scanner.class); when(client.newScanner(table)) @@ -624,13 +629,12 @@ public void suggestWithNoMatch() { verify(fake_scanner).setStartKey("nomatch".getBytes()); verify(fake_scanner).setStopKey("nomatci".getBytes()); verify(fake_scanner).setFamily(ID); - verify(fake_scanner).setQualifier(kind_array); + verify(fake_scanner).setQualifier(METRIC_ARRAY); } - - @PrepareForTest({HBaseClient.class, Scanner.class}) + @Test public void suggestWithMatches() { - uid = new UniqueId(client, table, kind, 3); + uid = new UniqueId(client, table, METRIC, 3); final Scanner fake_scanner = mock(Scanner.class); when(client.newScanner(table)) @@ -640,10 +644,10 @@ public void suggestWithMatches() { final byte[] foo_bar_id = { 0, 0, 1 }; { ArrayList row = new ArrayList(1); - row.add(new KeyValue("foo.bar".getBytes(), ID, kind_array, foo_bar_id)); + row.add(new KeyValue("foo.bar".getBytes(), ID, METRIC_ARRAY, foo_bar_id)); rows.add(row); row = new ArrayList(1); - row.add(new KeyValue("foo.baz".getBytes(), ID, kind_array, + row.add(new KeyValue("foo.baz".getBytes(), ID, METRIC_ARRAY, new byte[] { 0, 0, 2 })); rows.add(row); } @@ -1058,10 +1062,140 @@ public void longToUIDTooBig() throws Exception { UniqueId.longToUID(257, (short)1); } + @Test + public void deleteCached() throws Exception { + setupStorage(); + uid = new UniqueId(client, table, METRIC, 3); + uid.setTSDB(tsdb); + assertArrayEquals(UID, uid.getId("sys.cpu.user")); + assertEquals("sys.cpu.user", uid.getName(UID)); + + uid.deleteAsync("sys.cpu.user").join(); + try { + uid.getId("sys.cpu.user"); + fail("Expected a NoSuchUniqueName"); + } catch (NoSuchUniqueName nsun) { } + + try { + uid.getName(UID); + fail("Expected a NoSuchUniqueId"); + } catch (NoSuchUniqueId nsui) { } + + uid = new UniqueId(client, table, TAGK, 3); + uid.setTSDB(tsdb); + assertArrayEquals(UID, uid.getId("host")); + assertEquals("host", uid.getName(UID)); + + uid = new UniqueId(client, table, TAGV, 3); + uid.setTSDB(tsdb); + assertArrayEquals(UID, uid.getId("web01")); + assertEquals("web01", uid.getName(UID)); + } + + @Test + public void deleteNotCached() throws Exception { + setupStorage(); + uid = new UniqueId(client, table, METRIC, 3); + uid.setTSDB(tsdb); + uid.deleteAsync("sys.cpu.user").join(); + try { + uid.getId("sys.cpu.user"); + fail("Expected a NoSuchUniqueName"); + } catch (NoSuchUniqueName nsun) { } + + try { + uid.getName(UID); + fail("Expected a NoSuchUniqueId"); + } catch (NoSuchUniqueId nsui) { } + + uid = new UniqueId(client, table, TAGK, 3); + uid.setTSDB(tsdb); + assertArrayEquals(UID, uid.getId("host")); + assertEquals("host", uid.getName(UID)); + + uid = new UniqueId(client, table, TAGV, 3); + uid.setTSDB(tsdb); + assertArrayEquals(UID, uid.getId("web01")); + assertEquals("web01", uid.getName(UID)); + } + + @Test + public void deleteFailForwardDelete() throws Exception { + setupStorage(); + uid = new UniqueId(client, table, METRIC, 3); + uid.setTSDB(tsdb); + assertArrayEquals(UID, uid.getId("sys.cpu.user")); + assertEquals("sys.cpu.user", uid.getName(UID)); + + storage.throwException("sys.cpu.user".getBytes(), fakeHBaseException()); + try { + uid.deleteAsync("sys.cpu.user").join(); + fail("Expected HBaseException"); + } catch (HBaseException e) { } + catch (Exception e) { } + storage.clearExceptions(); + try { + uid.getName(UID); + fail("Expected a NoSuchUniqueId"); + } catch (NoSuchUniqueId nsui) { } + assertArrayEquals(UID, uid.getId("sys.cpu.user")); + // now it pollutes the cache + assertEquals("sys.cpu.user", uid.getName(UID)); + } + + @Test + public void deleteFailReverseDelete() throws Exception { + setupStorage(); + storage.throwException(UID, fakeHBaseException()); + uid = new UniqueId(client, table, METRIC, 3); + uid.setTSDB(tsdb); + try { + uid.deleteAsync("sys.cpu.user").join(); + fail("Expected HBaseException"); + } catch (HBaseException e) { } + catch (Exception e) { } + storage.clearExceptions(); + try { + uid.getId("sys.cpu.user"); + fail("Expected a NoSuchUniqueName"); + } catch (NoSuchUniqueName nsun) { } + assertEquals("sys.cpu.user", uid.getName(UID)); + } + + @Test + public void deleteNoSuchUniqueName() throws Exception { + setupStorage(); + uid = new UniqueId(client, table, METRIC, 3); + uid.setTSDB(tsdb); + storage.flushRow(table, "sys.cpu.user".getBytes()); + try { + uid.deleteAsync("sys.cpu.user").join(); + fail("Expected NoSuchUniqueName"); + } catch (NoSuchUniqueName e) { } + assertEquals("sys.cpu.user", uid.getName(UID)); + } + // ----------------- // // Helper functions. // // ----------------- // + private void setupStorage() throws Exception { + when(tsdb.getClient()).thenReturn(client); + storage = new MockBase(tsdb, client, true, true, true, true); + + final List families = new ArrayList(); + families.add(ID); + families.add(NAME); + storage.addTable(table, families); + + storage.addColumn(table, "sys.cpu.user".getBytes(), ID, METRIC_ARRAY, UID); + storage.addColumn(table, UID, NAME, METRIC_ARRAY, "sys.cpu.user".getBytes()); + storage.addColumn(table, "host".getBytes(), ID, TAGK_ARRAY, UID); + storage.addColumn(table, UID, NAME, TAGK_ARRAY, "host".getBytes()); + storage.addColumn(table, "web01".getBytes(), ID, TAGV_ARRAY, UID); + storage.addColumn(table, UID, NAME,TAGV_ARRAY, "web01".getBytes()); + } + private static byte[] emptyArray() { return eq(HBaseClient.EMPTY_ARRAY); } From 451d4a40535bdb5e1feae317fa1e04dee70bb0e7 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 10 Sep 2015 12:06:59 -0700 Subject: [PATCH 219/826] Add the TSDB.deleteUidAsync() method and modify the ctor so that we always set the TSDB in the UID objects. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 9867980423..3a44993c66 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -185,17 +185,13 @@ public TSDB(final HBaseClient client, final Config config) { tag_names = new UniqueId(this.client, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH); tag_values = new UniqueId(this.client, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH); compactionq = new CompactionQueue(this); + metrics.setTSDB(this); + tag_names.setTSDB(this); + tag_values.setTSDB(this); if (config.hasProperty("tsd.core.timezone")) { DateTime.setDefaultTimezone(config.getString("tsd.core.timezone")); } - if (config.enable_realtime_ts() || config.enable_realtime_uid()) { - // this is cleaner than another constructor and defaults to null. UIDs - // will be refactored with DAL code anyways - metrics.setTSDB(this); - tag_names.setTSDB(this); - tag_values.setTSDB(this); - } timer = Threads.newTimer("TSDB Timer"); @@ -1093,6 +1089,29 @@ public byte[] assignUid(final String type, final String name) { } } + /** + * Attempts to delete the given UID name mapping from the storage table as + * well as the local cache. + * @param type The type of UID to delete. Must be "metrics", "tagk" or "tagv" + * @param name The name of the UID to delete + * @return A deferred to wait on for completion, or an exception if thrown + * @throws IllegalArgumentException if the type is invalid + * @since 2.2 + */ + public Deferred deleteUidAsync(final String type, final String name) { + final UniqueIdType uid_type = UniqueId.stringToUniqueIdType(type); + switch (uid_type) { + case METRIC: + return metrics.deleteAsync(name); + case TAGK: + return tag_names.deleteAsync(name); + case TAGV: + return tag_values.deleteAsync(name); + default: + throw new IllegalArgumentException("Unrecognized UID type: " + uid_type); + } + } + /** @return the name of the UID table as a byte array for client requests */ public byte[] uidTable() { return this.uidtable; From 58ead0fc138a03606711f436e3a865d80aa86ad6 Mon Sep 17 00:00:00 2001 From: jan-mangs Date: Wed, 9 Sep 2015 22:54:23 -0700 Subject: [PATCH 220/826] Added support for deleting UID mappings so they disappear from suggest/queries. Signed-off-by: Chris Larsen --- src/tools/UidManager.java | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index c71b56d0d2..2885179754 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -64,6 +64,7 @@ static void usage(final ArgP argp, final String errmsg) { + " assign [names]:" + " Assign an ID for the given name(s).\n" + " rename : Renames this UID.\n" + + " delete : Deletes this UID.\n" + " fsck: [fix] [delete_unknown] Checks the consistency of UIDs.\n" + " fix - Fix errors. By default errors are logged.\n" + " delete_unknown - Remove columns with unknown qualifiers.\n" @@ -164,6 +165,18 @@ private static int runCommand(final TSDB tsdb, return 2; } return rename(tsdb.getClient(), table, idwidth, args); + } else if (args[0].equals("delete")) { + if (nargs != 3) { + usage("Wrong number of arguments"); + return 2; + } + + try { + return delete(tsdb, table, args); + } catch (Exception e) { + LOG.error("Unexpected exception", e); + return 4; + } } else if (args[0].equals("fsck")) { boolean fix = false; boolean fix_unknowns = false; @@ -390,6 +403,30 @@ private static int rename(final HBaseClient client, return 0; } + /** + * Implements the {@code delete} subcommand. + * @param client The HBase client to use. + * @param table The name of the HBase table to use. + * @param args Command line arguments ({@code assign name [names]}). + * @return The exit status of the command (0 means success). + */ + private static int delete(final TSDB tsdb, final byte[] table, + final String[] args) throws Exception { + final String kind = args[1]; + final String name = args[2]; + try { + tsdb.deleteUidAsync(kind, name).join(); + } catch (HBaseException e) { + LOG.error("error while processing delete " + name, e); + return 3; + } catch (NoSuchUniqueName e) { + LOG.error(e.getMessage()); + return 1; + } + LOG.info("UID " + kind + ' ' + name + " deleted."); + return 0; + } + /** * Implements the {@code fsck} subcommand. * @param client The HBase client to use. From 7180bb5c5410ebf26a9f903ef1a6f4d5897a25da Mon Sep 17 00:00:00 2001 From: Lois BURG Date: Thu, 10 Sep 2015 18:54:10 -0700 Subject: [PATCH 221/826] Add possibility to DELETE datapoints through the query endpoint Signed-off-by: Chris Larsen --- src/core/Query.java | 14 ++++++++++++++ src/core/SaltScanner.java | 33 ++++++++++++++++++++++++++++++++- src/core/TSQuery.java | 13 +++++++++++++ src/core/TsdbQuery.java | 23 +++++++++++++++++++++-- src/tsd/QueryRpc.java | 16 ++++++++++++++-- src/utils/Config.java | 1 + test/core/TestTsdbQuery.java | 16 ++++++++++++++++ test/tsd/TestQueryRpc.java | 11 +++++++++++ 8 files changed, 122 insertions(+), 5 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 751ff085b4..519e36ebf5 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -67,6 +67,20 @@ public interface Query { */ long getEndTime(); + /** + * Sets whether or not the data queried will be deleted. + * @param delete True if data should be deleted, false otherwise. + * @since 2.2 + */ + void setDelete(boolean delete); + + /** + * Returns whether or not the data queried will be deleted. + * @return A boolean + * @since 2.2 + */ + boolean getDelete(); + /** * Sets the time series to the query. * @param metric The metric to retrieve from the TSDB. diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 175c7757aa..4acfc39ac9 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -28,6 +28,7 @@ import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.DeleteRequest; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; import org.slf4j.Logger; @@ -90,6 +91,9 @@ public class SaltScanner { * are done.*/ private long start_time; // milliseconds. + /** Whether or not to delete the queried data */ + private final boolean delete; + /** A list of filters to iterate over when processing rows */ private final List filters; @@ -113,6 +117,26 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters) { + this(tsdb, metric, scanners, spans, filters, false); + } + + /** + * Default ctor that performs some validation. Call {@link scan} after + * construction to actually start fetching data. + * @param tsdb The TSDB to which we belong + * @param metric The metric we're expecting to fetch + * @param scanners A list of HBase scanners, one for each bucket + * @param spans The span map to store results in + * @param delete Whether or not to delete the queried data + * @param filters A list of filters for processing + * @throws IllegalArgumentException if any required data was missing or + * we had invalid parameters. + */ + public SaltScanner(final TSDB tsdb, final byte[] metric, + final List scanners, + final TreeMap spans, + final List filters, + final boolean delete) { if (Const.SALT_WIDTH() < 1) { throw new IllegalArgumentException( "Salting is disabled. Use the regular scanner"); @@ -148,6 +172,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.metric = metric; this.tsdb = tsdb; this.filters = filters; + this.delete = delete; } /** @@ -402,11 +427,17 @@ public Object call(final ArrayList group) throws Exception { } /** - * Finds or creates the span for this row, compacts it and stores it. + * Finds or creates the span for this row, compacts it and stores it. Also + * fires off a delete request for the row if told to. * @param key The row key to use for fetching the span * @param row The row to add */ void processRow(final byte[] key, final ArrayList row) { + if (delete) { + final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); + tsdb.getClient().delete(del); + } + List notes = annotations.get(key); if (notes == null) { notes = new ArrayList(); diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 378c3df455..5310d9a4cf 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -90,6 +90,9 @@ public final class TSQuery { /** Whether or not to include stats summary in the output */ private boolean show_summary; + /** Whether or not to delete the queried data */ + private boolean delete = false; + /** The query status for tracking over all performance of this query */ private QueryStats query_stats; @@ -350,6 +353,11 @@ public boolean getShowSummary() { return this.show_summary; } + /** @return Whether or not to delete the queried data @since 2.2 */ + public boolean getDelete() { + return this.delete; + } + /** @return the query stats object. Ignored during JSON serialization */ @JsonIgnore public QueryStats getQueryStats() { @@ -430,6 +438,11 @@ public void setShowSummary(boolean show_summary) { this.show_summary = show_summary; } + /** @param delete whether or not to delete the queried data @since 2.2 */ + public void setDelete(boolean delete) { + this.delete = delete; + } + /** @param query_stats the query stats object to associate with this query */ public void setQueryStats(final QueryStats query_stats) { this.query_stats = query_stats; diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 621c43aee7..c9266d9c35 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -28,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; @@ -79,6 +80,9 @@ 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; @@ -192,7 +196,17 @@ public long getEndTime() { } return end_time; } - + + @Override + public void setDelete(boolean delete) { + this.delete = delete; + } + + @Override + public boolean getDelete() { + return delete; + } + @Override public void setTimeSeries(final String metric, final Map tags, @@ -294,6 +308,7 @@ public Deferred configureFromQuery(final TSQuery query, final TSSubQuery sub_query = query.getQueries().get(index); setStartTime(query.startTime()); setEndTime(query.endTime()); + setDelete(query.getDelete()); query_index = index; // set common options @@ -362,7 +377,6 @@ public Object call(final byte[] uid) throws Exception { } } - @Override public void downsample(final long interval, final Aggregator downsampler, final FillPolicy fill_policy) { @@ -690,6 +704,11 @@ public Object call(final ArrayList group) throws Exception { * @param row The row to add */ void processRow(final byte[] key, final ArrayList row) { + if (delete) { + final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); + tsdb.getClient().delete(del); + } + Span datapoints = spans.get(key); if (datapoints == null) { datapoints = new Span(tsdb); diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 8d48a73234..58f1ae38fb 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -71,12 +71,19 @@ final class QueryRpc 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) { + // 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 && + !tsdb.getConfig().getBoolean("tsd.http.query.allow_delete")) { + throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, + "Bad request", + "Deleting data is not enabled (tsd.http.query.allow_delete=false)"); + } final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1] : ""; @@ -111,6 +118,11 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { data_query = this.parseQuery(tsdb, query); } + if (query.getAPIMethod() == HttpMethod.DELETE && + tsdb.getConfig().getBoolean("tsd.http.query.allow_delete")) { + data_query.setDelete(true); + } + // validate and then compile the queries try { LOG.debug(data_query.toString()); diff --git a/src/utils/Config.java b/src/utils/Config.java index a140f72df9..3344ff2e3c 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -515,6 +515,7 @@ protected void setDefaults() { default_map.put("tsd.storage.compaction.max_concurrent_flushes", "10000"); default_map.put("tsd.storage.compaction.flush_speed", "2"); default_map.put("tsd.http.show_stack_trace", "true"); + default_map.put("tsd.http.query.allow_delete", "false"); default_map.put("tsd.http.request.enable_chunked", "false"); default_map.put("tsd.http.request.max_chunk", "4096"); default_map.put("tsd.http.request.cors_domains", ""); diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index b26f9f5e65..0f94fd6e0e 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -453,6 +453,22 @@ public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() ForTesting.getGroupBys(query).get(0)); } + @Test + public void deleteDatapoints() throws Exception { + setDataPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + query.setStartTime(1356998400); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + + query.setDelete(true); + final DataPoints[] dps1 = query.run(); + assertEquals(1, dps1.length); + // second run should be empty + final DataPoints[] dps2 = query.run(); + assertEquals(0, dps2.length); + } + /** @return a simple TSQuery object for testing */ private TSQuery getTSQuery() { final TSQuery ts_query = new TSQuery(); diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index b7e8f72f8b..d498b963ff 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -521,5 +521,16 @@ public void executeWithBadDSFill() throws Exception { } } + @Test (expected = BadRequestException.class) + public void deleteDatapointsBadRequest() throws Exception { + HttpQuery query = NettyMocks.deleteQuery(tsdb, + "/api/query?start=1356998400&m=sum:sys.cpu.user", ""); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("Deleting data is not enabled")); + } + //TODO(cl) add unit tests for the rate options parsing } \ No newline at end of file From 506f6ea4fc3d407ada577551d15f216f6453f840 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 11 Sep 2015 18:12:54 -0700 Subject: [PATCH 222/826] Bump to version 2.3.0-SNAPSHOT Signed-off-by: Chris Larsen --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 29d2615325..0b8dcdd76e 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.2.0-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.3.0-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From df32821658ca2565823ec82ef7858382d4c5590e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 11 Sep 2015 18:24:23 -0700 Subject: [PATCH 223/826] Remove Tsuna's email from the GPG plugin the POM and add the Sonatype staging plugin. Signed-off-by: Chris Larsen --- pom.xml.in | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pom.xml.in b/pom.xml.in index a69416b4c5..9539eaa2a1 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -290,7 +290,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.4 + 1.5 sign-artifacts @@ -300,8 +300,17 @@ + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true - tsunanet@gmail.com + ossrh + https://oss.sonatype.org/ + false From 383df1b190d579af1d1bbecd6270ef9f89b2ba79 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 12 Sep 2015 12:46:59 -0700 Subject: [PATCH 224/826] Release 2.1.1 --- NEWS | 9 +++++++++ THANKS | 1 + configure.ac | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index b45f3dccef..8013e6daf1 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,14 @@ OpenTSDB - User visible changes. +* Version 2.1.1 (2015-09-12) + +Bug Fixes: + - Relax the pgrep regex to correctly find and kill the java process in the RPM init.d + script. + - Improve query performance slightly when aggregating multiple series. + - Fix the /api/search/lookup API call to properly handle the limit parameter. + - Fix the /api/query/last endpoint to properly handle missing tsdb-meta tables. + * Version 2.1.0 (2015-05-06) Bug Fixes: diff --git a/THANKS b/THANKS index b8005c0884..c53931ddea 100644 --- a/THANKS +++ b/THANKS @@ -32,6 +32,7 @@ Josh Thomas Kieren Hynd Kimoon Kim Kris Beevers +Lex Herbert Liangliang He Matt Jibson Mark Smith diff --git a/configure.ac b/configure.ac index 66ab5753b8..082b2d07b9 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.1.0], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.1.1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 42e07134c64b1388682302409ac1c9f3fa197199 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 12 Sep 2015 12:23:36 -0700 Subject: [PATCH 225/826] Bump AsyncHBase to the release version and remove old md5s Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.5.0.jar.md5 | 1 - third_party/hbase/asynchbase-1.6.0.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.0.jar.md5 | 1 + third_party/hbase/include.mk | 4 ++-- 6 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.5.0.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.6.0.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.7.0.jar.md5 diff --git a/third_party/hbase/asynchbase-1.5.0.jar.md5 b/third_party/hbase/asynchbase-1.5.0.jar.md5 deleted file mode 100644 index e20d2ff219..0000000000 --- a/third_party/hbase/asynchbase-1.5.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -12c61569f04eb88229c90dde9fa51848 diff --git a/third_party/hbase/asynchbase-1.6.0.jar.md5 b/third_party/hbase/asynchbase-1.6.0.jar.md5 deleted file mode 100644 index 7fcfbd8ec4..0000000000 --- a/third_party/hbase/asynchbase-1.6.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -6738dd73fd48d30cbf5c78f62bc18852 diff --git a/third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 deleted file mode 100644 index 4e13899eda..0000000000 --- a/third_party/hbase/asynchbase-1.7.0-20150517.200244-1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -78c317f4457b6add9671bc2a30df7bd1 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 deleted file mode 100644 index 00f09b9593..0000000000 --- a/third_party/hbase/asynchbase-1.7.0-20150904.040751-2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -af3e0778bf5b94f1302bc8a01504680a diff --git a/third_party/hbase/asynchbase-1.7.0.jar.md5 b/third_party/hbase/asynchbase-1.7.0.jar.md5 new file mode 100644 index 0000000000..5cb6466209 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.0.jar.md5 @@ -0,0 +1 @@ +f1aed41b7f16345d2f58797ffa77f36a \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index d04740e54c..e2e33dcc32 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.0-20150910.030815-3 +ASYNCHBASE_VERSION := 1.7.0 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar -ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.0-SNAPSHOT/ +ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From 797cfce1a4a7caa324c2b58143054318aed0bf5f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 12 Sep 2015 12:33:33 -0700 Subject: [PATCH 226/826] Pull in the updated NEWS and THANKS files --- NEWS | 27 ++++++++++++++++++++++++--- THANKS | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 02d24359e8..eb54e0282f 100644 --- a/NEWS +++ b/NEWS @@ -1,11 +1,32 @@ OpenTSDB - User visible changes. -* Version 2.1.0 RC1 (2015-04-04) +* Version 2.1.1 (2015-09-12) + +Bug Fixes: + - Relax the pgrep regex to correctly find and kill the java process in the RPM init.d + script. + - Improve query performance slightly when aggregating multiple series. + - Fix the /api/search/lookup API call to properly handle the limit parameter. + - Fix the /api/query/last endpoint to properly handle missing tsdb-meta tables. + +* Version 2.1.0 (2015-05-06) + +Bug Fixes: + - FSCK was not handling compacted and floating point duplicates properly. Now they + are merged correctly. + - TSMeta data updates were not loading the latest data from storage on response + - The config class will now trim spaces from booleans and integers + - On shutdown, the idle state handler could prevent the TSD from shutting down + gracefully. A new thread factory sets that thread as a daemon thread. + - TSMeta objects were not generated if multiple writes for the same data point arrived + in succession due to buffering atomic increments. Increments are no longer buffered. + - Updated paths to the deprecated Google Code repo for dependencies. + +* Version 2.1.0 RC2 (2015-04-04) Noteworthy Changes: - Handle idle connections in Netty by closing them after some period of inactivity - Support compressed HTTP responses - - Bug Fixes: - Various RPM script and package fixes @@ -166,4 +187,4 @@ along with this library. If not, see . Local Variables: mode: outline -End: +End: \ No newline at end of file diff --git a/THANKS b/THANKS index b8005c0884..c53931ddea 100644 --- a/THANKS +++ b/THANKS @@ -32,6 +32,7 @@ Josh Thomas Kieren Hynd Kimoon Kim Kris Beevers +Lex Herbert Liangliang He Matt Jibson Mark Smith From 680645a9af31c4de5fef2cef4e5f41e8b4736177 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 12 Sep 2015 13:48:45 -0700 Subject: [PATCH 227/826] Cut 2.2.0 RC1 --- NEWS | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ THANKS | 17 +++++++++++++++++ configure.ac | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index eb54e0282f..ac24f3fd81 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,54 @@ OpenTSDB - User visible changes. +* Version 2.2.0 RC1 (2015-09-12) + +Noteworthy Changes: + - Add the option to randomly assign UIDs to metrics to improve distribution across + HBase region servers. + - Introduce salting of data to improve distribution of high cardinality regions + across region servers. + - Introduce query stats for tracking various timings related to TSD queries. + - Add more stats endpoints including /threads, /jvm and /region_clients + - Allow for deleting UID mappings via CLI or the API + - Name the various threads for easier debugging, particularly for distinguishing + between TSD and AsyncHBase threads. + - Allow for pre-fetching all of the meta information for the tables to improve + performance. + - Update to the latest AsyncHBase with support for secure HBase clusters and RPC + timeouts. + - Allow for overriding metric and tag widths via the config file. (Be careful!) + - URLs from the API are now relative instead of absolute, allowing for easier reverse + proxy use. + - Allow for percent deviation in the Nagios check + - Let queries skip over unknown tag values that may not exist yet (via config) + - Add various query filters such as case (in)sensitive pipes, wildcards and pipes + over tag values. Filters do not work over metrics at this time. + - Add support for writing data points using Appends in HBase as a way of writing + compacted data without having to read and re-write at the top of each hour. + - Introduce an option to emit NaNs or Nulls in the JSON output when downsampling and + a bucket is missing values. + - Introduce query time flags to show the original query along with some timing stats + in the response. + - Introduce a storage exception handler plugin that will allow users to spool or + requeue data points that fail writes to HBase due to various issues. + - Rework the HTTP pipeline to support plugins with RPC implementations. + - Allow for some style options in the Gnuplot graphs. + - Allow for timing out long running HTTP queries. + - Text importer will now log and continue bad rows instead of failing. + - New percentile and count aggregators. + - Add the /api/annotations endpoint to fetch multiple annotations in one call. + - Add a class to support improved bulk imports by batching requests in memory for a + full hour before writing. + +Bug Fixes: + - Modify the .rpm build to allow dashes in the name. + - Allow the Nagios check script to handle 0 values properly in checks. + - Fix FSCK where floating point values were not processed correctly (#430) + - Fix missing information from the /appi/uid/tsmeta calls (#498) + - Fix more issues with the FSCK around deleting columns that were in the list (#436) + - Avoid OOM issues over Telnet when the sending client isn't reading errors off it's + socket fast enough by blocking writes. + * Version 2.1.1 (2015-09-12) Bug Fixes: diff --git a/THANKS b/THANKS index c53931ddea..33898d5163 100644 --- a/THANKS +++ b/THANKS @@ -11,20 +11,27 @@ copyright assignment. Adrian Muraru Adrien Mogenet Alex Ioffe +Andre Pech Andrey Stepachev Aravind Gottipati Arvind Jayaprakash Berk D. Demir +Bikrant Neupane Bryan Zubrod Chris McClymont +Cristian Sechel Christophe Furmaniak Dave Barr Filippo Giunchedi +Gabriel Nicolas Avellaneda Guenther Schmuelling Hugo Trippaers Jacek Masiulaniec Jari Takkala +James Royalty Jan Mangs +Jason Harvey +Jim Scott Jesse Chang Johan Zeeck Jonathan Works @@ -34,25 +41,35 @@ Kimoon Kim Kris Beevers Lex Herbert Liangliang He +Loïs Burg Matt Jibson +Matt Schallert +Marc Tamsky Mark Smith Martin Jansen +Michal Kimle Mike Bryant Mike Kobyakov +Nathan Owens Nicole Nagele Nikhil Benesch +Nitin Aggarwal Paula Keezer Peter Gotz Pradeep Chhetri +Rajesh G Ryan Berdeen +Sean Miller Siddartha Guthikonda Simon Matic Langford Slawek Ligus Sy Le Tay Ray Chuan +Thomas Krajca Thomas Sanchez Tibor Vass Tristan Colgate-McFarlane Tony Landells Vasiliy Kiryanov +Yulai Fu Zachary Kurey \ No newline at end of file diff --git a/configure.ac b/configure.ac index 29d2615325..3d43a30c24 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.2.0-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.0RC1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 1251a1b90b1c4ce5112610f83c946a732ac2ca5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stian=20=C3=98vrev=C3=A5ge?= Date: Wed, 16 Sep 2015 01:25:23 +0200 Subject: [PATCH 228/826] Fixed comments about zk_quorum. s/space/comma/ Fixed comments. zk_quorum list is comma separated, not space separated. --- build-aux/deb/opentsdb.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index d95b65efe2..f58d5cf14c 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -58,6 +58,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A space separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From fbc5c74a7d2a49d31b2ca436eca274e8e1c8c525 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 26 Sep 2015 14:42:20 -0700 Subject: [PATCH 229/826] Add the Exceptions utility class for parsing out deferred group exception causes. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/utils/Exceptions.java | 41 +++++++++++++++++++ test/utils/TestExceptions.java | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/utils/Exceptions.java create mode 100644 test/utils/TestExceptions.java diff --git a/Makefile.am b/Makefile.am index f7307fe915..97bdd24876 100644 --- a/Makefile.am +++ b/Makefile.am @@ -143,6 +143,7 @@ tsdb_SRC := \ src/utils/ByteArrayPair.java \ src/utils/Config.java \ src/utils/DateTime.java \ + src/utils/Exceptions.java \ src/utils/FileSystem.java \ src/utils/JSON.java \ src/utils/JSONException.java \ @@ -252,6 +253,7 @@ test_SRC := \ test/utils/TestByteArrayPair.java \ test/utils/TestConfig.java \ test/utils/TestDateTime.java \ + test/utils/TestExceptions.java \ test/utils/TestJSON.java \ test/utils/TestPair.java \ test/utils/TestPluginLoader.java diff --git a/src/utils/Exceptions.java b/src/utils/Exceptions.java new file mode 100644 index 0000000000..4f52111329 --- /dev/null +++ b/src/utils/Exceptions.java @@ -0,0 +1,41 @@ +// 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.utils; + +import com.stumbleupon.async.DeferredGroupException; + +/** + * A class with utility methods for dealing with Exceptions in OpenTSDB + * @since 2.2 + */ +public class Exceptions { + + /** + * Iterates through the stack trace, looking for the actual cause of the + * deferred group exception. These traces can be huge and truncated in the + * logs so it's really useful to be able to spit out the source. + * @param e A DeferredGroupException to parse + * @return The root cause of the exception if found. + */ + public static Throwable getCause(final DeferredGroupException e) { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + break; + } else { + ex = ex.getCause(); + } + } + return ex; + } +} diff --git a/test/utils/TestExceptions.java b/test/utils/TestExceptions.java new file mode 100644 index 0000000000..d83ffa715a --- /dev/null +++ b/test/utils/TestExceptions.java @@ -0,0 +1,73 @@ +// 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.utils; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import java.util.ArrayList; + +import org.junit.Before; +import org.junit.Test; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + +public class TestExceptions { + private ArrayList> deferreds; + + @Before + public void before() { + deferreds = new ArrayList>(1); + } + + @Test + public void oneLevel() throws Exception { + final RuntimeException ex = new RuntimeException("Boo!"); + deferreds.add(Deferred.fromError(ex)); + try { + Deferred.group(deferreds).join(); + fail("Expected a DeferredGroupException"); + } catch (DeferredGroupException dge) { + assertSame(ex, Exceptions.getCause(dge)); + } + } + + @Test + public void nested() throws Exception { + final RuntimeException ex = new RuntimeException("Boo!"); + deferreds.add(Deferred.fromError(ex)); + + final ArrayList> deferreds2 = + new ArrayList>(1); + deferreds2.add(Deferred.fromResult(null)); + + class LOne implements + Callback>, ArrayList> { + @Override + public Deferred> call(final ArrayList piff) + throws Exception { + return Deferred.group(deferreds); + } + } + + try { + Deferred.group(deferreds2).addCallbackDeferring(new LOne()).join(); + fail("Expected a DeferredGroupException"); + } catch (DeferredGroupException dge) { + assertSame(ex, Exceptions.getCause(dge)); + } + } + +} From ef538b431883e6fe7881c8709c01f58df9a572fb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 26 Sep 2015 15:07:16 -0700 Subject: [PATCH 230/826] Add the QueryUtil class that pulls some methods out of the TsdbQuery class so they can be shared elsewhere. Also make queries a tiny bit more efficient with salting by compiling the row key regex once instead of once for each bucket. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/core/TsdbQuery.java | 215 ++++--------------------------- src/query/QueryUtil.java | 267 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 193 deletions(-) create mode 100644 src/query/QueryUtil.java diff --git a/Makefile.am b/Makefile.am index 97bdd24876..f9395a0504 100644 --- a/Makefile.am +++ b/Makefile.am @@ -73,6 +73,7 @@ tsdb_SRC := \ src/meta/TSMeta.java \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ + src/query/QueryUtil.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index c9266d9c35..cef3748f15 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +38,7 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.Histogram; import net.opentsdb.uid.NoSuchUniqueId; @@ -86,7 +86,10 @@ final class TsdbQuery implements Query { /** 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; + /** * Tags by which we must group the results. * Each element is a tag ID. @@ -841,14 +844,12 @@ protected Scanner getScanner() throws HBaseException { */ protected Scanner getScanner(final int salt_bucket) throws HBaseException { final short metric_width = tsdb.metrics.width(); - final int metric_salt_width = metric_width + Const.SALT_WIDTH(); - final byte[] start_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; - final byte[] end_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; - if (Const.SALT_WIDTH() > 0) { - final byte[] salt = RowKey.getSaltBytes(salt_bucket); - System.arraycopy(salt, 0, start_row, 0, Const.SALT_WIDTH()); - System.arraycopy(salt, 0, end_row, 0, Const.SALT_WIDTH()); + // set the metric UID based on the TSUIDs if given, or the metric UID + if (tsuids != null && !tsuids.isEmpty()) { + final String tsuid = tsuids.get(0); + final String metric_uid = tsuid.substring(0, metric_width * 2); + metric = UniqueId.stringToUid(metric_uid); } // We search at least one row before and one row after the start & end @@ -857,33 +858,15 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { // rely on having a few extra data points before & after the exact start // & end dates in order to do proper rate calculation or downsampling near // the "edges" of the graph. - Bytes.setInt(start_row, (int) getScanStartTimeSeconds(), metric_salt_width); - Bytes.setInt(end_row, (end_time == UNSET - ? -1 // Will scan until the end (0xFFF...). - : (int) getScanEndTimeSeconds()), - metric_salt_width); - - // set the metric UID based on the TSUIDs if given, or the metric UID - if (tsuids != null && !tsuids.isEmpty()) { - final String tsuid = tsuids.get(0); - final String metric_uid = tsuid.substring(0, metric_width * 2); - metric = UniqueId.stringToUid(metric_uid); - System.arraycopy(metric, 0, start_row, Const.SALT_WIDTH(), metric_width); - System.arraycopy(metric, 0, end_row, Const.SALT_WIDTH(), metric_width); - } else { - System.arraycopy(metric, 0, start_row, Const.SALT_WIDTH(), metric_width); - System.arraycopy(metric, 0, end_row, Const.SALT_WIDTH(), metric_width); - } - - final Scanner scanner = tsdb.client.newScanner(tsdb.table); - scanner.setStartKey(start_row); - scanner.setStopKey(end_row); + final Scanner scanner = QueryUtil.getMetricScanner(tsdb, salt_bucket, metric, + (int) getScanStartTimeSeconds(), end_time == UNSET + ? -1 // Will scan until the end (0xFFF...). + : (int) getScanEndTimeSeconds(), tsdb.table, TSDB.FAMILY()); if (tsuids != null && !tsuids.isEmpty()) { createAndSetTSUIDFilter(scanner); } else if (filters.size() > 0) { createAndSetFilter(scanner); } - scanner.setFamily(TSDB.FAMILY); return scanner; } @@ -972,113 +955,14 @@ private long getScanEndTimeSeconds() { * @param scanner The scanner on which to add the filter. */ private void createAndSetFilter(final Scanner scanner) { - if (group_bys != null) { - Collections.sort(group_bys, Bytes.MEMCMP); + if (regex == null) { + regex = QueryUtil.getRowKeyUIDRegex(group_bys, row_key_literals); } - final short name_width = tsdb.tag_names.width(); - final short value_width = tsdb.tag_values.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()) + - (group_bys == null ? 0 : group_bys.size() * 3)))); - // In order to avoid re-allocations, reserve a bit more w/ groups ^^^ - - // 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(Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES) - .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. - buf.append("(?:.{").append(tagsize).append("})*"); - if (not_key) { - // start the lookahead as we have a key we expliclty do not want in the - // results - buf.append("(?!"); - } - buf.append("\\Q"); - - addId(buf, entry.getKey()); - 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); - 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. - buf.append("(?:.{").append(tagsize).append("})*$"); - scanner.setKeyRegexp(buf.toString(), CHARSET); + scanner.setKeyRegexp(regex, CHARSET); if (LOG.isDebugEnabled()) { - logRegexScanner(buf.toString()); + LOG.debug("Scanner regex: " + QueryUtil.byteRegexToString(regex)); } } - - /** - * Little helper to print out the regular expression by converting the UID - * bytes to an array. - * @param regexp The regex string to print to the debug log - * @since 2.2 - */ - void logRegexScanner(final String regexp) { - final StringBuilder buf = new StringBuilder(); - for (int i = 0; i < regexp.length(); i++) { - if (i > 0 && regexp.charAt(i - 1) == 'Q') { - if (regexp.charAt(i - 3) == '*') { - // tagk - byte[] tagk = new byte[TSDB.tagk_width()]; - for (int x = 0; x < TSDB.tagk_width(); x++) { - tagk[x] = (byte)regexp.charAt(i + x); - } - i += TSDB.tagk_width(); - buf.append(Arrays.toString(tagk)); - } else { - // tagv - byte[] tagv = new byte[TSDB.tagv_width()]; - for (int x = 0; x < TSDB.tagv_width(); x++) { - tagv[x] = (byte)regexp.charAt(i + x); - } - i += TSDB.tagv_width(); - buf.append(Arrays.toString(tagv)); - } - } else { - buf.append(regexp.charAt(i)); - } - } - LOG.debug("Scanner regex: " + buf.toString()); - } /** * Sets the server-side regexp filter on the scanner. @@ -1088,67 +972,12 @@ void logRegexScanner(final String regexp) { * @since 2.0 */ private void createAndSetTSUIDFilter(final Scanner scanner) { - Collections.sort(tsuids); - - // first, convert the tags to byte arrays and count up the total length - // so we can allocate the string builder - final short metric_width = tsdb.metrics.width(); - int tags_length = 0; - final ArrayList uids = new ArrayList(tsuids.size()); - for (final String tsuid : tsuids) { - final String tags = tsuid.substring(metric_width * 2); - final byte[] tag_bytes = UniqueId.stringToUid(tags); - tags_length += tag_bytes.length; - uids.add(tag_bytes); - } - - // Generate a regexp for our tags based on any metric and timestamp (since - // those are handled by the row start/stop) and the list of TSUID tagk/v - // pairs. The generated regex will look like: ^.{7}(tags|tags|tags)$ - // where each "tags" is similar to \\Q\000\000\001\000\000\002\\E - final StringBuilder buf = new StringBuilder( - 13 // "(?s)^.{N}(" + ")$" - + (tsuids.size() * 11) // "\\Q" + "\\E|" - + tags_length); // total # of bytes in tsuids tagk/v pairs - - // Alright, let's build this regexp. From the beginning... - buf.append("(?s)" // Ensure we use the DOTALL flag. - + "^.{") - // ... start by skipping the metric ID and timestamp. - .append(Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES) - .append("}("); - - for (final byte[] tags : uids) { - // quote the bytes - buf.append("\\Q"); - addId(buf, tags); - buf.append('|'); + if (regex == null) { + regex = QueryUtil.getRowKeyTSUIDRegex(tsuids); } - - // Replace the pipe of the last iteration, close and set - buf.setCharAt(buf.length() - 1, ')'); - buf.append("$"); - scanner.setKeyRegexp(buf.toString(), CHARSET); + scanner.setKeyRegexp(regex, CHARSET); } - - /** - * Appends the given ID to the given buffer, followed by "\\E". - */ - private static void addId(final StringBuilder buf, final byte[] id) { - boolean backslash = false; - for (final byte b : id) { - buf.append((char) (b & 0xFF)); - if (b == 'E' && backslash) { // If we saw a `\' and now we have a `E'. - // So we just terminated the quoted section because we just added \E - // to `buf'. So let's put a litteral \E now and start quoting again. - buf.append("\\\\E\\Q"); - } else { - backslash = b == '\\'; - } - } - buf.append("\\E"); - } - + @Override public String toString() { final StringBuilder buf = new StringBuilder(); diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java new file mode 100644 index 0000000000..ab168536ea --- /dev/null +++ b/src/query/QueryUtil.java @@ -0,0 +1,267 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +import net.opentsdb.core.Const; +import net.opentsdb.core.RowKey; +import net.opentsdb.core.TSDB; +import net.opentsdb.uid.UniqueId; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.Scanner; + +/** + * A simple class with utility methods for executing queries against the storage + * layer. + * @since 2.2 + */ +public class QueryUtil { + + /** + * Crafts a regular expression for scanning over data table rows and filtering + * time series that the user doesn't want. At least one of the parameters + * must be set and have values. + * NOTE: This method will sort the group bys. + * @param group_bys An optional list of tag keys that we want to group on. May + * be null. + * @param row_key_literals An optional list of key value pairs to filter on. + * May be null. + * @return A regular expression string to pass to the storage layer. + */ + public static String getRowKeyUIDRegex(final List group_bys, + final ByteMap row_key_literals) { + if (group_bys != null) { + Collections.sort(group_bys, Bytes.MEMCMP); + } + 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()) + + (group_bys == null ? 0 : group_bys.size() * 3)))); + // In order to avoid re-allocations, reserve a bit more w/ groups ^^^ + + // 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(Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES) + .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. + 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. + buf.append("(?:.{").append(tagsize).append("})*$"); + return buf.toString(); + } + + /** + * Creates a regular expression with a list of or'd TUIDs to compare + * against the rows in storage. + * @param tsuids The list of TSUIDs to scan for + * @return A regular expression string to pass to the storage layer. + */ + public static String getRowKeyTSUIDRegex(final List tsuids) { + Collections.sort(tsuids); + + // first, convert the tags to byte arrays and count up the total length + // so we can allocate the string builder + final short metric_width = TSDB.metrics_width(); + int tags_length = 0; + final ArrayList uids = new ArrayList(tsuids.size()); + for (final String tsuid : tsuids) { + final String tags = tsuid.substring(metric_width * 2); + final byte[] tag_bytes = UniqueId.stringToUid(tags); + tags_length += tag_bytes.length; + uids.add(tag_bytes); + } + + // Generate a regexp for our tags based on any metric and timestamp (since + // those are handled by the row start/stop) and the list of TSUID tagk/v + // pairs. The generated regex will look like: ^.{7}(tags|tags|tags)$ + // where each "tags" is similar to \\Q\000\000\001\000\000\002\\E + final StringBuilder buf = new StringBuilder( + 13 // "(?s)^.{N}(" + ")$" + + (tsuids.size() * 11) // "\\Q" + "\\E|" + + tags_length); // total # of bytes in tsuids tagk/v pairs + + // Alright, let's build this regexp. From the beginning... + buf.append("(?s)" // Ensure we use the DOTALL flag. + + "^.{") + // ... start by skipping the metric ID and timestamp. + .append(Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES) + .append("}("); + + for (final byte[] tags : uids) { + // quote the bytes + buf.append("\\Q"); + addId(buf, tags, true); + buf.append('|'); + } + + // Replace the pipe of the last iteration, close and set + buf.setCharAt(buf.length() - 1, ')'); + buf.append("$"); + return buf.toString(); + } + + /** + * Compiles an HBase scanner against the main data table + * @param tsdb The TSDB with a configured HBaseClient + * @param salt_bucket An optional salt bucket ID for salting the start/stop + * keys. + * @param metric The metric to scan for + * @param start The start time stamp in seconds + * @param stop The stop timestamp in seconds + * @param table The table name to scan over + * @param family The table family to scan over + * @return A scanner ready for processing. + */ + public static Scanner getMetricScanner(final TSDB tsdb, final int salt_bucket, + final byte[] metric, final int start, final int stop, + final byte[] table, final byte[] family) { + final short metric_width = TSDB.metrics_width(); + final int metric_salt_width = metric_width + Const.SALT_WIDTH(); + final byte[] start_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; + final byte[] end_row = new byte[metric_salt_width + Const.TIMESTAMP_BYTES]; + + if (Const.SALT_WIDTH() > 0) { + final byte[] salt = RowKey.getSaltBytes(salt_bucket); + System.arraycopy(salt, 0, start_row, 0, Const.SALT_WIDTH()); + System.arraycopy(salt, 0, end_row, 0, Const.SALT_WIDTH()); + } + + Bytes.setInt(start_row, start, metric_salt_width); + Bytes.setInt(end_row, stop, metric_salt_width); + + System.arraycopy(metric, 0, start_row, Const.SALT_WIDTH(), metric_width); + System.arraycopy(metric, 0, end_row, Const.SALT_WIDTH(), metric_width); + + final Scanner scanner = tsdb.getClient().newScanner(table); + scanner.setStartKey(start_row); + scanner.setStopKey(end_row); + scanner.setFamily(family); + return scanner; + } + + /** + * Appends the given UID to the given regular expression buffer + * @param buf The String buffer to modify + * @param id The UID to add + * @param close Whether or not to append "\\E" to the end + */ + public static void addId(final StringBuilder buf, final byte[] id, + final boolean close) { + boolean backslash = false; + for (final byte b : id) { + buf.append((char) (b & 0xFF)); + if (b == 'E' && backslash) { // If we saw a `\' and now we have a `E'. + // So we just terminated the quoted section because we just added \E + // to `buf'. So let's put a litteral \E now and start quoting again. + buf.append("\\\\E\\Q"); + } else { + backslash = b == '\\'; + } + } + if (close) { + buf.append("\\E"); + } + } + + /** + * Little helper to print out the regular expression by converting the UID + * bytes to an array. + * @param regexp The regex string to print to the debug log + */ + public static String byteRegexToString(final String regexp) { + final StringBuilder buf = new StringBuilder(); + for (int i = 0; i < regexp.length(); i++) { + if (i > 0 && regexp.charAt(i - 1) == 'Q') { + if (regexp.charAt(i - 3) == '*') { + // tagk + byte[] tagk = new byte[TSDB.tagk_width()]; + for (int x = 0; x < TSDB.tagk_width(); x++) { + tagk[x] = (byte)regexp.charAt(i + x); + } + i += TSDB.tagk_width(); + buf.append(Arrays.toString(tagk)); + } else { + // tagv + byte[] tagv = new byte[TSDB.tagv_width()]; + for (int x = 0; x < TSDB.tagv_width(); x++) { + tagv[x] = (byte)regexp.charAt(i + x); + } + i += TSDB.tagv_width(); + buf.append(Arrays.toString(tagv)); + } + } else { + buf.append(regexp.charAt(i)); + } + } + return buf.toString(); + } +} From 1a59e4843f2636abb71c27be6faafc4a60336236 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 26 Sep 2015 15:08:17 -0700 Subject: [PATCH 231/826] Fix for #568, modifying the /api/search/lookup method to properly handle salted tables. Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 559 ++++++++++++++------ test/search/TestTimeSeriesLookup.java | 164 +++--- test/search/TestTimeSeriesLookupSalted.java | 40 ++ 3 files changed, 484 insertions(+), 279 deletions(-) create mode 100644 test/search/TestTimeSeriesLookupSalted.java diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index 5683f352df..6c244b90f4 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -14,20 +14,25 @@ import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import net.opentsdb.core.Const; +import net.opentsdb.core.Internal; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; import net.opentsdb.meta.TSMeta; +import net.opentsdb.query.QueryUtil; import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.ByteArrayPair; +import net.opentsdb.utils.Exceptions; import net.opentsdb.utils.Pair; import org.hbase.async.Bytes; @@ -36,6 +41,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + /** * Lookup series related to a metric, tagk, tagv or any combination thereof. * This class doesn't handle wild-card searching yet. @@ -87,6 +96,21 @@ public class TimeSeriesLookup { /** The TSD to use for lookups */ private final TSDB tsdb; + /** The metric UID if given by the query, post resolution */ + private byte[] metric_uid; + + /** Tag UID pairs if given in the query. Key or value may be null. */ + private List pairs; + + /** The compiled row key regex for HBase filtering */ + private String rowkey_regex; + + /** Post scan filtering if we have a lot of values to look at */ + private String tagv_filter; + + /** The results to send to the caller */ + private final List tsuids; + /** * Default ctor * @param tsdb The TSD to which we belong @@ -96,6 +120,7 @@ public class TimeSeriesLookup { public TimeSeriesLookup(final TSDB tsdb, final SearchQuery query) { this.tsdb = tsdb; this.query = query; + tsuids = Collections.synchronizedList(new ArrayList()); } /** @@ -110,26 +135,89 @@ public TimeSeriesLookup(final TSDB tsdb, final SearchQuery query) { * UID. */ public List lookup() { - LOG.info(query.toString()); - boolean limit_reached = false; - final StringBuilder tagv_filter = new StringBuilder(); - final Scanner scanner = getScanner(tagv_filter); - final List tsuids = new ArrayList(); - final Pattern tagv_regex = tagv_filter.length() > 1 ? - Pattern.compile(tagv_filter.toString()) : null; + try { + return lookupAsync().join(); + } catch (InterruptedException e) { + LOG.error("Interrupted performing lookup", e); + Thread.currentThread().interrupt(); + return null; + } catch (DeferredGroupException e) { + final Throwable ex = Exceptions.getCause(e); + if (ex instanceof NoSuchUniqueName) { + throw (NoSuchUniqueName)ex; + } + throw new RuntimeException("Unexpected exception", ex); + } catch (NoSuchUniqueName e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Unexpected exception", e); + } + } + + /** + * Lookup time series associated with the given metric, tagk, tagv or tag + * pairs. Either the meta table or the data table will be scanned. If no + * metric is given, a full table scan must be performed and this call may take + * a long time to complete. + * When dumping to stdout, if an ID can't be looked up, it will be logged and + * skipped. + * @return A list of TSUIDs matching the given lookup query. + * @throws NoSuchUniqueName if any of the given names fail to resolve to a + * UID. + * @since 2.2 + */ + public Deferred> lookupAsync() { + final Pattern tagv_regex = tagv_filter != null ? + Pattern.compile(tagv_filter) : null; + // we don't really know what size the UIDs will resolve to so just grab // a decent amount. final StringBuffer buf = to_stdout ? new StringBuffer(2048) : null; final long start = System.currentTimeMillis(); - - ArrayList> rows; - byte[] last_tsuid = null; // used to avoid dupes when scanning the data table - - try { - // synchronous to avoid stack overflows when scanning across the main data - // table. - while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { + final int limit; + if (query.getLimit() > 0) { + if (query.useMeta() || Const.SALT_WIDTH() < 1) { + limit = query.getLimit(); + } else if (query.getLimit() < Const.SALT_BUCKETS()) { + limit = 1; + } else { + limit = query.getLimit() / Const.SALT_BUCKETS(); + } + } else { + limit = 0; + } + + class ScannerCB implements Callback, ArrayList>> { + private final Scanner scanner; + // used to avoid dupes when scanning the data table + private byte[] last_tsuid = null; + private int rows_read; + + ScannerCB(final Scanner scanner) { + this.scanner = scanner; + } + + Deferred> scan() { + return scanner.nextRows().addCallback(this); + } + + @Override + public List call(final ArrayList> rows) + throws Exception { + if (rows == null) { + scanner.close(); + if (query.useMeta() || Const.SALT_WIDTH() < 1) { + LOG.debug("Lookup query matched " + tsuids.size() + " time series in " + + (System.currentTimeMillis() - start) + " ms"); + } + return tsuids; + } + for (final ArrayList row : rows) { + if (limit > 0 && rows_read >= limit) { + // little recursion to close the scanner and log above. + return call(null); + } final byte[] tsuid = query.useMeta() ? row.get(0).key() : UniqueId.getTSUIDFromKey(row.get(0).key(), TSDB.metrics_width(), Const.TIMESTAMP_BYTES); @@ -161,203 +249,326 @@ public List lookup() { buf.append(tag_pair.getKey()).append("=") .append(tag_pair.getValue()).append(" "); } - System.out.println(buf.toString()); } catch (NoSuchUniqueId nsui) { LOG.error("Unable to resolve UID in TSUID (" + UniqueId.uidToString(tsuid) + ") " + nsui.getMessage()); } - buf.setLength(0); // reset the buffer so we can re-use it + buf.setLength(0); // reset the buffer so we can re-use it } else { - if(tsuids.size() < query.getLimit()) { - tsuids.add(tsuid); - } else { - limit_reached = true; - break; - } + tsuids.add(tsuid); } + ++rows_read; } - if(limit_reached) { - break; + + scan(); + return tsuids; + } + + @Override + public String toString() { + return "Scanner callback"; + } + } + + class CompleteCB implements Callback, ArrayList>> { + @Override + public List call(final ArrayList> unused) throws Exception { + LOG.debug("Lookup query matched " + tsuids.size() + " time series in " + + (System.currentTimeMillis() - start) + " ms"); + return tsuids; + } + @Override + public String toString() { + return "Final async lookup callback"; + } + } + + class UIDCB implements Callback>, Object> { + @Override + public Deferred> call(Object arg0) throws Exception { + if (!query.useMeta() && Const.SALT_WIDTH() > 0 && metric_uid != null) { + final ArrayList>> deferreds = + new ArrayList>>(Const.SALT_BUCKETS()); + for (int i = 0; i < Const.SALT_BUCKETS(); i++) { + deferreds.add(new ScannerCB(getScanner(i)).scan()); + } + return Deferred.group(deferreds).addCallback(new CompleteCB()); + } else { + return new ScannerCB(getScanner(0)).scan(); } } - } catch (Exception e) { - throw new RuntimeException("Shouldn't be here", e); - } finally { - scanner.close(); + @Override + public String toString() { + return "UID resolution callback"; + } } - LOG.debug("Lookup query matched " + tsuids.size() + " time series in " + - (System.currentTimeMillis() - start) + " ms"); - return tsuids; + return resolveUIDs().addCallbackDeferring(new UIDCB()); } /** - * Configures the scanner for iterating over the meta or data tables. If the - * metric has been set, then we scan a small slice of the table where the - * metric lies, otherwise we have to scan the whole table. If tags are - * given then we setup a row key regex - * @return A configured scanner + * Resolves the metric and tag strings to their UIDs + * @return A deferred to wait on for resolution to complete. */ - private Scanner getScanner(final StringBuilder tagv_filter) { - final Scanner scanner = tsdb.getClient().newScanner( - query.useMeta() ? tsdb.metaTable() : tsdb.dataTable()); - scanner.setFamily( - query.useMeta() ? TSMeta.FAMILY : TSDB.FAMILY()); + private Deferred resolveUIDs() { - // if a metric is given, we need to resolve it's UID and set the start key - // to the UID and the stop key to the next row by incrementing the UID. - if (query.getMetric() != null && !query.getMetric().isEmpty() && - !query.getMetric().equals("*")) { - final byte[] metric_uid = tsdb.getUID(UniqueIdType.METRIC, - query.getMetric()); - LOG.debug("Found UID (" + UniqueId.uidToString(metric_uid) + - ") for metric (" + query.getMetric() + ")"); - scanner.setStartKey(metric_uid); - long uid = UniqueId.uidToLong(metric_uid, TSDB.metrics_width()); - uid++; // TODO - see what happens when this rolls over - scanner.setStopKey(UniqueId.longToUID(uid, TSDB.metrics_width())); - } else { - LOG.debug("Performing full table scan, no metric provided"); + class TagsCB implements Callback> { + @Override + public Object call(final ArrayList ignored) throws Exception { + rowkey_regex = getRowKeyRegex(); + return null; + } } - if (query.getTags() != null && !query.getTags().isEmpty()) { - final List pairs = - new ArrayList(query.getTags().size()); - for (Pair tag : query.getTags()) { - final byte[] tagk = tag.getKey() != null && !tag.getKey().equals("*")? - tsdb.getUID(UniqueIdType.TAGK, tag.getKey()) : null; - final byte[] tagv = tag.getValue() != null && !tag.getValue().equals("*")? - tsdb.getUID(UniqueIdType.TAGV, tag.getValue()) : null; - pairs.add(new ByteArrayPair(tagk, tagv)); - } - // remember, tagks are sorted in the row key so we need to supply a sorted - // regex or matching will fail. - Collections.sort(pairs); - - final short name_width = TSDB.tagk_width(); - final short value_width = TSDB.tagv_width(); - final short tagsize = (short) (name_width + value_width); - - int index = 0; - final StringBuilder buf = new StringBuilder( - 22 // "^.{N}" + "(?:.{M})*" + "$" + wiggle - + ((13 + tagsize) // "(?:.{M})*\\Q" + tagsize bytes + "\\E" - * (pairs.size()))); - buf.append("(?s)^.{").append(TSDB.metrics_width()) - .append("}"); - if (!query.useMeta()) { - buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); - } - buf.append("(?:.{").append(tagsize).append("})*"); - - // at the top of the list will be the null=tagv pairs. We want to compile - // a separate regex for them. - for (; index < pairs.size(); index++) { - if (pairs.get(index).getKey() != null) { - break; + class PairResolution implements Callback> { + @Override + public Object call(final ArrayList tags) throws Exception { + if (tags.size() < 2) { + throw new IllegalArgumentException("Somehow we received an array " + + "that wasn't two bytes in size! " + tags); } - - if (index > 0) { - buf.append("|"); - } - buf.append("(?:.{").append(name_width).append("})"); - buf.append("\\Q"); - addId(buf, pairs.get(index).getValue()); - buf.append("\\E"); - } - buf.append("(?:.{").append(tagsize).append("})*") - .append("$"); - - if (index > 0 && index < pairs.size()) { - // we had one or more tagvs to lookup AND we have tagk or tag pairs to - // filter on, so we dump the previous regex into the tagv_filter and - // continue on with a row key - tagv_filter.append(buf.toString()); - LOG.debug("Setting tagv filter: " + buf.toString()); - } else if (index >= pairs.size()) { - // in this case we don't have any tagks to deal with so we can just - // pass the previously compiled regex to the rowkey filter of the - // scanner - scanner.setKeyRegexp(buf.toString(), CHARSET); - LOG.debug("Setting scanner row key filter with tagvs only: " + - buf.toString()); + pairs.add(new ByteArrayPair(tags.get(0), tags.get(1))); + return Deferred.fromResult(null); } - - // catch any left over tagk/tag pairs - if (index < pairs.size()){ - buf.setLength(0); - buf.append("(?s)^.{").append(TSDB.metrics_width()) - .append("}"); - if (!query.useMeta()) { - buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + } + + class TagResolution implements Callback, Object> { + @Override + public Deferred call(final Object unused) throws Exception { + if (query.getTags() == null || query.getTags().isEmpty()) { + return Deferred.fromResult(null); } - ByteArrayPair last_pair = null; - for (; index < pairs.size(); index++) { - if (last_pair != null && last_pair.getValue() == null && - Bytes.memcmp(last_pair.getKey(), pairs.get(index).getKey()) == 0) { - // tagk=null is a wildcard so we don't need to bother adding - // tagk=tagv pairs with the same tagk. - LOG.debug("Skipping pair due to wildcard: " + pairs.get(index)); - } else if (last_pair != null && - Bytes.memcmp(last_pair.getKey(), pairs.get(index).getKey()) == 0) { - // in this case we're ORing e.g. "host=web01|host=web02" - buf.append("|\\Q"); - addId(buf, pairs.get(index).getKey()); - addId(buf, pairs.get(index).getValue()); - buf.append("\\E"); + pairs = Collections.synchronizedList( + new ArrayList(query.getTags().size())); + final ArrayList> deferreds = + new ArrayList>(pairs.size()); + + for (final Pair tags : query.getTags()) { + final ArrayList> deferred_tags = + new ArrayList>(2); + if (tags.getKey() != null && !tags.getKey().equals("*")) { + deferred_tags.add(tsdb.getUIDAsync(UniqueIdType.TAGK, tags.getKey())); } else { - if (last_pair != null) { - buf.append(")"); - } - // moving on to the next tagk set - buf.append("(?:.{6})*"); // catch tag pairs in between - buf.append("(?:"); - if (pairs.get(index).getKey() != null && - pairs.get(index).getValue() != null) { - buf.append("\\Q"); - addId(buf, pairs.get(index).getKey()); - addId(buf, pairs.get(index).getValue()); - buf.append("\\E"); - } else { - buf.append("\\Q"); - addId(buf, pairs.get(index).getKey()); - buf.append("\\E"); - buf.append("(?:.{").append(value_width).append("})+"); - } + deferred_tags.add(Deferred.fromResult(null)); + } + if (tags.getValue() != null && !tags.getValue().equals("*")) { + deferred_tags.add(tsdb.getUIDAsync(UniqueIdType.TAGV, tags.getValue())); + } else { + deferred_tags.add(Deferred.fromResult(null)); } - last_pair = pairs.get(index); + deferreds.add(Deferred.groupInOrder(deferred_tags) + .addCallback(new PairResolution())); } - buf.append(")(?:.{").append(tagsize).append("})*").append("$"); - - scanner.setKeyRegexp(buf.toString(), CHARSET); - LOG.debug("Setting scanner row key filter: " + buf.toString()); + return Deferred.group(deferreds).addCallback(new TagsCB()); + } + } + + class MetricCB implements Callback, byte[]> { + @Override + public Deferred call(final byte[] uid) throws Exception { + metric_uid = uid; + LOG.debug("Found UID (" + UniqueId.uidToString(metric_uid) + + ") for metric (" + query.getMetric() + ")"); + return new TagResolution().call(null); + } + } + + if (query.getMetric() != null && !query.getMetric().isEmpty() && + !query.getMetric().equals("*")) { + return tsdb.getUIDAsync(UniqueIdType.METRIC, query.getMetric()) + .addCallbackDeferring(new MetricCB()); + } else { + try { + return new TagResolution().call(null); + } catch (Exception e) { + return Deferred.fromError(e); + } + } + } + + /** Compiles a scanner with the given salt ID if salting is enabled AND we're + * not scanning the meta table. + * @param salt An ID for the salt bucket + * @return A scanner to send to HBase. + */ + private Scanner getScanner(final int salt) { + final Scanner scanner = tsdb.getClient().newScanner( + query.useMeta() ? tsdb.metaTable() : tsdb.dataTable()); + scanner.setFamily(query.useMeta() ? TSMeta.FAMILY : TSDB.FAMILY()); + + if (metric_uid != null) { + byte[] key; + if (query.useMeta() || Const.SALT_WIDTH() < 1) { + key = metric_uid; + } else { + key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width()]; + key[0] = (byte)salt; + System.arraycopy(metric_uid, 0, key, Const.SALT_WIDTH(), metric_uid.length); + } + scanner.setStartKey(key); + long uid = UniqueId.uidToLong(metric_uid, TSDB.metrics_width()); + uid++; + if (uid < Internal.getMaxUnsignedValueOnBytes(TSDB.metrics_width())) { + // if random metrics are enabled we could see a metric with the max UID + // value. If so, we need to leave the stop key as null + if (query.useMeta() || Const.SALT_WIDTH() < 1) { + key = UniqueId.longToUID(uid, TSDB.metrics_width()); + } else { + key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width()]; + key[0] = (byte)salt; + System.arraycopy(UniqueId.longToUID(uid, TSDB.metrics_width()), 0, + key, Const.SALT_WIDTH(), metric_uid.length); + } + scanner.setStopKey(key); } } + + if (rowkey_regex != null) { + scanner.setKeyRegexp(rowkey_regex, CHARSET); + if (LOG.isDebugEnabled()) { + LOG.debug("Scanner regex: " + QueryUtil.byteRegexToString(rowkey_regex)); + } + } + return scanner; } /** - * Appends the given ID to the given buffer, escaping where appropriate - * @param buf The string buffer to append to - * @param id The ID to append + * Constructs a row key regular expression to pass to HBase if the user gave + * some tags in the query + * @return The regular expression to use. */ - private static void addId(final StringBuilder buf, final byte[] id) { - boolean backslash = false; - for (final byte b : id) { - buf.append((char) (b & 0xFF)); - if (b == 'E' && backslash) { // If we saw a `\' and now we have a `E'. - // So we just terminated the quoted section because we just added \E - // to `buf'. So let's put a litteral \E now and start quoting again. - buf.append("\\\\E\\Q"); - } else { - backslash = b == '\\'; + private String getRowKeyRegex() { + final StringBuilder tagv_buffer = new StringBuilder(); + // remember, tagks are sorted in the row key so we need to supply a sorted + // regex or matching will fail. + Collections.sort(pairs); + + final short name_width = TSDB.tagk_width(); + final short value_width = TSDB.tagv_width(); + final short tagsize = (short) (name_width + value_width); + + int index = 0; + final StringBuilder buf = new StringBuilder( + 22 // "^.{N}" + "(?:.{M})*" + "$" + wiggle + + ((13 + tagsize) // "(?:.{M})*\\Q" + tagsize bytes + "\\E" + * (pairs.size()))); + buf.append("(?s)^.{").append(query.useMeta() ? TSDB.metrics_width() : + TSDB.metrics_width() + Const.SALT_WIDTH()) + .append("}"); + if (!query.useMeta()) { + buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + } + buf.append("(?:.{").append(tagsize).append("})*"); + + // at the top of the list will be the null=tagv pairs. We want to compile + // a separate regex for them. + for (; index < pairs.size(); index++) { + if (pairs.get(index).getKey() != null) { + break; } + + if (index > 0) { + buf.append("|"); + } + buf.append("(?:.{").append(name_width).append("})"); + buf.append("\\Q"); + QueryUtil.addId(buf, pairs.get(index).getValue(), true); } + buf.append("(?:.{").append(tagsize).append("})*") + .append("$"); + + if (index > 0 && index < pairs.size()) { + // we had one or more tagvs to lookup AND we have tagk or tag pairs to + // filter on, so we dump the previous regex into the tagv_filter and + // continue on with a row key + tagv_buffer.append(buf.toString()); + LOG.debug("Setting tagv filter: " + QueryUtil.byteRegexToString(buf.toString())); + } else if (index >= pairs.size()) { + // in this case we don't have any tagks to deal with so we can just + // pass the previously compiled regex to the rowkey filter of the + // scanner + LOG.debug("Setting scanner row key filter with tagvs only: " + + QueryUtil.byteRegexToString(buf.toString())); + if (tagv_buffer.length() > 0) { + tagv_filter = tagv_buffer.toString(); + } + return buf.toString(); + } + + // catch any left over tagk/tag pairs + if (index < pairs.size()){ + buf.setLength(0); + buf.append("(?s)^.{").append(query.useMeta() ? TSDB.metrics_width() : + TSDB.metrics_width() + Const.SALT_WIDTH()) + .append("}"); + if (!query.useMeta()) { + buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + } + + ByteArrayPair last_pair = null; + for (; index < pairs.size(); index++) { + if (last_pair != null && last_pair.getValue() == null && + Bytes.memcmp(last_pair.getKey(), pairs.get(index).getKey()) == 0) { + // tagk=null is a wildcard so we don't need to bother adding + // tagk=tagv pairs with the same tagk. + LOG.debug("Skipping pair due to wildcard: " + pairs.get(index)); + } else if (last_pair != null && + Bytes.memcmp(last_pair.getKey(), pairs.get(index).getKey()) == 0) { + // in this case we're ORing e.g. "host=web01|host=web02" + buf.append("|\\Q"); + QueryUtil.addId(buf, pairs.get(index).getKey(), false); + QueryUtil.addId(buf, pairs.get(index).getValue(), true); + } else { + if (last_pair != null) { + buf.append(")"); + } + // moving on to the next tagk set + buf.append("(?:.{6})*"); // catch tag pairs in between + buf.append("(?:"); + if (pairs.get(index).getKey() != null && + pairs.get(index).getValue() != null) { + buf.append("\\Q"); + QueryUtil.addId(buf, pairs.get(index).getKey(), false); + QueryUtil.addId(buf, pairs.get(index).getValue(), true); + } else { + buf.append("\\Q"); + QueryUtil.addId(buf, pairs.get(index).getKey(), true); + buf.append("(?:.{").append(value_width).append("})+"); + } + } + last_pair = pairs.get(index); + } + buf.append(")(?:.{").append(tagsize).append("})*").append("$"); + } + if (tagv_buffer.length() > 0) { + tagv_filter = tagv_buffer.toString(); + } + return buf.toString(); } /** @param to_stdout Whether or not to dump to standard out as we scan */ public void setToStdout(final boolean to_stdout) { this.to_stdout = to_stdout; } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("query={") + .append(query) + .append("}, to_stdout=") + .append(to_stdout) + .append(", metric_uid=") + .append(metric_uid == null ? "null" : Arrays.toString(metric_uid)) + .append(", pairs=") + .append(pairs) + .append(", rowkey_regex=") + .append(rowkey_regex) + .append(", tagv_filter=") + .append(tagv_filter); + return buf.toString(); + + } } diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index af0b73fd17..cdf3e3de22 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -16,13 +16,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; -import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import net.opentsdb.core.BaseTsdbTest; import net.opentsdb.core.Const; +import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; import net.opentsdb.storage.MockBase; @@ -31,6 +32,7 @@ import net.opentsdb.utils.Config; import net.opentsdb.utils.Pair; +import org.hbase.async.Bytes; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; @@ -49,14 +51,7 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, KeyValue.class, Scanner.class, TimeSeriesLookup.class}) -public class TestTimeSeriesLookup { - private Config config; - private TSDB tsdb = null; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); - private MockBase storage = null; +public class TestTimeSeriesLookup extends BaseTsdbTest { // tsuids private static List test_tsuids = new ArrayList(7); @@ -64,70 +59,26 @@ public class TestTimeSeriesLookup { test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }); test_tsuids.add(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1 }); - test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 1, 0, 0, 4, 0, 0, 5}); - test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 1, 0, 0, 2, 0, 0, 4, 0, 0, 5}); - test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 1, + test_tsuids.add(new byte[] { 0, 0, 4, 0, 0, 1, 0, 0, 1, 0, 0, 3, 0, 0, 5}); + test_tsuids.add(new byte[] { 0, 0, 4, 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 5}); + test_tsuids.add(new byte[] { 0, 0, 4, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 1, 0, 0, 9, 0, 0, 3}); - test_tsuids.add(new byte[] { 0, 0, 3, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 10, + test_tsuids.add(new byte[] { 0, 0, 4, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 10, 0, 0, 9, 0, 0, 3}); } @Before - public void before() throws Exception { - config = new Config(false); - tsdb = new TSDB(client, config); - - // replace the "real" field objects with mocks - Field met = tsdb.getClass().getDeclaredField("metrics"); - met.setAccessible(true); - met.set(tsdb, metrics); - - Field tagk = tsdb.getClass().getDeclaredField("tag_names"); - tagk.setAccessible(true); - tagk.set(tsdb, tag_names); - - Field tagv = tsdb.getClass().getDeclaredField("tag_values"); - tagv.setAccessible(true); - tagv.set(tsdb, tag_values); - - // mock UniqueId - when(metrics.getIdAsync("sys.cpu.user")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(metrics.getIdAsync("sys.cpu.system")) - .thenReturn(Deferred.fromError( - new NoSuchUniqueName("sys.cpu.system", "metric"))); - when(metrics.getIdAsync("sys.cpu.nice")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(metrics.getIdAsync("sys.cpu.idle")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); + public void beforeLocal() { when(metrics.getIdAsync("no.values")) .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 11 })); - - when(tag_names.getIdAsync("host")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_names.getIdAsync("dc")) - .thenReturn(Deferred.fromError( - new NoSuchUniqueName("dc", "metric"))); - when(tag_names.getIdAsync("owner")) + when(metrics.getIdAsync("filtered")) .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 4 })); - - when(tag_values.getIdAsync("web01")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); - when(tag_values.getIdAsync("web02")) - .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 2 })); - when(tag_values.getIdAsync("web03")) - .thenReturn(Deferred.fromError( - new NoSuchUniqueName("web03", "metric"))); - - when(metrics.width()).thenReturn((short)3); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); } - + @Test public void metricOnlyMeta() throws Exception { generateMeta(); - final SearchQuery query = new SearchQuery("sys.cpu.user"); + final SearchQuery query = new SearchQuery(METRIC_STRING); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); assertNotNull(tsuids); @@ -150,7 +101,7 @@ public void metricOnlyMetaStar() throws Exception { @Test public void metricOnlyData() throws Exception { generateData(); - final SearchQuery query = new SearchQuery("sys.cpu.user"); + final SearchQuery query = new SearchQuery(METRIC_STRING); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -163,7 +114,7 @@ public void metricOnlyData() throws Exception { @Test public void metricOnly2Meta() throws Exception { generateMeta(); - final SearchQuery query = new SearchQuery("sys.cpu.nice"); + final SearchQuery query = new SearchQuery(METRIC_B_STRING); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); assertNotNull(tsuids); @@ -174,7 +125,7 @@ public void metricOnly2Meta() throws Exception { @Test public void metricOnly2Data() throws Exception { generateData(); - final SearchQuery query = new SearchQuery("sys.cpu.nice"); + final SearchQuery query = new SearchQuery(METRIC_B_STRING); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -186,7 +137,7 @@ public void metricOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchMetricMeta() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); - final SearchQuery query = new SearchQuery("sys.cpu.system"); + final SearchQuery query = new SearchQuery(NSUN_METRIC); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); lookup.lookup(); } @@ -217,7 +168,7 @@ public void tagkOnlyMeta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", null)); + tags.add(new Pair(TAGK_STRING, null)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -233,7 +184,7 @@ public void tagkOnlyMetaStar() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "*")); + tags.add(new Pair(TAGK_STRING, "*")); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -249,11 +200,12 @@ public void tagkOnlyData() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", null)); + tags.add(new Pair(TAGK_STRING, null)); final SearchQuery query = new SearchQuery(tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); + Collections.sort(tsuids, Bytes.MEMCMP); // for salting assertNotNull(tsuids); assertEquals(5, tsuids.size()); for (int i = 0; i < 5; i++) { @@ -266,7 +218,7 @@ public void tagkOnly2Meta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("owner", null)); + tags.add(new Pair(TAGK_B_STRING, null)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -281,11 +233,12 @@ public void tagkOnly2Data() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair("owner", null)); + tags.add(new Pair(TAGK_B_STRING, null)); final SearchQuery query = new SearchQuery(tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); + Collections.sort(tsuids, Bytes.MEMCMP); // for salting assertNotNull(tsuids); assertEquals(2, tsuids.size()); assertArrayEquals(test_tsuids.get(3), tsuids.get(0)); @@ -297,7 +250,7 @@ public void noSuchTagkMeta() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); final List> tags = new ArrayList>(1); - tags.add(new Pair("dc", null)); + tags.add(new Pair(NSUN_TAGK, null)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); lookup.lookup(); @@ -308,7 +261,7 @@ public void tagvOnlyMeta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web01")); + tags.add(new Pair(null, TAGV_STRING)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -325,7 +278,7 @@ public void tagvOnlyMetaStar() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("*", "web01")); + tags.add(new Pair("*", TAGV_STRING)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -342,11 +295,12 @@ public void tagvOnlyData() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web01")); + tags.add(new Pair(null, TAGV_STRING)); final SearchQuery query = new SearchQuery(tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); + Collections.sort(tsuids, Bytes.MEMCMP); // for salting assertNotNull(tsuids); assertEquals(4, tsuids.size()); assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); @@ -360,7 +314,7 @@ public void tagvOnly2Meta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web02")); + tags.add(new Pair(null, TAGV_B_STRING)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -375,11 +329,12 @@ public void tagvOnly2Data() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web02")); + tags.add(new Pair(null, TAGV_B_STRING)); final SearchQuery query = new SearchQuery(tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); + Collections.sort(tsuids, Bytes.MEMCMP); // for salting assertNotNull(tsuids); assertEquals(2, tsuids.size()); assertArrayEquals(test_tsuids.get(1), tsuids.get(0)); @@ -391,7 +346,7 @@ public void noSuchTagvMeta() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web03")); + tags.add(new Pair(null, NSUN_TAGV)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); lookup.lookup(); @@ -402,8 +357,8 @@ public void metricAndTagkMeta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", null)); - final SearchQuery query = new SearchQuery("sys.cpu.nice", + tags.add(new Pair(TAGK_STRING, null)); + final SearchQuery query = new SearchQuery(METRIC_B_STRING, tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -417,8 +372,8 @@ public void metricAndTagkMetaStar() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "*")); - final SearchQuery query = new SearchQuery("sys.cpu.nice", + tags.add(new Pair(TAGK_STRING, "*")); + final SearchQuery query = new SearchQuery(METRIC_B_STRING, tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -432,8 +387,8 @@ public void metricAndTagkData() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", null)); - final SearchQuery query = new SearchQuery("sys.cpu.nice", + tags.add(new Pair(TAGK_STRING, null)); + final SearchQuery query = new SearchQuery(METRIC_B_STRING, tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); @@ -448,9 +403,8 @@ public void metricAndTagvMeta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web02")); - final SearchQuery query = new SearchQuery("sys.cpu.idle", - tags); + tags.add(new Pair(null, TAGV_B_STRING)); + final SearchQuery query = new SearchQuery("filtered", tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); assertNotNull(tsuids); @@ -463,9 +417,8 @@ public void metricAndTagvMetaStar() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("*", "web02")); - final SearchQuery query = new SearchQuery("sys.cpu.idle", - tags); + tags.add(new Pair("*", TAGV_B_STRING)); + final SearchQuery query = new SearchQuery("filtered",tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); assertNotNull(tsuids); @@ -478,9 +431,8 @@ public void metricAndTagvData() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair(null, "web02")); - final SearchQuery query = new SearchQuery("sys.cpu.idle", - tags); + tags.add(new Pair(null, TAGV_B_STRING)); + final SearchQuery query = new SearchQuery("filtered", tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -494,9 +446,8 @@ public void metricAndTagPairMeta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "web01")); - final SearchQuery query = new SearchQuery("sys.cpu.idle", - tags); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); + final SearchQuery query = new SearchQuery("filtered", tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); assertNotNull(tsuids); @@ -509,9 +460,8 @@ public void metricAndTagPairData() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "web01")); - final SearchQuery query = new SearchQuery("sys.cpu.idle", - tags); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); + final SearchQuery query = new SearchQuery("filtered", tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); query.setUseMeta(false); @@ -526,7 +476,7 @@ public void tagPairOnlyMeta() throws Exception { generateMeta(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "web01")); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); final SearchQuery query = new SearchQuery(tags); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -542,11 +492,12 @@ public void tagPairOnlyData() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "web01")); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); final SearchQuery query = new SearchQuery(tags); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); + Collections.sort(tsuids, Bytes.MEMCMP); // for salting assertNotNull(tsuids); assertEquals(3, tsuids.size()); assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); @@ -559,7 +510,7 @@ public void limitVerification() throws Exception { generateData(); final List> tags = new ArrayList>(1); - tags.add(new Pair("host", "web01")); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); final SearchQuery query = new SearchQuery(tags); query.setUseMeta(false); query.setLimit(1); @@ -598,11 +549,14 @@ private void generateData() { final byte[] qual = new byte[] { 0, 0 }; final byte[] val = new byte[] { 1 }; for (final byte[] tsuid : test_tsuids) { - byte[] row_key = new byte[tsuid.length + Const.TIMESTAMP_BYTES]; - System.arraycopy(tsuid, 0, row_key, 0, TSDB.metrics_width()); + byte[] row_key = new byte[Const.SALT_WIDTH() + tsuid.length + + Const.TIMESTAMP_BYTES]; + System.arraycopy(tsuid, 0, row_key, Const.SALT_WIDTH(), + TSDB.metrics_width()); System.arraycopy(tsuid, TSDB.metrics_width(), row_key, - TSDB.metrics_width() + Const.TIMESTAMP_BYTES, + Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES, tsuid.length - TSDB.metrics_width()); + RowKey.prefixKeyWithSalt(row_key); storage.addColumn(row_key, qual, val); } } diff --git a/test/search/TestTimeSeriesLookupSalted.java b/test/search/TestTimeSeriesLookupSalted.java new file mode 100644 index 0000000000..a8f1109125 --- /dev/null +++ b/test/search/TestTimeSeriesLookupSalted.java @@ -0,0 +1,40 @@ +// 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.search; + +import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.HBaseClient; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, + KeyValue.class, Scanner.class, TimeSeriesLookup.class, Const.class }) +public class TestTimeSeriesLookupSalted extends TestTimeSeriesLookup { + + @Before + public void beforeLocalSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(4); + } +} From 802576fddf0cb3052e79c6897389bfca55e49951 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 27 Sep 2015 13:54:53 -0700 Subject: [PATCH 232/826] Fix up the search API for lookups to be fully asynchronous. Thanks to to @Dieken for his work on the salting patch. Signed-off-by: Chris Larsen --- src/tsd/SearchRpc.java | 145 ++++++++++---- test/search/TestTimeSeriesLookup.java | 68 ++++--- test/tsd/TestSearchRpc.java | 262 +++++++++++++++++--------- 3 files changed, 320 insertions(+), 155 deletions(-) diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 12431a0abe..5d8a59b09e 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -16,10 +16,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; @@ -29,6 +34,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Exceptions; import net.opentsdb.utils.Pair; /** @@ -166,44 +172,119 @@ private void processLookup(final TSDB tsdb, final HttpQuery query, "Missing metric and tags. Please supply at least one value."); } final long start = System.currentTimeMillis(); - try { - final List tsuids = - new TimeSeriesLookup(tsdb, search_query).lookup(); - - search_query.setTotalResults(tsuids.size()); - // TODO maybe track in nanoseconds so we can get a floating point. But most - // lookups will probably take a fair amount of time. - search_query.setTime(System.currentTimeMillis() - start); + + class MetricCB implements Callback { + final Map series; + MetricCB(final Map series) { + this.series = series; + } - final List results = new ArrayList(tsuids.size()); + @Override + public Object call(final String name) throws Exception { + series.put("metric", name); + return null; + } + } + + class TagsCB implements Callback> { + final Map series; + TagsCB(final Map series) { + this.series = series; + } - Map series; - List tag_ids; + @Override + public Object call(final HashMap names) throws Exception { + series.put("tags", names); + return null; + } + } + + class Serialize implements Callback> { + final List results; + Serialize(final List results) { + this.results = results; + } - // TODO - honor limit and pagination - for (final byte[] tsuid : tsuids) { - series = new HashMap((tsuid.length / 2) + 1); - try { + @Override + public Object call(final ArrayList ignored) throws Exception { + search_query.setResults(results); + search_query.setTime(System.currentTimeMillis() - start); + query.sendReply(query.serializer().formatSearchResultsV1(search_query)); + return null; + } + } + + class LookupCB implements Callback, List> { + @Override + public Deferred call(final List tsuids) throws Exception { + final List results = new ArrayList(tsuids.size()); + search_query.setTotalResults(tsuids.size()); + + final ArrayList> deferreds = + new ArrayList>(tsuids.size()); + + for (final byte[] tsuid : tsuids) { + // has to be concurrent if the uid table is split across servers + final Map series = + new ConcurrentHashMap(3); + results.add(series); + series.put("tsuid", UniqueId.uidToString(tsuid)); - series.put("metric", RowKey.metricNameAsync(tsdb, tsuid) - .joinUninterruptibly()); - tag_ids = UniqueId.getTagPairsFromTSUID(tsuid); - series.put("tags", Tags.resolveIdsAsync(tsdb, tag_ids) - .joinUninterruptibly()); - } catch (NoSuchUniqueId nsui) { - throw new BadRequestException(HttpResponseStatus.NOT_FOUND, - "Unable to resolve one or more UIDs", nsui); - } catch (Exception e) { - throw new RuntimeException("Shouldn't be here", e); + deferreds.add(RowKey.metricNameAsync(tsdb, tsuid) + .addCallback(new MetricCB(series))); + + final List tag_ids = UniqueId.getTagPairsFromTSUID(tsuid); + deferreds.add(Tags.resolveIdsAsync(tsdb, tag_ids) + .addCallback(new TagsCB(series))); } - results.add(series); + + return Deferred.group(deferreds).addCallback(new Serialize(results)); } - - search_query.setResults(results); - query.sendReply(query.serializer().formatSearchResultsV1(search_query)); - } catch (NoSuchUniqueName nsun) { - throw new BadRequestException(HttpResponseStatus.NOT_FOUND, - "Unable to resolve one or more names", nsun); } + + class ErrCB implements Callback { + @Override + public Object call(final Exception e) throws Exception { + if (e instanceof NoSuchUniqueId) { + query.sendReply(HttpResponseStatus.NOT_FOUND, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to resolve one or more TSUIDs", (NoSuchUniqueId)e))); + } else if (e instanceof NoSuchUniqueName) { + query.sendReply(HttpResponseStatus.NOT_FOUND, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to resolve one or more UIDs", (NoSuchUniqueName)e))); + } else if (e instanceof DeferredGroupException) { + final Throwable ex = Exceptions.getCause((DeferredGroupException)e); + if (ex instanceof NoSuchUniqueId) { + query.sendReply(HttpResponseStatus.NOT_FOUND, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to resolve one or more TSUIDs", (NoSuchUniqueId)ex))); + } else if (ex instanceof NoSuchUniqueName) { + query.sendReply(HttpResponseStatus.NOT_FOUND, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.NOT_FOUND, + "Unable to resolve one or more UIDs", (NoSuchUniqueName)ex))); + } else { + query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + "Unexpected exception", ex))); + } + } else { + query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, + query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + "Unexpected exception", e))); + } + return null; + } + } + + new TimeSeriesLookup(tsdb, search_query).lookupAsync() + .addCallback(new LookupCB()) + .addErrback(new ErrCB()); } } diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index cdf3e3de22..d07c14501b 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -54,7 +54,7 @@ public class TestTimeSeriesLookup extends BaseTsdbTest { // tsuids - private static List test_tsuids = new ArrayList(7); + public static List test_tsuids = new ArrayList(7); static { test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }); @@ -69,6 +69,7 @@ public class TestTimeSeriesLookup extends BaseTsdbTest { @Before public void beforeLocal() { + storage = new MockBase(tsdb, client, true, true, true, true); when(metrics.getIdAsync("no.values")) .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 11 })); when(metrics.getIdAsync("filtered")) @@ -77,7 +78,7 @@ public void beforeLocal() { @Test public void metricOnlyMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final SearchQuery query = new SearchQuery(METRIC_STRING); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -90,7 +91,7 @@ public void metricOnlyMeta() throws Exception { // returns everything @Test public void metricOnlyMetaStar() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final SearchQuery query = new SearchQuery("*"); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -100,7 +101,7 @@ public void metricOnlyMetaStar() throws Exception { @Test public void metricOnlyData() throws Exception { - generateData(); + generateData(tsdb, storage); final SearchQuery query = new SearchQuery(METRIC_STRING); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); @@ -113,7 +114,7 @@ public void metricOnlyData() throws Exception { @Test public void metricOnly2Meta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final SearchQuery query = new SearchQuery(METRIC_B_STRING); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -124,7 +125,7 @@ public void metricOnly2Meta() throws Exception { @Test public void metricOnly2Data() throws Exception { - generateData(); + generateData(tsdb, storage); final SearchQuery query = new SearchQuery(METRIC_B_STRING); query.setUseMeta(false); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); @@ -136,7 +137,6 @@ public void metricOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchMetricMeta() throws Exception { - storage = new MockBase(tsdb, client, true, true, true, true); final SearchQuery query = new SearchQuery(NSUN_METRIC); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); lookup.lookup(); @@ -144,7 +144,7 @@ public void noSuchMetricMeta() throws Exception { @Test public void metricOnlyNoValuesMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final SearchQuery query = new SearchQuery("no.values"); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); final List tsuids = lookup.lookup(); @@ -154,7 +154,7 @@ public void metricOnlyNoValuesMeta() throws Exception { @Test public void metricOnlyNoValuesData() throws Exception { - generateData(); + generateData(tsdb, storage); final SearchQuery query = new SearchQuery("no.values"); final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); query.setUseMeta(false); @@ -165,7 +165,7 @@ public void metricOnlyNoValuesData() throws Exception { @Test public void tagkOnlyMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, null)); @@ -181,7 +181,7 @@ public void tagkOnlyMeta() throws Exception { @Test public void tagkOnlyMetaStar() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, "*")); @@ -197,7 +197,7 @@ public void tagkOnlyMetaStar() throws Exception { @Test public void tagkOnlyData() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, null)); @@ -215,7 +215,7 @@ public void tagkOnlyData() throws Exception { @Test public void tagkOnly2Meta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_B_STRING, null)); @@ -230,7 +230,7 @@ public void tagkOnly2Meta() throws Exception { @Test public void tagkOnly2Data() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_B_STRING, null)); @@ -247,7 +247,6 @@ public void tagkOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchTagkMeta() throws Exception { - storage = new MockBase(tsdb, client, true, true, true, true); final List> tags = new ArrayList>(1); tags.add(new Pair(NSUN_TAGK, null)); @@ -258,7 +257,7 @@ public void noSuchTagkMeta() throws Exception { @Test public void tagvOnlyMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(null, TAGV_STRING)); @@ -275,7 +274,7 @@ public void tagvOnlyMeta() throws Exception { @Test public void tagvOnlyMetaStar() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair("*", TAGV_STRING)); @@ -292,7 +291,7 @@ public void tagvOnlyMetaStar() throws Exception { @Test public void tagvOnlyData() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(null, TAGV_STRING)); @@ -311,7 +310,7 @@ public void tagvOnlyData() throws Exception { @Test public void tagvOnly2Meta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(null, TAGV_B_STRING)); @@ -326,7 +325,7 @@ public void tagvOnly2Meta() throws Exception { @Test public void tagvOnly2Data() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(null, TAGV_B_STRING)); @@ -343,7 +342,6 @@ public void tagvOnly2Data() throws Exception { @Test (expected = NoSuchUniqueName.class) public void noSuchTagvMeta() throws Exception { - storage = new MockBase(tsdb, client, true, true, true, true); final List> tags = new ArrayList>(1); tags.add(new Pair(null, NSUN_TAGV)); @@ -354,7 +352,7 @@ public void noSuchTagvMeta() throws Exception { @Test public void metricAndTagkMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, null)); @@ -369,7 +367,7 @@ public void metricAndTagkMeta() throws Exception { @Test public void metricAndTagkMetaStar() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, "*")); @@ -384,7 +382,7 @@ public void metricAndTagkMetaStar() throws Exception { @Test public void metricAndTagkData() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, null)); @@ -400,7 +398,7 @@ public void metricAndTagkData() throws Exception { @Test public void metricAndTagvMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(null, TAGV_B_STRING)); @@ -414,7 +412,7 @@ public void metricAndTagvMeta() throws Exception { @Test public void metricAndTagvMetaStar() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair("*", TAGV_B_STRING)); @@ -428,7 +426,7 @@ public void metricAndTagvMetaStar() throws Exception { @Test public void metricAndTagvData() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(null, TAGV_B_STRING)); @@ -443,7 +441,7 @@ public void metricAndTagvData() throws Exception { @Test public void metricAndTagPairMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, TAGV_STRING)); @@ -457,7 +455,7 @@ public void metricAndTagPairMeta() throws Exception { @Test public void metricAndTagPairData() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, TAGV_STRING)); @@ -473,7 +471,7 @@ public void metricAndTagPairData() throws Exception { @Test public void tagPairOnlyMeta() throws Exception { - generateMeta(); + generateMeta(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, TAGV_STRING)); @@ -489,7 +487,7 @@ public void tagPairOnlyMeta() throws Exception { @Test public void tagPairOnlyData() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, TAGV_STRING)); @@ -507,7 +505,7 @@ public void tagPairOnlyData() throws Exception { @Test public void limitVerification() throws Exception { - generateData(); + generateData(tsdb, storage); final List> tags = new ArrayList>(1); tags.add(new Pair(TAGK_STRING, TAGV_STRING)); @@ -526,8 +524,7 @@ public void limitVerification() throws Exception { /** * Stores some data in the mock tsdb-meta table for unit testing */ - private void generateMeta() { - storage = new MockBase(tsdb, client, true, true, true, true); + public static void generateMeta(final TSDB tsdb, final MockBase storage) { final List families = new ArrayList(1); families.add(TSMeta.FAMILY); storage.addTable("tsdb-meta".getBytes(), families); @@ -542,8 +539,7 @@ private void generateMeta() { /** * Stores some data in the mock tsdb data table for unit testing */ - private void generateData() { - storage = new MockBase(tsdb, client, true, true, true, true); + public static void generateData(final TSDB tsdb, final MockBase storage) { storage.setFamily("t".getBytes(MockBase.ASCII())); final byte[] qual = new byte[] { 0, 0 }; diff --git a/test/tsd/TestSearchRpc.java b/test/tsd/TestSearchRpc.java index 5232fdf512..a881b59990 100644 --- a/test/tsd/TestSearchRpc.java +++ b/test/tsd/TestSearchRpc.java @@ -16,6 +16,7 @@ import static org.mockito.Matchers.anyChar; import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import static org.junit.Assert.assertEquals; @@ -24,26 +25,29 @@ import java.lang.reflect.Field; import java.nio.charset.Charset; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; +import net.opentsdb.core.BaseTsdbTest; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; +import net.opentsdb.search.SearchPlugin; import net.opentsdb.search.SearchQuery; +import net.opentsdb.search.TestTimeSeriesLookup; import net.opentsdb.search.TimeSeriesLookup; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; -import net.opentsdb.utils.JSON; import net.opentsdb.utils.Pair; +import org.hbase.async.Bytes; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; @@ -54,31 +58,26 @@ 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 com.stumbleupon.async.Deferred; +import com.sun.java_cup.internal.runtime.Scanner; @RunWith(PowerMockRunner.class) -@PrepareForTest({TSDB.class, Config.class, HttpQuery.class, UniqueId.class, - RowKey.class, Tags.class, TimeSeriesLookup.class, SearchRpc.class}) -public final class TestSearchRpc { - private TSDB tsdb = null; +@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, UniqueId.class, + RowKey.class, Tags.class, TimeSeriesLookup.class, SearchRpc.class, + SearchPlugin.class, Scanner.class }) +public final class TestSearchRpc extends BaseTsdbTest { + private SearchPlugin mock_plugin; private SearchRpc rpc = new SearchRpc(); private SearchQuery search_query = null; - private TimeSeriesLookup mock_lookup = null; private static final Charset UTF = Charset.forName("UTF-8"); - private static List test_tsuids = new ArrayList(3); - static { - test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); - test_tsuids.add(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 2 }); - test_tsuids.add(new byte[] { 0, 0, 2, 0, 0, 1, 0, 0, 1 }); - } @Before - public void before() throws Exception { - tsdb = NettyMocks.getMockedHTTPTSDB(); + public void beforeLocal() throws Exception { + HttpQuery.initializeSerializerMaps(tsdb); } @Test @@ -221,9 +220,7 @@ public void searchMissingQuery() throws Exception { @Test (expected = BadRequestException.class) public void searchPluginNotEnabled() throws Exception { - when(tsdb.executeSearch((SearchQuery)any())) - .thenThrow(new IllegalStateException( - "Searching has not been enabled on this TSD")); + Whitebox.setInternalState(tsdb, "search", (SearchPlugin)null); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsmeta?query=*"); rpc.execute(tsdb, query); @@ -244,48 +241,122 @@ public void searchInvalidStartIndex() throws Exception { } @Test - public void searchLookup() throws Exception { - setupAnswerLookupQuery(); + public void searchLookupTagkOnlyMeta() throws Exception { + setupLookup(true); final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/lookup?m={host=}"); rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String result = query.response().getContent().toString(UTF); assertTrue(result.contains("\"host\":\"web01\"")); - assertTrue(result.contains("\"totalResults\":3")); + assertTrue(result.contains("\"totalResults\":5")); + assertTrue(result.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(result.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(result.contains("\"tsuid\":\"000002000001000001\"")); + assertTrue(result.contains("\"tsuid\":\"000004000001000001000003000005\"")); + assertTrue(result.contains("\"tsuid\":\"000004000001000002000003000005\"")); } @Test - public void searchLookupPOST() throws Exception { - setupAnswerLookupQuery(); - SearchQuery q = new SearchQuery(); - q.setTags(new ArrayList>(2)); - q.getTags().add(new Pair("host", "web01")); - q.getTags().add(new Pair("dc", "phx")); - + public void searchLookupPOSTTagkOnlyMeta() throws Exception { + setupLookup(true); final HttpQuery query = NettyMocks.postQuery(tsdb, - "/api/search/lookup", "{\"tags\":[{\"key\":\"host\",\"value\":\"web01\"}]}"); + "/api/search/lookup", "{\"tags\":[{\"key\":\"host\",\"value\":null}]}"); + query.setSerializer(); + rpc.execute(tsdb, query); + + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String result = query.response().getContent().toString(UTF); + assertTrue(result.contains("\"host\":\"web01\"")); + assertTrue(result.contains("\"totalResults\":5")); + assertTrue(result.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(result.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(result.contains("\"tsuid\":\"000002000001000001\"")); + assertTrue(result.contains("\"tsuid\":\"000004000001000001000003000005\"")); + assertTrue(result.contains("\"tsuid\":\"000004000001000002000003000005\"")); + } + + @Test + public void searchLookupPOSTTagkOnlyData() throws Exception { + setupLookup(false); + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/search/lookup", + "{\"tags\":[{\"key\":\"host\",\"value\":null}],\"useMeta\":false}"); rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String result = query.response().getContent().toString(UTF); assertTrue(result.contains("\"host\":\"web01\"")); - assertTrue(result.contains("\"totalResults\":3")); + assertTrue(result.contains("\"totalResults\":5")); + assertTrue(result.contains("\"tsuid\":\"000001000001000001\"")); + assertTrue(result.contains("\"tsuid\":\"000001000001000002\"")); + assertTrue(result.contains("\"tsuid\":\"000002000001000001\"")); + assertTrue(result.contains("\"tsuid\":\"000004000001000001000003000005\"")); + assertTrue(result.contains("\"tsuid\":\"000004000001000002000003000005\"")); + } + + @Test + public void searchLookupNoMetaTable() throws Exception { + setupLookup(false); + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/search/lookup", + "{\"tags\":[{\"key\":\"host\",\"value\":null}]}"); + rpc.execute(tsdb, query); + + assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR, + query.response().getStatus()); + final String result = query.response().getContent().toString(UTF); + assertTrue(result.contains("\"code\":500")); + assertTrue(result.contains("\"message\":\"Unexpected exception\"")); } @Test (expected = BadRequestException.class) public void searchLookupMissingQuery() throws Exception { - setupAnswerLookupQuery(); - final HttpQuery query = NettyMocks.getQuery(tsdb, - "/api/search/lookup"); + setupLookup(true); + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/lookup"); rpc.execute(tsdb, query); } @Test (expected = BadRequestException.class) public void searchLookupBadQuery() throws Exception { - setupAnswerLookupQuery(); + setupLookup(true); + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/lookup?m={"); + rpc.execute(tsdb, query); + } + + @Test + public void searchLookupNSUN() throws Exception { + setupLookup(true); final HttpQuery query = NettyMocks.getQuery(tsdb, - "/api/search/lookup?m={"); + "/api/search/lookup?m=" + NSUN_METRIC); rpc.execute(tsdb, query); + + assertEquals(HttpResponseStatus.NOT_FOUND, query.response().getStatus()); + final String result = query.response().getContent().toString(UTF); + assertTrue(result.contains("\"code\":404")); + assertTrue(result.contains("\"details\":\"No such name")); + } + + @Test + public void searchLookupNSUI() throws Exception { + setupLookup(true); + when(metrics.getNameAsync(new byte[] { 0, 0, 4 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromError( + new NoSuchUniqueId("metrics", new byte[] { 0, 0, 4 })); + } + }); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/search/lookup", "{\"tags\":[{\"key\":\"host\",\"value\":null}]}"); + query.setSerializer(); + rpc.execute(tsdb, query); + + assertEquals(HttpResponseStatus.NOT_FOUND, query.response().getStatus()); + final String result = query.response().getContent().toString(UTF); + assertTrue(result.contains("\"code\":404")); + assertTrue(result.contains("\"details\":\"No such unique ID")); } /** @@ -294,7 +365,10 @@ public void searchLookupBadQuery() throws Exception { * responses for parsing tests. */ private void setupAnswerSearchQuery() { - when(tsdb.executeSearch((SearchQuery)any())).thenAnswer( + mock_plugin = mock(SearchPlugin.class); + Whitebox.setInternalState(tsdb, "search", mock_plugin); + + when(mock_plugin.executeQuery((SearchQuery)any())).thenAnswer( new Answer>() { @Override @@ -392,56 +466,70 @@ public Deferred answer(InvocationOnMock invocation) }); } - - @SuppressWarnings("unchecked") - private void setupAnswerLookupQuery() throws Exception { - PowerMockito.mockStatic(RowKey.class); - when(RowKey.metricNameAsync(tsdb, test_tsuids.get(0))) - .thenReturn(Deferred.fromResult("sys.cpu.user")); - when(RowKey.metricNameAsync(tsdb, test_tsuids.get(1))) - .thenReturn(Deferred.fromResult("sys.cpu.user")); - when(RowKey.metricNameAsync(tsdb, test_tsuids.get(2))) - .thenReturn(Deferred.fromResult("sys.cpu.nice")); - PowerMockito.mockStatic(UniqueId.class); - final List pair_a = new ArrayList(2); - pair_a.add(new byte[] { 0, 0, 1 }); - pair_a.add(new byte[] { 0, 0, 1 }); + private void setupLookup(final boolean use_meta) { + storage = new MockBase(tsdb, client, true, true, true, true); + if (use_meta) { + TestTimeSeriesLookup.generateMeta(tsdb, storage); + } else { + TestTimeSeriesLookup.generateData(tsdb, storage); + } - final List pair_b = new ArrayList(2); - pair_b.add(new byte[] { 0, 0, 1 }); - pair_b.add(new byte[] { 0, 0, 2 }); - - when(UniqueId.getTagPairsFromTSUID(test_tsuids.get(0))) - .thenReturn(pair_a); - when(UniqueId.getTagPairsFromTSUID(test_tsuids.get(1))) - .thenReturn(pair_b); - when(UniqueId.getTagPairsFromTSUID(test_tsuids.get(2))) - .thenReturn(pair_a); - when(UniqueId.uidToString((byte[])any())).thenCallRealMethod(); - - PowerMockito.mockStatic(Tags.class); - final HashMap tags_a = new HashMap(1); - tags_a.put("host", "web01"); - - final HashMap tags_b = new HashMap(1); - tags_b.put("host", "web02"); - - when(Tags.resolveIdsAsync(tsdb, pair_a)) - .thenReturn(Deferred.fromResult(tags_a)); - when(Tags.resolveIdsAsync(tsdb, pair_b)) - .thenReturn(Deferred.fromResult(tags_b)); - - when(Tags.parseWithMetric(anyString(), anyList())).thenCallRealMethod(); - when(Tags.splitString(anyString(), anyChar())).thenCallRealMethod(); - PowerMockito.doCallRealMethod().when(Tags.class, "parse", - anyList(), anyString()); - - mock_lookup = mock(TimeSeriesLookup.class); - PowerMockito.whenNew(TimeSeriesLookup.class) - .withArguments((TSDB)any(), (SearchQuery)any()) - .thenReturn(mock_lookup); - - when(mock_lookup.lookup()).thenReturn(test_tsuids); + when(metrics.getNameAsync(new byte[] { 0, 0, 4 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("filtered"); + } + }); + when(tag_names.getNameAsync(new byte[] { 0, 0, 6 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("6"); + } + }); + when(tag_names.getNameAsync(new byte[] { 0, 0, 8 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("8"); + } + }); + when(tag_names.getNameAsync(new byte[] { 0, 0, 9 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("9"); + } + }); + when(tag_values.getNameAsync(new byte[] { 0, 0, 7 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("7"); + } + }); + when(tag_values.getNameAsync(new byte[] { 0, 0, 5 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("5"); + } + }); + when(tag_values.getNameAsync(new byte[] { 0, 0, 10 })) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult("10"); + } + }); } } From 718dafa3718c92dd425be64ecc3402a779080f90 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 3 Oct 2015 19:11:16 -0700 Subject: [PATCH 233/826] Version to 2.2.0RC2-SNAPSHOT Signed-off-by: Chris Larsen --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 3d43a30c24..b9d6c136f9 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.2.0RC1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.0RC2-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 1827ae909732bfe32c1a5de58071872d8cb19a00 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 3 Oct 2015 19:00:57 -0700 Subject: [PATCH 234/826] Bump the AsyncHBase version to 1.7.1-SNAPSHOT to fix decoding bugs with pre 0.96 region servers. Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 | 1 + third_party/hbase/include.mk | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 b/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 new file mode 100644 index 0000000000..75abc13db6 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 @@ -0,0 +1 @@ +898d34a463b52e570addf0f0160add48 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index e2e33dcc32..0e25495aba 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.0 +ASYNCHBASE_VERSION := 1.7.1-20151004.015637-1 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://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.1-SNAPSHOT/ $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From 41b48b5700227bca3d3614b063c5f67d014eed70 Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Thu, 10 Sep 2015 18:38:59 +0100 Subject: [PATCH 235/826] Allow overriding the maximum number of tags that can be submitted with a metric via the config (defaults to 8) Also convert Const.MAX_NUM_TAGS to Const.MAX_NUM_TAGS() Signed-off-by: Chris Larsen --- src/core/Const.java | 22 +++++++++++++++++-- src/core/IncomingDataPoints.java | 4 ++-- src/core/TSDB.java | 3 +++ test/core/TestIncomingDataPoints.java | 1 + test/core/TestTSDB.java | 22 +++++++++++++++++-- test/core/TestTsdbQueryAggregatorsSalted.java | 1 + test/core/TestTsdbQueryDownsampleSalted.java | 1 + test/core/TestTsdbQuerySalted.java | 1 + test/core/TestTsdbQuerySaltedAppend.java | 1 + test/tools/TestFsckSalted.java | 1 + 10 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/core/Const.java b/src/core/Const.java index 7aa503a744..d9f97c2aab 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -19,8 +19,26 @@ public final class Const { public static final short TIMESTAMP_BYTES = 4; /** Maximum number of tags allowed per data point. */ - public static final short MAX_NUM_TAGS = 8; - // 8 is an aggressive limit on purpose. Can always be increased later. + private static short MAX_NUM_TAGS = 8; + public static short MAX_NUM_TAGS() { + return MAX_NUM_TAGS; + } + + /** + * -------------- WARNING ---------------- + * Package private method to override the maximum number of tags. + * 8 is an aggressive limit on purpose to avoid performance issues. + * @param tags The number of tags to allow + * @throws IllegalArgumentException if the number of tags is less + * than 1 (OpenTSDB requires at least one tag per metric). + */ + static void setMaxNumTags(final short tags) { + if (tags < 1) { + throw new IllegalArgumentException("tsd.storage.max_tags must be greater than 0"); + } + MAX_NUM_TAGS = tags; + } + /** Number of LSBs in time_deltas reserved for flags. */ public static final short FLAG_BITS = 4; diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index ea415c69c4..1aded5a6b7 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -100,9 +100,9 @@ static void checkMetricAndTags(final String metric, if (tags.size() <= 0) { throw new IllegalArgumentException("Need at least one tag (metric=" + metric + ", tags=" + tags + ')'); - } else if (tags.size() > Const.MAX_NUM_TAGS) { + } else if (tags.size() > Const.MAX_NUM_TAGS()) { throw new IllegalArgumentException("Too many tags: " + tags.size() - + " maximum allowed: " + Const.MAX_NUM_TAGS + ", tags: " + tags); + + " maximum allowed: " + Const.MAX_NUM_TAGS() + ", tags: " + tags); } Tags.validateString("metric name", metric); diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 3a44993c66..64cf75c779 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -164,6 +164,9 @@ public TSDB(final HBaseClient client, final Config config) { if (config.hasProperty("tsd.storage.uid.width.tagv")) { TAG_VALUE_WIDTH = config.getShort("tsd.storage.uid.width.tagv"); } + if (config.hasProperty("tsd.storage.max_tags")) { + Const.setMaxNumTags(config.getShort("tsd.storage.max_tags")); + } if (config.hasProperty("tsd.storage.salt.buckets")) { Const.setSaltBuckets(config.getInt("tsd.storage.salt.buckets")); } diff --git a/test/core/TestIncomingDataPoints.java b/test/core/TestIncomingDataPoints.java index e1a333e5ce..60f8696b54 100644 --- a/test/core/TestIncomingDataPoints.java +++ b/test/core/TestIncomingDataPoints.java @@ -37,6 +37,7 @@ public void metricNameAsyncSalted() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); final IncomingDataPoints dps = new IncomingDataPoints(tsdb); dps.setSeries(METRIC_STRING, tags); diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index ec8f3a738c..ae3fc98230 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -94,7 +94,20 @@ public void ctorOverrideUIDWidths() throws Exception { config.overrideConfig("tsd.storage.uid.width.tagv", "3"); new TSDB(client, config); } - + + @Test + public void ctorOverrideMaxNumTags() throws Exception { + assertEquals(8, Const.MAX_NUM_TAGS()); + + config.overrideConfig("tsd.storage.max_tags", "12"); + new TSDB(client, config); + assertEquals(12, Const.MAX_NUM_TAGS()); + + // IMPORTANT Restore + config.overrideConfig("tsd.storage.max_tags", "8"); + new TSDB(client, config); + } + @Test public void ctorOverrideSalt() throws Exception { assertEquals(20, Const.SALT_BUCKETS()); @@ -111,7 +124,7 @@ public void ctorOverrideSalt() throws Exception { config.overrideConfig("tsd.storage.salt.width", "0"); new TSDB(client, config); } - + @Test public void initializePluginsDefaults() { // no configured plugin path, plugins disabled, no exceptions @@ -836,6 +849,7 @@ public void addPointWithSalt() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); setupAddPointStorage(); @@ -852,6 +866,7 @@ public void addPointWithSaltDifferentTags() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); setupAddPointStorage(); tags.put(TAGK_STRING, TAGV_B_STRING); @@ -869,6 +884,7 @@ public void addPointWithSaltDifferentTime() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); setupAddPointStorage(); @@ -986,6 +1002,7 @@ public void addPointAppendWithSalt() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); Whitebox.setInternalState(config, "enable_appends", true); setupAddPointStorage(); @@ -1002,6 +1019,7 @@ public void addPointAppendAppendingWithSalt() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); Whitebox.setInternalState(config, "enable_appends", true); setupAddPointStorage(); diff --git a/test/core/TestTsdbQueryAggregatorsSalted.java b/test/core/TestTsdbQueryAggregatorsSalted.java index bcab6dd77f..5ad0cf8d33 100644 --- a/test/core/TestTsdbQueryAggregatorsSalted.java +++ b/test/core/TestTsdbQueryAggregatorsSalted.java @@ -30,6 +30,7 @@ public void beforeLocal() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); query = new TsdbQuery(tsdb); } diff --git a/test/core/TestTsdbQueryDownsampleSalted.java b/test/core/TestTsdbQueryDownsampleSalted.java index bd8249f2f5..d4a679c8cf 100644 --- a/test/core/TestTsdbQueryDownsampleSalted.java +++ b/test/core/TestTsdbQueryDownsampleSalted.java @@ -30,6 +30,7 @@ public void beforeLocal() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); query = new TsdbQuery(tsdb); } diff --git a/test/core/TestTsdbQuerySalted.java b/test/core/TestTsdbQuerySalted.java index 695dbf151c..3061da6221 100644 --- a/test/core/TestTsdbQuerySalted.java +++ b/test/core/TestTsdbQuerySalted.java @@ -29,6 +29,7 @@ public void beforeLocal() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); query = new TsdbQuery(tsdb); } diff --git a/test/core/TestTsdbQuerySaltedAppend.java b/test/core/TestTsdbQuerySaltedAppend.java index 81e58cdccc..1f95bf0616 100644 --- a/test/core/TestTsdbQuerySaltedAppend.java +++ b/test/core/TestTsdbQuerySaltedAppend.java @@ -24,6 +24,7 @@ public void beforeLocal() { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); query = new TsdbQuery(tsdb); } } diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java index 226ca4fedb..306dd67c7f 100644 --- a/test/tools/TestFsckSalted.java +++ b/test/tools/TestFsckSalted.java @@ -15,6 +15,7 @@ public void beforeLocal() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); GLOBAL_ROW = new byte[] {0, 0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}; ROW = MockBase.stringToBytes("0000000150E22700000001000001"); From c4225ffbe32800b7e49e61d1ff8249a926bc983e Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Mon, 5 Oct 2015 09:59:58 +0800 Subject: [PATCH 236/826] fix /api/search/lookup when salting is enabled When salting is enabled, the row key is expected to be "salt + tsuid", we can construct row key and still call RowKey.metricNameAsync(), but it's a little more efficient to directly extact metric uid from tsuid and call TSDB.getUidName(). This fixes issue https://github.com/OpenTSDB/opentsdb/issues/568. Signed-off-by: Chris Larsen --- src/tsd/SearchRpc.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 5d8a59b09e..64b3ee38f7 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -13,6 +13,7 @@ package net.opentsdb.tsd; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Exceptions; import net.opentsdb.utils.Pair; @@ -230,7 +232,8 @@ public Deferred call(final List tsuids) throws Exception { results.add(series); series.put("tsuid", UniqueId.uidToString(tsuid)); - deferreds.add(RowKey.metricNameAsync(tsdb, tsuid) + byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + deferreds.add(tsdb.getUidName(UniqueIdType.METRIC, metric_uid) .addCallback(new MetricCB(series))); final List tag_ids = UniqueId.getTagPairsFromTSUID(tsuid); From 766b32bdd58af0858959813cfab917519c5715b9 Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Tue, 6 Oct 2015 22:39:55 +0100 Subject: [PATCH 237/826] Allow "tsdb import" to read from STDIN --- src/tools/TextImporter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tools/TextImporter.java b/src/tools/TextImporter.java index a8c72ef586..2f33f009be 100644 --- a/src/tools/TextImporter.java +++ b/src/tools/TextImporter.java @@ -259,6 +259,10 @@ public String toString() { * @throws IOException when shit happens. */ private static BufferedReader open(final String path) throws IOException { + if (path.equals("-")) { + return new BufferedReader(new InputStreamReader(System.in)); + } + InputStream is = new FileInputStream(path); if (path.endsWith(".gz")) { is = new GZIPInputStream(is); From 6dfe801822c320ae02683beda9a282c38dbd46f1 Mon Sep 17 00:00:00 2001 From: Hutt Li Date: Thu, 8 Oct 2015 11:57:53 +1030 Subject: [PATCH 238/826] enable /api/uid/rename endpoint --- src/core/TSDB.java | 46 ++++++++ src/tsd/HttpJsonSerializer.java | 29 +++++ src/tsd/HttpSerializer.java | 25 +++++ src/tsd/UniqueIdRpc.java | 67 ++++++++++++ src/uid/UniqueId.java | 23 ++++ test/core/TestTSDB.java | 46 ++++++++ test/tsd/TestHttpJsonSerializer.java | 63 +++++++++++ test/tsd/TestUniqueIdRpc.java | 152 +++++++++++++++++++++++++++ test/uid/TestUniqueId.java | 89 ++++++++++++++++ 9 files changed, 540 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 64cf75c779..c6a8e31410 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1115,6 +1115,52 @@ public Deferred deleteUidAsync(final String type, final String name) { } } + /** + * Attempts to rename a UID from existing name to the given name + * Used by the UniqueIdRpc call to rename name of existing metrics, tagks or + * tagvs. The name must pass validation. If the UID doesn't exist, the method + * will throw an error. Chained IllegalArgumentException is directly exposed + * to caller. If the rename was successful, this method returns. + * @param type The type of uid to rename, one of metric, tagk and tagv + * @param oldname The existing name of the uid object + * @param newname The new name to be used on the uid object + * @throws IllegalArgumentException if error happened + * @since 2.2 + */ + public void renameUid(final String type, final String oldname, + final String newname) { + Tags.validateString(type, oldname); + Tags.validateString(type, newname); + if (type.toLowerCase().equals("metric")) { + try { + this.metrics.getId(oldname); + this.metrics.rename(oldname, newname); + } catch (NoSuchUniqueName nsue) { + throw new IllegalArgumentException("Name(\"" + oldname + + "\") does not exist"); + } + } else if (type.toLowerCase().equals("tagk")) { + try { + this.tag_names.getId(oldname); + this.tag_names.rename(oldname, newname); + } catch (NoSuchUniqueName nsue) { + throw new IllegalArgumentException("Name(\"" + oldname + + "\") does not exist"); + } + } else if (type.toLowerCase().equals("tagv")) { + try { + this.tag_values.getId(oldname); + this.tag_values.rename(oldname, newname); + } catch (NoSuchUniqueName nsue) { + throw new IllegalArgumentException("Name(\"" + oldname + + "\") does not exist"); + } + } else { + LOG.warn("Unknown type name: " + type); + throw new IllegalArgumentException("Unknown type name"); + } + } + /** @return the name of the UID table as a byte array for client requests */ public byte[] uidTable() { return this.uidtable; diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 57fcf1b567..67211e0c67 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -192,6 +192,26 @@ public HashMap> parseUidAssignV1() { } } + /** + * Parses metric, tagk or tagv, and name to rename UID + * @return as hash map of type and name + * @throws JSONException if parsing failed + * @throws BadRequestException if the content was missing or parsing failed + */ + public HashMap parseUidRenameV1() { + final String json = query.getContent(); + if (json == null || json.isEmpty()) { + throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, + "Missing message content", + "Supply valid JSON formatted data in the body of your request"); + } + try { + return JSON.parseToObject(json, TR_HASH_MAP); + } catch (IllegalArgumentException iae) { + throw new BadRequestException("Unable to parse the given JSON", iae); + } + } + /** * Parses a timeseries data query * @return A TSQuery with data ready to validate @@ -533,6 +553,15 @@ public ChannelBuffer formatUidAssignV1(final return this.serializeJSON(response); } + /** + * Format a response from the Uid Rename RPC + * @param response A map of result and error of the rename + * @return A JSON structure + * @throws JSONException if serialization failed + */ + public ChannelBuffer formatUidRenameV1(final Map response) { + return this.serializeJSON(response); + } /** * Format the results from a timeseries data query * @param data_query The TSQuery object used to fetch the results diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 8f16ea9590..8078c6ecfd 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -198,6 +198,18 @@ public HashMap> parseUidAssignV1() { " has not implemented parseUidAssignV1"); } + /** + * Parses metrics, tagk or tagvs type and name to rename UID + * @return as hash map of type and name + * @throws BadRequestException if the plugin has not implemented this method + */ + public HashMap parseUidRenameV1() { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented parseUidRenameV1"); + } + /** * Parses a SearchQuery request * @return The parsed search query @@ -443,6 +455,19 @@ public ChannelBuffer formatUidAssignV1(final " has not implemented formatUidAssignV1"); } + /** + * Format a response from the Uid Rename RPC + * @param response A map of result and reason for error of the rename + * @return A ChannelBuffer object to pass on to the caller + * @throws BadRequestException if the plugin has not implemented this method + */ + public ChannelBuffer formatUidRenameV1(final Map response) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented formatUidRenameV1"); + } + /** * Format the results from a timeseries data query * @param query The TSQuery object used to fetch the results diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 1747e3f188..0318d84b7d 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -62,6 +62,9 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { } else if (endpoint.toLowerCase().equals("tsmeta")) { this.handleTSMeta(tsdb, query); return; + } else if (endpoint.toLowerCase().equals("rename")) { + this.handleRename(tsdb, query); + return; } else { throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "Other UID endpoints have not been implemented yet"); @@ -477,6 +480,70 @@ private UIDMeta parseUIDMetaQS(final HttpQuery query) { return meta; } + /** + * Rename UID to a new name of the given metric, tagk or tagv names + *

    + * This handler supports GET and POST whereby the GET command can parse query + * strings with the {@code type} and {@code name} as their parameters. + *

    + * @param tsdb The TSDB from the RPC router + * @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"); + } + + final HashMap source; + if (query.method() == HttpMethod.POST) { + source = query.serializer().parseUidRenameV1(); + } else { + source = new HashMap(3); + final String[] types = {"metric", "tagk", "tagv", "name"}; + for (int i = 0; i < types.length; i++) { + final String value = query.getQueryStringParam(types[i]); + if (value!= null && !value.isEmpty()) { + source.put(types[i], value); + } + } + } + String type = null; + String oldname = null; + String newname = null; + for (Map.Entry entry : source.entrySet()) { + if (entry.getKey().equals("name")) { + newname = entry.getValue(); + } else { + type = entry.getKey(); + oldname = entry.getValue(); + } + } + + // we need a type/value and new name + if (type == null || oldname == null || newname == null) { + throw new BadRequestException("Missing necessary values to rename UID"); + } + + HashMap response = new HashMap(2); + try { + tsdb.renameUid(type, oldname, newname); + response.put("result", "true"); + } catch (IllegalArgumentException e) { + response.put("result", "false"); + response.put("error", e.getMessage()); + } + + if (!response.containsKey("error")) { + query.sendReply(query.serializer().formatUidRenameV1(response)); + } else { + query.sendReply(HttpResponseStatus.BAD_REQUEST, + query.serializer().formatUidRenameV1(response)); + } + } + /** * Used with verb overrides to parse out values from a query string * @param query The query to parse diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index e94c988648..ea443da2e7 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -15,10 +15,13 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.xml.bind.DatatypeConverter; @@ -104,6 +107,9 @@ public enum UniqueIdType { /** Map of pending UID assignments */ private final HashMap> pending_assignments = new HashMap>(); + /** Set of UID rename */ + private final Set renaming_id_names = + Collections.synchronizedSet(new HashSet()); /** Number of times we avoided reading from HBase thanks to the cache. */ private volatile int cache_hits; @@ -869,6 +875,7 @@ public Object call(Object ignored) throws Exception { */ public void rename(final String oldname, final String newname) { final byte[] row = getId(oldname); + final String row_string = fromBytes(row); { byte[] id = null; try { @@ -883,6 +890,15 @@ public void rename(final String oldname, final String newname) { } } + if (renaming_id_names.contains(row_string) + || renaming_id_names.contains(newname)) { + throw new IllegalArgumentException("Ongoing rename on the same ID(\"" + + Arrays.toString(row) + "\") or an identical new name(\"" + newname + + "\")"); + } + renaming_id_names.add(row_string); + renaming_id_names.add(newname); + final byte[] newnameb = toBytes(newname); // Update the reverse mapping first, so that if we die before updating @@ -898,6 +914,8 @@ public void rename(final String oldname, final String newname) { LOG.error("When trying rename(\"" + oldname + "\", \"" + newname + "\") on " + this + ": Failed to update reverse" + " mapping for ID=" + Arrays.toString(row), e); + renaming_id_names.remove(row_string); + renaming_id_names.remove(newname); throw e; } @@ -911,6 +929,8 @@ public void rename(final String oldname, final String newname) { LOG.error("When trying rename(\"" + oldname + "\", \"" + newname + "\") on " + this + ": Failed to create the" + " new forward mapping with ID=" + Arrays.toString(row), e); + renaming_id_names.remove(row_string); + renaming_id_names.remove(newname); throw e; } @@ -935,6 +955,9 @@ public void rename(final String oldname, final String newname) { + " old forward mapping for ID=" + Arrays.toString(row); LOG.error("WTF? " + msg, e); throw new RuntimeException(msg, e); + } finally { + renaming_id_names.remove(row_string); + renaming_id_names.remove(newname); } // Success! } diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index ae3fc98230..91a97ec96e 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -401,6 +401,52 @@ public void assignUidInvalidCharacter() { tsdb.assignUid("metric", "Not!A:Valid@Name"); } + @Test (expected = IllegalArgumentException.class) + public void renameUidInvalidNewname() { + tsdb.renameUid("metric", "existing", null); + } + + @Test (expected = IllegalArgumentException.class) + public void renameUidNonexistentMetric() { + when(metrics.getId("sys.cpu.1")).thenThrow( + new NoSuchUniqueName("metric", "sys.cpu.1")); + tsdb.renameUid("metric", "sys.cpu.1", "sys.cpu.2"); + } + + @Test + public void renameUidMetric() { + tsdb.renameUid("metric", "sys.cpu.1", "sys.cpu.2"); + } + + @Test (expected = IllegalArgumentException.class) + public void renameUidNonexistentTagk() { + when(tag_names.getId("datacenter")).thenThrow( + new NoSuchUniqueName("tagk", "datacenter")); + tsdb.renameUid("tagk", "datacenter", "datacluster"); + } + + @Test + public void renameUidTagk() { + tsdb.renameUid("tagk", "datacenter", "datacluster"); + } + + @Test (expected = IllegalArgumentException.class) + public void renameUidNonexistentTagv() { + when(tag_values.getId("localhost")).thenThrow( + new NoSuchUniqueName("tagv", "localhost")); + tsdb.renameUid("tagv", "localhost", "127.0.0.1"); + } + + @Test + public void renameUidTagv() { + tsdb.renameUid("tagv", "localhost", "127.0.0.1"); + } + + @Test (expected = IllegalArgumentException.class) + public void renameUidBadType() { + tsdb.renameUid("wrongtype", METRIC_STRING, METRIC_STRING); + } + @Test public void uidTable() { assertNotNull(tsdb.uidTable()); diff --git a/test/tsd/TestHttpJsonSerializer.java b/test/tsd/TestHttpJsonSerializer.java index a4669a87b1..70feb5ceb2 100644 --- a/test/tsd/TestHttpJsonSerializer.java +++ b/test/tsd/TestHttpJsonSerializer.java @@ -163,6 +163,37 @@ public void parseSuggestV1NotJSON() throws Exception { serdes.parseSuggestV1(); } + @Test + public void parseUidRenameV1() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "", + "{\"metric\":\"sys.cpu.1\",\"name\":\"sys.cpu.2\"}", ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + HashMap map = serdes.parseUidRenameV1(); + assertNotNull(map); + assertEquals("sys.cpu.1", map.get("metric")); + } + + @Test (expected = BadRequestException.class) + public void parseUidRenameV1NoContent() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "", null, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + serdes.parseUidRenameV1(); + } + + @Test (expected = BadRequestException.class) + public void parseUidRenameV1EmptyContent() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "", "", ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + serdes.parseUidRenameV1(); + } + + @Test (expected = BadRequestException.class) + public void parseUidRenameV1NotJSON() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "", "NOT JSON", ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + serdes.parseUidRenameV1(); + } + @Test public void formatSuggestV1() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, ""); @@ -194,6 +225,38 @@ public void formatSuggestV1Null() throws Exception { serdes.formatSuggestV1(null); } + @Test + public void formatUidRenameV1Success() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final HashMap map = new HashMap(2); + map.put("result", "true"); + ChannelBuffer cb = serdes.formatUidRenameV1(map); + assertNotNull(cb); + assertEquals("{\"result\":\"true\"}", + cb.toString(Charset.forName("UTF-8"))); + } + + @Test + public void formatUidRenameV1Failed() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + final HashMap map = new HashMap(2); + map.put("result", "false"); + map.put("error", "known"); + ChannelBuffer cb = serdes.formatUidRenameV1(map); + assertNotNull(cb); + assertEquals("{\"error\":\"known\",\"result\":\"false\"}", + cb.toString(Charset.forName("UTF-8"))); + } + + @Test (expected = IllegalArgumentException.class) + public void formatUidRenameV1Null() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + HttpJsonSerializer serdes = new HttpJsonSerializer(query); + serdes.formatUidRenameV1(null); + } + @Test public void formatSerializersV1() throws Exception { HttpQuery.initializeSerializerMaps(tsdb); diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index 1e7c5240b6..46b72e3a59 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -14,6 +14,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -505,6 +506,157 @@ public void stringToUniqueIdTypeEmpty() throws Exception { UniqueId.stringToUniqueIdType("Not a type"); } + // Test /api/uid/rename ---------------------- + + @Test (expected = BadRequestException.class) + public void renameBadMethod() throws Exception { + HttpQuery query = NettyMocks.putQuery(tsdb, "/api/uid/rename", ""); + rpc.execute(tsdb, query); + } + + @Test + public void renamePostMetric() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", + "{\"metric\":\"sys.cpu.1\",\"name\":\"sys.cpu.2\"}"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test + public void renamePostTagk() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", + "{\"tagk\":\"datacenter\",\"name\":\"datacluster\"}"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test + public void renamePostTagv() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", + "{\"tagv\":\"localhost\",\"name\":\"127.0.0.1\"}"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test (expected = BadRequestException.class) + public void renamePostNoName() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", + "{\"tagk\":\"localhost\",\"not_name\":\"127.0.0.1\"}"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renamePostNoType() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", + "{\"name\":\"127.0.0.1\"}"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renamePostNotJSON() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", "Not JSON"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renamePostZeroLengthContent() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", ""); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renamePostEmptyJSON() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/uid/rename", "{}"); + rpc.execute(tsdb, query); + } + + @Test + public void renameQsMetric() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?metric=sys.cpu.1&name=sys.cpu.2"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test + public void renameQsTagk() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?tagk=datacenter&name=datacluster"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test + public void renameQsTagv() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?tagv=localhost&name=127.0.0.1"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test + public void renameQsSkipUnsupportedParam() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?tagv=localhost&name=127.0.0.1&drop=db"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + assertEquals("{\"result\":\"true\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + + @Test (expected = BadRequestException.class) + public void renameQsMissingType() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?name=127.0.0.1"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renameQsMissingName() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?metric=sys.cpu.1"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renameQsNoParamValue() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?metric=&name=sys.cpu.2"); + rpc.execute(tsdb, query); + } + + @Test (expected = BadRequestException.class) + public void renameQsNoParam() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?"); + rpc.execute(tsdb, query); + } + + @Test + public void renameRenameException() throws Exception { + final String message = "New name already exists"; + doThrow(new IllegalArgumentException(message)).when(tsdb).renameUid("tagv", + "localhost", "localhost"); + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/uid/rename?tagv=localhost&name=localhost"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + assertEquals("{\"error\":\"" + message + "\",\"result\":\"false\"}", + query.response().getContent().toString(Charset.forName("UTF-8"))); + } + // Teset /api/uid/uidmeta -------------------- @Test diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index a35aeeae3c..e6a81dfe92 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -27,6 +27,7 @@ import org.hbase.async.AtomicIncrementRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.HBaseException; @@ -1062,6 +1063,82 @@ public void longToUIDTooBig() throws Exception { UniqueId.longToUID(257, (short)1); } + @Test + public void rename() throws Exception { + uid = new UniqueId(client, table, METRIC, 3); + final byte[] foo_id = { 0, 'a', 0x42 }; + final byte[] foo_name = { 'f', 'o', 'o' }; + + ArrayList kvs = new ArrayList(1); + kvs.add(new KeyValue(foo_name, ID, METRIC_ARRAY, foo_id)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)) + .thenReturn(Deferred.>fromResult(null)); + when(client.put(anyPut())).thenAnswer(answerTrue()); + when(client.delete(anyDelete())).thenAnswer(answerTrue()); + + uid.rename("foo", "bar"); + } + + @Test (expected = IllegalArgumentException.class) + public void renameNewNameExists() throws Exception { + uid = new UniqueId(client, table, METRIC, 3); + final byte[] foo_id = { 0, 'a', 0x42 }; + final byte[] foo_name = { 'f', 'o', 'o' }; + final byte[] bar_id = { 1, 'b', 0x43 }; + final byte[] bar_name = { 'b', 'a', 'r' }; + + ArrayList foo_kvs = new ArrayList(1); + ArrayList bar_kvs = new ArrayList(1); + foo_kvs.add(new KeyValue(foo_name, ID, METRIC_ARRAY, foo_id)); + bar_kvs.add(new KeyValue(bar_name, ID, METRIC_ARRAY, bar_id)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(foo_kvs)) + .thenReturn(Deferred.fromResult(bar_kvs)); + when(client.put(anyPut())).thenAnswer(answerTrue()); + when(client.delete(anyDelete())).thenAnswer(answerTrue()); + + uid.rename("foo", "bar"); + } + + @Test (expected = IllegalStateException.class) + public void renameRaceCondition() throws Exception { + // Simulate a race between client A(default) and client B. + // A and B rename same UID to different name. + // B waits till A start to invoke PutRequest to start. + + uid = new UniqueId(client, table, METRIC, 3); + HBaseClient client_b = mock(HBaseClient.class); + final UniqueId uid_b = new UniqueId(client_b, table, METRIC, 3); + + final byte[] foo_id = { 0, 'a', 0x42 }; + final byte[] foo_name = { 'f', 'o', 'o' }; + + ArrayList kvs = new ArrayList(1); + kvs.add(new KeyValue(foo_name, ID, METRIC_ARRAY, foo_id)); + + when(client_b.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)) + .thenReturn(Deferred.>fromResult(null)); + when(client_b.put(anyPut())).thenAnswer(answerTrue()); + when(client_b.delete(anyDelete())).thenAnswer(answerTrue()); + + final Answer> the_race = new Answer>() { + public Deferred answer(final InvocationOnMock inv) throws Exception { + uid_b.rename("foo", "xyz"); + return Deferred.fromResult(true); + } + }; + + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)) + .thenReturn(Deferred.>fromResult(null)); + when(client.put(anyPut())).thenAnswer(the_race); + when(client.delete(anyDelete())).thenAnswer(answerTrue()); + + uid.rename("foo", "bar"); + } + @Test public void deleteCached() throws Exception { setupStorage(); @@ -1220,6 +1297,18 @@ private static PutRequest anyPut() { return any(PutRequest.class); } + private static DeleteRequest anyDelete() { + return any(DeleteRequest.class); + } + + private static Answer> answerTrue() { + return new Answer>() { + public Deferred answer(final InvocationOnMock inv) { + return Deferred.fromResult(true); + } + }; + } + @SuppressWarnings("unchecked") private static Callback> anyByteCB() { return any(Callback.class); From 3cf9efbda8de321072c039d41e33830cc71de80b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Oct 2015 15:04:54 -0700 Subject: [PATCH 239/826] Fix #585 by adding salting to the IncomingDataPoints and BatchedDataPoints classes. Signed-off-by: Chris Larsen --- src/core/BatchedDataPoints.java | 7 +++++-- src/core/IncomingDataPoints.java | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 2f0629691b..3fcea6018c 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -103,6 +103,7 @@ public void setSeries(final String metric, final Map tags) { IncomingDataPoints.checkMetricAndTags(metric, tags); try { row_key = IncomingDataPoints.rowKeyTemplate(tsdb, metric, tags); + RowKey.prefixKeyWithSalt(row_key); reset(); } catch (RuntimeException e) { @@ -222,7 +223,8 @@ private Deferred addPointInternal(final long timestamp, */ if (base_time == Long.MIN_VALUE) { base_time = incomingBaseTime; - Bytes.setInt(row_key, (int) base_time, tsdb.metrics.width()); + Bytes.setInt(row_key, (int) base_time, + tsdb.metrics.width() + Const.SALT_WIDTH()); } if (incomingBaseTime - base_time >= Const.MAX_TIMESPAN) { @@ -302,7 +304,8 @@ public Deferred metricNameAsync() { if (row_key == null) { throw new IllegalStateException("Instance was not properly constructed!"); } - final byte[] id = Arrays.copyOfRange(row_key, 0, tsdb.metrics.width()); + final byte[] id = Arrays.copyOfRange(row_key, 0, + tsdb.metrics.width() + Const.SALT_WIDTH()); return tsdb.metrics.getNameAsync(id); } diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 1aded5a6b7..20ca6d51ac 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -201,6 +201,7 @@ public void setSeries(final String metric, final Map tags) { checkMetricAndTags(metric, tags); try { row = rowKeyTemplate(tsdb, metric, tags); + RowKey.prefixKeyWithSalt(row); } catch (RuntimeException e) { throw e; } catch (Exception e) { @@ -242,6 +243,8 @@ private long updateBaseTime(final long timestamp) { // internal datastructures. row = Arrays.copyOf(row, row.length); Bytes.setInt(row, (int) base_time, tsdb.metrics.width()); + RowKey.prefixKeyWithSalt(row); // in case the timestamp will be involved in + // salting later tsdb.scheduleForCompaction(row, (int) base_time); return base_time; } @@ -343,7 +346,7 @@ private void grow() { /** Extracts the base timestamp from the row key. */ private long baseTime() { - return Bytes.getUnsignedInt(row, tsdb.metrics.width()); + return Bytes.getUnsignedInt(row, Const.SALT_WIDTH() + tsdb.metrics.width()); } public Deferred addPoint(final long timestamp, final long value) { From a3ba38d6856e9da30fd97851a21f737860771492 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 27 Oct 2015 16:46:00 -0700 Subject: [PATCH 240/826] Fix test dependencies where DeleteRequest was pulled from Zookeeper instead of AsyncHBase. Signed-off-by: Chris Larsen --- test/tools/TestDumpSeries.java | 2 +- test/tools/TestTextImporter.java | 2 +- test/tools/TestUID.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tools/TestDumpSeries.java b/test/tools/TestDumpSeries.java index 85100f7852..375f3c4710 100644 --- a/test/tools/TestDumpSeries.java +++ b/test/tools/TestDumpSeries.java @@ -30,8 +30,8 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; diff --git a/test/tools/TestTextImporter.java b/test/tools/TestTextImporter.java index 991562a748..851a9af0f4 100644 --- a/test/tools/TestTextImporter.java +++ b/test/tools/TestTextImporter.java @@ -37,8 +37,8 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index ab271dc131..e7a0bc939c 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -23,8 +23,8 @@ import net.opentsdb.storage.MockBase; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; From f84ef2a09d5515eb84b429cc74df32d542a976ab Mon Sep 17 00:00:00 2001 From: Christos Soulios Date: Wed, 28 Oct 2015 12:07:32 -0700 Subject: [PATCH 241/826] Autoconf bigtable Added build support for bigtable data store using autotools dependencies. Script build-bigtable.sh performs builds using asyncbigtable library as a dependency. Signed-off-by: Chris Larsen --- Makefile.am | 21 ++++- build-bigtable.sh | 9 ++ configure.ac | 10 ++ pom.xml.in | 92 ++++++++++++------- .../alpn-boot-7.1.3.v20150130.jar.md5 | 1 + third_party/alpn-boot/include.mk | 23 +++++ ...027.224128-1-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 23 +++++ third_party/include.mk | 15 ++- tsdb.in | 17 +++- 10 files changed, 174 insertions(+), 38 deletions(-) create mode 100644 build-bigtable.sh create mode 100644 third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 create mode 100644 third_party/alpn-boot/include.mk create mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 create mode 100644 third_party/asyncbigtable/include.mk diff --git a/Makefile.am b/Makefile.am index f9395a0504..56c92b14b4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -153,7 +153,6 @@ tsdb_SRC := \ src/utils/Threads.java tsdb_DEPS = \ - $(ASYNCHBASE) \ $(GUAVA) \ $(LOG4J_OVER_SLF4J) \ $(LOGBACK_CLASSIC) \ @@ -162,12 +161,25 @@ tsdb_DEPS = \ $(JACKSON_CORE) \ $(JACKSON_DATABIND) \ $(NETTY) \ - $(PROTOBUF) \ $(SLF4J_API) \ $(SUASYNC) \ - $(ZOOKEEPER) \ $(APACHE_MATH) +if BIGTABLE +tsdb_DEPS += \ + $(ALPN_BOOT) \ + $(ASYNCBIGTABLE) +maven_profile_bigtable := true +maven_profile_hbase := false +else +tsdb_DEPS += \ + $(ASYNCHBASE) \ + $(PROTOBUF) \ + $(ZOOKEEPER) +maven_profile_bigtable := false +maven_profile_hbase := true +endif + test_SRC := \ test/core/SeekableViewsForTest.java \ test/core/BaseTsdbTest.java \ @@ -664,6 +676,7 @@ pom.xml: pom.xml.in Makefile echo ''; \ sed <$< \ -e 's/@ASYNCHBASE_VERSION@/$(ASYNCHBASE_VERSION)/' \ + -e 's/@ASYNCBIGTABLE_VERSION@/$(ASYNCBIGTABLE_VERSION)/' \ -e 's/@GUAVA_VERSION@/$(GUAVA_VERSION)/' \ -e 's/@GWT_VERSION@/$(GWT_VERSION)/' \ -e 's/@HAMCREST_VERSION@/$(HAMCREST_VERSION)/' \ @@ -684,6 +697,8 @@ pom.xml: pom.xml.in Makefile -e 's/@spec_title@/$(spec_title)/' \ -e 's/@spec_vendor@/$(spec_vendor)/' \ -e 's/@spec_version@/$(PACKAGE_VERSION)/' \ + -e 's/@maven_profile_hbase@/$(maven_profile_hbase)/' \ + -e 's/@maven_profile_bigtable@/$(maven_profile_bigtable)/' \ ; \ } >$@-t mv $@-t ../$@ diff --git a/build-bigtable.sh b/build-bigtable.sh new file mode 100644 index 0000000000..1076da2f8e --- /dev/null +++ b/build-bigtable.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -xe +test -f configure || ./bootstrap +test -d build || mkdir build +cd build +test -f Makefile || ../configure --with-bigtable "$@" +MAKE=make +[ `uname -s` = "FreeBSD" ] && MAKE=gmake +exec ${MAKE} "$@" \ No newline at end of file diff --git a/configure.ac b/configure.ac index 0b8dcdd76e..dea9bd6147 100644 --- a/configure.ac +++ b/configure.ac @@ -24,6 +24,16 @@ AC_CONFIG_FILES([ AC_CONFIG_FILES([opentsdb.spec]) AC_CONFIG_FILES([build-aux/fetchdep.sh], [chmod +x build-aux/fetchdep.sh]) +AC_ARG_WITH([bigtable], + [AS_HELP_STRING([--with-bigtable], [enable bigtable backend])], + [with_bigtable=yes], + [with_bigtable=no]) + +AS_IF([test "x$with_bigtable" = "xyes"], + [AM_CONDITIONAL(BIGTABLE, true)], + [AM_CONDITIONAL(BIGTABLE, false)] +) + TSDB_FIND_PROG([md5], [md5sum md5 gmd5sum digest]) if test x`basename "$MD5"` = x'digest'; then MD5='digest -a md5' diff --git a/pom.xml.in b/pom.xml.in index 9539eaa2a1..fee38d15af 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -355,42 +355,12 @@ @SUASYNC_VERSION@ - - org.apache.zookeeper - zookeeper - @ZOOKEEPER_VERSION@ - - - log4j - log4j - - - org.slf4j - slf4j-log4j12 - - - jline - jline - - - junit - junit - - - - org.slf4j slf4j-api @SLF4J_API_VERSION@ - - org.hbase - asynchbase - @ASYNCHBASE_VERSION@ - - org.apache.commons commons-math3 @@ -481,7 +451,67 @@ UTF-8 - + + + + + hbase + + @maven_profile_hbase@ + + + + + org.hbase + asynchbase + @ASYNCHBASE_VERSION@ + + + + org.apache.zookeeper + zookeeper + @ZOOKEEPER_VERSION@ + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + jline + jline + + + junit + junit + + + + + + + + + bigtable + + @maven_profile_bigtable@ + + + + + com.pythian.opentsdb + asyncbigtable + @ASYNCBIGTABLE_VERSION@ + jar-with-dependencies + + + + + + org.sonatype.oss oss-parent diff --git a/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 new file mode 100644 index 0000000000..b51d10325d --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 @@ -0,0 +1 @@ +b10366c9301e954bcedbf9130b6381c7 diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk new file mode 100644 index 0000000000..d7ead1358d --- /dev/null +++ b/third_party/alpn-boot/include.mk @@ -0,0 +1,23 @@ +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +ALPN_BOOT_VERSION := 7.1.3.v20150130 +ALPN_BOOT := third_party/alpn-boot/alpn-boot-$(ALPN_BOOT_VERSION).jar +ALBPN_BOOT_BASE_URL := http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/$(ALPN_BOOT_VERSION) + +$(ALPN_BOOT): $(ALPN_BOOT).md5 + set dummy "$(ALBPN_BOOT_BASE_URL)" "$(ALPN_BOOT)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(ALPN_BOOT) diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..ef206ef18b --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +ef53c7422ee741867be4e925484f8d88 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk new file mode 100644 index 0000000000..e9eca08073 --- /dev/null +++ b/third_party/asyncbigtable/include.mk @@ -0,0 +1,23 @@ +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +ASYNCBIGTABLE_VERSION := 0.2.0-20151027.224128-1 +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.2.0-SNAPSHOT/ + +$(ASYNCBIGTABLE): $(ASYNCBIGTABLE).md5 + set dummy "$(ASYNCBIGTABLE_BASE_URL)" "$(ASYNCBIGTABLE)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(ASYNCBIGTABLE) \ No newline at end of file diff --git a/third_party/include.mk b/third_party/include.mk index 56649734d1..ab92e5b4fe 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -21,7 +21,6 @@ THIRD_PARTY = include third_party/guava/include.mk include third_party/gwt/include.mk include third_party/hamcrest/include.mk -include third_party/hbase/include.mk include third_party/jackson/include.mk include third_party/javassist/include.mk include third_party/junit/include.mk @@ -30,9 +29,19 @@ include third_party/mockito/include.mk include third_party/netty/include.mk include third_party/objenesis/include.mk include third_party/powermock/include.mk -include third_party/protobuf/include.mk include third_party/slf4j/include.mk include third_party/suasync/include.mk include third_party/validation-api/include.mk -include third_party/zookeeper/include.mk include third_party/apache/include.mk + +if BIGTABLE +include third_party/alpn-boot/include.mk +include third_party/asyncbigtable/include.mk +ASYNCHBASE_VERSION = 0.0 +ZOOKEEPER_VERSION = 0.0 +else +include third_party/hbase/include.mk +include third_party/protobuf/include.mk +include third_party/zookeeper/include.mk +ASYNCBIGTABLE_VERSION = 0.0 +endif \ No newline at end of file diff --git a/tsdb.in b/tsdb.in index d96c53d540..8eaf5d6c28 100644 --- a/tsdb.in +++ b/tsdb.in @@ -106,4 +106,19 @@ shift JAVA=${JAVA-'java'} JVMARGS=${JVMARGS-'-enableassertions -enablesystemassertions'} test -r "$localdir/tsdb.local" && . "$localdir/tsdb.local" -exec $JAVA $JVMARGS -classpath "$CLASSPATH" net.opentsdb.tools.$MAINCLASS "$@" + +if [[ $CLASSPATH == *"asyncbigtable"* ]] +then + USE_BIGTABLE=1 + echo "Running OpenTSDB with Bigtable support" + + test -n "$HBASE_CONF" || { + echo >&2 'The environment variable HBASE_CONF must be set' + exit 1 + } + + ALPN_BOOT_JAR=$(find $localdir -name alpn-boot\*.jar) + exec $JAVA $JVMARGS -classpath "$CLASSPATH:$HBASE_CONF" -Xbootclasspath/p:$ALPN_BOOT_JAR net.opentsdb.tools.$MAINCLASS "$@" +else + exec $JAVA $JVMARGS -classpath "$CLASSPATH" net.opentsdb.tools.$MAINCLASS "$@" +fi From df92d8fcdd21afcfa884a12b8caa8a2dda5c6d36 Mon Sep 17 00:00:00 2001 From: Hong Dai Thanh Date: Wed, 28 Oct 2015 10:22:31 +0700 Subject: [PATCH 242/826] s/space/comma/ in instruction for specifying ZooKeeper quorum Signed-off-by: Chris Larsen --- build-aux/deb/opentsdb.conf | 2 +- build-aux/rpm/opentsdb.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index d95b65efe2..f58d5cf14c 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -58,6 +58,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A space separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index 11f66ca6cf..a515418a7e 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -58,6 +58,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A space separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From 13067fa9a20e2c8e35d147c17fade8a9a789eed4 Mon Sep 17 00:00:00 2001 From: Hong Dai Thanh Date: Wed, 28 Oct 2015 10:22:31 +0700 Subject: [PATCH 243/826] s/space/comma/ in instruction for specifying ZooKeeper quorum Signed-off-by: Chris Larsen --- build-aux/rpm/opentsdb.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index 11f66ca6cf..a515418a7e 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -58,6 +58,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A space separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From 108f23ed8e28e6325ece0a70322e12a9b423fbdf Mon Sep 17 00:00:00 2001 From: Hong Dai Thanh Date: Wed, 28 Oct 2015 10:22:31 +0700 Subject: [PATCH 244/826] s/space/comma/ in instruction for specifying ZooKeeper quorum Signed-off-by: Chris Larsen --- build-aux/rpm/opentsdb.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index 11f66ca6cf..a515418a7e 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -58,6 +58,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A space separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From cbdb50d299849cc4f07ce7977d52584a99cca318 Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Fri, 23 Oct 2015 22:36:08 +0800 Subject: [PATCH 245/826] remove debug log with wrong INFO level to save disk space Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 765ac0eb16..7c34cbfc2a 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -525,7 +525,6 @@ final class TSMetaCB implements Callback, Long> { @Override public Deferred call(final Long incremented_value) throws Exception { -LOG.info("Value: " + incremented_value); if (incremented_value > 1) { // TODO - maybe update the search index every X number of increments? // Otherwise the search engine would only get last_updated/count From 2cce79a2ba03cbbe56964e1b2e86978c06700888 Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Fri, 23 Oct 2015 22:36:08 +0800 Subject: [PATCH 246/826] remove debug log with wrong INFO level to save disk space Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 765ac0eb16..7c34cbfc2a 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -525,7 +525,6 @@ final class TSMetaCB implements Callback, Long> { @Override public Deferred call(final Long incremented_value) throws Exception { -LOG.info("Value: " + incremented_value); if (incremented_value > 1) { // TODO - maybe update the search index every X number of increments? // Otherwise the search engine would only get last_updated/count From 13c86109309dff393cf5921bbef0a5eb8759886e Mon Sep 17 00:00:00 2001 From: Jim Westfall Date: Mon, 19 Oct 2015 17:05:58 -0700 Subject: [PATCH 247/826] QueryUi: URL.decode() url/query string before use Some browsers (aka firefox) like to encode { and } as %7B and %7D. This causes problem when parsing the query string since its using { and } to figure out the metric and tags. Without this the UI thows an error like the following: Request failed: Bad Request: No such name for 'metrics': 'server.nic.usage.mbit%7Bhost=host1%7D' Signed-off-by: Chris Larsen --- src/tsd/client/QueryUi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 4e2eccdd9d..cf78c6feca 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -780,7 +780,7 @@ private static QueryString getQueryString(final String qs) { } private void refreshFromQueryString() { - final QueryString qs = getQueryString(History.getToken()); + final QueryString qs = getQueryString(URL.decode(History.getToken())); maybeSetTextbox(qs, "start", start_datebox.getTextBox()); maybeSetTextbox(qs, "end", end_datebox.getTextBox()); @@ -961,7 +961,7 @@ public void got(final JSONValue json) { if (autoreload.getValue()) { history += "&autoreload=" + autoreoload_interval.getText(); } - if (!history.equals(History.getToken())) { + if (!history.equals(URL.decode(History.getToken()))) { History.newItem(history, false); } From 68c7b5633dd1dc57bf054840082d0e22457bf90a Mon Sep 17 00:00:00 2001 From: Jim Westfall Date: Mon, 19 Oct 2015 17:05:58 -0700 Subject: [PATCH 248/826] QueryUi: URL.decode() url/query string before use Some browsers (aka firefox) like to encode { and } as %7B and %7D. This causes problem when parsing the query string since its using { and } to figure out the metric and tags. Without this the UI thows an error like the following: Request failed: Bad Request: No such name for 'metrics': 'server.nic.usage.mbit%7Bhost=host1%7D' Signed-off-by: Chris Larsen --- src/tsd/client/QueryUi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 4e2eccdd9d..cf78c6feca 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -780,7 +780,7 @@ private static QueryString getQueryString(final String qs) { } private void refreshFromQueryString() { - final QueryString qs = getQueryString(History.getToken()); + final QueryString qs = getQueryString(URL.decode(History.getToken())); maybeSetTextbox(qs, "start", start_datebox.getTextBox()); maybeSetTextbox(qs, "end", end_datebox.getTextBox()); @@ -961,7 +961,7 @@ public void got(final JSONValue json) { if (autoreload.getValue()) { history += "&autoreload=" + autoreoload_interval.getText(); } - if (!history.equals(History.getToken())) { + if (!history.equals(URL.decode(History.getToken()))) { History.newItem(history, false); } From fb414f702a4f2b28da5f94925037851c4ec9141e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 29 Oct 2015 13:20:55 -0700 Subject: [PATCH 249/826] Bump the asyncbigtable version to take advantage of the rowfilter patch to properly enable regex matching in bigtable. Signed-off-by: Chris Larsen --- ...able-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..75838d73bc --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +d690357777c2c32e530429f72c54b2d4 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index e9eca08073..bfde52e600 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCBIGTABLE_VERSION := 0.2.0-20151027.224128-1 +ASYNCBIGTABLE_VERSION := 0.2.1-20151029.200718-1 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.2.0-SNAPSHOT/ +ASYNCBIGTABLE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/com/pythian/opentsdb/asyncbigtable/0.2.1-SNAPSHOT/ $(ASYNCBIGTABLE): $(ASYNCBIGTABLE).md5 set dummy "$(ASYNCBIGTABLE_BASE_URL)" "$(ASYNCBIGTABLE)"; shift; $(FETCH_DEPENDENCY) From 8d4d282b38ff5eb703d4ddc1fc868c19de39ec4a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 29 Oct 2015 14:33:36 -0700 Subject: [PATCH 250/826] Remove the HBASE_CONF requirement. All settings can be made in the opentsdb.conf file. Signed-off-by: Chris Larsen --- tsdb.in | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tsdb.in b/tsdb.in index 8eaf5d6c28..534deb03c1 100644 --- a/tsdb.in +++ b/tsdb.in @@ -112,11 +112,6 @@ then USE_BIGTABLE=1 echo "Running OpenTSDB with Bigtable support" - test -n "$HBASE_CONF" || { - echo >&2 'The environment variable HBASE_CONF must be set' - exit 1 - } - ALPN_BOOT_JAR=$(find $localdir -name alpn-boot\*.jar) exec $JAVA $JVMARGS -classpath "$CLASSPATH:$HBASE_CONF" -Xbootclasspath/p:$ALPN_BOOT_JAR net.opentsdb.tools.$MAINCLASS "$@" else From 5f5fe8c87ffa80b0e07fcb51df9836b0bfb0b63e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 29 Oct 2015 14:56:59 -0700 Subject: [PATCH 251/826] Bump the AsyncBigtable client to fix commons-lang dependency. Signed-off-by: Chris Larsen --- ...gtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 | 1 - ...gtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 | 1 - ...gtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 2 +- 4 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 delete mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 create mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 deleted file mode 100644 index ef206ef18b..0000000000 --- a/third_party/asyncbigtable/asyncbigtable-0.2.0-20151027.224128-1-jar-with-dependencies.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -ef53c7422ee741867be4e925484f8d88 \ No newline at end of file diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 deleted file mode 100644 index 75838d73bc..0000000000 --- a/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.200718-1-jar-with-dependencies.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -d690357777c2c32e530429f72c54b2d4 \ No newline at end of file diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..dbfa539ba6 --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +e07097fbc7023fd0ee108368a7ad7c73 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index bfde52e600..9e903a49f7 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCBIGTABLE_VERSION := 0.2.1-20151029.200718-1 +ASYNCBIGTABLE_VERSION := 0.2.1-20151029.214823-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.2.1-SNAPSHOT/ From f6342f9a737fb2990e00d2e1fa77432098da988a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 29 Oct 2015 17:26:24 -0700 Subject: [PATCH 252/826] Add a script to the ALPN include file to detect the Java version and download the proper version. And add MD5 files for each one. Signed-off-by: Chris Larsen --- .../alpn-boot-7.0.0.v20140317.jar.md5 | 1 + .../alpn-boot-7.1.0.v20141016.jar.md5 | 1 + .../alpn-boot-7.1.1.v20141016.jar.md5 | 1 + .../alpn-boot-7.1.2.v20141202.jar.md5 | 1 + .../alpn-boot-8.0.0.v20140317.jar.md5 | 1 + .../alpn-boot-8.1.0.v20141016.jar.md5 | 1 + .../alpn-boot-8.1.1.v20141016.jar.md5 | 1 + .../alpn-boot-8.1.2.v20141202.jar.md5 | 1 + .../alpn-boot-8.1.3.v20150130.jar.md5 | 1 + .../alpn-boot-8.1.4.v20150727.jar.md5 | 1 + .../alpn-boot-8.1.5.v20150921.jar.md5 | 1 + third_party/alpn-boot/include.mk | 43 ++++++++++++++++++- 12 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 diff --git a/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 b/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 new file mode 100644 index 0000000000..6e005e9074 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 @@ -0,0 +1 @@ +81e4f665ff2bf40720f9b345cee6b429 diff --git a/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 new file mode 100644 index 0000000000..529b4c8b5d --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 @@ -0,0 +1 @@ +b1569a1f34a0ca61d34c3c3e5020a8ef diff --git a/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 new file mode 100644 index 0000000000..d125a86172 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 @@ -0,0 +1 @@ +d9add9c8eb6c087e408b076e6d823ddd diff --git a/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 new file mode 100644 index 0000000000..2ea2c00f10 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 @@ -0,0 +1 @@ +391f659c583e2ea0f05515a6f6147620 diff --git a/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 b/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 new file mode 100644 index 0000000000..b34b6459eb --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 @@ -0,0 +1 @@ +de73395f7e20619699a07063e640d5f7 diff --git a/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 new file mode 100644 index 0000000000..89c8317c71 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 @@ -0,0 +1 @@ +d4a325fdb7e86bd0d9ac583998165a84 diff --git a/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 new file mode 100644 index 0000000000..5993e27253 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 @@ -0,0 +1 @@ +4655c087dda15743449ff31717d98e50 diff --git a/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 new file mode 100644 index 0000000000..01c9bb0cef --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 @@ -0,0 +1 @@ +9689564f4d7cc15918568f7006b85bf5 diff --git a/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 new file mode 100644 index 0000000000..a964d2acca --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 @@ -0,0 +1 @@ +a5803d4ff6ce36d15c750104a117dfb1 diff --git a/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 new file mode 100644 index 0000000000..c830ed3e9f --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 @@ -0,0 +1 @@ +1543b3403ae451ca2ec0944de403f6cc diff --git a/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 new file mode 100644 index 0000000000..38a5c04b33 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 @@ -0,0 +1 @@ +b05ac69bd8697c4bfc4cf896dea63c94 diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk index d7ead1358d..c4f94eb448 100644 --- a/third_party/alpn-boot/include.mk +++ b/third_party/alpn-boot/include.mk @@ -13,7 +13,48 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ALPN_BOOT_VERSION := 7.1.3.v20150130 +# ALPN_BOOT_VERSION := 7.1.3.v20150130 +ALPN_BOOT_VERSION = $(shell version= ;\ + if [[ "@JAVA@" ]]; then \ + version=$$("@JAVA@" -version 2>&1 | awk -F '"' '/version/ {print $$2}'); \ + else\ + echo "Failed to parse Java version";\ + exit 1;\ + fi; \ + if [[ $$version =~ ^([0-9]+\.[0-9]+)\.([0-9])[_Uu]([0-9]+)$$ ]]; then \ + major=$${BASH_REMATCH[1]};\ + minor=$${BASH_REMATCH[2]}; \ + sub=$${BASH_REMATCH[3]}; \ + if [[ $$major = "1.7" ]]; then \ + if [[ $$sub < 71 ]]; then \ + echo "7.1.0.v20141016"; \ + elif [[ $$sub < 75 ]]; then \ + echo "7.1.2.v20141202"; \ + else \ + echo "7.1.3.v20150130"; \ + fi \ + elif [[ $$major = "1.8" ]]; then \ + if [[ $$sub < 25 ]]; then \ + echo "8.1.0.v20141016"; \ + elif [[ $$sub < 31 ]]; then \ + echo "8.1.2.v20141202"; \ + elif [[ $$sub < 51 ]]; then \ + echo "8.1.3.v20150130"; \ + elif [[ $$sub < 60 ]]; then \ + echo "8.1.4.v20150727"; \ + else \ + echo "8.1.5.v20150921"; \ + fi \ + else \ + echo "Unsupported major Java version: $$major"; \ + exit 1; \ + fi \ + else \ + echo "Possibly invalid Java version (couldn't parse): $$version"; \ + exit 1; \ + fi) + + ALPN_BOOT := third_party/alpn-boot/alpn-boot-$(ALPN_BOOT_VERSION).jar ALBPN_BOOT_BASE_URL := http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/$(ALPN_BOOT_VERSION) From ab13472281bb3a82ce3c503a3d157fc65298fd8b Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Mon, 6 Apr 2015 13:36:41 -0700 Subject: [PATCH 253/826] Write branch info for Version API Signed-off-by: Chris Larsen --- build-aux/gen_build_data.sh | 3 +++ src/tsd/RpcManager.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build-aux/gen_build_data.sh b/build-aux/gen_build_data.sh index 927f2e63b1..4f211efbbc 100755 --- a/build-aux/gen_build_data.sh +++ b/build-aux/gen_build_data.sh @@ -39,6 +39,7 @@ eval "$sh" # Sets the timestamp and date variables. user=`whoami` host=`hostname` repo=`pwd` +branch=`git branch | grep -h '\*.*' | awk '{print $2}'` sh=`git rev-list --pretty=format:%h HEAD --max-count=1 \ | sed '1s/commit /full_rev=/;2s/^/short_rev=/'` @@ -92,6 +93,8 @@ public final class $CLASS { public static final String host = "$host"; /** Path to the repository in which this package was built. */ public static final String repo = "$repo"; + /** Git branch */ + public static final String branch = "$branch"; /** Human readable string describing the revision of this package. */ public static final String revisionString() { diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 96b98d6f46..99ecd57eb0 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -623,7 +623,8 @@ public void execute(final TSDB tsdb, final HttpQuery query) throws version.put("user", BuildData.user); version.put("host", BuildData.host); version.put("repo", BuildData.repo); - + version.put("branch", BuildData.branch); + if (query.apiVersion() > 0) { query.sendReply(query.serializer().formatVersionV1(version)); } else { From de0bceaa3b8ce4cb8137523a67eb391e42a5f790 Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Thu, 29 Oct 2015 23:46:42 +0800 Subject: [PATCH 254/826] fix stuck metasync when salting is enabled MetaScanner extracts (salt_width + metric_width) bytes from tsuid to metric uid, this triggers IllegalArgumentException in UniqueId.getNameAsync(id), then MetaScanner.call() won't never call result.callback(null), and result.joinUninterruptibly() in MetaSync.run() never returns. BTW, I checked all usages of Const.SALT_WIDTH() and fixed some other similar wrong calculations. Signed-off-by: Chris Larsen --- src/core/BatchedDataPoints.java | 2 +- src/search/TimeSeriesLookup.java | 4 ++-- src/tools/MetaSync.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 3fcea6018c..c56143f234 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -304,7 +304,7 @@ public Deferred metricNameAsync() { if (row_key == null) { throw new IllegalStateException("Instance was not properly constructed!"); } - final byte[] id = Arrays.copyOfRange(row_key, 0, + final byte[] id = Arrays.copyOfRange(row_key, Const.SALT_WIDTH(), tsdb.metrics.width() + Const.SALT_WIDTH()); return tsdb.metrics.getNameAsync(id); } diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index 6c244b90f4..d640644c04 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -403,7 +403,7 @@ private Scanner getScanner(final int salt) { key = metric_uid; } else { key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width()]; - key[0] = (byte)salt; + System.arraycopy(RowKey.getSaltBytes(salt), 0, key, 0, Const.SALT_WIDTH()); System.arraycopy(metric_uid, 0, key, Const.SALT_WIDTH(), metric_uid.length); } scanner.setStartKey(key); @@ -416,7 +416,7 @@ private Scanner getScanner(final int salt) { key = UniqueId.longToUID(uid, TSDB.metrics_width()); } else { key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width()]; - key[0] = (byte)salt; + System.arraycopy(RowKey.getSaltBytes(salt), 0, key, 0, Const.SALT_WIDTH()); System.arraycopy(UniqueId.longToUID(uid, TSDB.metrics_width()), 0, key, Const.SALT_WIDTH(), metric_uid.length); } diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index 2303650a0e..b3c7510d23 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -392,7 +392,7 @@ public Object call(ArrayList> rows) // now process the UID metric meta data final byte[] metric_uid_bytes = - Arrays.copyOfRange(tsuid, 0, Const.SALT_WIDTH() + TSDB.metrics_width()); + Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); final String metric_uid = UniqueId.uidToString(metric_uid_bytes); Long last_get = metric_uids.get(metric_uid); From ffb96291f0183b2af0344b6fb26aa17314e317a6 Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Thu, 29 Oct 2015 23:46:42 +0800 Subject: [PATCH 255/826] fix stuck metasync when salting is enabled MetaScanner extracts (salt_width + metric_width) bytes from tsuid to metric uid, this triggers IllegalArgumentException in UniqueId.getNameAsync(id), then MetaScanner.call() won't never call result.callback(null), and result.joinUninterruptibly() in MetaSync.run() never returns. BTW, I checked all usages of Const.SALT_WIDTH() and fixed some other similar wrong calculations. Signed-off-by: Chris Larsen --- src/core/BatchedDataPoints.java | 2 +- src/search/TimeSeriesLookup.java | 4 ++-- src/tools/MetaSync.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 3fcea6018c..c56143f234 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -304,7 +304,7 @@ public Deferred metricNameAsync() { if (row_key == null) { throw new IllegalStateException("Instance was not properly constructed!"); } - final byte[] id = Arrays.copyOfRange(row_key, 0, + final byte[] id = Arrays.copyOfRange(row_key, Const.SALT_WIDTH(), tsdb.metrics.width() + Const.SALT_WIDTH()); return tsdb.metrics.getNameAsync(id); } diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index 6c244b90f4..d640644c04 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -403,7 +403,7 @@ private Scanner getScanner(final int salt) { key = metric_uid; } else { key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width()]; - key[0] = (byte)salt; + System.arraycopy(RowKey.getSaltBytes(salt), 0, key, 0, Const.SALT_WIDTH()); System.arraycopy(metric_uid, 0, key, Const.SALT_WIDTH(), metric_uid.length); } scanner.setStartKey(key); @@ -416,7 +416,7 @@ private Scanner getScanner(final int salt) { key = UniqueId.longToUID(uid, TSDB.metrics_width()); } else { key = new byte[Const.SALT_WIDTH() + TSDB.metrics_width()]; - key[0] = (byte)salt; + System.arraycopy(RowKey.getSaltBytes(salt), 0, key, 0, Const.SALT_WIDTH()); System.arraycopy(UniqueId.longToUID(uid, TSDB.metrics_width()), 0, key, Const.SALT_WIDTH(), metric_uid.length); } diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index 2303650a0e..b3c7510d23 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -392,7 +392,7 @@ public Object call(ArrayList> rows) // now process the UID metric meta data final byte[] metric_uid_bytes = - Arrays.copyOfRange(tsuid, 0, Const.SALT_WIDTH() + TSDB.metrics_width()); + Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); final String metric_uid = UniqueId.uidToString(metric_uid_bytes); Long last_get = metric_uids.get(metric_uid); From ba636dc5de0adb2da9775d1afacf601f6b4002a9 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sun, 12 Jul 2015 18:55:42 -0700 Subject: [PATCH 256/826] Increase max number of rows to be returned per Scanner round trip via a config variable Signed-off-by: Chris Larsen --- src/query/QueryUtil.java | 1 + src/utils/Config.java | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index ab168536ea..1fc1363632 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -202,6 +202,7 @@ public static Scanner getMetricScanner(final TSDB tsdb, final int salt_bucket, System.arraycopy(metric, 0, end_row, Const.SALT_WIDTH(), metric_width); final Scanner scanner = tsdb.getClient().newScanner(table); + scanner.setMaxNumRows(tsdb.getConfig().scanner_maxNumRows()); scanner.setStartKey(start_row); scanner.setStopKey(end_row); scanner.setFamily(family); diff --git a/src/utils/Config.java b/src/utils/Config.java index 3344ff2e3c..554891e0b1 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -102,6 +102,9 @@ public class Config { /** tsd.core.tree.enable_processing */ private boolean enable_tree_processing = false; + + /** tsd.storage.hbase.scanner.maxNumRows */ + private int scanner_max_num_rows = 128; /** * The list of properties configured to their defaults or modified by users @@ -219,6 +222,11 @@ public boolean enable_tsuid_incrementing() { public boolean enable_tsuid_tracking() { return enable_tsuid_tracking; } + + /** @return maximum number of rows to be fetched per round trip while scanning HBase */ + public int scanner_maxNumRows() { + return scanner_max_num_rows; + } /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { @@ -498,6 +506,7 @@ protected void setDefaults() { default_map.put("tsd.search.enable", "false"); default_map.put("tsd.search.plugin", ""); default_map.put("tsd.stats.canonical", "false"); + default_map.put("tsd.storage.hbase.scanner.maxNumRows", "128"); default_map.put("tsd.storage.fix_duplicates", "false"); default_map.put("tsd.storage.flush_interval", "1000"); default_map.put("tsd.storage.hbase.data_table", "tsdb"); @@ -635,6 +644,8 @@ protected void loadStaticVariables() { } enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); + scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); + } /** From 602ee5676cf3c6cc2acdf6076f4fd96048ed9801 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 11:38:47 -0700 Subject: [PATCH 257/826] Add the Multiply aggregator Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 5edb598aa7..d70872e0de 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -53,6 +53,11 @@ public enum Interpolation { public static final Aggregator AVG = new Avg( Interpolation.LERP, "avg"); + /** Return the product of two time series + * @since 2.3 */ + public static final Aggregator MULTIPLY = new Multiply( + Interpolation.LERP, "multiply"); + /** Aggregator that returns the Standard Deviation of the data points. */ public static final Aggregator DEV = new StdDev( Interpolation.LERP, "dev"); @@ -139,6 +144,7 @@ public enum Interpolation { aggregators.put("min", MIN); aggregators.put("max", MAX); aggregators.put("avg", AVG); + aggregators.put("mult", MULTIPLY); aggregators.put("dev", DEV); aggregators.put("count", COUNT); aggregators.put("zimsum", ZIMSUM); @@ -313,6 +319,32 @@ public double runDouble(final Doubles values) { } + private static final class Multiply extends Aggregator { + + public Multiply(final Interpolation method, final String name) { + super(method, name); + } + + @Override + public long runLong(Longs values) { + long result = values.nextLongValue(); + while (values.hasNextValue()) { + result *= values.nextLongValue(); + } + return result; + } + + @Override + public double runDouble(Doubles values) { + double result = values.nextDoubleValue(); + while (values.hasNextValue()) { + result *= values.nextDoubleValue(); + } + return result; + } + + } + /** * Standard Deviation aggregator. * Can compute without storing all of the data points in memory at the same From d21d43a9e4e7f02fd1afa1d3f00c51c4bbbeb885 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 12:11:55 -0700 Subject: [PATCH 258/826] Make the AggregationIterator class and ctor public. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 28d77b9939..a53636b751 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -115,7 +115,7 @@ * to a special, really large value (too large to be a valid timestamp). *

    */ -final class AggregationIterator implements SeekableView, DataPoint, +public class AggregationIterator implements SeekableView, DataPoint, Aggregator.Longs, Aggregator.Doubles { private static final Logger LOG = @@ -290,7 +290,7 @@ public static AggregationIterator create(final List spans, * @param rate If {@code true}, the rate of the series will be used instead * of the actual values. */ - private AggregationIterator(final SeekableView[] iterators, + public AggregationIterator(final SeekableView[] iterators, final long start_time, final long end_time, final Aggregator aggregator, From c34e99496edff9622c1474167f9ee63fddfea251 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 12:19:23 -0700 Subject: [PATCH 259/826] Add the PostAggregatedDataPoints class to store an array of processed data points along with the original results containing meta data about the time series. Signed-off-by: Chris Larsen --- .../expression/PostAggregatedDataPoints.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/query/expression/PostAggregatedDataPoints.java diff --git a/src/query/expression/PostAggregatedDataPoints.java b/src/query/expression/PostAggregatedDataPoints.java new file mode 100644 index 0000000000..f8323516be --- /dev/null +++ b/src/query/expression/PostAggregatedDataPoints.java @@ -0,0 +1,176 @@ +// 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.hbase.async.Bytes.ByteMap; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.SeekableView; +import net.opentsdb.meta.Annotation; + +import com.google.common.collect.Maps; +import com.stumbleupon.async.Deferred; + +public class PostAggregatedDataPoints implements DataPoints { + + private final DataPoints baseDataPoints; + private final DataPoint[] points; + + private String alias = null; + + public PostAggregatedDataPoints(DataPoints baseDataPoints, DataPoint[] points) { + this.baseDataPoints = baseDataPoints; + this.points = points; + } + + @Override + public String metricName() { + if (alias != null) return alias; + else return baseDataPoints.metricName(); + } + + @Override + public Deferred metricNameAsync() { + if (alias != null) return Deferred.fromResult(alias); + return baseDataPoints.metricNameAsync(); + } + + @Override + public Map getTags() { + if (alias != null) return Maps.newHashMap(); + else return baseDataPoints.getTags(); + } + + @Override + public Deferred> getTagsAsync() { + Map def = new HashMap(); + if (alias != null) return Deferred.fromResult(def); + return baseDataPoints.getTagsAsync(); + } + + @Override + public List getAggregatedTags() { + return baseDataPoints.getAggregatedTags(); + } + + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + public Deferred> getAggregatedTagsAsync() { + return baseDataPoints.getAggregatedTagsAsync(); + } + + @Override + public List getTSUIDs() { + return baseDataPoints.getTSUIDs(); + } + + @Override + public List getAnnotations() { + return baseDataPoints.getAnnotations(); + } + + @Override + public int size() { + return points.length; + } + + @Override + public int aggregatedSize() { + return points.length; + } + + @Override + public SeekableView iterator() { + return new SeekableViewImpl(points); + } + + @Override + public long timestamp(int i) { + return points[i].timestamp(); + } + + @Override + public boolean isInteger(int i) { + return points[i].isInteger(); + } + + @Override + public long longValue(int i) { + return points[i].longValue(); + } + + @Override + public double doubleValue(int i) { + return points[i].doubleValue(); + } + + static class SeekableViewImpl implements SeekableView { + + private int pos=0; + private final DataPoint[] dps; + + public SeekableViewImpl(DataPoint[] dps) { + this.dps = dps; + } + + @Override + public boolean hasNext() { + return pos < dps.length; + } + + @Override + public DataPoint next() { + if (hasNext()) { + return dps[pos++]; + } else { + throw new NoSuchElementException("tsdb uses exceptions to determine end of iterators"); + } + } + + @Override + public void remove() { + throw new RuntimeException("Not supported exception"); + } + + @Override + public void seek(long timestamp) { + for (int i=pos; i= timestamp) { + break; + } else { + pos++; + } + } + } + } + + @Override + public ByteMap getTagUids() { + return baseDataPoints.getTagUids(); + } + + @Override + public int getQueryIndex() { + return baseDataPoints.getQueryIndex(); + } + +} From 223b624d5bd20f03c5e5566e07cf4698e00b2420 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 12:28:07 -0700 Subject: [PATCH 260/826] Cleanup, add comments and unit tests to PostAggregatedDataPoints. Also return an empty list for aggregated tags similar to the tags. We may want to revisit that at some point as it could still be useful to have the tags and the agg tags and just use the alias for metrics. Signed-off-by: Chris Larsen --- Makefile.am | 2 + .../expression/PostAggregatedDataPoints.java | 116 ++++++--- .../TestPostAggregatedDataPoints.java | 220 ++++++++++++++++++ 3 files changed, 300 insertions(+), 38 deletions(-) create mode 100644 test/query/expression/TestPostAggregatedDataPoints.java diff --git a/Makefile.am b/Makefile.am index 56c92b14b4..0aae69b4c9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ tsdb_SRC := \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ + src/query/expression/PostAggregatedDataPoints.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ @@ -218,6 +219,7 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/expression/TestPostAggregatedDataPoints.java \ test/query/filter/TestTagVFilter.java \ test/query/filter/TestTagVLiteralOrFilter.java \ test/query/filter/TestTagVNotKeyFilter.java \ diff --git a/src/query/expression/PostAggregatedDataPoints.java b/src/query/expression/PostAggregatedDataPoints.java index f8323516be..872def9c49 100644 --- a/src/query/expression/PostAggregatedDataPoints.java +++ b/src/query/expression/PostAggregatedDataPoints.java @@ -12,7 +12,7 @@ // see . package net.opentsdb.query.expression; -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -24,68 +24,109 @@ import net.opentsdb.core.SeekableView; import net.opentsdb.meta.Annotation; -import com.google.common.collect.Maps; import com.stumbleupon.async.Deferred; +/** + * A class to store an array of data points processed through expressions along + * with the original meta data of the result set (metric, tags, etc). + * @since 2.3 + */ public class PostAggregatedDataPoints implements DataPoints { - private final DataPoints baseDataPoints; + /** The original results from storage, used for fetching meta data */ + private final DataPoints base_data_points; + + /** The results of the expression calculation */ private final DataPoint[] points; + /** An optional alias for the results */ private String alias = null; - public PostAggregatedDataPoints(DataPoints baseDataPoints, DataPoint[] points) { - this.baseDataPoints = baseDataPoints; + /** + * Default ctor + * @param base_data_points The original results from storage for fetching meta + * @param points The results of the expression calculation + */ + public PostAggregatedDataPoints(final DataPoints base_data_points, + final DataPoint[] points) { + if (base_data_points == null) { + throw new IllegalArgumentException("base_data_points cannot be null"); + } + if (points == null) { + throw new IllegalArgumentException("points cannot be null"); + } + this.base_data_points = base_data_points; this.points = points; } @Override public String metricName() { - if (alias != null) return alias; - else return baseDataPoints.metricName(); + if (alias != null) { + return alias; + } else { + return base_data_points.metricName(); + } } @Override public Deferred metricNameAsync() { - if (alias != null) return Deferred.fromResult(alias); - return baseDataPoints.metricNameAsync(); + if (alias != null) { + return Deferred.fromResult(alias); + } + return base_data_points.metricNameAsync(); } @Override public Map getTags() { - if (alias != null) return Maps.newHashMap(); - else return baseDataPoints.getTags(); + if (alias != null) { + return Collections.emptyMap(); + } else { + return base_data_points.getTags(); + } } @Override public Deferred> getTagsAsync() { - Map def = new HashMap(); - if (alias != null) return Deferred.fromResult(def); - return baseDataPoints.getTagsAsync(); + if (alias != null) { + return Deferred.fromResult(Collections.emptyMap()); + } + return base_data_points.getTagsAsync(); } @Override public List getAggregatedTags() { - return baseDataPoints.getAggregatedTags(); - } - - public void setAlias(String alias) { - this.alias = alias; + if (alias != null) { + return Collections.emptyList(); + } + return base_data_points.getAggregatedTags(); } @Override public Deferred> getAggregatedTagsAsync() { - return baseDataPoints.getAggregatedTagsAsync(); + if (alias != null) { + return Deferred.fromResult(Collections.emptyList()); + } + return base_data_points.getAggregatedTagsAsync(); } @Override public List getTSUIDs() { - return baseDataPoints.getTSUIDs(); + return base_data_points.getTSUIDs(); } @Override public List getAnnotations() { - return baseDataPoints.getAnnotations(); + return base_data_points.getAnnotations(); + } + + @Override + public ByteMap getTagUids() { + return base_data_points.getTagUids(); + } + + @Override + public int getQueryIndex() { + return base_data_points.getQueryIndex(); } @Override @@ -122,13 +163,17 @@ public long longValue(int i) { public double doubleValue(int i) { return points[i].doubleValue(); } - + + /** + * An iterator working over the data points resulting from the expression + * calculation. + */ static class SeekableViewImpl implements SeekableView { - private int pos=0; + private int pos = 0; private final DataPoint[] dps; - - public SeekableViewImpl(DataPoint[] dps) { + + SeekableViewImpl(final DataPoint[] dps) { this.dps = dps; } @@ -142,18 +187,18 @@ public DataPoint next() { if (hasNext()) { return dps[pos++]; } else { - throw new NoSuchElementException("tsdb uses exceptions to determine end of iterators"); + throw new NoSuchElementException("no more elements"); } } @Override public void remove() { - throw new RuntimeException("Not supported exception"); + throw new UnsupportedOperationException(); } @Override public void seek(long timestamp) { - for (int i=pos; i= timestamp) { break; } else { @@ -163,14 +208,9 @@ public void seek(long timestamp) { } } - @Override - public ByteMap getTagUids() { - return baseDataPoints.getTagUids(); - } - - @Override - public int getQueryIndex() { - return baseDataPoints.getQueryIndex(); + /** @param alias The alias to set for the time series. Used in place of + * the metric and nulls out all tags. */ + public void setAlias(String alias) { + this.alias = alias; } - } diff --git a/test/query/expression/TestPostAggregatedDataPoints.java b/test/query/expression/TestPostAggregatedDataPoints.java new file mode 100644 index 0000000000..aa940d9072 --- /dev/null +++ b/test/query/expression/TestPostAggregatedDataPoints.java @@ -0,0 +1,220 @@ +// 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.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.meta.Annotation; + +import org.hbase.async.Bytes.ByteMap; +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({ Annotation.class }) +public class TestPostAggregatedDataPoints { + private static int NUM_POINTS = 5; + private static String METRIC_NAME = "sys.cpu"; + private static long BASE_TIME = 1356998400000L; + private static int TIME_INTERVAL = 60000; + + private DataPoints base_data_points; + private DataPoint[] points; + private Map tags; + private List agg_tags; + private List tsuids; + private List annotations; + private ByteMap tag_uids; + + @Before + public void before() throws Exception { + base_data_points = PowerMockito.mock(DataPoints.class); + points = new MutableDataPoint[NUM_POINTS]; + + long ts = BASE_TIME; + for (int i = 0; i < NUM_POINTS; i++) { + MutableDataPoint mdp = new MutableDataPoint(); + mdp.reset(ts, i); + points[i] = mdp; + ts += TIME_INTERVAL; + } + + tags = new HashMap(1); + tags.put("colo", "lga"); + agg_tags = new ArrayList(1); + agg_tags.add("host"); + tsuids = new ArrayList(1); + tsuids.add("0101010202"); // just 1 byte UIDs for kicks + annotations = new ArrayList(1); + annotations.add(PowerMockito.mock(Annotation.class)); + tag_uids = new ByteMap(); + tag_uids.put(new byte[] { 1 }, new byte[] { 1 }); + + when(base_data_points.metricName()).thenReturn(METRIC_NAME); + when(base_data_points.metricNameAsync()).thenReturn( + Deferred.fromResult(METRIC_NAME)); + when(base_data_points.getTags()).thenReturn(tags); + when(base_data_points.getTagsAsync()).thenReturn(Deferred.fromResult(tags)); + when(base_data_points.getAggregatedTags()).thenReturn(agg_tags); + when(base_data_points.getAggregatedTagsAsync()).thenReturn( + Deferred.fromResult(agg_tags)); + when(base_data_points.getTSUIDs()).thenReturn(tsuids); + when(base_data_points.getAnnotations()).thenReturn(annotations); + when(base_data_points.getTagUids()).thenReturn(tag_uids); + when(base_data_points.getQueryIndex()).thenReturn(42); + } + + @Test + public void ctorDefaults() throws Exception { + final PostAggregatedDataPoints dps = new PostAggregatedDataPoints( + base_data_points, points); + assertEquals(METRIC_NAME, dps.metricName()); + assertEquals(METRIC_NAME, dps.metricNameAsync().join()); + assertSame(tags, dps.getTags()); + assertSame(tags, dps.getTagsAsync().join()); + assertSame(agg_tags, dps.getAggregatedTags()); + assertSame(agg_tags, dps.getAggregatedTagsAsync().join()); + assertSame(tsuids, dps.getTSUIDs()); + assertSame(annotations, dps.getAnnotations()); + assertSame(tag_uids, dps.getTagUids()); + assertEquals(42, dps.getQueryIndex()); + assertEquals(5, dps.size()); + assertEquals(5, dps.aggregatedSize()); + + // values + final SeekableView iterator = dps.iterator(); + int values = 0; + long value = 0; + long ts = BASE_TIME; + while(iterator.hasNext()) { + final DataPoint dp = iterator.next(); + assertEquals(value++, dp.longValue()); + assertEquals(ts, dp.timestamp()); + ts += TIME_INTERVAL; + values++; + } + assertEquals(5, values); + assertFalse(iterator.hasNext()); + try { + iterator.next(); + fail("Expected a NoSuchElementException"); + } catch (NoSuchElementException e) { } + + assertEquals(BASE_TIME + (4 * TIME_INTERVAL), dps.timestamp(4)); + assertEquals(4, dps.longValue(4)); + assertTrue(dps.isInteger(4)); + try { + assertEquals(4, dps.doubleValue(4), 0.00); + fail("Expected a ClassCastException"); + } catch (ClassCastException e) { } + + try { + dps.timestamp(5); + fail("Expected a ArrayIndexOutOfBoundsException"); + } catch (ArrayIndexOutOfBoundsException e) { } + try { + dps.isInteger(5); + fail("Expected a ArrayIndexOutOfBoundsException"); + } catch (ArrayIndexOutOfBoundsException e) { } + try { + dps.longValue(5); + fail("Expected a ArrayIndexOutOfBoundsException"); + } catch (ArrayIndexOutOfBoundsException e) { } + try { + dps.doubleValue(5); + fail("Expected a ArrayIndexOutOfBoundsException"); + } catch (ArrayIndexOutOfBoundsException e) { } + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullBase() throws Exception { + new PostAggregatedDataPoints(null, points); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullPoints() throws Exception { + new PostAggregatedDataPoints(base_data_points, null); + } + + @Test + public void alias() throws Exception { + final PostAggregatedDataPoints dps = new PostAggregatedDataPoints( + base_data_points, points); + final String alias = "ein"; + dps.setAlias(alias); + assertEquals(alias, dps.metricName()); + assertEquals(alias, dps.metricNameAsync().join()); + assertTrue(dps.getTags().isEmpty()); + assertTrue(dps.getTagsAsync().join().isEmpty()); + assertTrue(dps.getAggregatedTags().isEmpty()); + assertTrue(dps.getAggregatedTagsAsync().join().isEmpty()); + assertSame(tsuids, dps.getTSUIDs()); + assertSame(annotations, dps.getAnnotations()); + assertSame(tag_uids, dps.getTagUids()); + assertEquals(42, dps.getQueryIndex()); + assertEquals(5, dps.size()); + assertEquals(5, dps.aggregatedSize()); + } + + @Test + public void emptyPoints() throws Exception { + points = new MutableDataPoint[0]; + final PostAggregatedDataPoints dps = new PostAggregatedDataPoints( + base_data_points, points); + assertEquals(METRIC_NAME, dps.metricName()); + assertEquals(METRIC_NAME, dps.metricNameAsync().join()); + assertSame(tags, dps.getTags()); + assertSame(tags, dps.getTagsAsync().join()); + assertSame(agg_tags, dps.getAggregatedTags()); + assertSame(agg_tags, dps.getAggregatedTagsAsync().join()); + assertSame(tsuids, dps.getTSUIDs()); + assertSame(annotations, dps.getAnnotations()); + assertSame(tag_uids, dps.getTagUids()); + assertEquals(42, dps.getQueryIndex()); + assertEquals(0, dps.size()); + assertEquals(0, dps.aggregatedSize()); + + // values + final SeekableView iterator = dps.iterator(); + assertFalse(iterator.hasNext()); + try { + iterator.next(); + fail("Expected a NoSuchElementException"); + } catch (NoSuchElementException e) { } + } +} From 5e229f00c08ee1647cf1ddd8793d1d761e0aec07 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 16:09:22 -0700 Subject: [PATCH 261/826] Rework the SeekableViewsForTest generator to allow for configurable starting values and increment values. Signed-off-by: Chris Larsen --- test/core/SeekableViewsForTest.java | 132 ++++++++++++++++------------ 1 file changed, 74 insertions(+), 58 deletions(-) diff --git a/test/core/SeekableViewsForTest.java b/test/core/SeekableViewsForTest.java index dab66d7c37..e53dc27b15 100644 --- a/test/core/SeekableViewsForTest.java +++ b/test/core/SeekableViewsForTest.java @@ -32,7 +32,8 @@ public static SeekableView fromArray(final DataPoint[] data_points) { } /** - * Creates a {@link SeekableView} that generates a sequence of data points. + * Creates a {@link SeekableView} that generates a sequence of data points + * where the starting value is 1 and it is incremented by 1 each iteration. * @param start_time Starting timestamp * @param sample_period Average sample period of data points * @param num_data_points Total number of data points to generate @@ -43,8 +44,28 @@ public static SeekableView generator(final long start_time, final long sample_period, final int num_data_points, final boolean is_integer) { + return generator(start_time, sample_period, num_data_points, + is_integer, 0, 1); + } + + /** + * Creates a {@link SeekableView} that generates a sequence of data points. + * @param start_time Starting timestamp + * @param sample_period Average sample period of data points + * @param num_data_points Total number of data points to generate + * @param is_integer True to generate a sequence of integer data points. + * @param starting_value The starting data point value. + * @param increment How much to increment the values each iteration. + * @return A {@link SeekableView} object + */ + public static SeekableView generator(final long start_time, + final long sample_period, + final int num_data_points, + final boolean is_integer, + final double starting_value, + final double increment) { return new DataPointGenerator(start_time, sample_period, num_data_points, - is_integer); + is_integer, starting_value, increment); } /** Iterates an array of data points. */ @@ -88,32 +109,38 @@ public void seek(long timestamp) { /** Generates a sequence of data points. */ private static class DataPointGenerator implements SeekableView { - private final long start_time_ms; private final long sample_period_ms; private final int num_data_points; private final boolean is_integer; + private final double increment; private final MutableDataPoint current_data = new MutableDataPoint(); - private int current = 0; - + private final MutableDataPoint next_data = new MutableDataPoint(); + private int dps_emitted = 0; + DataPointGenerator(final long start_time_ms, final long sample_period_ms, - final int num_data_points, final boolean is_integer) { - this.start_time_ms = start_time_ms; + final int num_data_points, final boolean is_integer, + final double starting_value, final double increment) { this.sample_period_ms = sample_period_ms; this.num_data_points = num_data_points; this.is_integer = is_integer; - rewind(); + this.increment = increment; + if (is_integer) { + next_data.reset(start_time_ms, (long)starting_value); + } else { + next_data.reset(start_time_ms, starting_value); + } } @Override public boolean hasNext() { - return current < num_data_points; + return dps_emitted < num_data_points; } @Override public DataPoint next() { if (hasNext()) { - generateData(); - ++current; + current_data.reset(next_data); + advance(); return current_data; } throw new NoSuchElementException("no more values"); @@ -126,44 +153,32 @@ public void remove() { @Override public void seek(long timestamp) { - rewind(); - current = (int)((timestamp -1 - start_time_ms) / sample_period_ms); - if (current < 0) { - current = 0; - } - while (generateTimestamp() < timestamp) { - ++current; + while (next_data.timestamp() < timestamp && dps_emitted < num_data_points) { + advance(); } } - - private void rewind() { - current = 0; - generateData(); - } - - private void generateData() { + + private void advance() { if (is_integer) { - current_data.reset(generateTimestamp(), current); + next_data.reset(next_data.timestamp() + sample_period_ms, + next_data.longValue() + (long)increment); } else { - current_data.reset(generateTimestamp(), (double)current); + next_data.reset(next_data.timestamp() + sample_period_ms, + next_data.doubleValue() + increment); } - } - - private long generateTimestamp() { - long timestamp = start_time_ms + sample_period_ms * current; - return timestamp + (((current % 2) == 0) ? -1000 : 1000); + dps_emitted++; } } @Test public void testDataPointGenerator() { - DataPointGenerator dpg = new DataPointGenerator(100000, 10000, 5, true); + SeekableView dpg = generator(100000, 10000, 5, true); DataPoint[] expected_data_points = new DataPoint[] { - MutableDataPoint.ofLongValue(99000, 0), - MutableDataPoint.ofLongValue(111000, 1), - MutableDataPoint.ofLongValue(119000, 2), - MutableDataPoint.ofLongValue(131000, 3), - MutableDataPoint.ofLongValue(139000, 4), + MutableDataPoint.ofLongValue(100000, 0), + MutableDataPoint.ofLongValue(110000, 1), + MutableDataPoint.ofLongValue(120000, 2), + MutableDataPoint.ofLongValue(130000, 3), + MutableDataPoint.ofLongValue(140000, 4), }; for (DataPoint expected: expected_data_points) { assertTrue(dpg.hasNext()); @@ -176,13 +191,13 @@ public void testDataPointGenerator() { @Test public void testDataPointGenerator_double() { - DataPointGenerator dpg = new DataPointGenerator(100000, 10000, 5, false); + SeekableView dpg = generator(100000, 10000, 5, false); DataPoint[] expected_data_points = new DataPoint[] { - MutableDataPoint.ofDoubleValue(99000, 0), - MutableDataPoint.ofDoubleValue(111000, 1), - MutableDataPoint.ofDoubleValue(119000, 2), - MutableDataPoint.ofDoubleValue(131000, 3), - MutableDataPoint.ofDoubleValue(139000, 4), + MutableDataPoint.ofDoubleValue(100000, 0), + MutableDataPoint.ofDoubleValue(110000, 1), + MutableDataPoint.ofDoubleValue(120000, 2), + MutableDataPoint.ofDoubleValue(130000, 3), + MutableDataPoint.ofDoubleValue(140000, 4), }; for (DataPoint expected: expected_data_points) { assertTrue(dpg.hasNext()); @@ -195,12 +210,12 @@ public void testDataPointGenerator_double() { @Test public void testDataPointGenerator_seek() { - DataPointGenerator dpg = new DataPointGenerator(100000, 10000, 5, true); + SeekableView dpg = generator(100000, 10000, 5, true); dpg.seek(119000); DataPoint[] expected_data_points = new DataPoint[] { - MutableDataPoint.ofLongValue(119000, 2), - MutableDataPoint.ofLongValue(131000, 3), - MutableDataPoint.ofLongValue(139000, 4), + MutableDataPoint.ofLongValue(120000, 2), + MutableDataPoint.ofLongValue(130000, 3), + MutableDataPoint.ofLongValue(140000, 4), }; for (DataPoint expected: expected_data_points) { assertTrue(dpg.hasNext()); @@ -213,13 +228,14 @@ public void testDataPointGenerator_seek() { @Test public void testDataPointGenerator_seekToFirst() { - DataPointGenerator dpg = new DataPointGenerator(100000, 10000, 5, true); + SeekableView dpg = generator(100000, 10000, 5, true); dpg.seek(100000); DataPoint[] expected_data_points = new DataPoint[] { - MutableDataPoint.ofLongValue(111000, 1), - MutableDataPoint.ofLongValue(119000, 2), - MutableDataPoint.ofLongValue(131000, 3), - MutableDataPoint.ofLongValue(139000, 4), + MutableDataPoint.ofLongValue(100000, 0), + MutableDataPoint.ofLongValue(110000, 1), + MutableDataPoint.ofLongValue(120000, 2), + MutableDataPoint.ofLongValue(130000, 3), + MutableDataPoint.ofLongValue(140000, 4), }; for (DataPoint expected: expected_data_points) { assertTrue(dpg.hasNext()); @@ -232,13 +248,13 @@ public void testDataPointGenerator_seekToFirst() { @Test public void testDataPointGenerator_seekToSecond() { - DataPointGenerator dpg = new DataPointGenerator(100000, 10000, 5, true); + SeekableView dpg = generator(100000, 10000, 5, true); dpg.seek(100001); DataPoint[] expected_data_points = new DataPoint[] { - MutableDataPoint.ofLongValue(111000, 1), - MutableDataPoint.ofLongValue(119000, 2), - MutableDataPoint.ofLongValue(131000, 3), - MutableDataPoint.ofLongValue(139000, 4), + MutableDataPoint.ofLongValue(110000, 1), + MutableDataPoint.ofLongValue(120000, 2), + MutableDataPoint.ofLongValue(130000, 3), + MutableDataPoint.ofLongValue(140000, 4), }; for (DataPoint expected: expected_data_points) { assertTrue(dpg.hasNext()); From 02fd5ae865022e5899dde8a10fd88977884e526d Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 16:24:58 -0700 Subject: [PATCH 262/826] Add a MovingAverage expression implementation for the Graphite endpoint. Signed-off-by: Chris Larsen --- src/query/expression/MovingAverage.java | 235 ++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 src/query/expression/MovingAverage.java diff --git a/src/query/expression/MovingAverage.java b/src/query/expression/MovingAverage.java new file mode 100644 index 0000000000..d9bda06a26 --- /dev/null +++ b/src/query/expression/MovingAverage.java @@ -0,0 +1,235 @@ +package net.opentsdb.query.expression; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import net.opentsdb.core.AggregationIterator; +import net.opentsdb.core.Aggregator; +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.Aggregators.Interpolation; + +import com.google.common.collect.Lists; + +public class MovingAverage implements Expression { + + @Override + public DataPoints[] evaluate(final TSQuery data_query, List queryResults, List params) { + if (queryResults == null || queryResults.isEmpty()) { + return new DataPoints[]{}; + } + + if (params == null || params.isEmpty()) { + throw new NullPointerException("Need aggregation window for moving average"); + } + + String param = params.get(0); + if (param == null || param.length() == 0) { + throw new NullPointerException("Invalid window='" + param + "'"); + } + + param = param.trim(); + + long numPoints = -1; + boolean isTimeUnit = false; + if (param.matches("[0-9]+")) { + numPoints = Integer.parseInt(param); + } else if (param.startsWith("'") && param.endsWith("'")) { + numPoints = parseParam(param); + isTimeUnit = true; + } + + if (numPoints <= 0) { + throw new RuntimeException("numPoints <= 0"); + } + + int size = 0; + for (DataPoints[] results: queryResults) { + size = size + results.length; + } + + PostAggregatedDataPoints[] seekablePoints = new PostAggregatedDataPoints[size]; + int ix=0; + // one or more queries (m=...&m=...&m=...) + for (DataPoints[] results: queryResults) { + // group bys (m=sum:foo{host=*}) + for (DataPoints dpoints: results) { + List mutablePoints = new ArrayList(); + for (DataPoint point: dpoints) { + mutablePoints.add(point.isInteger() ? + MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) + : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); + } + + seekablePoints[ix++] = new PostAggregatedDataPoints(dpoints, + mutablePoints.toArray(new DataPoint[mutablePoints.size()])); + } + } + + SeekableView[] views = new SeekableView[size]; + for (int i=0; i points = Lists.newArrayList(); + while (view.hasNext()) { + DataPoint mdp = view.next(); + points.add(mdp.isInteger() ? + MutableDataPoint.ofLongValue(mdp.timestamp(), mdp.longValue()) : + MutableDataPoint.ofDoubleValue(mdp.timestamp(), mdp.doubleValue())); + } + + if (queryResults.size() > 0 && queryResults.get(0).length > 0) { + return new DataPoints[]{new PostAggregatedDataPoints(queryResults.get(0)[0], + points.toArray(new DataPoint[points.size()]))}; + } else { + return new DataPoints[]{}; + } + } + + public long parseParam(String param) { + char[] chars = param.toCharArray(); + int tuIndex = 0; + for (int c = 1; c < chars.length; c++) { + if (Character.isDigit(chars[c])) { + tuIndex++; + } else { + break; + } + } + + if (tuIndex == 0) { + throw new RuntimeException("Invalid Parameter: " + param); + } + + int time = Integer.parseInt(param.substring(1, tuIndex + 1)); + String unit = param.substring(tuIndex+1, param.length() - 1); + + if ("min".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.MINUTES); + } else if ("hr".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.HOURS); + } else if ("sec".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.SECONDS); + } else { + throw new RuntimeException("unknown time unit=" + unit); + } + + } + + @Override + public String writeStringField(List queryParams, String innerExpression) { + return "movingAverage(" + innerExpression + ")"; + } + + static final class MovingAverageAggregator extends Aggregator { + private LinkedList list = new LinkedList(); + private final long numPoints; + private final boolean isTimeUnit; + + public MovingAverageAggregator(final Interpolation method, final String name, long numPoints, boolean isTimeUnit) { + super(method, name); + this.numPoints = numPoints; + this.isTimeUnit = isTimeUnit; + } + + @Override + public long runLong(final Longs values) { + long sum = values.nextLongValue(); + while (values.hasNextValue()) { + sum += values.nextLongValue(); + } + + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + list.addFirst(new SumPoint(ts, sum)); + } + + long result=0; int count=0; + + Iterator iter = list.iterator(); + SumPoint first = iter.next(); + boolean conditionMet = false; + + // now sum up the preceeding points + while(iter.hasNext()) { + SumPoint next = iter.next(); + result += (Long) next.val; + count++; + if (!isTimeUnit && count >= numPoints) { + conditionMet = true; + break; + } else if (isTimeUnit && ((first.ts - next.ts) > numPoints)) { + conditionMet = true; + break; + } + } + + if (!conditionMet || count == 0) { + return 0; + } + + return result/count; + } + + @Override + public double runDouble(Doubles values) { + double sum = values.nextDoubleValue(); + while (values.hasNextValue()) { + sum += values.nextDoubleValue(); + } + + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + list.addFirst(new SumPoint(ts, sum)); + } + + double result=0; int count=0; + + Iterator iter = list.iterator(); + SumPoint first = iter.next(); + boolean conditionMet = false; + + // now sum up the preceeding points + while(iter.hasNext()) { + SumPoint next = iter.next(); + result += (Double) next.val; + count++; + if (!isTimeUnit && count >= numPoints) { + conditionMet = true; + break; + } else if (isTimeUnit && ((first.ts - next.ts) > numPoints)) { + conditionMet = true; + break; + } + } + + if (!conditionMet || count == 0) { + return 0; + } + + return result/count; + } + + class SumPoint { + long ts; + Object val; + public SumPoint(long ts, Object val) { + this.ts = ts; + this.val = val; + } + } + } +} From 052de63ee8e3c946d27e5a3593bf05b128a9c975 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 19:05:24 -0700 Subject: [PATCH 263/826] Fix up the MovingAverage expression function. - Fix an off-by-one error - Fix the timed window so it kicks out the first value as we don't know what the previous timestamp was so we don't know if it should be in our window. - Reduce the linked list when a value falls out of the window - Add OpenTSDB time units to the window size parser - Force it to output in doubles to avoid flip-flopping integer and double computations, possibly losing precision - Add comments and cleanup formatting - Add Unit tests Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/MovingAverage.java | 377 ++++++++++------ test/query/expression/TestMovingAverage.java | 438 +++++++++++++++++++ 3 files changed, 687 insertions(+), 130 deletions(-) create mode 100644 test/query/expression/TestMovingAverage.java diff --git a/Makefile.am b/Makefile.am index 0aae69b4c9..5f97368a75 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ tsdb_SRC := \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ + src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ @@ -219,6 +220,7 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/filter/TestTagVFilter.java \ test/query/filter/TestTagVLiteralOrFilter.java \ diff --git a/src/query/expression/MovingAverage.java b/src/query/expression/MovingAverage.java index d9bda06a26..8366a492d8 100644 --- a/src/query/expression/MovingAverage.java +++ b/src/query/expression/MovingAverage.java @@ -1,3 +1,15 @@ +// 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; @@ -11,225 +23,330 @@ import net.opentsdb.core.Aggregators; 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; -import com.google.common.collect.Lists; - +/** + * Implements a moving average function windowed on either the number of + * data points or a unit of time. + * @since 2.3 + */ public class MovingAverage implements Expression { - + @Override - public DataPoints[] evaluate(final TSQuery data_query, List queryResults, List params) { - if (queryResults == null || queryResults.isEmpty()) { + 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[]{}; } - if (params == null || params.isEmpty()) { - throw new NullPointerException("Need aggregation window for moving average"); + throw new IllegalArgumentException("Missing moving average window size"); } String param = params.get(0); - if (param == null || param.length() == 0) { - throw new NullPointerException("Invalid window='" + param + "'"); + if (param == null || param.isEmpty()) { + throw new IllegalArgumentException("Missing moving average window size"); } - param = param.trim(); - long numPoints = -1; - boolean isTimeUnit = false; - if (param.matches("[0-9]+")) { - numPoints = Integer.parseInt(param); + long condition = -1; + boolean is_time_unit = false; + if (param.matches("^[0-9]+$")) { + try { + condition = Integer.parseInt(param); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException( + "Invalid parameter, must be an integer", nfe); + } } else if (param.startsWith("'") && param.endsWith("'")) { - numPoints = parseParam(param); - isTimeUnit = true; + condition = parseParam(param); + is_time_unit = true; + } else { + throw new IllegalArgumentException("Unparseable window size: " + param); } - - if (numPoints <= 0) { - throw new RuntimeException("numPoints <= 0"); + if (condition <= 0) { + throw new IllegalArgumentException("Moving average window must be an " + + "integer greater than zero"); } - int size = 0; - for (DataPoints[] results: queryResults) { - size = size + results.length; + int num_results = 0; + for (final DataPoints[] results : query_results) { + num_results += results.length; } - PostAggregatedDataPoints[] seekablePoints = new PostAggregatedDataPoints[size]; - int ix=0; + final PostAggregatedDataPoints[] post_agg_results = + new PostAggregatedDataPoints[num_results]; + int ix = 0; // one or more queries (m=...&m=...&m=...) - for (DataPoints[] results: queryResults) { + for (final DataPoints[] sub_query_result : query_results) { // group bys (m=sum:foo{host=*}) - for (DataPoints dpoints: results) { - List mutablePoints = new ArrayList(); - for (DataPoint point: dpoints) { - mutablePoints.add(point.isInteger() ? - MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) - : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); + for (final DataPoints dps: sub_query_result) { + // TODO(cl) - Avoid iterating and copying if we can help it. We should + // be able to pass the original DataPoints object to the seekable view + // and then iterate through it. + final List mutable_points = new ArrayList(); + for (final DataPoint point: dps) { + // avoid flip-flopping between integers and floats, always use double + // for average. + mutable_points.add( + MutableDataPoint.ofDoubleValue(point.timestamp(), point.toDouble())); } - seekablePoints[ix++] = new PostAggregatedDataPoints(dpoints, - mutablePoints.toArray(new DataPoint[mutablePoints.size()])); + post_agg_results[ix++] = new PostAggregatedDataPoints(dps, + mutable_points.toArray(new DataPoint[mutable_points.size()])); } } - SeekableView[] views = new SeekableView[size]; - for (int i=0; i points = Lists.newArrayList(); + // TODO(cl) - here's a good place to return the AggregationIterators instead + // of processing them in situ and making copies + final List points = new ArrayList(); while (view.hasNext()) { DataPoint mdp = view.next(); - points.add(mdp.isInteger() ? - MutableDataPoint.ofLongValue(mdp.timestamp(), mdp.longValue()) : - MutableDataPoint.ofDoubleValue(mdp.timestamp(), mdp.doubleValue())); + points.add(MutableDataPoint.ofDoubleValue(mdp.timestamp(), mdp.toDouble())); } - if (queryResults.size() > 0 && queryResults.get(0).length > 0) { - return new DataPoints[]{new PostAggregatedDataPoints(queryResults.get(0)[0], + if (query_results.size() > 0 && query_results.get(0).length > 0) { + return new DataPoints[]{new PostAggregatedDataPoints(query_results.get(0)[0], points.toArray(new DataPoint[points.size()]))}; } else { return new DataPoints[]{}; } } - public long parseParam(String param) { - char[] chars = param.toCharArray(); - int tuIndex = 0; + /** + * Parses the parameter string to fetch the window size + *

    + * Package private for UTs + * @param param The string to parse + * @return The window size (number of points or a unit of time in ms) + */ + long parseParam(final String param) { + if (param == null || param.isEmpty()) { + throw new IllegalArgumentException( + "Window parameter may not be null or empty"); + } + final char[] chars = param.toCharArray(); + int idx = 0; for (int c = 1; c < chars.length; c++) { if (Character.isDigit(chars[c])) { - tuIndex++; + idx++; } else { break; } } - - if (tuIndex == 0) { - throw new RuntimeException("Invalid Parameter: " + param); + if (idx < 1) { + throw new IllegalArgumentException("Invalid moving window parameter: " + + param); } - int time = Integer.parseInt(param.substring(1, tuIndex + 1)); - String unit = param.substring(tuIndex+1, param.length() - 1); - - if ("min".equals(unit)) { - return TimeUnit.MILLISECONDS.convert(time, TimeUnit.MINUTES); - } else if ("hr".equals(unit)) { - return TimeUnit.MILLISECONDS.convert(time, TimeUnit.HOURS); - } else if ("sec".equals(unit)) { - return TimeUnit.MILLISECONDS.convert(time, TimeUnit.SECONDS); - } else { - throw new RuntimeException("unknown time unit=" + unit); + try { + final int time = Integer.parseInt(param.substring(1, idx + 1)); + final String unit = param.substring(idx + 1, param.length() - 1); + + // TODO(CL) - add a Graphite unit parser to DateTime for this kind of conversion + if ("day".equals(unit) || "d".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.DAYS); + } else if ("hr".equals(unit) || "hour".equals(unit) || "h".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.HOURS); + } else if ("min".equals(unit) || "m".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.MINUTES); + } else if ("sec".equals(unit) || "s".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.SECONDS); + } else { + throw new IllegalArgumentException("Unknown time unit=" + unit + + " in window=" + param); + } + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Unable to parse moving window " + + "parameter: " + param, nfe); } - } @Override - public String writeStringField(List queryParams, String innerExpression) { - return "movingAverage(" + innerExpression + ")"; + public String writeStringField(final List query_params, + final String inner_expression) { + return "movingAverage(" + inner_expression + ")"; } + /** + * An aggregator that expects a single data point for each iteration. The + * values are prepended to a linked list. Next it iterates over the list until + * it either runs out of values (and returns a 0 with the proper timestamp) or + * returns the average of all values in the given window (time or number based). + *

    + * Package private for unit testing + */ static final class MovingAverageAggregator extends Aggregator { - private LinkedList list = new LinkedList(); - private final long numPoints; - private final boolean isTimeUnit; + /** The individual values in the window */ + private final LinkedList accumulation; + /** The condition to satisfy, either a time unit or # of data points */ + private final long condition; + /** Whether or not the condition is a time unit or the # of data points */ + private final boolean is_time_unit; + /** Sentinel used to kick out the first timed window value */ + private boolean window_started; - public MovingAverageAggregator(final Interpolation method, final String name, long numPoints, boolean isTimeUnit) { + /** + * Ctor for this implementation + * @param method The interpolation method to use (ignored) + * @param name The name of this aggregator + * @param condition The windowing condition + * @param is_time_unit Whether or not the condition is a time unit or + * the # of data points + */ + public MovingAverageAggregator(final Interpolation method, final String name, + final long condition, final boolean is_time_unit) { super(method, name); - this.numPoints = numPoints; - this.isTimeUnit = isTimeUnit; + this.condition = condition; + this.is_time_unit = is_time_unit; + accumulation = new LinkedList(); } @Override public long runLong(final Longs values) { - long sum = values.nextLongValue(); - while (values.hasNextValue()) { - sum += values.nextLongValue(); + final long value = values.nextLongValue(); + if (values.hasNextValue()) { + throw new IllegalDataException( + "There should only be one value in " + values); } + final long ts = ((DataPoint) values).timestamp(); + accumulation.addFirst(MutableDataPoint.ofLongValue(ts, value)); - if (values instanceof DataPoint) { - long ts = ((DataPoint) values).timestamp(); - list.addFirst(new SumPoint(ts, sum)); + // for timed windows we need to skip the first data point in the series + // as we have no idea what the previous value's timestamp was. + if (is_time_unit && !window_started) { + window_started = true; + return 0; } - - long result=0; int count=0; - - Iterator iter = list.iterator(); - SumPoint first = iter.next(); - boolean conditionMet = false; - - // now sum up the preceeding points + + long sum = 0; + int count = 0; + final Iterator iter = accumulation.iterator(); + boolean condition_met = false; + long time_window_cumulation = 0; // how many ms are in our window + long last_ts = -1; // the timestamp of the previous dp + + // now sum up the preceding points while(iter.hasNext()) { - SumPoint next = iter.next(); - result += (Long) next.val; + final DataPoint dp = iter.next(); + if (is_time_unit) { + if (last_ts < 0) { + last_ts = dp.timestamp(); + } else { + time_window_cumulation += last_ts - dp.timestamp(); + last_ts = dp.timestamp(); + if (time_window_cumulation >= condition) { + condition_met = true; + break; + } + } + } + // cast to long if we dumped a double in there + sum += dp.isInteger() ? dp.longValue() : dp.doubleValue(); count++; - if (!isTimeUnit && count >= numPoints) { - conditionMet = true; - break; - } else if (isTimeUnit && ((first.ts - next.ts) > numPoints)) { - conditionMet = true; + if (!is_time_unit && count >= condition) { + condition_met = true; break; } } + while (iter.hasNext()) { + // should drop the last entry in the linked list to avoid accumulating + // everything in memory + iter.next(); + iter.remove(); + } - if (!conditionMet || count == 0) { + if (!condition_met || count == 0) { return 0; } - - return result/count; + return sum / count; } @Override public double runDouble(Doubles values) { - double sum = values.nextDoubleValue(); - while (values.hasNextValue()) { - sum += values.nextDoubleValue(); + final double value = values.nextDoubleValue(); + if (values.hasNextValue()) { + throw new IllegalDataException( + "There should only be one value in " + values); } - - if (values instanceof DataPoint) { - long ts = ((DataPoint) values).timestamp(); - list.addFirst(new SumPoint(ts, sum)); + final long ts = ((DataPoint) values).timestamp(); + accumulation.addFirst(MutableDataPoint.ofDoubleValue(ts, value)); + + // for timed windows we need to skip the first data point in the series + // as we have no idea what the previous value's timestamp was. + if (is_time_unit && !window_started) { + window_started = true; + return 0; } - - double result=0; int count=0; - - Iterator iter = list.iterator(); - SumPoint first = iter.next(); - boolean conditionMet = false; - - // now sum up the preceeding points + + double sum = 0; + int count = 0; + final Iterator iter = accumulation.iterator(); + boolean condition_met = false; + long time_window_cumulation = 0; // how many ms are in our window + long last_ts = -1; // the timestamp of the previous dp + + // now sum up the preceding points while(iter.hasNext()) { - SumPoint next = iter.next(); - result += (Double) next.val; - count++; - if (!isTimeUnit && count >= numPoints) { - conditionMet = true; - break; - } else if (isTimeUnit && ((first.ts - next.ts) > numPoints)) { - conditionMet = true; + final DataPoint dp = iter.next(); + + if (is_time_unit) { + if (last_ts < 0) { + last_ts = dp.timestamp(); + } else { + time_window_cumulation += last_ts - dp.timestamp(); + last_ts = dp.timestamp(); + if (time_window_cumulation >= condition) { + condition_met = true; + break; + } + } + } + + // cast to double if we dumped a long in there + final double v = dp.isInteger() ? dp.longValue() : dp.doubleValue(); + if (!Double.isNaN(v)) { + // skip NaNs to avoid NaNing everything in the window. + sum += v; + count++; + } + + if (!is_time_unit && count >= condition) { + condition_met = true; break; } } - - if (!conditionMet || count == 0) { - return 0; + + while (iter.hasNext()) { + // should drop the last entry in the linked list to avoid accumulating + // everything in memory + iter.next(); + iter.remove(); } - return result/count; - } - - class SumPoint { - long ts; - Object val; - public SumPoint(long ts, Object val) { - this.ts = ts; - this.val = val; + if (!condition_met || count == 0) { + return 0; } + return sum/count; } } } diff --git a/test/query/expression/TestMovingAverage.java b/test/query/expression/TestMovingAverage.java new file mode 100644 index 0000000000..3791c6f0f5 --- /dev/null +++ b/test/query/expression/TestMovingAverage.java @@ -0,0 +1,438 @@ +// 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.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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestMovingAverage { + 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 MovingAverage 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.metricName()).thenReturn(METRIC); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new MovingAverage(); + } + + @Test + public void evaluateWindow1dps() throws Exception { + params.add("1"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].metricName()); + + long ts = START_TIME; + double v = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateWindow2dps() throws Exception { + params.add("2"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + if (v < 1) { + v = 1.5; + } else { + v += 1; + } + } + } + + @Test + public void evaluateWindow5dps() throws Exception { + params.add("5"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + if (ts == 1356998640000L) { + v = 3.0; + } + } + } + + @Test + public void evaluateWindow6dps() throws Exception { + params.add("6"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + } + } + + @Test + public void evaluateWindow1min() throws Exception { + params.add("'1min'"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + if (v < 1) { + v = 2; + } else { + v += 1; + } + } + } + + @Test + public void evaluateWindow2min() throws Exception { + params.add("'2min'"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + if (ts == 1356998520000L) { + v = 2.5; + } else if (v > 0) { + v += 1; + } + } + } + + @Test + public void evaluateWindow3min() throws Exception { + params.add("'3min'"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].metricName()); + + long ts = START_TIME; + double v = 0; + for (DataPoint dp : results[0]) { + System.out.println(dp.timestamp() + " : " + dp.doubleValue()); + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + if (ts == 1356998580000L) { + v = 3; + } else if (v > 0) { + v += 1; + } + } + } + + @Test + public void evaluateWindow4min() throws Exception { + params.add("'4min'"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + if (ts == 1356998640000L) { + v = 3.5; + } + } + } + + @Test + public void evaluateWindow5min() throws Exception { + params.add("'5min'"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC, results[0].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; + } + } + + @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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 (expected = IllegalArgumentException.class) + public void evaluateEmptyParams() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowIsZeroDataPoints() throws Exception { + params.add("0"); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowIsZeroTime() throws Exception { + params.add("'0sec'"); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowTimedMissingQuotes() throws Exception { + params.add("60sec"); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowNull() throws Exception { + params.add(null); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowEmpty() throws Exception { + params.add(""); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowUnknown() throws Exception { + params.add("somethingelse"); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateWindowNotFirstParam() throws Exception { + params.add("somethingelse"); + params.add("60"); + func.evaluate(data_query, query_results, params); + } + + @Test + public void writeStringField() throws Exception { + params.add("1"); + assertEquals("movingAverage(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("movingAverage(null)", func.writeStringField(params, null)); + assertEquals("movingAverage()", func.writeStringField(params, "")); + assertEquals("movingAverage(inner_expression)", + func.writeStringField(null, "inner_expression")); + } + + @Test + public void parseParam() throws Exception { + // second + assertEquals(1000, func.parseParam("'1sec'")); + assertEquals(1000, func.parseParam("'1s'")); + assertEquals(5000, func.parseParam("'5sec'")); + assertEquals(5000, func.parseParam("'5s'")); + + // minute + assertEquals(60000, func.parseParam("'1min'")); + assertEquals(60000, func.parseParam("'1m'")); + assertEquals(300000, func.parseParam("'5min'")); + assertEquals(300000, func.parseParam("'5m'")); + + // hour + assertEquals(3600000, func.parseParam("'1hr")); + assertEquals(3600000, func.parseParam("'1h'")); + assertEquals(3600000, func.parseParam("'1hour'")); + assertEquals(18000000, func.parseParam("'5hr'")); + assertEquals(18000000, func.parseParam("'5h'")); + assertEquals(18000000, func.parseParam("'5hour'")); + + // day + assertEquals(86400000, func.parseParam("'1day'")); + assertEquals(86400000, func.parseParam("'1d'")); + assertEquals(432000000, func.parseParam("'5day'")); + assertEquals(432000000, func.parseParam("'5d'")); + + // TODO - fix it, closing with a 1 seems to work instead of a ' + assertEquals(1000, func.parseParam("'1sec1")); + + // missing quotes + try { + func.parseParam("'1sec"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + func.parseParam("1sec'"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + func.parseParam("1sec"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // no numbers or units + try { + func.parseParam("'sec'"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + func.parseParam("'60'"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // null or empty or short + try { + func.parseParam(null); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + func.parseParam(""); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + func.parseParam("'"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // floating point + try { + func.parseParam("'1.5sec'"); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } +} From 7828583568cc6bf85c5c19efd307b9588c5435d4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 16:07:12 -0700 Subject: [PATCH 264/826] Make the runDouble() and runLong() abstract methods public in the Aggregator class so we can implement it in other locations. Signed-off-by: Chris Larsen --- src/core/Aggregator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Aggregator.java b/src/core/Aggregator.java index baf1721b62..0b6a1a7818 100644 --- a/src/core/Aggregator.java +++ b/src/core/Aggregator.java @@ -92,14 +92,14 @@ public interface Doubles { * @param values The sequence to aggregate. * @return The aggregated value. */ - abstract long runLong(Longs values); + public abstract long runLong(Longs values); /** * Aggregates a sequence of {@code double}s. * @param values The sequence to aggregate. * @return The aggregated value. */ - abstract double runDouble(Doubles values); + public abstract double runDouble(Doubles values); /** * Returns the interpolation method to use when working with data points From 424d34b236445c2128f480065f2063628aac6cc1 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 16:14:55 -0700 Subject: [PATCH 265/826] Add the Expression interface used to implement Graphite expressions. Signed-off-by: Chris Larsen --- src/query/expression/Expression.java | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/query/expression/Expression.java diff --git a/src/query/expression/Expression.java b/src/query/expression/Expression.java new file mode 100644 index 0000000000..f8d799c86c --- /dev/null +++ b/src/query/expression/Expression.java @@ -0,0 +1,27 @@ +// 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.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSQuery; + +public interface Expression { + + public DataPoints[] evaluate(TSQuery data_query, + List results, List params); + + public String writeStringField(List params, String inner_expression); + +} From 256ba00ca9bde854ae4135c18d15acf57dc9b453 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 16:18:09 -0700 Subject: [PATCH 266/826] Add some notes to the Expression interface. Signed-off-by: Chris Larsen --- src/query/expression/Expression.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/query/expression/Expression.java b/src/query/expression/Expression.java index f8d799c86c..d7d15acc1d 100644 --- a/src/query/expression/Expression.java +++ b/src/query/expression/Expression.java @@ -17,11 +17,30 @@ import net.opentsdb.core.DataPoints; import net.opentsdb.core.TSQuery; +/** + * The interface for various expressions/functions used when querying OpenTSDB. + * @since 2.3 + */ public interface Expression { + /** + * Computes a set of results given the results of a {@link TSQuery} that may + * include multiple metrics and/or group by result sets. + * @param data_query The original query from the user + * @param results The results of the query + * @param params Parameters parsed from the expression endpoint related to + * the implementing function + * @return An array of data points resulting from the implementation + */ public DataPoints[] evaluate(TSQuery data_query, List results, List params); + /** + * TODO - document me! + * @param params + * @param inner_expression + * @return + */ public String writeStringField(List params, String inner_expression); } From a05de9afe87162568d2e269a7c7f610ded049905 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 20:15:20 -0700 Subject: [PATCH 267/826] Fix up the MovingAverage function to handle group bys and sub queries. Previously it tried to aggregate group bys which doesn't make sense for a moving average. Signed-off-by: Chris Larsen --- src/query/expression/MovingAverage.java | 49 ++++++------ test/query/expression/TestMovingAverage.java | 78 ++++++++++++++++++++ 2 files changed, 99 insertions(+), 28 deletions(-) diff --git a/src/query/expression/MovingAverage.java b/src/query/expression/MovingAverage.java index 8366a492d8..5a86b90c7c 100644 --- a/src/query/expression/MovingAverage.java +++ b/src/query/expression/MovingAverage.java @@ -54,7 +54,7 @@ public DataPoints[] evaluate(final TSQuery data_query, throw new IllegalArgumentException("Missing moving average window size"); } param = param.trim(); - + long condition = -1; boolean is_time_unit = false; if (param.matches("^[0-9]+$")) { @@ -103,35 +103,28 @@ public DataPoints[] evaluate(final TSQuery data_query, } } - final SeekableView[] views = new SeekableView[num_results]; - for (int i=0; i points = new ArrayList(); - while (view.hasNext()) { - DataPoint mdp = view.next(); - points.add(MutableDataPoint.ofDoubleValue(mdp.timestamp(), mdp.toDouble())); - } - - if (query_results.size() > 0 && query_results.get(0).length > 0) { - return new DataPoints[]{new PostAggregatedDataPoints(query_results.get(0)[0], - points.toArray(new DataPoint[points.size()]))}; - } else { - return new DataPoints[]{}; + final DataPoints[] results = new DataPoints[num_results]; + for (int i = 0; i < num_results; i++) { + final Aggregator moving_average = new MovingAverageAggregator( + Aggregators.Interpolation.LERP, "movingAverage", + condition, is_time_unit); + final SeekableView[] metrics_groups = new SeekableView[] { + post_agg_results[i].iterator() }; + final SeekableView view = new AggregationIterator(metrics_groups, + data_query.startTime(), data_query.endTime(), + moving_average, + Aggregators.Interpolation.LERP, false); + final List points = new ArrayList(); + while (view.hasNext()) { + final DataPoint mdp = view.next(); + points.add(MutableDataPoint.ofDoubleValue(mdp.timestamp(), mdp.toDouble())); + } + results[i] = new PostAggregatedDataPoints(post_agg_results[i], + points.toArray(new DataPoint[points.size()])); } + return results; } - + /** * Parses the parameter string to fetch the window size *

    diff --git a/test/query/expression/TestMovingAverage.java b/test/query/expression/TestMovingAverage.java index 3791c6f0f5..4a59cf38a1 100644 --- a/test/query/expression/TestMovingAverage.java +++ b/test/query/expression/TestMovingAverage.java @@ -275,6 +275,84 @@ public void evaluateWindow5min() throws Exception { } } + @Test + public void evaluateGroupBy() 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.metricName()).thenReturn("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 = 1; + 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 = 10; + 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 evaluateSubQuery() 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.metricName()).thenReturn("sys.mem"); + DataPoints[] group_bys2 = new DataPoints[] { dps2 }; + query_results.add(group_bys2); + + 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 = 1; + 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 = 10; + 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"); From 36f6bc7a7035961d0ea39f15030c2221a89d54e9 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 19:46:21 -0700 Subject: [PATCH 268/826] Add the HighestMax function for use in top-n queries to determine which time series had the highest maximum value. Signed-off-by: Chris Larsen --- src/query/expression/HighestMax.java | 239 +++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 src/query/expression/HighestMax.java diff --git a/src/query/expression/HighestMax.java b/src/query/expression/HighestMax.java new file mode 100644 index 0000000000..664ee8bbd3 --- /dev/null +++ b/src/query/expression/HighestMax.java @@ -0,0 +1,239 @@ +// 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.query.expression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import net.opentsdb.core.AggregationIterator; +import net.opentsdb.core.Aggregator; +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.Aggregators.Interpolation; + +public class HighestMax implements Expression { + + @Override + public DataPoints[] evaluate(TSQuery query, List queryResults, + List params) { + if (queryResults == null || queryResults.isEmpty()) { + throw new NullPointerException("Query results cannot be empty"); + } + + if (params == null || params.isEmpty()) { + throw new NullPointerException("Need aggregation window for moving average"); + } + + String param = params.get(0); + if (param == null || param.length() == 0) { + throw new NullPointerException("Invalid window='" + param + "'"); + } + + int k = Integer.parseInt(param.trim()); + + int size = 0; + for (DataPoints[] results: queryResults) { + size = size + results.length; + } + + PostAggregatedDataPoints[] seekablePoints = new PostAggregatedDataPoints[size]; + int ix=0; + for (DataPoints[] results: queryResults) { + for (DataPoints dpoints: results) { + List mutablePoints = new ArrayList(); + for (DataPoint point: dpoints) { + mutablePoints.add(point.isInteger() ? + MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) + : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); + } + seekablePoints[ix++] = new PostAggregatedDataPoints(dpoints, + mutablePoints.toArray(new DataPoint[mutablePoints.size()])); + } + } + + if (k >= size) { + return seekablePoints; + } + + SeekableView[] views = new SeekableView[size]; + for (int i=0; i() { + @Override + public int compare(Entry o1, Entry o2) { + // we want in descending order + return -1 * Double.compare(o1.val, o2.val); + } + }); + + DataPoints[] results = new DataPoints[k]; + for (int i=0; i queryParams, String innerExpression) { + return "highestMax(" + innerExpression + ")"; + } + + + static class MaxCacheAggregator extends Aggregator { + + private final int size; + private final long[] maxLongs; + private final double[] maxDoubles; + private boolean hasLongs = false; + private boolean hasDoubles = false; + + private long start; + private long end; + + public MaxCacheAggregator(Interpolation method, String name, int size, + long startTimeInMillis, long endTimeInMillis) { + super(method, name); + this.size = size; + this.start = startTimeInMillis; + this.end = endTimeInMillis; + + this.maxLongs = new long[size]; + this.maxDoubles = new double[size]; + + for (int i=0; i end) { + return 0; + } + } + + long[] longs = new long[size]; + int ix = 0; + longs[ix++] = values.nextLongValue(); + while (values.hasNextValue()) { + longs[ix++] = values.nextLongValue(); + } + + for (int i=0; i end) { + return 0; + } + } + + double[] doubles = new double[size]; + int ix = 0; + doubles[ix++] = values.nextDoubleValue(); + while (values.hasNextValue()) { + doubles[ix++] = values.nextDoubleValue(); + } + for (int i=0; i Date: Sat, 31 Oct 2015 21:00:17 -0700 Subject: [PATCH 269/826] Clean up the HighestMax expression function: - Fix a bug when the result set is smaller than the topn value - Comments and formatting Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/HighestMax.java | 240 +++++++++++------- test/query/expression/TestHighestMax.java | 288 ++++++++++++++++++++++ 3 files changed, 436 insertions(+), 94 deletions(-) create mode 100644 test/query/expression/TestHighestMax.java diff --git a/Makefile.am b/Makefile.am index 5f97368a75..df8051b7f2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ tsdb_SRC := \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ + src/query/expression/HighestMax.java \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ src/query/filter/TagVFilter.java \ @@ -220,6 +221,7 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/expression/TestHighestMax.java \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/filter/TestTagVFilter.java \ diff --git a/src/query/expression/HighestMax.java b/src/query/expression/HighestMax.java index 664ee8bbd3..5ba67cbcd9 100644 --- a/src/query/expression/HighestMax.java +++ b/src/query/expression/HighestMax.java @@ -27,151 +27,202 @@ import net.opentsdb.core.TSQuery; import net.opentsdb.core.Aggregators.Interpolation; +/** + * Implements top-n functionality by iterating over each of the time series, + * finding the max value for each time series within the query time range, + * and up to "n" time series with the highest values, sorted in descending + * order. + * @since 2.3 + */ public class HighestMax implements Expression { @Override - public DataPoints[] evaluate(TSQuery query, List queryResults, - List params) { - if (queryResults == null || queryResults.isEmpty()) { - throw new NullPointerException("Query results cannot be empty"); + 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[]{}; + } + // TODO(cl) - allow for empty top-n maybe? Just sort the results by max? if (params == null || params.isEmpty()) { - throw new NullPointerException("Need aggregation window for moving average"); + throw new IllegalArgumentException("Need aggregation window for moving average"); } String param = params.get(0); if (param == null || param.length() == 0) { - throw new NullPointerException("Invalid window='" + param + "'"); + throw new IllegalArgumentException("Missing top n value " + + "(number of series to return)"); } - int k = Integer.parseInt(param.trim()); + int topn = 0; + if (param.matches("^[0-9]+$")) { + try { + topn = Integer.parseInt(param); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException( + "Invalid parameter, must be an integer", nfe); + } + } else { + throw new IllegalArgumentException("Unparseable top n value: " + param); + } + if (topn < 1) { + throw new IllegalArgumentException("Top n value must be greater " + + "than zero: " + topn); + } - int size = 0; - for (DataPoints[] results: queryResults) { - size = size + results.length; + int num_results = 0; + for (DataPoints[] results: query_results) { + num_results += results.length; } - PostAggregatedDataPoints[] seekablePoints = new PostAggregatedDataPoints[size]; - int ix=0; - for (DataPoints[] results: queryResults) { - for (DataPoints dpoints: results) { - List mutablePoints = new ArrayList(); - for (DataPoint point: dpoints) { - mutablePoints.add(point.isInteger() ? - MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) - : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); + final PostAggregatedDataPoints[] post_agg_results = + new PostAggregatedDataPoints[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) { + // TODO(cl) - Avoid iterating and copying if we can help it. We should + // be able to pass the original DataPoints object to the seekable view + // and then iterate through it. + final List mutable_points = new ArrayList(); + for (final DataPoint point : dps) { + mutable_points.add(point.isInteger() ? + MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) + : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); } - seekablePoints[ix++] = new PostAggregatedDataPoints(dpoints, - mutablePoints.toArray(new DataPoint[mutablePoints.size()])); + post_agg_results[ix++] = new PostAggregatedDataPoints(dps, + mutable_points.toArray(new DataPoint[mutable_points.size()])); } } - - if (k >= size) { - return seekablePoints; - } - - SeekableView[] views = new SeekableView[size]; - for (int i=0; i() { + Arrays.sort(max_by_ts, new Comparator() { @Override - public int compare(Entry o1, Entry o2) { + public int compare(TopNSortingEntry o1, TopNSortingEntry o2) { // we want in descending order return -1 * Double.compare(o1.val, o2.val); } }); - DataPoints[] results = new DataPoints[k]; - for (int i=0; i queryParams, String innerExpression) { - return "highestMax(" + innerExpression + ")"; + public String writeStringField(final List query_params, + final String inner_expression) { + return "highestMax(" + inner_expression + ")"; } - + /** + * Aggregator that stores the overall maximum value for the entire series + */ static class MaxCacheAggregator extends Aggregator { - - private final int size; - private final long[] maxLongs; - private final double[] maxDoubles; - private boolean hasLongs = false; - private boolean hasDoubles = false; - + /** The total number of series in the result set, including sub queries and + * group bys */ + private final int total_series; + /** An array of maximum integers by time series */ + private final long[] max_longs; + /** An array of maximum doubles by time series */ + private final double[] max_doubles; + /** Whether or not any of the series contain integers */ + private boolean has_longs = false; + /** Whether or not any of the series contain doubles */ + private boolean has_doubles = false; + /** Query start time in milliseconds for filtering */ private long start; + /** Query end time in milliseconds for filtering */ private long end; - public MaxCacheAggregator(Interpolation method, String name, int size, - long startTimeInMillis, long endTimeInMillis) { + /** + * An aggregator that keeps track of the maximum values for each time series + * in the result set. + * @param method The interpolation method (not used) + * @param name The name of the aggregator + * @param total_series The total number of series in the result set, + * including sub queries and group bys + * @param start Query start time in milliseconds for filtering + * @param end Query end time in milliseconds for filtering + */ + public MaxCacheAggregator(final Interpolation method, final String name, + final int total_series, final long start, final long end) { super(method, name); - this.size = size; - this.start = startTimeInMillis; - this.end = endTimeInMillis; - - this.maxLongs = new long[size]; - this.maxDoubles = new double[size]; - - for (int i=0; i. +package net.opentsdb.query.expression; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestHighestMax { + + 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 HighestMax 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.metricName()).thenReturn(METRIC); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new HighestMax(); + } + + @Test + public void evaluateTopN1with2SeriesLong() 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.metricName()).thenReturn("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(1, results.length); + assertEquals("sys.mem", results[0].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN2with2SeriesLong() throws Exception { + params.add("2"); + 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.metricName()).thenReturn("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("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN100with2SeriesLong() throws Exception { + params.add("100"); + 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.metricName()).thenReturn("sys.mem"); + DataPoints[] group_bys2 = new DataPoints[] { dps2 }; + query_results.add(group_bys2); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN100with2SubQuerySeriesLong() throws Exception { + params.add("100"); + 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.metricName()).thenReturn("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("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + 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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 (expected = IllegalArgumentException.class) + public void evaluateEmptyParams() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnNull() throws Exception { + params.add(null); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnEmpty() throws Exception { + params.add(""); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnZero() throws Exception { + params.add("0"); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnNotaNumber() throws Exception { + params.add("not a number"); + func.evaluate(data_query, query_results, params); + } + + @Test + public void writeStringField() throws Exception { + params.add("1"); + assertEquals("highestMax(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("highestMax(null)", func.writeStringField(params, null)); + assertEquals("highestMax()", func.writeStringField(params, "")); + assertEquals("highestMax(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From 365cbb162621fd258214d97982d06b22e6ff4e1a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 21:43:57 -0700 Subject: [PATCH 270/826] Add another override to the SeekableViewsForTest to let it emit values as integers when the values would be whole numbers, otherwise doubles. This is useful for testing the aggregation iterators when we are dealing with series containing both floats and ints. Signed-off-by: Chris Larsen --- test/core/SeekableViewsForTest.java | 75 ++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/test/core/SeekableViewsForTest.java b/test/core/SeekableViewsForTest.java index e53dc27b15..4b638e5ac6 100644 --- a/test/core/SeekableViewsForTest.java +++ b/test/core/SeekableViewsForTest.java @@ -64,8 +64,32 @@ public static SeekableView generator(final long start_time, final boolean is_integer, final double starting_value, final double increment) { + return generator(start_time, sample_period, num_data_points, + is_integer, starting_value, increment, false); + } + + /** + * Creates a {@link SeekableView} that generates a sequence of data points. + * @param start_time Starting timestamp + * @param sample_period Average sample period of data points + * @param num_data_points Total number of data points to generate + * @param is_integer True to generate a sequence of integer data points. + * @param starting_value The starting data point value. + * @param increment How much to increment the values each iteration. + * @param wholes_as_integer Whether or not to return whole numbers (1.0, 2.0, + * etc) as integers to test for functions that should support both. + * Note: Ignored if is_integer is true. + * @return A {@link SeekableView} object + */ + public static SeekableView generator(final long start_time, + final long sample_period, + final int num_data_points, + final boolean is_integer, + final double starting_value, + final double increment, + final boolean wholes_as_integer) { return new DataPointGenerator(start_time, sample_period, num_data_points, - is_integer, starting_value, increment); + is_integer, starting_value, increment, wholes_as_integer); } /** Iterates an array of data points. */ @@ -115,19 +139,28 @@ private static class DataPointGenerator implements SeekableView { private final double increment; private final MutableDataPoint current_data = new MutableDataPoint(); private final MutableDataPoint next_data = new MutableDataPoint(); + private final boolean wholes_as_integer; private int dps_emitted = 0; DataPointGenerator(final long start_time_ms, final long sample_period_ms, final int num_data_points, final boolean is_integer, - final double starting_value, final double increment) { + final double starting_value, final double increment, + final boolean wholes_as_integer) { this.sample_period_ms = sample_period_ms; this.num_data_points = num_data_points; this.is_integer = is_integer; this.increment = increment; + this.wholes_as_integer = wholes_as_integer; if (is_integer) { next_data.reset(start_time_ms, (long)starting_value); } else { - next_data.reset(start_time_ms, starting_value); + if (wholes_as_integer && + (starting_value == Math.floor(starting_value)) && + !Double.isInfinite(starting_value)) { + next_data.reset(start_time_ms, (long)starting_value); + } else { + next_data.reset(start_time_ms, starting_value); + } } } @@ -163,13 +196,20 @@ private void advance() { next_data.reset(next_data.timestamp() + sample_period_ms, next_data.longValue() + (long)increment); } else { - next_data.reset(next_data.timestamp() + sample_period_ms, - next_data.doubleValue() + increment); + final double next = next_data.toDouble() + increment; + if (wholes_as_integer && + (next == Math.floor(next)) && !Double.isInfinite(next)) { + next_data.reset(next_data.timestamp() + sample_period_ms, (long)next); + } else { + next_data.reset(next_data.timestamp() + sample_period_ms, next); + } } dps_emitted++; } + + } - + @Test public void testDataPointGenerator() { SeekableView dpg = generator(100000, 10000, 5, true); @@ -264,4 +304,27 @@ public void testDataPointGenerator_seekToSecond() { } assertFalse(dpg.hasNext()); } + + @Test + public void testDataPointGeneratorWholes() { + SeekableView dpg = generator(100000, 10000, 5, false, 0, 1.5, true); + DataPoint[] expected_data_points = new DataPoint[] { + MutableDataPoint.ofLongValue(100000, 0), + MutableDataPoint.ofDoubleValue(110000, 1.5), + MutableDataPoint.ofLongValue(120000, 3), + MutableDataPoint.ofDoubleValue(130000, 4.5), + MutableDataPoint.ofLongValue(140000, 6), + }; + for (DataPoint expected: expected_data_points) { + assertTrue(dpg.hasNext()); + DataPoint dp = dpg.next(); + assertEquals(expected.timestamp(), dp.timestamp()); + if (expected.isInteger()) { + assertEquals(expected.longValue(), dp.longValue()); + } else { + assertEquals(expected.doubleValue(), dp.doubleValue(), 0.001); + } + } + assertFalse(dpg.hasNext()); + } } From ff60a5b58a2ede4d74b4c1ee920ecb6ae874b02d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 22:21:22 -0700 Subject: [PATCH 271/826] Change the HighestMax.TopNSortingEntry class to implement a comparator so that it can be re-used a little more. Also add doubles and mixed int/double unit tests to HighestMax Signed-off-by: Chris Larsen --- src/query/expression/HighestMax.java | 20 +++--- test/query/expression/TestHighestMax.java | 75 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/query/expression/HighestMax.java b/src/query/expression/HighestMax.java index 5ba67cbcd9..73029b23f4 100644 --- a/src/query/expression/HighestMax.java +++ b/src/query/expression/HighestMax.java @@ -136,13 +136,7 @@ public DataPoints[] evaluate(final TSQuery data_query, } } - Arrays.sort(max_by_ts, new Comparator() { - @Override - public int compare(TopNSortingEntry o1, TopNSortingEntry o2) { - // we want in descending order - return -1 * Double.compare(o1.val, o2.val); - } - }); + Arrays.sort(max_by_ts); final int result_count = Math.min(topn, num_results); final DataPoints[] results = new DataPoints[result_count]; @@ -154,19 +148,26 @@ public int compare(TopNSortingEntry o1, TopNSortingEntry o2) { } /** - * Helper class for sorting the series + * Helper class for sorting the series. It will sort from highest to lowest. */ - static class TopNSortingEntry { + static class TopNSortingEntry implements Comparable { final double val; final int pos; + public TopNSortingEntry(final double val, final int pos) { this.val = val; this.pos = pos; } + @Override public String toString() { return "{" + val + "," + pos + "}"; } + + @Override + public int compareTo(final TopNSortingEntry o) { + return -1 * Double.compare(val, o.val); + } } @Override @@ -264,6 +265,7 @@ public double runDouble(Doubles values) { doubles[ix++] = values.nextDoubleValue(); } for (int i = 0; i < total_series;i++) { + // TODO(cl) - Properly handle NaNs here max_doubles[i] = Math.max(max_doubles[i], doubles[i]); } diff --git a/test/query/expression/TestHighestMax.java b/test/query/expression/TestHighestMax.java index 52f1e405b6..2426204bc5 100644 --- a/test/query/expression/TestHighestMax.java +++ b/test/query/expression/TestHighestMax.java @@ -13,6 +13,7 @@ package net.opentsdb.query.expression; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -220,6 +221,80 @@ public void evaluateTopN100with2SubQuerySeriesLong() throws Exception { } } + @Test + public void evaluateTopN2with2SeriesDouble() throws Exception { + params.add("2"); + 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.metricName()).thenReturn("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("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + double v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v += 1.5; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.toDouble(), 0.001); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN1with2SeriesLongDoubleMixed() throws Exception { + params.add("1"); + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, false, 10, 1.5, true); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricName()).thenReturn("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(1, results.length); + assertEquals("sys.mem", results[0].metricName()); + + long ts = START_TIME; + double v = 10; + boolean toggle = true; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + if (toggle) { + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue(), 0.001); + } else { + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + } + toggle = !toggle; + ts += INTERVAL; + v += 1.5; + } + } + @Test (expected = IllegalArgumentException.class) public void evaluateNullQuery() throws Exception { params.add("1"); From 9015ad3b70abecfaa54f24d9d98719467e07b20a Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sat, 31 Oct 2015 21:07:00 -0700 Subject: [PATCH 272/826] Add the HighestCurrent expression function to return the top n series with the maximum current value, sorted by value. Signed-off-by: Chris Larsen --- src/query/expression/HighestCurrent.java | 240 +++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 src/query/expression/HighestCurrent.java diff --git a/src/query/expression/HighestCurrent.java b/src/query/expression/HighestCurrent.java new file mode 100644 index 0000000000..aa19df885f --- /dev/null +++ b/src/query/expression/HighestCurrent.java @@ -0,0 +1,240 @@ +// 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.Arrays; +import java.util.Comparator; +import java.util.List; + +import net.opentsdb.core.AggregationIterator; +import net.opentsdb.core.Aggregator; +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.Aggregators.Interpolation; + +public class HighestCurrent implements Expression { + + @Override + public DataPoints[] evaluate(TSQuery data_query, List queryResults, + List params) { + if (queryResults == null || queryResults.isEmpty()) { + throw new NullPointerException("Query results cannot be empty"); + } + + if (params == null || params.isEmpty()) { + throw new NullPointerException("Need aggregation window for moving average"); + } + + String param = params.get(0); + if (param == null || param.length() == 0) { + throw new NullPointerException("Invalid window='" + param + "'"); + } + + int k = Integer.parseInt(param.trim()); + + int size = 0; + for (DataPoints[] results: queryResults) { + size = size + results.length; + } + + PostAggregatedDataPoints[] seekablePoints = new PostAggregatedDataPoints[size]; + int ix=0; + for (DataPoints[] results: queryResults) { + for (DataPoints dpoints: results) { + List mutablePoints = new ArrayList(); + for (DataPoint point: dpoints) { + mutablePoints.add(point.isInteger() ? + MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) + : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); + } + seekablePoints[ix++] = new PostAggregatedDataPoints(dpoints, + mutablePoints.toArray(new DataPoint[mutablePoints.size()])); + } + } + + if (k >= size) { + return seekablePoints; + } + + SeekableView[] views = new SeekableView[size]; + for (int i=0; i() { + @Override + public int compare(Entry o1, Entry o2) { + return -1 * Double.compare(o1.val, o2.val); + } + }); + + DataPoints[] results = new DataPoints[k]; + for (int i=0; i queryParams, String innerExpression) { + return "highestCurrent(" + innerExpression + ")"; + } + + + public static class MaxLatestAggregator extends Aggregator { + private final int size; + private final long[] maxLongs; + private final double[] maxDoubles; + private final long start; + private final long end; + private boolean hasLongs = false; + private boolean hasDoubles = false; + private long latestTS = -1; + + public MaxLatestAggregator(Interpolation method, String name, int size, + long startTimeInMillis, long endTimeInMillis) { + super(method, name); + this.size = size; + this.start = startTimeInMillis; + this.end = endTimeInMillis; + + + this.maxLongs = new long[size]; + this.maxDoubles = new double[size]; + + for (int i=0; i end) { + return 0; + } + } + + long[] longs = new long[size]; + int ix = 0; + longs[ix++] = values.nextLongValue(); + while (values.hasNextValue()) { + longs[ix++] = values.nextLongValue(); + } + + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + if (ts > latestTS) { + System.arraycopy(longs, 0, maxLongs, 0, size); + } + } + + hasLongs = true; + return 0; + } + + @Override + public double runDouble(Doubles values) { + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + //data point falls outside required range + if (ts < start || ts > end) { + return 0; + } + } + + double[] doubles = new double[size]; + int ix = 0; + doubles[ix++] = values.nextDoubleValue(); + while (values.hasNextValue()) { + doubles[ix++] = values.nextDoubleValue(); + } + + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + if (ts > latestTS) { + System.arraycopy(doubles, 0, maxDoubles, 0, size); + } + } + + hasDoubles = true; + return 0; + } + + public long[] getLongMaxes() { + return maxLongs; + } + + public double[] getDoubleMaxes() { + return maxDoubles; + } + + public boolean hasLongs() { + return hasLongs; + } + + public boolean hasDoubles() { + return hasDoubles; + } + + } +} From 9b0d86baf448b1a81a318d347b8d8c0f0f4586ab Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 31 Oct 2015 22:22:47 -0700 Subject: [PATCH 273/826] Cleanup the HighestCurrent class with code formatting and comments. Add a unit test for HighestCurrent. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/HighestCurrent.java | 249 ++++++----- test/query/expression/TestHighestCurrent.java | 393 ++++++++++++++++++ 3 files changed, 539 insertions(+), 105 deletions(-) create mode 100644 test/query/expression/TestHighestCurrent.java diff --git a/Makefile.am b/Makefile.am index df8051b7f2..05cd94b15a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ tsdb_SRC := \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ + src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ @@ -221,6 +222,7 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ diff --git a/src/query/expression/HighestCurrent.java b/src/query/expression/HighestCurrent.java index aa19df885f..11d9f04f82 100644 --- a/src/query/expression/HighestCurrent.java +++ b/src/query/expression/HighestCurrent.java @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.List; import net.opentsdb.core.AggregationIterator; @@ -26,146 +25,184 @@ import net.opentsdb.core.SeekableView; import net.opentsdb.core.TSQuery; import net.opentsdb.core.Aggregators.Interpolation; - +import net.opentsdb.query.expression.HighestMax.TopNSortingEntry; + +/** + * Implements top-n functionality by iterating over each of the time series, + * sorting and returning the top "n" time series with the highest current (or + * latest) value. + * @since 2.3 + */ public class HighestCurrent implements Expression { @Override - public DataPoints[] evaluate(TSQuery data_query, List queryResults, - List params) { - if (queryResults == null || queryResults.isEmpty()) { - throw new NullPointerException("Query results cannot be empty"); + 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[]{}; + } + // TODO(cl) - allow for empty top-n maybe? Just sort the results by max? if (params == null || params.isEmpty()) { - throw new NullPointerException("Need aggregation window for moving average"); + throw new IllegalArgumentException("Need aggregation window for moving average"); } String param = params.get(0); if (param == null || param.length() == 0) { - throw new NullPointerException("Invalid window='" + param + "'"); + throw new IllegalArgumentException("Missing top n value " + + "(number of series to return)"); } - int k = Integer.parseInt(param.trim()); + int topn = 0; + if (param.matches("^[0-9]+$")) { + try { + topn = Integer.parseInt(param); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException( + "Invalid parameter, must be an integer", nfe); + } + } else { + throw new IllegalArgumentException("Unparseable top n value: " + param); + } + if (topn < 1) { + throw new IllegalArgumentException("Top n value must be greater " + + "than zero: " + topn); + } - int size = 0; - for (DataPoints[] results: queryResults) { - size = size + results.length; + int num_results = 0; + for (DataPoints[] results: query_results) { + num_results += results.length; } - PostAggregatedDataPoints[] seekablePoints = new PostAggregatedDataPoints[size]; - int ix=0; - for (DataPoints[] results: queryResults) { - for (DataPoints dpoints: results) { - List mutablePoints = new ArrayList(); - for (DataPoint point: dpoints) { - mutablePoints.add(point.isInteger() ? - MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) - : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); + final PostAggregatedDataPoints[] post_agg_results = + new PostAggregatedDataPoints[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) { + // TODO(cl) - Avoid iterating and copying if we can help it. We should + // be able to pass the original DataPoints object to the seekable view + // and then iterate through it. + final List mutable_points = new ArrayList(); + for (final DataPoint point : dps) { + mutable_points.add(point.isInteger() ? + MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) + : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); } - seekablePoints[ix++] = new PostAggregatedDataPoints(dpoints, - mutablePoints.toArray(new DataPoint[mutablePoints.size()])); + post_agg_results[ix++] = new PostAggregatedDataPoints(dps, + mutable_points.toArray(new DataPoint[mutable_points.size()])); } } - - if (k >= size) { - return seekablePoints; - } - - SeekableView[] views = new SeekableView[size]; - for (int i=0; i() { - @Override - public int compare(Entry o1, Entry o2) { - return -1 * Double.compare(o1.val, o2.val); - } - }); + Arrays.sort(max_by_ts); - DataPoints[] results = new DataPoints[k]; - for (int i=0; i queryParams, String innerExpression) { - return "highestCurrent(" + innerExpression + ")"; + public String writeStringField(final List query_params, + final String inner_expression) { + return "highestCurrent(" + inner_expression + ")"; } - + /** + * Aggregator that stores only the latest value for each series so that they + * can be sorted on it + */ public static class MaxLatestAggregator extends Aggregator { - private final int size; - private final long[] maxLongs; - private final double[] maxDoubles; - private final long start; - private final long end; - private boolean hasLongs = false; - private boolean hasDoubles = false; - private long latestTS = -1; - - public MaxLatestAggregator(Interpolation method, String name, int size, - long startTimeInMillis, long endTimeInMillis) { + /** The total number of series in the result set, including sub queries and + * group bys */ + private final int total_series; + /** An array of maximum integers by time series */ + private final long[] max_longs; + /** An array of maximum doubles by time series */ + private final double[] max_doubles; + /** Whether or not any of the series contain integers */ + private boolean has_longs = false; + /** Whether or not any of the series contain doubles */ + private boolean has_doubles = false; + /** Query start time in milliseconds for filtering */ + private long start; + /** Query end time in milliseconds for filtering */ + private long end; + /** The most recent timestamp in the different series */ + private long latest_ts = -1; + + /** + * An aggregator that keeps track of the maximum latest value for each series + * @param method The interpolation method (not used) + * @param name The name of the aggregator + * @param total_series The total number of series in the result set, + * including sub queries and group bys + * @param start Query start time in milliseconds for filtering + * @param end Query end time in milliseconds for filtering + */ + public MaxLatestAggregator(final Interpolation method, final String name, + final int total_series, final long start, final long end) { super(method, name); - this.size = size; - this.start = startTimeInMillis; - this.end = endTimeInMillis; - - - this.maxLongs = new long[size]; - this.maxDoubles = new double[size]; - - for (int i=0; i latestTS) { - System.arraycopy(longs, 0, maxLongs, 0, size); + final long ts = ((DataPoint) values).timestamp(); + if (ts > latest_ts) { + System.arraycopy(longs, 0, max_longs, 0, total_series); } } - hasLongs = true; + has_longs = true; return 0; } @Override public double runDouble(Doubles values) { + // TODO(cl) - Can we get anything other than a DataPoint? if (values instanceof DataPoint) { long ts = ((DataPoint) values).timestamp(); //data point falls outside required range @@ -202,7 +240,8 @@ public double runDouble(Doubles values) { } } - double[] doubles = new double[size]; + // TODO(cl) - Properly handle NaNs here + final double[] doubles = new double[total_series]; int ix = 0; doubles[ix++] = values.nextDoubleValue(); while (values.hasNextValue()) { @@ -210,30 +249,30 @@ public double runDouble(Doubles values) { } if (values instanceof DataPoint) { - long ts = ((DataPoint) values).timestamp(); - if (ts > latestTS) { - System.arraycopy(doubles, 0, maxDoubles, 0, size); + final long ts = ((DataPoint) values).timestamp(); + if (ts > latest_ts) { + System.arraycopy(doubles, 0, max_doubles, 0, total_series); } } - hasDoubles = true; + has_doubles = true; return 0; } public long[] getLongMaxes() { - return maxLongs; + return max_longs; } public double[] getDoubleMaxes() { - return maxDoubles; + return max_doubles; } public boolean hasLongs() { - return hasLongs; + return has_longs; } public boolean hasDoubles() { - return hasDoubles; + return has_doubles; } } diff --git a/test/query/expression/TestHighestCurrent.java b/test/query/expression/TestHighestCurrent.java new file mode 100644 index 0000000000..145c81f41d --- /dev/null +++ b/test/query/expression/TestHighestCurrent.java @@ -0,0 +1,393 @@ +// 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.assertTrue; +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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestHighestCurrent { + + 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 HighestCurrent 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.metricName()).thenReturn(METRIC); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new HighestCurrent(); + } + + @Test + public void evaluateTopN1with2SeriesLong() 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.metricName()).thenReturn("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(1, results.length); + assertEquals("sys.mem", results[0].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN2with2SeriesLong() throws Exception { + params.add("2"); + 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.metricName()).thenReturn("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("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN100with2SeriesLong() throws Exception { + params.add("100"); + 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.metricName()).thenReturn("sys.mem"); + DataPoints[] group_bys2 = new DataPoints[] { dps2 }; + query_results.add(group_bys2); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN100with2SubQuerySeriesLong() throws Exception { + params.add("100"); + 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.metricName()).thenReturn("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("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + long v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN2with2SeriesDouble() throws Exception { + params.add("2"); + 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.metricName()).thenReturn("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("sys.mem", results[0].metricName()); + assertEquals(METRIC, results[1].metricName()); + + long ts = START_TIME; + double v = 10; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v += 1.5; + } + + ts = START_TIME; + v = 1; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.toDouble(), 0.001); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateTopN1with2SeriesLongDoubleMixed() throws Exception { + params.add("1"); + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + NUM_POINTS, false, 10, 1.5, true); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricName()).thenReturn("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(1, results.length); + assertEquals("sys.mem", results[0].metricName()); + + long ts = START_TIME; + double v = 10; + boolean toggle = true; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + if (toggle) { + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue(), 0.001); + } else { + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + } + toggle = !toggle; + ts += INTERVAL; + v += 1.5; + } + } + + @Test + public void evaluateTopN1with2SeriesDiffSpan() throws Exception { + params.add("1"); + // in this case one series ends earlier than the other so it's removed + // even though it's last value was greater than the winners. + SeekableView view2 = SeekableViewsForTest.generator(START_TIME, INTERVAL, + 3, true, 10, 1); + DataPoints dps2 = PowerMockito.mock(DataPoints.class); + when(dps2.iterator()).thenReturn(view2); + when(dps2.metricName()).thenReturn("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(1, results.length); + assertEquals(METRIC, results[0].metricName()); + + long ts = START_TIME; + long v = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + 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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 (expected = IllegalArgumentException.class) + public void evaluateEmptyParams() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnNull() throws Exception { + params.add(null); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnEmpty() throws Exception { + params.add(""); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnZero() throws Exception { + params.add("0"); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateTopnNotaNumber() throws Exception { + params.add("not a number"); + func.evaluate(data_query, query_results, params); + } + + @Test + public void writeStringField() throws Exception { + params.add("1"); + assertEquals("highestCurrent(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("highestCurrent(null)", func.writeStringField(params, null)); + assertEquals("highestCurrent()", func.writeStringField(params, "")); + assertEquals("highestCurrent(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From a8237109c9551b5c13ec1ebd80ba34b90d539db3 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sun, 1 Nov 2015 09:08:28 -0800 Subject: [PATCH 274/826] Add the Scale function for multiplying each series by some factor. Signed-off-by: Chris Larsen --- src/query/expression/Scale.java | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/query/expression/Scale.java diff --git a/src/query/expression/Scale.java b/src/query/expression/Scale.java new file mode 100644 index 0000000000..ee7ecd6420 --- /dev/null +++ b/src/query/expression/Scale.java @@ -0,0 +1,72 @@ +// 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.List; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSQuery; + +public class Scale implements Expression { + @Override + public DataPoints[] evaluate(TSQuery data_query, List queryResults, List params) { + if (queryResults == null || queryResults.isEmpty()) { + throw new NullPointerException("Query results cannot be empty"); + } + + if (params == null || params.isEmpty()) { + throw new NullPointerException("Scaling parameter not available"); + } + + String factor = params.get(0); + factor = factor.replaceAll("'|\"", "").trim(); + double scaleFactor = Double.parseDouble(factor); + + DataPoints[] inputPoints = queryResults.get(0); + DataPoints[] outputPoints = new DataPoints[inputPoints.length]; + + for (int i=0; i queryParams, String innerExpression) { + return "scale(" + innerExpression + ")"; + } + +} From 4b7ea0f11fa5490934fafbbd9f774ef7fcf8c441 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 Nov 2015 09:39:55 -0800 Subject: [PATCH 275/826] Fix up the Scale expression: - Apply scale to all sub queries instead of only the first one - Fix the .size() issue in the data points array sizing - Cleanup, formatting and unit tests Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/Scale.java | 100 ++++--- test/query/expression/TestScale.java | 402 +++++++++++++++++++++++++++ 3 files changed, 473 insertions(+), 31 deletions(-) create mode 100644 test/query/expression/TestScale.java diff --git a/Makefile.am b/Makefile.am index 05cd94b15a..f04c683ddd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,6 +78,7 @@ tsdb_SRC := \ src/query/expression/HighestMax.java \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ + src/query/expression/Scale.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ @@ -226,6 +227,7 @@ test_SRC := \ test/query/expression/TestHighestMax.java \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ + test/query/expression/TestScale.java \ test/query/filter/TestTagVFilter.java \ test/query/filter/TestTagVLiteralOrFilter.java \ test/query/filter/TestTagVNotKeyFilter.java \ diff --git a/src/query/expression/Scale.java b/src/query/expression/Scale.java index ee7ecd6420..2a7bf3649f 100644 --- a/src/query/expression/Scale.java +++ b/src/query/expression/Scale.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.query.expression; +import java.util.ArrayList; import java.util.List; import net.opentsdb.core.DataPoint; @@ -20,53 +21,90 @@ import net.opentsdb.core.SeekableView; import net.opentsdb.core.TSQuery; +/** + * Multiplies each data point in the series by the given factor. + * @since 2.3 + */ public class Scale implements Expression { + @Override - public DataPoints[] evaluate(TSQuery data_query, List queryResults, List params) { - if (queryResults == null || queryResults.isEmpty()) { - throw new NullPointerException("Query results cannot be empty"); + 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[]{}; } - if (params == null || params.isEmpty()) { - throw new NullPointerException("Scaling parameter not available"); + throw new IllegalArgumentException("Missing scaling factor"); } - String factor = params.get(0); - factor = factor.replaceAll("'|\"", "").trim(); - double scaleFactor = Double.parseDouble(factor); - - DataPoints[] inputPoints = queryResults.get(0); - DataPoints[] outputPoints = new DataPoints[inputPoints.length]; - - for (int i=0; i dps = new ArrayList(); + final boolean scale_is_int = (scale_factor == Math.floor(scale_factor)) && + !Double.isInfinite(scale_factor); + final SeekableView view = points.iterator(); while (view.hasNext()) { DataPoint pt = view.next(); - if (pt.isInteger()) { - dps[i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), scaleFactor * pt.longValue()); + if (pt.isInteger() && scale_is_int) { + dps.add(MutableDataPoint.ofLongValue(pt.timestamp(), + (long)scale_factor * pt.longValue())); } else { - dps[i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), scaleFactor * pt.doubleValue()); + // NaNs are fine here, they'll just be re-computed as NaN + dps.add(MutableDataPoint.ofDoubleValue(pt.timestamp(), + scale_factor * pt.toDouble())); } - i++; } - - return new PostAggregatedDataPoints(points, dps); + final DataPoint[] results = new DataPoint[dps.size()]; + dps.toArray(results); + return new PostAggregatedDataPoints(points, results); } @Override - public String writeStringField(List queryParams, String innerExpression) { - return "scale(" + innerExpression + ")"; + public String writeStringField(final List query_params, + final String inner_expression) { + return "scale(" + inner_expression + ")"; } } diff --git a/test/query/expression/TestScale.java b/test/query/expression/TestScale.java new file mode 100644 index 0000000000..f7f35dd687 --- /dev/null +++ b/test/query/expression/TestScale.java @@ -0,0 +1,402 @@ +// 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.assertTrue; +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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestScale { + + 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 Scale 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.metricName()).thenReturn(METRIC); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new Scale(); + } + + @Test + public void evaluateFactor1GroupByLong() 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateFactor1GroupByDouble() throws Exception { + params.add("1"); + 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals((long)v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + 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 evaluateFactor1point5GroupBy() throws Exception { + params.add("1.5"); + 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.metricName()).thenReturn("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 = 1.5; + for (DataPoint dp : results[0]) { + System.out.println(dp.timestamp() + " : " + dp.toDouble()); + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(v, dp.doubleValue(), 0.001); + ts += INTERVAL; + v += 1.5; + } + ts = START_TIME; + v = 15; + 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 evaluateFactor1024GroupBy() throws Exception { + params.add("1024"); + 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.metricName()).thenReturn("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 = 1024; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1024; + } + ts = START_TIME; + v = 10240; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1024; + } + } + + @Test + public void evaluateFactor1SubQuerySeries() 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateFactor0GroupByLong() throws Exception { + params.add("0"); + 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.metricName()).thenReturn("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; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(0, dp.longValue()); + ts += INTERVAL; + } + ts = START_TIME; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(0, dp.longValue()); + ts += INTERVAL; + } + } + + @Test + public void evaluateFactorNegative1GroupByLong() 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.metricName()).thenReturn("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 = -1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v -= 1; + } + ts = START_TIME; + v = -10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + 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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 (expected = IllegalArgumentException.class) + public void evaluateEmptyParams() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateScaleNull() throws Exception { + params.add(null); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateScaleEmpty() throws Exception { + params.add(""); + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateScaleNotaNumber() throws Exception { + params.add("not a number"); + func.evaluate(data_query, query_results, params); + } + + @Test + public void writeStringField() throws Exception { + params.add("1"); + assertEquals("scale(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("scale(null)", func.writeStringField(params, null)); + assertEquals("scale()", func.writeStringField(params, "")); + assertEquals("scale(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From 2f45ca75a2b02a84e263435460a56e5009c1e579 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sun, 1 Nov 2015 09:44:27 -0800 Subject: [PATCH 276/826] Add the Absolute expression function for calculating absolute value on all data points in a series. Signed-off-by: Chris Larsen --- src/query/expression/Absolute.java | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/query/expression/Absolute.java diff --git a/src/query/expression/Absolute.java b/src/query/expression/Absolute.java new file mode 100644 index 0000000000..3f9e406ecc --- /dev/null +++ b/src/query/expression/Absolute.java @@ -0,0 +1,65 @@ +// 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.List; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSQuery; + +public class Absolute implements Expression { + + @Override + public DataPoints[] evaluate(TSQuery data_query, List queryResults, List params) { + if (queryResults == null || queryResults.isEmpty()) { + throw new NullPointerException("Query results cannot be empty"); + } + + DataPoints[] inputPoints = queryResults.get(0); + DataPoints[] outputPoints = new DataPoints[inputPoints.length]; + + for (int i=0; i queryParams, String innerExpression) { + return "absolute(" + innerExpression + ")"; + } + +} From ac70a06ba188343fd6d4c8b4767967c3f1192814 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 Nov 2015 09:56:26 -0800 Subject: [PATCH 277/826] Fixup the Absolute expression: - Enable it to handle all sub query results too - Fix the .size() issue in the data points array sizing - Fix the Integer to double conversion that shouldn't have been - Cleanup, formatting and unit tests Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/Absolute.java | 66 +++-- test/query/expression/TestAbsolute.java | 307 ++++++++++++++++++++++++ 3 files changed, 354 insertions(+), 21 deletions(-) create mode 100644 test/query/expression/TestAbsolute.java diff --git a/Makefile.am b/Makefile.am index f04c683ddd..de306c4076 100644 --- a/Makefile.am +++ b/Makefile.am @@ -74,6 +74,7 @@ tsdb_SRC := \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ + src/query/expression/Absolute.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ src/query/expression/MovingAverage.java \ @@ -223,6 +224,7 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/expression/TestAbsolute.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ test/query/expression/TestMovingAverage.java \ diff --git a/src/query/expression/Absolute.java b/src/query/expression/Absolute.java index 3f9e406ecc..2b3cfb13f1 100644 --- a/src/query/expression/Absolute.java +++ b/src/query/expression/Absolute.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.query.expression; +import java.util.ArrayList; import java.util.List; import net.opentsdb.core.DataPoint; @@ -20,41 +21,64 @@ import net.opentsdb.core.SeekableView; import net.opentsdb.core.TSQuery; +/** + * Modifies each data point in the series with the absolute value, tossing away + * the signed component. + * @since 2.3 + */ public class Absolute implements Expression { @Override - public DataPoints[] evaluate(TSQuery data_query, List queryResults, List params) { - if (queryResults == null || queryResults.isEmpty()) { - throw new NullPointerException("Query results cannot be empty"); + 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"); } - - DataPoints[] inputPoints = queryResults.get(0); - DataPoints[] outputPoints = new DataPoints[inputPoints.length]; - - for (int i=0; i dps = new ArrayList(); - SeekableView view = points.iterator(); - int i=0; + final SeekableView view = points.iterator(); while (view.hasNext()) { DataPoint pt = view.next(); if (pt.isInteger()) { - dps[i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), Math.abs(pt.longValue())); + dps.add(MutableDataPoint.ofLongValue( + pt.timestamp(), Math.abs(pt.longValue()))); } else { - dps[i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), Math.abs(pt.doubleValue())); + dps.add(MutableDataPoint.ofDoubleValue( + pt.timestamp(), Math.abs(pt.doubleValue()))); } - i++; } - - return new PostAggregatedDataPoints(points, dps); + final DataPoint[] results = new DataPoint[dps.size()]; + dps.toArray(results); + return new PostAggregatedDataPoints(points, results); } @Override diff --git a/test/query/expression/TestAbsolute.java b/test/query/expression/TestAbsolute.java new file mode 100644 index 0000000000..3d675ee0d6 --- /dev/null +++ b/test/query/expression/TestAbsolute.java @@ -0,0 +1,307 @@ +// 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.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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestAbsolute { + + 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 Absolute 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.metricName()).thenReturn(METRIC); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new Absolute(); + } + + @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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals((long)v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + 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 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals((long)v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + 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.metricName()).thenReturn("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 = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + 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 { + 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("absolute(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("absolute(null)", func.writeStringField(params, null)); + assertEquals("absolute()", func.writeStringField(params, "")); + assertEquals("absolute(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From a6a0a01ce9401f497ef3d76309feec4586566849 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sun, 1 Nov 2015 10:05:29 -0800 Subject: [PATCH 278/826] Add the ExpressionFactory class that stores a map of the available functions. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionFactory.java | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/query/expression/ExpressionFactory.java diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java new file mode 100644 index 0000000000..d18b0cf007 --- /dev/null +++ b/src/query/expression/ExpressionFactory.java @@ -0,0 +1,29 @@ +package net.opentsdb.query.expression; + +import java.util.HashMap; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; + +public class ExpressionFactory { + + private static Map availableFunctions = + new HashMap(); + + static { + availableFunctions.put("scale", new Scale()); + availableFunctions.put("absolute", new Absolute()); + availableFunctions.put("movingAverage", new MovingAverage()); + availableFunctions.put("highestCurrent", new HighestCurrent()); + availableFunctions.put("highestMax", new HighestMax()); + } + + @VisibleForTesting + static void addFunction(String name, Expression expr) { + availableFunctions.put(name, expr); + } + + public static Expression getByName(String funcName) { + return availableFunctions.get(funcName); + } +} From ebc2181dab6a1b02149df3ae795c63a859ef998a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 Nov 2015 10:11:36 -0800 Subject: [PATCH 279/826] Fixup the ExpressionFactory by adding comments and changing the getter to throw an exception if the requested function isn't found. Also add unit tests Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/ExpressionFactory.java | 73 ++++++++++++--- .../expression/TestExpressionFactory.java | 93 +++++++++++++++++++ 3 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 test/query/expression/TestExpressionFactory.java diff --git a/Makefile.am b/Makefile.am index de306c4076..8b4b7741c4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,6 +75,7 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ + src/query/expression/ExpressionFactory.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ src/query/expression/MovingAverage.java \ @@ -225,6 +226,7 @@ test_SRC := \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ test/query/expression/TestAbsolute.java \ + test/query/expression/TestExpressionFactory.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ test/query/expression/TestMovingAverage.java \ diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index d18b0cf007..3c61528dea 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -1,29 +1,74 @@ +// 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.HashMap; import java.util.Map; -import com.google.common.annotations.VisibleForTesting; - -public class ExpressionFactory { +/** + * A static class that stores and instantiates a static map of the available + * functions. + * TODO - Enable plugable expression and load from the class path. + * Since 2.3 + */ +public final class ExpressionFactory { - private static Map availableFunctions = + private static Map available_functions = new HashMap(); static { - availableFunctions.put("scale", new Scale()); - availableFunctions.put("absolute", new Absolute()); - availableFunctions.put("movingAverage", new MovingAverage()); - availableFunctions.put("highestCurrent", new HighestCurrent()); - availableFunctions.put("highestMax", new HighestMax()); + available_functions.put("scale", new Scale()); + available_functions.put("absolute", new Absolute()); + available_functions.put("movingAverage", new MovingAverage()); + available_functions.put("highestCurrent", new HighestCurrent()); + available_functions.put("highestMax", new HighestMax()); } - @VisibleForTesting - static void addFunction(String name, Expression expr) { - availableFunctions.put(name, expr); + /** Don't instantiate me! */ + private ExpressionFactory() { } + + /** + * Add an expression to the map. + * WARNING: The map is not thread safe so don't use this to dynamically + * modify the map while the TSD is running. + * @param name The name of the expression + * @param expr The expression object to store. + * @throws IllegalArgumentException if the name is null or empty or the + * function is null. + */ + static void addFunction(final String name, final Expression expr) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Missing function name"); + } + if (expr == null) { + throw new IllegalArgumentException("Function cannot be null"); + } + available_functions.put(name, expr); } - public static Expression getByName(String funcName) { - return availableFunctions.get(funcName); + /** + * Returns the expression function given the name + * @param function The name of the expression to use + * @return The expression when located + * @throws UnsupportedOperationException if the requested function hasn't + * been stored in the map. + */ + public static Expression getByName(final String function) { + final Expression expression = available_functions.get(function); + if (expression == null) { + throw new UnsupportedOperationException("Function " + function + + " has not been implemented"); + } + return expression; } } diff --git a/test/query/expression/TestExpressionFactory.java b/test/query/expression/TestExpressionFactory.java new file mode 100644 index 0000000000..b0024fa44f --- /dev/null +++ b/test/query/expression/TestExpressionFactory.java @@ -0,0 +1,93 @@ +// 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.assertTrue; + +import java.util.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSQuery; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +public class TestExpressionFactory { + + @Test + public void getByName() throws Exception { + // pick a couple of implementations + Expression e = ExpressionFactory.getByName("scale"); + assertTrue(e instanceof Scale); + + e = ExpressionFactory.getByName("highestMax"); + assertTrue(e instanceof HighestMax); + } + + @Test (expected = UnsupportedOperationException.class) + public void getByNameNoSuchFunction() throws Exception { + ExpressionFactory.getByName("I don't exist"); + } + + @Test (expected = UnsupportedOperationException.class) + public void getByNameNullName() throws Exception { + ExpressionFactory.getByName(null); + } + + @Test (expected = UnsupportedOperationException.class) + public void getByNameEmptyName() throws Exception { + ExpressionFactory.getByName(""); + } + + @Test + public void addFunction() throws Exception { + ExpressionFactory.addFunction("testExpr", new TestExpr()); + final Expression e = ExpressionFactory.getByName("testExpr"); + assertTrue(e instanceof TestExpr); + } + + @Test (expected = IllegalArgumentException.class) + public void addFunctionNullName() throws Exception { + ExpressionFactory.addFunction(null, new TestExpr()); + } + + @Test (expected = IllegalArgumentException.class) + public void addFunctionEmptyName() throws Exception { + ExpressionFactory.addFunction("", new TestExpr()); + } + + @Test (expected = IllegalArgumentException.class) + public void addFunctionNullFunction() throws Exception { + ExpressionFactory.addFunction("testExpr", null); + } + + /** Dummy expression class used for testing */ + private static class TestExpr implements Expression { + @Override + public DataPoints[] evaluate(TSQuery data_query, + List results, List params) { + return null; + } + @Override + public String writeStringField(List params, + String inner_expression) { + return null; + } + } +} From 97f662c457fcaf6fcace46c7495c4c138147d349 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sun, 1 Nov 2015 10:24:36 -0800 Subject: [PATCH 280/826] Add the expression tree to store the processing order for functions from the Graphite endpoint. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionTree.java | 151 +++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/query/expression/ExpressionTree.java diff --git a/src/query/expression/ExpressionTree.java b/src/query/expression/ExpressionTree.java new file mode 100644 index 0000000000..6a48300f78 --- /dev/null +++ b/src/query/expression/ExpressionTree.java @@ -0,0 +1,151 @@ +// 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 com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSQuery; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ExpressionTree { + + private final Expression expr; + private final TSQuery data_query; + + private List subExpressions; + private List funcParams; + private Map subMetricQueries; + private Map parameterSourceIndex = Maps.newHashMap(); + + private static final Joiner DOUBLE_COMMA_JOINER = Joiner.on(",").skipNulls(); + + enum Parameter { + SUB_EXPRESSION, + METRIC_QUERY + } + + public ExpressionTree(String exprName, TSQuery data_query) { + this(ExpressionFactory.getByName(exprName), data_query); + } + + public ExpressionTree(Expression expr, TSQuery data_query) { + this.expr = expr; + this.data_query = data_query; + } + + public void addSubExpression(ExpressionTree child, int paramIndex) { + if (subExpressions == null) { + subExpressions = Lists.newArrayList(); + } + subExpressions.add(child); + parameterSourceIndex.put(paramIndex, Parameter.SUB_EXPRESSION); + } + + public void addSubMetricQuery(String metricQuery, int magic, + int paramIndex) { + if (subMetricQueries == null) { + subMetricQueries = Maps.newHashMap(); + } + subMetricQueries.put(magic, metricQuery); + parameterSourceIndex.put(paramIndex, Parameter.METRIC_QUERY); + } + + public void addFunctionParameter(String param) { + if (funcParams == null) { + funcParams = Lists.newArrayList(); + } + funcParams.add(param); + } + + public DataPoints[] evaluate(List queryResults) { + List materialized = Lists.newArrayList(); + List metricQueryKeys = null; + if (subMetricQueries != null && subMetricQueries.size() > 0) { + metricQueryKeys = Lists.newArrayList(subMetricQueries.keySet()); + Collections.sort(metricQueryKeys); + } + + int metricPointer = 0; + int subExprPointer = 0; + for (int i=0; i strs = Lists.newArrayList(); + if (subExpressions != null) { + for (ExpressionTree sub : subExpressions) { + strs.add(sub.toString()); + } + } + + if (subMetricQueries != null) { + String subMetrics = clean(subMetricQueries.values()); + if (subMetrics != null && subMetrics.length() > 0) { + strs.add(subMetrics); + } + } + + String innerExpression = DOUBLE_COMMA_JOINER.join(strs); + return expr.writeStringField(funcParams, innerExpression); + } + + private String clean(Collection values) { + if (values == null || values.size() == 0) { + return ""; + } + + List strs = Lists.newArrayList(); + for (String v : values) { + String tmp = v.replaceAll("\\{.*\\}", ""); + int ix = tmp.lastIndexOf(':'); + if (ix < 0) { + strs.add(tmp); + } else { + strs.add(tmp.substring(ix+1)); + } + } + + return DOUBLE_COMMA_JOINER.join(strs); + } + +} \ No newline at end of file From 0fed45f0ad401ed9631c74f9e4db77075f8dc4f2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 Nov 2015 12:38:18 -0800 Subject: [PATCH 281/826] Cleanup the ExpressionTree class a bit, add unit tests and comments. I think this class can be cleaned up further, particulary with better ctors and index tracking but we'll add the rest of the expression support classes and see how it goes. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/ExpressionTree.java | 238 +++++++++---- test/query/expression/TestExpressionTree.java | 319 ++++++++++++++++++ 3 files changed, 493 insertions(+), 66 deletions(-) create mode 100644 test/query/expression/TestExpressionTree.java diff --git a/Makefile.am b/Makefile.am index 8b4b7741c4..61d9e199de 100644 --- a/Makefile.am +++ b/Makefile.am @@ -76,6 +76,7 @@ tsdb_SRC := \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ src/query/expression/ExpressionFactory.java \ + src/query/expression/ExpressionTree.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ src/query/expression/MovingAverage.java \ @@ -227,6 +228,7 @@ test_SRC := \ test/meta/TestUIDMeta.java \ test/query/expression/TestAbsolute.java \ test/query/expression/TestExpressionFactory.java \ + test/query/expression/TestExpressionTree.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ test/query/expression/TestMovingAverage.java \ diff --git a/src/query/expression/ExpressionTree.java b/src/query/expression/ExpressionTree.java index 6a48300f78..28bc5feaf2 100644 --- a/src/query/expression/ExpressionTree.java +++ b/src/query/expression/ExpressionTree.java @@ -12,10 +12,13 @@ // see . package net.opentsdb.query.expression; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; + import net.opentsdb.core.DataPoints; +import net.opentsdb.core.IllegalDataException; import net.opentsdb.core.TSQuery; import java.util.Collection; @@ -23,121 +26,205 @@ import java.util.List; import java.util.Map; +/** + * A node in a tree of nested expressions. The tree may link to other nodes as + * sub expressions. Evaluating the tree evaluates all sub expressions. + *

    + * Before calling {@link evaluate} you MUST call a one or a combination of + * {@link addSubExpression}, {@link addSubMetricQuery} and optionally + * {@link addFunctionParameter} + *

    + * TODO(cl) - Cleanup needed. Tracking the indices can likely be done better + * and it would be good to have a ctor that sets the sub or metric query. + * @since 2.3 + */ public class ExpressionTree { - - private final Expression expr; - private final TSQuery data_query; - - private List subExpressions; - private List funcParams; - private Map subMetricQueries; - private Map parameterSourceIndex = Maps.newHashMap(); - + /** Used for the toString() helpers */ private static final Joiner DOUBLE_COMMA_JOINER = Joiner.on(",").skipNulls(); - + + /** An enumerator of the different query types */ enum Parameter { SUB_EXPRESSION, METRIC_QUERY } + + /** The root expression for the tree */ + private final Expression expression; + /** The original time series query */ + private final TSQuery data_query; + /** An optional list of sub expressions */ + private List sub_expressions; + /** A list of parameters for the root expression */ + private List func_params; + /** A mapping of result indices to sub metric queries */ + private Map sub_metric_queries; + /** A mapping of query types to their result index */ + private Map parameter_index = Maps.newHashMap(); - public ExpressionTree(String exprName, TSQuery data_query) { - this(ExpressionFactory.getByName(exprName), data_query); + /** + * Creates a tree with a root and no children + * @param expression_name The name of the expression to lookup in the factory + * @param data_query The original query + * @throws UnsupportedOperationException if the expression is not implemented + */ + public ExpressionTree(final String expression_name, final TSQuery data_query) { + this(ExpressionFactory.getByName(expression_name), data_query); } - - public ExpressionTree(Expression expr, TSQuery data_query) { - this.expr = expr; + + /** + * Creates a tree with a root and no children + * @param expression The expression to use + * @param data_query The original query + */ + public ExpressionTree(final Expression expression, final TSQuery data_query) { + this.expression = expression; this.data_query = data_query; } - public void addSubExpression(ExpressionTree child, int paramIndex) { - if (subExpressions == null) { - subExpressions = Lists.newArrayList(); + public void addSubExpression(final ExpressionTree child, final int param_index) { + if (child == null) { + throw new IllegalArgumentException("Cannot add a null child tree"); + } + if (child == this) { + throw new IllegalDataException("Recursive sub expression detected: " + + this); } - subExpressions.add(child); - parameterSourceIndex.put(paramIndex, Parameter.SUB_EXPRESSION); + if (param_index < 0) { + throw new IllegalArgumentException("Parameter index must be 0 or greater"); + } + if (sub_expressions == null) { + sub_expressions = Lists.newArrayList(); + } + sub_expressions.add(child); + parameter_index.put(param_index, Parameter.SUB_EXPRESSION); } - public void addSubMetricQuery(String metricQuery, int magic, - int paramIndex) { - if (subMetricQueries == null) { - subMetricQueries = Maps.newHashMap(); + /** + * Sets the metric query key and index, setting the Parameter type to + * METRIC_QUERY + * @param metric_query The metric query id + * @param sub_query_index The index of the metric query + * @param param_index The index of the parameter (??) + */ + public void addSubMetricQuery(final String metric_query, + final int sub_query_index, + final int param_index) { + if (metric_query == null || metric_query.isEmpty()) { + throw new IllegalArgumentException("Metric query cannot be null or empty"); + } + if (sub_query_index < 0) { + throw new IllegalArgumentException("Sub query index must be 0 or greater"); } - subMetricQueries.put(magic, metricQuery); - parameterSourceIndex.put(paramIndex, Parameter.METRIC_QUERY); + if (param_index < 0) { + throw new IllegalArgumentException("Parameter index must be 0 or greater"); + } + if (sub_metric_queries == null) { + sub_metric_queries = Maps.newHashMap(); + } + sub_metric_queries.put(sub_query_index, metric_query); + parameter_index.put(param_index, Parameter.METRIC_QUERY); } - - public void addFunctionParameter(String param) { - if (funcParams == null) { - funcParams = Lists.newArrayList(); + + /** + * Adds parameters for the root expression only. + * @param param The parameter to add, cannot be null or empty + * @throws IllegalArgumentException if the parameter is null or empty + */ + public void addFunctionParameter(final String param) { + if (param == null || param.isEmpty()) { + throw new IllegalArgumentException("Parameter cannot be null or empty"); } - funcParams.add(param); + if (func_params == null) { + func_params = Lists.newArrayList(); + } + func_params.add(param); } - public DataPoints[] evaluate(List queryResults) { - List materialized = Lists.newArrayList(); - List metricQueryKeys = null; - if (subMetricQueries != null && subMetricQueries.size() > 0) { - metricQueryKeys = Lists.newArrayList(subMetricQueries.keySet()); - Collections.sort(metricQueryKeys); + /** + * Processes the expression tree, including sub expressions, and returns the + * results. + * TODO(cl) - More tests around indices, etc. This can likely be cleaned up. + * @param query_results The result set to pass to the expressions + * @return The result set or an exception will bubble up if something wasn't + * configured properly. + */ + public DataPoints[] evaluate(final List query_results) { + // TODO - size the array + final List materialized = Lists.newArrayList(); + List metric_query_keys = null; + if (sub_metric_queries != null && sub_metric_queries.size() > 0) { + metric_query_keys = Lists.newArrayList(sub_metric_queries.keySet()); + Collections.sort(metric_query_keys); } - int metricPointer = 0; - int subExprPointer = 0; - for (int i=0; i strs = Lists.newArrayList(); - if (subExpressions != null) { - for (ExpressionTree sub : subExpressions) { + final List strs = Lists.newArrayList(); + if (sub_expressions != null) { + for (ExpressionTree sub : sub_expressions) { strs.add(sub.toString()); } } - if (subMetricQueries != null) { - String subMetrics = clean(subMetricQueries.values()); - if (subMetrics != null && subMetrics.length() > 0) { - strs.add(subMetrics); + if (sub_metric_queries != null) { + final String sub_metrics = clean(sub_metric_queries.values()); + if (sub_metrics != null && sub_metrics.length() > 0) { + strs.add(sub_metrics); } } - String innerExpression = DOUBLE_COMMA_JOINER.join(strs); - return expr.writeStringField(funcParams, innerExpression); + final String inner_expression = DOUBLE_COMMA_JOINER.join(strs); + return expression.writeStringField(func_params, inner_expression); } - private String clean(Collection values) { + /** + * Helper to clean out some characters + * @param values The collection of strings to cleanup + * @return An empty string if values was empty or a cleaned up string + */ + private String clean(final Collection values) { if (values == null || values.size() == 0) { return ""; } - List strs = Lists.newArrayList(); + final List strs = Lists.newArrayList(); for (String v : values) { - String tmp = v.replaceAll("\\{.*\\}", ""); - int ix = tmp.lastIndexOf(':'); + final String tmp = v.replaceAll("\\{.*\\}", ""); + final int ix = tmp.lastIndexOf(':'); if (ix < 0) { strs.add(tmp); } else { @@ -148,4 +235,23 @@ private String clean(Collection values) { return DOUBLE_COMMA_JOINER.join(strs); } + @VisibleForTesting + List subExpressions() { + return sub_expressions; + } + + @VisibleForTesting + List funcParams() { + return func_params; + } + + @VisibleForTesting + Map subMetricQueries() { + return sub_metric_queries; + } + + @VisibleForTesting + Map parameterIndex() { + return parameter_index; + } } \ No newline at end of file diff --git a/test/query/expression/TestExpressionTree.java b/test/query/expression/TestExpressionTree.java new file mode 100644 index 0000000000..ad1a5f37c7 --- /dev/null +++ b/test/query/expression/TestExpressionTree.java @@ -0,0 +1,319 @@ +// 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.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.SeekableViewsForTest; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.ExpressionTree.Parameter; + +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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestExpressionTree { + private final static String EXPR_NAME = "treeTestExpr"; + 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 TreeTestExpr test_expression; + + @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.metricName()).thenReturn(METRIC); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + test_expression = new TreeTestExpr(); + ExpressionFactory.addFunction(EXPR_NAME, test_expression); + } + + @Test + public void ctorString() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + assertEquals(EXPR_NAME + "()", tree.toString()); + assertNull(tree.subExpressions()); + assertNull(tree.funcParams()); + assertNull(tree.subMetricQueries()); + assertTrue(tree.parameterIndex().isEmpty()); + } + + @Test (expected = UnsupportedOperationException.class) + public void ctorStringNull() throws Exception { + new ExpressionTree((String)null, data_query); + } + + @Test (expected = UnsupportedOperationException.class) + public void ctorStringEmpty() throws Exception { + new ExpressionTree("", data_query); + } + + @Test (expected = UnsupportedOperationException.class) + public void ctorStringUnknown() throws Exception { + new ExpressionTree("No such method", data_query); + } + + @Test + public void ctorExpression() throws Exception { + final ExpressionTree tree = new ExpressionTree(test_expression, data_query); + assertEquals(EXPR_NAME + "()", tree.toString()); + assertNull(tree.subExpressions()); + assertNull(tree.funcParams()); + assertNull(tree.subMetricQueries()); + assertTrue(tree.parameterIndex().isEmpty()); + } + + @Test + public void addSubExpression() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + final ExpressionTree child = new ExpressionTree("scale", data_query); + tree.addSubExpression(child, 1); + assertEquals(1, tree.subExpressions().size()); + assertSame(child, tree.subExpressions().get(0)); + assertNull(tree.funcParams()); + assertNull(tree.subMetricQueries()); + assertEquals(1, tree.parameterIndex().size()); + assertEquals(Parameter.SUB_EXPRESSION, tree.parameterIndex().get(1)); + } + + @Test (expected = IllegalArgumentException.class) + public void addSubExpressionNullTree() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubExpression(null, 1); + } + + @Test (expected = IllegalArgumentException.class) + public void addSubExpressionNegativeIndex() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubExpression(null, 1); + } + + @Test (expected = IllegalDataException.class) + public void addSubExpressionRecursion() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubExpression(tree, 1); + } + + @Test + public void addSubMetricQuery() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery(METRIC, 1, 1); + assertEquals(EXPR_NAME + "(" + METRIC + ")", tree.toString()); + assertNull(tree.subExpressions()); + assertNull(tree.funcParams()); + assertEquals(1, tree.subMetricQueries().size()); + assertEquals(METRIC, tree.subMetricQueries().get(1)); + assertEquals(1, tree.parameterIndex().size()); + assertEquals(Parameter.METRIC_QUERY, tree.parameterIndex().get(1)); + } + + @Test (expected = IllegalArgumentException.class) + public void addSubMetricQueryNullMetric() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery(null, 1, 1); + } + + @Test (expected = IllegalArgumentException.class) + public void addSubMetricQueryEmptyMetric() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery("", 1, 1); + } + + @Test (expected = IllegalArgumentException.class) + public void addSubMetricQueryNegativeQueryIndex() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery(METRIC, -1, 1); + } + + @Test (expected = IllegalArgumentException.class) + public void addSubMetricQueryNegativeParamIndex() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery(METRIC, 1, -1); + } + + @Test + public void addFunctionParameter() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addFunctionParameter("vimes"); + assertEquals(EXPR_NAME + "()", tree.toString()); + assertNull(tree.subExpressions()); + assertEquals(1, tree.funcParams().size()); + assertEquals("vimes", tree.funcParams().get(0)); + assertNull(tree.subMetricQueries()); + assertTrue(tree.parameterIndex().isEmpty()); + } + + @Test (expected = IllegalArgumentException.class) + public void addFunctionParameterNull() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addFunctionParameter(null); + } + + @Test (expected = IllegalArgumentException.class) + public void addFunctionParameterEmpty() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addFunctionParameter(""); + } + + @Test + public void evaluateNothingSet() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + assertEquals(EXPR_NAME + "()", tree.toString()); + + final DataPoints[] response = tree.evaluate(query_results); + assertEquals(1, response.length); + assertSame(data_query, test_expression.data_query); + assertEquals(0, test_expression.results.size()); + assertNull(test_expression.params); + } + + @Test + public void evaluateSubMetricQuerySet() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery(METRIC, 0, 0); + assertEquals(EXPR_NAME + "(" + METRIC + ")", tree.toString()); + + final DataPoints[] response = tree.evaluate(query_results); + assertEquals(1, response.length); + assertSame(data_query, test_expression.data_query); + assertEquals(1, test_expression.results.size()); + assertNull(test_expression.params); + } + + @Test + public void evaluateSubMetricQuerySetWithParam() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + tree.addSubMetricQuery(METRIC, 0, 0); + tree.addFunctionParameter("foo"); + assertEquals(EXPR_NAME + "(" + METRIC + ")", tree.toString()); + + final DataPoints[] response = tree.evaluate(query_results); + assertEquals(1, response.length); + assertSame(data_query, test_expression.data_query); + assertEquals(1, test_expression.results.size()); + assertEquals(1, test_expression.params.size()); + assertEquals("foo", test_expression.params.get(0)); + } + + @Test + public void evaluateSubExpressionSet() throws Exception { + final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); + final ExpressionTree child = spy(new ExpressionTree("scale", data_query)); + child.addSubMetricQuery(METRIC, 0, 0); + child.addFunctionParameter("1"); + tree.addSubExpression(child, 0); + assertEquals(EXPR_NAME + "(scale(" + METRIC + "))", tree.toString()); + + final DataPoints[] response = tree.evaluate(query_results); + assertEquals(1, response.length); + assertSame(data_query, test_expression.data_query); + assertEquals(1, test_expression.results.size()); + assertNull(test_expression.params); + verify(child, times(1)).evaluate(query_results); + } + +// TODO - fix this up +// @Test +// public void evaluateSubExpressionAndSubMetricSet() throws Exception { +// final ExpressionTree tree = new ExpressionTree(EXPR_NAME, data_query); +// final ExpressionTree child = spy(new ExpressionTree("scale", data_query)); +// child.addSubMetricQuery(METRIC, 0, 0); +// child.addFunctionParameter("1"); +// tree.addSubExpression(child, 0); +// +// tree.addSubMetricQuery(METRIC, 1, 0); +// tree.addFunctionParameter("foo"); +// +// assertEquals(EXPR_NAME + "(scale(" + METRIC + ")," + METRIC + ")", +// tree.toString()); +// +// final DataPoints[] response = tree.evaluate(query_results); +// assertEquals(1, response.length); +// assertSame(data_query, test_expression.data_query); +// assertEquals(1, test_expression.results.size()); +// assertEquals(1, test_expression.params.size()); +// assertEquals("foo", test_expression.params.get(0)); +// verify(child, times(1)).evaluate(query_results); +// } + + // TODO - more tests around indexes, etc unless we cleanup the class + + private class TreeTestExpr implements Expression { + TSQuery data_query; + List results; + List params; + + @Override + public DataPoints[] evaluate(TSQuery data_query, + List results, List params) { + this.data_query = data_query; + this.results = results; + this.params = params; + + // Returns an array the size of the total incoming results array + int num_results = 0; + for (DataPoints[] r: query_results) { + num_results += r.length; + } + final DataPoints[] response = new DataPoints[num_results]; + return response; + } + @Override + public String writeStringField(List params, + String inner_expression) { + return EXPR_NAME + "(" + inner_expression + ")"; + } + } +} From 726933b9aa351a04882e9317aa550885c4e38489 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Sun, 1 Nov 2015 13:00:56 -0800 Subject: [PATCH 282/826] Add the ExpressionReader class for parsing expressions. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionReader.java | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/query/expression/ExpressionReader.java diff --git a/src/query/expression/ExpressionReader.java b/src/query/expression/ExpressionReader.java new file mode 100644 index 0000000000..dfcc8a5766 --- /dev/null +++ b/src/query/expression/ExpressionReader.java @@ -0,0 +1,94 @@ +package net.opentsdb.query.expression; + +import com.google.common.base.Preconditions; + +public class ExpressionReader { + + protected final char[] chars; + + private int mark = 0; + + public ExpressionReader(char[] chars) { + Preconditions.checkNotNull(chars); + this.chars = chars; + } + + public int getMark() { + return mark; + } + + public char peek() { + return chars[mark]; + } + + public char next() { + return chars[mark++]; + } + + public void skip(int num) { + mark+=num; + } + + public boolean isNextChar(char c) { + return peek() == c; + } + + public boolean isNextSeq(CharSequence seq) { + Preconditions.checkNotNull(seq); + for (int i=0; i Date: Sun, 1 Nov 2015 13:48:50 -0800 Subject: [PATCH 283/826] Cleanup the ExpressionReader class with comments, formatting, EOF checks and unit tests. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/ExpressionReader.java | 160 ++++++++++----- .../expression/TestExpressionReader.java | 193 ++++++++++++++++++ 3 files changed, 306 insertions(+), 49 deletions(-) create mode 100644 test/query/expression/TestExpressionReader.java diff --git a/Makefile.am b/Makefile.am index 61d9e199de..df63c21a19 100644 --- a/Makefile.am +++ b/Makefile.am @@ -76,6 +76,7 @@ tsdb_SRC := \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ src/query/expression/ExpressionFactory.java \ + src/query/expression/ExpressionReader.java \ src/query/expression/ExpressionTree.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ @@ -228,6 +229,7 @@ test_SRC := \ test/meta/TestUIDMeta.java \ test/query/expression/TestAbsolute.java \ test/query/expression/TestExpressionFactory.java \ + test/query/expression/TestExpressionReader.java \ test/query/expression/TestExpressionTree.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ diff --git a/src/query/expression/ExpressionReader.java b/src/query/expression/ExpressionReader.java index dfcc8a5766..01cf7e0105 100644 --- a/src/query/expression/ExpressionReader.java +++ b/src/query/expression/ExpressionReader.java @@ -1,94 +1,156 @@ +// 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 com.google.common.base.Preconditions; +import java.util.NoSuchElementException; +/** + * Parses a Graphite style expression. + * Please use {@link #isEOF()} before any method call. Otherwise the methods + * will throw a NoSuchElementException. + * @since 2.3 + */ public class ExpressionReader { - + /** The character array to parse */ protected final char[] chars; + /** The current index in the character array */ private int mark = 0; - public ExpressionReader(char[] chars) { - Preconditions.checkNotNull(chars); - this.chars = chars; + /** + * Default ctor + * @param chars The characters to parse + */ + public ExpressionReader(final char[] chars) { + if (chars == null) { + throw new IllegalArgumentException("Character set cannot be null"); + } + this.chars = chars; } + /** @return the current index */ public int getMark() { - return mark; + return mark; } + /** @return the current character without advancing the index */ public char peek() { - return chars[mark]; + if (isEOF()) { + throw new NoSuchElementException("Index " + mark + " is out of bounds " + + chars.length); + } + return chars[mark]; } + /** @return the current character and advances the index */ public char next() { - return chars[mark++]; + if (isEOF()) { + throw new NoSuchElementException("Index " + mark + " is out of bounds " + + chars.length); + } + return chars[mark++]; } - public void skip(int num) { - mark+=num; + /** @param the number of characters to skip */ + public void skip(final int num) { + if (num < 0) { + throw new UnsupportedOperationException("Skipping backwards is not allowed"); + } + mark += num; } - public boolean isNextChar(char c) { - return peek() == c; + /** + * Checks to see if the next character matches the parameter + * @param c The character to check for + * @return True if they match, false if not + */ + public boolean isNextChar(final char c) { + return peek() == c; } - public boolean isNextSeq(CharSequence seq) { - Preconditions.checkNotNull(seq); - for (int i=0; i= chars.length) { + return false; + } + if (chars[mark + i] != seq.charAt(i)) { + return false; } + } - return true; + return true; } + /** @return the name of the function */ public String readFuncName() { - StringBuilder builder = new StringBuilder(); - while (peek() != '(' && !Character.isWhitespace(peek())) { - builder.append(next()); - } - return builder.toString(); + // in case we get something like " function(foo)" consume a bit + skipWhitespaces(); + StringBuilder builder = new StringBuilder(); + while (peek() != '(' && !Character.isWhitespace(peek())) { + builder.append(next()); + } + skipWhitespaces(); // increment over whitespace after + return builder.toString(); } + /** @return Whether or not the index is at the end of the character array */ public boolean isEOF() { - return mark == chars.length; + return mark >= chars.length; } + /** Increments the mark over white spaces */ public void skipWhitespaces() { - for (int i=mark; i Date: Mon, 2 Nov 2015 11:54:58 -0800 Subject: [PATCH 284/826] Add missing copyright header to TestExpressionReader Signed-off-by: Chris Larsen --- test/query/expression/TestExpressionReader.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/query/expression/TestExpressionReader.java b/test/query/expression/TestExpressionReader.java index 632e37b168..3e4e7bd653 100644 --- a/test/query/expression/TestExpressionReader.java +++ b/test/query/expression/TestExpressionReader.java @@ -1,3 +1,15 @@ +// 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; From 2b192bc4891f1bb50cfb2b2d540477624282ffa2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 30 Oct 2015 20:26:34 -0700 Subject: [PATCH 285/826] Add support for compiling with Cassandra support using AsyncCassandra Signed-off-by: Chris Larsen --- Makefile.am | 12 +++++++ build-cassandra.sh | 9 ++++++ configure.ac | 12 ++++++- pom.xml.in | 18 +++++++++++ src/core/Aggregators.java | 32 +++++++++++++++++++ ...102.192826-2-jar-with-dependencies.jar.md5 | 1 + third_party/asynccassandra/include.mk | 23 +++++++++++++ third_party/include.mk | 9 ++++++ 8 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 build-cassandra.sh create mode 100644 third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 create mode 100644 third_party/asynccassandra/include.mk diff --git a/Makefile.am b/Makefile.am index df63c21a19..1f078b8338 100644 --- a/Makefile.am +++ b/Makefile.am @@ -180,6 +180,14 @@ tsdb_DEPS += \ $(ASYNCBIGTABLE) maven_profile_bigtable := true maven_profile_hbase := false +maven_profile_cassandra := false +else +if CASSANDRA +tsdb_DEPS += \ + $(ASYNCCASSANDRA) +maven_profile_bigtable := false +maven_profile_hbase := false +maven_profile_cassandra := true else tsdb_DEPS += \ $(ASYNCHBASE) \ @@ -187,6 +195,8 @@ tsdb_DEPS += \ $(ZOOKEEPER) maven_profile_bigtable := false maven_profile_hbase := true +maven_profile_cassandra := false +endif endif test_SRC := \ @@ -695,6 +705,7 @@ pom.xml: pom.xml.in Makefile sed <$< \ -e 's/@ASYNCHBASE_VERSION@/$(ASYNCHBASE_VERSION)/' \ -e 's/@ASYNCBIGTABLE_VERSION@/$(ASYNCBIGTABLE_VERSION)/' \ + -e 's/@ASYNCCASSANDRA_VERSION@/$(ASYNCCASSANDRA_VERSION)/' \ -e 's/@GUAVA_VERSION@/$(GUAVA_VERSION)/' \ -e 's/@GWT_VERSION@/$(GWT_VERSION)/' \ -e 's/@HAMCREST_VERSION@/$(HAMCREST_VERSION)/' \ @@ -717,6 +728,7 @@ pom.xml: pom.xml.in Makefile -e 's/@spec_version@/$(PACKAGE_VERSION)/' \ -e 's/@maven_profile_hbase@/$(maven_profile_hbase)/' \ -e 's/@maven_profile_bigtable@/$(maven_profile_bigtable)/' \ + -e 's/@maven_profile_cassandrae@/$(maven_profile_cassandra)/' \ ; \ } >$@-t mv $@-t ../$@ diff --git a/build-cassandra.sh b/build-cassandra.sh new file mode 100644 index 0000000000..a29a383e34 --- /dev/null +++ b/build-cassandra.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -xe +test -f configure || ./bootstrap +test -d build || mkdir build +cd build +test -f Makefile || ../configure --with-cassandra "$@" +MAKE=make +[ `uname -s` = "FreeBSD" ] && MAKE=gmake +exec ${MAKE} "$@" \ No newline at end of file diff --git a/configure.ac b/configure.ac index dea9bd6147..8212e4ffe6 100644 --- a/configure.ac +++ b/configure.ac @@ -25,7 +25,7 @@ AC_CONFIG_FILES([opentsdb.spec]) AC_CONFIG_FILES([build-aux/fetchdep.sh], [chmod +x build-aux/fetchdep.sh]) AC_ARG_WITH([bigtable], - [AS_HELP_STRING([--with-bigtable], [enable bigtable backend])], + [AS_HELP_STRING([--with-bigtable], [Enable Google's Bigtable backend])], [with_bigtable=yes], [with_bigtable=no]) @@ -34,6 +34,16 @@ AS_IF([test "x$with_bigtable" = "xyes"], [AM_CONDITIONAL(BIGTABLE, false)] ) +AC_ARG_WITH([cassandra], + [AS_HELP_STRING([--with-cassandra], [Enable Cassandra backend])], + [with_cassandra=yes], + [with_cassandra=no]) + +AS_IF([test "x$with_cassandra" = "xyes"], + [AM_CONDITIONAL(CASSANDRA, true)], + [AM_CONDITIONAL(CASSANDRA, false)] +) + TSDB_FIND_PROG([md5], [md5sum md5 gmd5sum digest]) if test x`basename "$MD5"` = x'digest'; then MD5='digest -a md5' diff --git a/pom.xml.in b/pom.xml.in index fee38d15af..cf68ef34e6 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -510,6 +510,24 @@ + + + + cassandra + + @maven_profile_cassandra@ + + + + + net.opentsdb + asynccassandra + @ASYNCCASSANDRA_VERSION@ + jar-with-dependencies + + + + diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index d70872e0de..9cc26f5108 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -77,6 +77,11 @@ public enum Interpolation { public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); + /** Return the product of two time series + * @since 2.3 */ + public static final Aggregator MULTIPLY = new Multiply( + Interpolation.LERP, "multiply"); + /** Aggregator that returns the number of data points. * WARNING: This currently interpolates with zero-if-missing. In this case * counts will be off when counting multiple time series. Only use this when @@ -150,6 +155,7 @@ public enum Interpolation { aggregators.put("zimsum", ZIMSUM); aggregators.put("mimmin", MIMMIN); aggregators.put("mimmax", MIMMAX); + aggregators.put("multiply", MULTIPLY); PercentileAgg[] percentiles = { p999, p99, p95, p90, p75, p50, @@ -518,4 +524,30 @@ public double runDouble(final Doubles values) { } } + + private static final class Multiply extends Aggregator { + + public Multiply(final Interpolation method, final String name) { + super(method, name); + } + + @Override + public long runLong(Longs values) { + long result = values.nextLongValue(); + while (values.hasNextValue()) { + result *= values.nextLongValue(); + } + return result; + } + + @Override + public double runDouble(Doubles values) { + double result = values.nextDoubleValue(); + while (values.hasNextValue()) { + double d = values.nextDoubleValue(); + result *= d; + } + return result; + } + } } diff --git a/third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 b/third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..10cf1edf3e --- /dev/null +++ b/third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +cce1a4b5736fcdcc3ced33982c3069d1 \ No newline at end of file diff --git a/third_party/asynccassandra/include.mk b/third_party/asynccassandra/include.mk new file mode 100644 index 0000000000..4d8658a262 --- /dev/null +++ b/third_party/asynccassandra/include.mk @@ -0,0 +1,23 @@ +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +ASYNCCASSANDRA_VERSION := 0.0.1-20151102.192826-2 +ASYNCCASSANDRA := third_party/asynccassandra/asynccassandra-$(ASYNCCASSANDRA_VERSION)-jar-with-dependencies.jar +ASYNCCASSANDRA_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/net/opentsdb/asynccassandra/0.0.1-SNAPSHOT/ + +$(ASYNCCASSANDRA): $(ASYNCCASSANDRA).md5 + set dummy "$(ASYNCCASSANDRA_BASE_URL)" "$(ASYNCCASSANDRA)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(ASYNCCASSANDRA) \ No newline at end of file diff --git a/third_party/include.mk b/third_party/include.mk index ab92e5b4fe..d7767763b2 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -37,6 +37,13 @@ include third_party/apache/include.mk if BIGTABLE include third_party/alpn-boot/include.mk include third_party/asyncbigtable/include.mk +ASYNCCASSANDRA_VERSION = 0.0 +ASYNCHBASE_VERSION = 0.0 +ZOOKEEPER_VERSION = 0.0 +else +if CASSANDRA +include third_party/asynccassandra/include.mk +ASYNCBIGTABLE_VERSION = 0.0 ASYNCHBASE_VERSION = 0.0 ZOOKEEPER_VERSION = 0.0 else @@ -44,4 +51,6 @@ include third_party/hbase/include.mk include third_party/protobuf/include.mk include third_party/zookeeper/include.mk ASYNCBIGTABLE_VERSION = 0.0 +ASYNCCASSANDRA_VERSION = 0.0 +endif endif \ No newline at end of file From b0a0d194ab7802be9c2f9f72213bbea470c0e451 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 2 Nov 2015 12:29:44 -0800 Subject: [PATCH 286/826] Fix the Cassandra merge that pulled in a duplicate MUTLIPLY agg. --- src/core/Aggregators.java | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 9cc26f5108..ce18e6e560 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -77,11 +77,6 @@ public enum Interpolation { public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); - /** Return the product of two time series - * @since 2.3 */ - public static final Aggregator MULTIPLY = new Multiply( - Interpolation.LERP, "multiply"); - /** Aggregator that returns the number of data points. * WARNING: This currently interpolates with zero-if-missing. In this case * counts will be off when counting multiple time series. Only use this when @@ -155,7 +150,6 @@ public enum Interpolation { aggregators.put("zimsum", ZIMSUM); aggregators.put("mimmin", MIMMIN); aggregators.put("mimmax", MIMMAX); - aggregators.put("multiply", MULTIPLY); PercentileAgg[] percentiles = { p999, p99, p95, p90, p75, p50, @@ -525,29 +519,4 @@ public double runDouble(final Doubles values) { } - private static final class Multiply extends Aggregator { - - public Multiply(final Interpolation method, final String name) { - super(method, name); - } - - @Override - public long runLong(Longs values) { - long result = values.nextLongValue(); - while (values.hasNextValue()) { - result *= values.nextLongValue(); - } - return result; - } - - @Override - public double runDouble(Doubles values) { - double result = values.nextDoubleValue(); - while (values.hasNextValue()) { - double d = values.nextDoubleValue(); - result *= d; - } - return result; - } - } } From e4f3220aa29100503f50a709f48f44fb43d31e23 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 2 Nov 2015 12:42:34 -0800 Subject: [PATCH 287/826] Add the missing Expression.java class to the makefile. Doh! Signed-off-by: Chris Larsen --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index 1f078b8338..9a82421d58 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,6 +75,7 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ + src/query/expression/Expression.java \ src/query/expression/ExpressionFactory.java \ src/query/expression/ExpressionReader.java \ src/query/expression/ExpressionTree.java \ From 516ce2ab5df55ab2147d13be34d80890fff44c84 Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Mon, 2 Nov 2015 11:40:39 -0800 Subject: [PATCH 288/826] Add the Expressions class for parsing an expression into an ExpressionTree. Signed-off-by: Chris Larsen --- src/query/expression/Expressions.java | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/query/expression/Expressions.java diff --git a/src/query/expression/Expressions.java b/src/query/expression/Expressions.java new file mode 100644 index 0000000000..757f1c3eb0 --- /dev/null +++ b/src/query/expression/Expressions.java @@ -0,0 +1,100 @@ +// 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.List; + +import com.google.common.base.Preconditions; +import net.opentsdb.core.TSQuery; + +public class Expressions { + + public static ExpressionTree parse(String expr, + List metricQueries, + TSQuery data_query) { + Preconditions.checkNotNull(expr); + if (expr.indexOf('(') == -1 || expr.indexOf(')') == -1) { + throw new RuntimeException("Invalid Expression: " + expr); + } + + ExpressionReader reader = new ExpressionReader(expr.toCharArray()); + reader.skipWhitespaces(); + + String funcName = reader.readFuncName(); + Expression rootExpr = ExpressionFactory.getByName(funcName); + if (rootExpr == null) { + throw new RuntimeException("Could not find evaluator " + + "for function '" + funcName + "'"); + } + + ExpressionTree root = new ExpressionTree(rootExpr, data_query); + + reader.skipWhitespaces(); + if (reader.peek() == '(') { + reader.next(); + parse(reader, metricQueries, root, data_query); + } + + return root; + } + + private static void parse(ExpressionReader reader, List metricQueries, + ExpressionTree root, TSQuery data_query) { + + int parameterIndex = 0; + reader.skipWhitespaces(); + if (reader.peek() != ')') { + String param = reader.readNextParameter(); + parseParam(param, metricQueries, root, data_query, parameterIndex++); + } + + while (true) { + reader.skipWhitespaces(); + if (reader.peek() == ')') { + return; + } else if (reader.isNextSeq(",,")) { + reader.skip(2); //swallow the ",," delimiter + reader.skipWhitespaces(); + String param = reader.readNextParameter(); + parseParam(param, metricQueries, root, data_query, parameterIndex++); + } else { + throw new RuntimeException("Invalid delimiter in parameter " + + "list at pos=" + reader.getMark() + ", expr=" + + reader.toString()); + } + } + } + + private static void parseParam(String param, List metricQueries, + ExpressionTree root, TSQuery data_query, int index) { + if (param == null || param.length() == 0) { + throw new RuntimeException("Invalid Parameter in " + + "Expression"); + } + + if (param.indexOf('(') > 0 && param.indexOf(')') > 0) { + // sub expression + ExpressionTree subTree = parse(param, metricQueries, data_query); + root.addSubExpression(subTree, index); + } else if (param.indexOf(':') >= 0) { + // metric query + metricQueries.add(param); + root.addSubMetricQuery(param, metricQueries.size() - 1, index); + } else { + // expression parameter + root.addFunctionParameter(param); + } + } + +} + From 932db038836b97929e36bbecd986d15c74940623 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 2 Nov 2015 12:03:12 -0800 Subject: [PATCH 289/826] Cleanup Expressions with comments, formatting and the start of some UTs Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/Expressions.java | 169 +++++++++++++-------- test/query/expression/TestExpressions.java | 95 ++++++++++++ 3 files changed, 199 insertions(+), 67 deletions(-) create mode 100644 test/query/expression/TestExpressions.java diff --git a/Makefile.am b/Makefile.am index 9a82421d58..4cbcf80320 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,6 +78,7 @@ tsdb_SRC := \ src/query/expression/Expression.java \ src/query/expression/ExpressionFactory.java \ src/query/expression/ExpressionReader.java \ + src/query/expression/Expressions.java \ src/query/expression/ExpressionTree.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ @@ -241,6 +242,7 @@ test_SRC := \ test/query/expression/TestAbsolute.java \ test/query/expression/TestExpressionFactory.java \ test/query/expression/TestExpressionReader.java \ + test/query/expression/TestExpressions.java \ test/query/expression/TestExpressionTree.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ diff --git a/src/query/expression/Expressions.java b/src/query/expression/Expressions.java index 757f1c3eb0..341134bb05 100644 --- a/src/query/expression/Expressions.java +++ b/src/query/expression/Expressions.java @@ -14,87 +14,122 @@ import java.util.List; -import com.google.common.base.Preconditions; import net.opentsdb.core.TSQuery; +/** + * Static class with helpers to parse and deal with expressions + * @since 2.3 + */ public class Expressions { - public static ExpressionTree parse(String expr, - List metricQueries, - TSQuery data_query) { - Preconditions.checkNotNull(expr); - if (expr.indexOf('(') == -1 || expr.indexOf(')') == -1) { - throw new RuntimeException("Invalid Expression: " + expr); - } - - ExpressionReader reader = new ExpressionReader(expr.toCharArray()); - reader.skipWhitespaces(); - - String funcName = reader.readFuncName(); - Expression rootExpr = ExpressionFactory.getByName(funcName); - if (rootExpr == null) { - throw new RuntimeException("Could not find evaluator " + - "for function '" + funcName + "'"); - } + /** No instantiation for you! */ + private Expressions() { } + + /** + * Parses an expression into a tree + * @param expression The expression to parse (as a string) + * @param metric_queries A list to store the parsed metrics in + * @param data_query The time series query + * @return The parsed tree ready for evaluation + * @throws IllegalArgumentException if the expression was null, empty or + * invalid. + * @throws UnsupportedOperationException if the requested function couldn't + * be found. + */ + public static ExpressionTree parse(final String expression, + final List metric_queries, + final TSQuery data_query) { + if (expression == null || expression.isEmpty()) { + throw new IllegalArgumentException("Expression may not be null or empty"); + } + if (expression.indexOf('(') == -1 || expression.indexOf(')') == -1) { + throw new IllegalArgumentException("Invalid Expression: " + expression); + } - ExpressionTree root = new ExpressionTree(rootExpr, data_query); + final ExpressionReader reader = new ExpressionReader(expression.toCharArray()); + // consume any whitespace ahead of the expression + reader.skipWhitespaces(); - reader.skipWhitespaces(); - if (reader.peek() == '(') { - reader.next(); - parse(reader, metricQueries, root, data_query); - } + final String function_name = reader.readFuncName(); + final Expression root_expression = ExpressionFactory.getByName(function_name); - return root; + final ExpressionTree root = new ExpressionTree(root_expression, data_query); + reader.skipWhitespaces(); + + if (reader.peek() == '(') { + reader.next(); + parse(reader, metric_queries, root, data_query); } - private static void parse(ExpressionReader reader, List metricQueries, - ExpressionTree root, TSQuery data_query) { + return root; + } + + /** + * Helper to parse out the function(s) and parameters + * @param reader The reader used for iterating over the expression + * @param metric_queries A list to store the parsed metrics in + * @param root The root tree + * @param data_query The time series query + */ + private static void parse(final ExpressionReader reader, + final List metric_queries, + final ExpressionTree root, + final TSQuery data_query) { + + int parameter_index = 0; + reader.skipWhitespaces(); + if (reader.peek() != ')') { + final String param = reader.readNextParameter(); + parseParam(param, metric_queries, root, data_query, parameter_index++); + } - int parameterIndex = 0; + while (!reader.isEOF()) { + reader.skipWhitespaces(); + if (reader.peek() == ')') { + return; + } else if (reader.isNextSeq(",,")) { + reader.skip(2); //swallow the ",," delimiter reader.skipWhitespaces(); - if (reader.peek() != ')') { - String param = reader.readNextParameter(); - parseParam(param, metricQueries, root, data_query, parameterIndex++); - } - - while (true) { - reader.skipWhitespaces(); - if (reader.peek() == ')') { - return; - } else if (reader.isNextSeq(",,")) { - reader.skip(2); //swallow the ",," delimiter - reader.skipWhitespaces(); - String param = reader.readNextParameter(); - parseParam(param, metricQueries, root, data_query, parameterIndex++); - } else { - throw new RuntimeException("Invalid delimiter in parameter " + - "list at pos=" + reader.getMark() + ", expr=" - + reader.toString()); - } - } + final String param = reader.readNextParameter(); + parseParam(param, metric_queries, root, data_query, parameter_index++); + } else { + throw new IllegalArgumentException("Invalid delimiter in parameter " + + "list at pos=" + reader.getMark() + ", expr=" + + reader.toString()); + } + } + } + + /** + * Helper that parses out the parameter from the expression + * @param param The parameter to parse + * @param metric_queries A list to store the parsed metrics in + * @param root The root tree + * @param data_query The time series query + * @param index Index of the parameter + */ + private static void parseParam(final String param, + final List metric_queries, + final ExpressionTree root, + final TSQuery data_query, + final int index) { + if (param == null || param.length() == 0) { + throw new IllegalArgumentException("Parameter cannot be null or empty"); } - private static void parseParam(String param, List metricQueries, - ExpressionTree root, TSQuery data_query, int index) { - if (param == null || param.length() == 0) { - throw new RuntimeException("Invalid Parameter in " + - "Expression"); - } - - if (param.indexOf('(') > 0 && param.indexOf(')') > 0) { - // sub expression - ExpressionTree subTree = parse(param, metricQueries, data_query); - root.addSubExpression(subTree, index); - } else if (param.indexOf(':') >= 0) { - // metric query - metricQueries.add(param); - root.addSubMetricQuery(param, metricQueries.size() - 1, index); - } else { - // expression parameter - root.addFunctionParameter(param); - } + if (param.indexOf('(') > 0 && param.indexOf(')') > 0) { + // sub expression + final ExpressionTree sub_tree = parse(param, metric_queries, data_query); + root.addSubExpression(sub_tree, index); + } else if (param.indexOf(':') >= 0) { + // metric query + metric_queries.add(param); + root.addSubMetricQuery(param, metric_queries.size() - 1, index); + } else { + // expression parameter + root.addFunctionParameter(param); } + } } diff --git a/test/query/expression/TestExpressions.java b/test/query/expression/TestExpressions.java new file mode 100644 index 0000000000..92f6ea4fc1 --- /dev/null +++ b/test/query/expression/TestExpressions.java @@ -0,0 +1,95 @@ +// 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.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.core.TSQuery; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestExpressions { + private TSQuery data_query; + private List metric_queries; + + @Before + public void before() throws Exception { + data_query = mock(TSQuery.class); + metric_queries = new ArrayList(); + } + + @Test + public void parse() throws Exception { + final ExpressionTree tree = Expressions.parse( + "scale(sys.cpu)", metric_queries, data_query); + assertEquals("scale()", tree.toString()); + } + + @Test + public void parseWithWhitespace() throws Exception { + final ExpressionTree tree = Expressions.parse( + " scale(sys.cpu)", metric_queries, data_query); + assertEquals("scale()", tree.toString()); + } + + @Test (expected = IllegalArgumentException.class) + public void parseNullExpression() throws Exception { + Expressions.parse(null, metric_queries, data_query); + } + + @Test (expected = IllegalArgumentException.class) + public void parseEmptyExpression() throws Exception { + Expressions.parse("", metric_queries, data_query); + } + + @Test (expected = IllegalArgumentException.class) + public void parseMissingOpenParens() throws Exception { + Expressions.parse("scalesys.cpu)", metric_queries, data_query); + } + + @Test (expected = IllegalArgumentException.class) + public void parseMissingClosingParens() throws Exception { + Expressions.parse("scale(sys.cpu", metric_queries, data_query); + } + + // TODO - These two may be problematic and need validation/fixing? + @Test + public void parseNullMetricQueries() throws Exception { + final ExpressionTree tree = Expressions.parse( + "scale(sys.cpu)", null, data_query); + assertEquals("scale()", tree.toString()); + } + + @Test + public void parseNullTSQuery() throws Exception { + final ExpressionTree tree = Expressions.parse( + "scale(sys.cpu)", metric_queries, null); + assertEquals("scale()", tree.toString()); + } + + //TODO - Need to add more tests around parsing nested functions and params +} From acfe754cde001dc06f79d9f6f32a39d4e277fc1b Mon Sep 17 00:00:00 2001 From: Arjun Satish Date: Mon, 2 Nov 2015 12:15:50 -0800 Subject: [PATCH 290/826] Some more tests around function parsing for the Expressions Signed-off-by: Chris Larsen --- test/query/expression/TestExpressions.java | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/query/expression/TestExpressions.java b/test/query/expression/TestExpressions.java index 92f6ea4fc1..60dd6ec7c1 100644 --- a/test/query/expression/TestExpressions.java +++ b/test/query/expression/TestExpressions.java @@ -13,11 +13,13 @@ package net.opentsdb.query.expression; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.List; +import net.opentsdb.core.DataPoints; import net.opentsdb.core.TSQuery; import org.junit.Before; @@ -40,6 +42,7 @@ public class TestExpressions { public void before() throws Exception { data_query = mock(TSQuery.class); metric_queries = new ArrayList(); + ExpressionFactory.addFunction("foo", new FooExpression()); } @Test @@ -55,6 +58,40 @@ public void parseWithWhitespace() throws Exception { " scale(sys.cpu)", metric_queries, data_query); assertEquals("scale()", tree.toString()); } + + @Test + public void parseMultiParameter() { + final String expr = "foo(sum:proc.sys.cpu,, sum:proc.meminfo.memfree)"; + final ExpressionTree tree = Expressions.parse(expr, metric_queries, null); + assertEquals("foo(proc.sys.cpu,proc.meminfo.memfree)", tree.toString()); + assertEquals(2, metric_queries.size()); + assertEquals("sum:proc.sys.cpu", metric_queries.get(0)); + assertEquals("sum:proc.meminfo.memfree", metric_queries.get(1)); + assertNull(tree.funcParams()); + } + + @Test + public void parseNestedExpr() { + final String expr = "foo(sum:proc.sys.cpu,, foo(sum:proc.a.b))"; + final ExpressionTree tree = Expressions.parse(expr, metric_queries, null); + assertEquals("foo(foo(proc.a.b),proc.sys.cpu)", tree.toString()); + assertEquals(2, metric_queries.size()); + assertEquals("sum:proc.sys.cpu", metric_queries.get(0)); + assertEquals("sum:proc.a.b", metric_queries.get(1)); + assertNull(tree.funcParams()); + } + + @Test + public void parseExprWithParam() { + final String expr = "foo(sum:proc.sys.cpu,, 100,, 3.1415)"; + final ExpressionTree tree = Expressions.parse(expr, metric_queries, null); + assertEquals("foo(proc.sys.cpu)", tree.toString()); + assertEquals(1, metric_queries.size()); + assertEquals("sum:proc.sys.cpu", metric_queries.get(0)); + assertEquals(2, tree.funcParams().size()); + assertEquals("100", tree.funcParams().get(0)); + assertEquals("3.1415", tree.funcParams().get(1)); + } @Test (expected = IllegalArgumentException.class) public void parseNullExpression() throws Exception { @@ -92,4 +129,19 @@ public void parseNullTSQuery() throws Exception { } //TODO - Need to add more tests around parsing nested functions and params + + /** Dummy test expression implementation */ + private static class FooExpression implements Expression { + @Override + public DataPoints[] evaluate(final TSQuery data_query, + final List query_results, final List params) { + return new DataPoints[0]; + } + + @Override + public String writeStringField(final List query_params, + final String inner_expressions) { + return "foo(" + inner_expressions + ")"; + } + } } From 8c74b89b34686fe0186caba3dfca35a99fba67ad Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 3 Nov 2015 11:43:52 -0800 Subject: [PATCH 291/826] Add some really ugly makefile code to compile the expression parser source files and add the source and classes to the TSDB jar. TODO - clean this up! It's not pretty but it seems to function and it doesn't cleanup after itself yet. Signed-off-by: Chris Larsen --- .gitignore | 2 +- Makefile.am | 26 +++++++--- src/parser.jj | 68 +++++++++++++++++++++++++ third_party/include.mk | 1 + third_party/javacc/include.mk | 23 +++++++++ third_party/javacc/javacc-6.1.2.jar.md5 | 1 + 6 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 src/parser.jj create mode 100644 third_party/javacc/include.mk create mode 100644 third_party/javacc/javacc-6.1.2.jar.md5 diff --git a/.gitignore b/.gitignore index 24afdb80df..41729cc410 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ guava-rpm-maker/\.project src-main src-test plugin_test.jar -/bin/ +bin/ diff --git a/Makefile.am b/Makefile.am index 4cbcf80320..7ab7eb5924 100644 --- a/Makefile.am +++ b/Makefile.am @@ -171,6 +171,7 @@ tsdb_DEPS = \ $(JACKSON_ANNOTATIONS) \ $(JACKSON_CORE) \ $(JACKSON_DATABIND) \ + $(JAVACC) \ $(NETTY) \ $(SLF4J_API) \ $(SUASYNC) \ @@ -194,7 +195,7 @@ else tsdb_DEPS += \ $(ASYNCHBASE) \ $(PROTOBUF) \ - $(ZOOKEEPER) + $(ZOOKEEPER) maven_profile_bigtable := false maven_profile_hbase := true maven_profile_cassandra := false @@ -329,11 +330,11 @@ test_DEPS = \ $(tsdb_DEPS) \ $(JAVASSIST) \ $(JUNIT) \ - $(HAMCREST) \ + $(HAMCREST) \ $(MOCKITO) \ - $(OBJENESIS) \ + $(OBJENESIS) \ $(POWERMOCK_MOCKITO) \ - $(jar) + $(jar) httpui_SRC := \ src/tsd/client/DateTimeBox.java \ @@ -347,6 +348,11 @@ httpui_SRC := \ httpui_DEPS = src/tsd/QueryUi.gwt.xml +# TODO(CL) - There is likely a MUCH better way to compile and add the expression sources and jars. +expr_package = net/opentsdb/query/expression/parser +expr_src_dir = $(abs_builddir)/src/$(expr_package) +get_expr_classes = `classes=''; for f in $(packagedir)$(expr_package)/*.class; do classes="$$classes $$f"; done; echo $$classes;` + #dist_pkgdata_DATA = src/logback.xml dist_static_DATA = src/tsd/static/favicon.ico @@ -405,7 +411,7 @@ install-exec-hook: $(builddata_SRC): .git/HEAD $(tsdb_SRC) $(top_srcdir)/build-aux/gen_build_data.sh $(srcdir)/build-aux/gen_build_data.sh $(builddata_SRC) $(package).$(builddata_subpackage) $(PACKAGE_VERSION) -jar: $(jar) .javac-unittests-stamp .gwtc-stamp +jar: runjavacc $(jar) .javac-unittests-stamp .gwtc-stamp JAVA_COMPILE := $(JAVAC) $(AM_JAVACFLAGS) -d . @@ -420,6 +426,9 @@ filter_src = \ src="$$src $$i";; \ esac; \ done; \ + for f in $(expr_src_dir)/*.java; do \ + src="$$src $$f"; \ + done; \ test -n "$$src" || exit 0 # Touches all the targets if any of the dependencies are newer. # This is useful to force-recompile all files if one of the @@ -438,7 +447,7 @@ find_jar = test -f "$$jar" && echo "$$jar" || echo "$(srcdir)/$$jar" get_dep_classpath = `for jar in $(tsdb_DEPS); do $(find_jar); done | tr '\n' ':'` .javac-stamp: $(tsdb_SRC) $(builddata_SRC) @$(filter_src); cp=$(get_dep_classpath); \ - echo "$(JAVA_COMPILE) -cp $$cp $$src"; \ + echo "DO THA COMPILE!!! $(JAVA_COMPILE) -cp $$cp $$src"; \ $(JAVA_COMPILE) -cp $$cp $$src @touch "$@" @@ -653,7 +662,7 @@ manifest: .javac-stamp .git/HEAD echo "Implementation-Vendor: $(spec_vendor)"; } >"$@" $(jar): manifest .javac-stamp $(classes) - $(JAR) cfm `basename $(jar)` manifest $(classes_with_nested_classes) \ + $(JAR) cfm `basename $(jar)` manifest $(classes_with_nested_classes) $(get_expr_classes) \ || { rv=$$? && rm -f `basename $(jar)` && exit $$rv; } # ^^^^^^^^^^^^^^^^^^^^^^^ # I've seen cases where `jar' exits with an error but leaves a partially built .jar file! @@ -677,6 +686,9 @@ $(JAVADOC_DIR)/index.html: $(tsdb_SRC) -link $(JDK_JAVADOC) -link $(NETTY_JAVADOC) -link $(SUASYNC_JAVADOC) \ $? $(builddata_SRC) +runjavacc: + $(JAVA) -cp $(JAVACC) javacc -STATIC:false -LOOKAHEAD:5 -OUTPUT_DIRECTORY:$(expr_src_dir) $(abs_srcdir)/src/parser.jj; echo PWD: `pwd`; + dist-hook: $(mkdir_p) $(distdir)/.git echo $(git_version) >$(distdir)/.git/HEAD diff --git a/src/parser.jj b/src/parser.jj new file mode 100644 index 0000000000..8517b29f07 --- /dev/null +++ b/src/parser.jj @@ -0,0 +1,68 @@ +PARSER_BEGIN(SyntaxChecker) +package net.opentsdb.query.expression.parser; + +import net.opentsdb.query.expression.ExpressionTree; +import net.opentsdb.core.TSQuery; + +import java.util.List; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; + +/** +* A simple class for validating the expressions +* @since 2.3 +*/ +public class SyntaxChecker { + + private TSQuery data_query; + private List metricQueries; + + public void setTSQuery(TSQuery data_query) { + this.data_query = data_query; + } + + public void setMetricQueries(List metricQueries) { + this.metricQueries = metricQueries; + } + + public static void main(String[] args) { + try { + new SyntaxChecker(new java.io.StringReader(args[0])).EXPRESSION(); + System.out.println("Syntax is okay"); + } catch (Throwable e) { + // Catching Throwable is ugly but JavaCC throws Error objects! + System.out.println("Syntax check failed: " + e.getMessage()); + } + } +} + +PARSER_END(SyntaxChecker) + +SKIP: { " " | "\t" | "\n" | "\r" } +TOKEN: { } +TOKEN: { } + +ExpressionTree EXPRESSION(): {Token name; int paramIndex=0;} { + name= { ExpressionTree tree=new ExpressionTree(name.image,data_query); } + "(" PARAMETER(tree, paramIndex++) ("," PARAMETER(tree, paramIndex++))* ")" + {return tree;} +} + +void PARAMETER(ExpressionTree tree, int paramIndex): {String metric; Token param; ExpressionTree subTree;} { + subTree=EXPRESSION() {tree.addSubExpression(subTree, paramIndex);} | + metric=METRIC() {metricQueries.add(metric); tree.addSubMetricQuery(metric, metricQueries.size()-1, paramIndex);} | + param= {tree.addFunctionParameter(param.image);} +} + +// metric is agg:[interval-agg:][rate:]metric[{tag=value,...}] +String METRIC() : {Token agg,itvl,rate,metric,tagk,tagv; StringBuilder builder = new StringBuilder(); + Joiner JOINER = Joiner.on(",").skipNulls(); + List tagPairs = Lists.newArrayList(); + } { + agg = ":" { builder.append(agg.image).append(":"); } + (itvl= ":" { builder.append(itvl.image).append(":"); })? + (rate= ":" { builder.append(rate.image).append(":"); })? + metric= { builder.append(metric.image); } + ("{" tagk= "=" tagv= {tagPairs.add(tagk+"="+tagv);} + ("," tagk= "=" tagv= {tagPairs.add(tagk+"="+tagv);})* "}")? + {if (tagPairs.size() > 0) builder.append("{").append(JOINER.join(tagPairs)).append("}"); return builder.toString();} } \ No newline at end of file diff --git a/third_party/include.mk b/third_party/include.mk index d7767763b2..3a25a241a9 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -22,6 +22,7 @@ include third_party/guava/include.mk include third_party/gwt/include.mk include third_party/hamcrest/include.mk include third_party/jackson/include.mk +include third_party/javacc/include.mk include third_party/javassist/include.mk include third_party/junit/include.mk include third_party/logback/include.mk diff --git a/third_party/javacc/include.mk b/third_party/javacc/include.mk new file mode 100644 index 0000000000..2c7f29785a --- /dev/null +++ b/third_party/javacc/include.mk @@ -0,0 +1,23 @@ +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +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): $(JAVACC).md5 + set dummy "$(JAVACC_BASE_URL)" "$(JAVACC)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(JAVACC) diff --git a/third_party/javacc/javacc-6.1.2.jar.md5 b/third_party/javacc/javacc-6.1.2.jar.md5 new file mode 100644 index 0000000000..84d9e28cfc --- /dev/null +++ b/third_party/javacc/javacc-6.1.2.jar.md5 @@ -0,0 +1 @@ +c74b2df75b4c46209d6da22bf4dad976 From d9c61a5db4a1fc0f92597c63a77b5d5868cf8471 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 3 Nov 2015 12:27:57 -0800 Subject: [PATCH 292/826] Move the parser options into the .jj file and add a javacc compliation plugin to the pom. Signed-off-by: Chris Larsen --- Makefile.am | 2 +- pom.xml.in | 22 ++++++++++++++++++++++ src/parser.jj | 6 ++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 7ab7eb5924..acc7c609fa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -687,7 +687,7 @@ $(JAVADOC_DIR)/index.html: $(tsdb_SRC) $? $(builddata_SRC) runjavacc: - $(JAVA) -cp $(JAVACC) javacc -STATIC:false -LOOKAHEAD:5 -OUTPUT_DIRECTORY:$(expr_src_dir) $(abs_srcdir)/src/parser.jj; echo PWD: `pwd`; + $(JAVA) -cp $(JAVACC) javacc -OUTPUT_DIRECTORY:$(expr_src_dir) $(abs_srcdir)/src/parser.jj; echo PWD: `pwd`; dist-hook: $(mkdir_p) $(distdir)/.git diff --git a/pom.xml.in b/pom.xml.in index cf68ef34e6..8e37d22217 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -314,6 +314,28 @@ + + com.helger.maven + ph-javacc-maven-plugin + 2.8.0 + + + jjc + generate-sources + + javacc + + + 1.6 + true + net.opentsdb.query.expression.parser + ${basedir}/src/ + ${project.build.directory}/generated-sources/ + + + + + diff --git a/src/parser.jj b/src/parser.jj index 8517b29f07..29b00c9e40 100644 --- a/src/parser.jj +++ b/src/parser.jj @@ -1,3 +1,9 @@ +/** Options required by Maven */ +options { + STATIC = false; + LOOKAHEAD = 5; +} + PARSER_BEGIN(SyntaxChecker) package net.opentsdb.query.expression.parser; From 94b7e60feb174b5fa6d0773d9c0540aa75ce6c58 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 3 Nov 2015 15:39:59 -0800 Subject: [PATCH 293/826] Add the /api/query/gexp endpoint for an experimental URI endpoint to handle the Graphite style expressions. Thanks to Arjun Satish and Turn for the code. More tests, functions and support in the built in GUI to come. Signed-off-by: Chris Larsen --- src/query/expression/Expressions.java | 31 +++++++++ src/tsd/QueryRpc.java | 61 +++++++++++++++-- test/tsd/TestQueryRpc.java | 96 ++++++++++++++++++--------- 3 files changed, 152 insertions(+), 36 deletions(-) diff --git a/src/query/expression/Expressions.java b/src/query/expression/Expressions.java index 341134bb05..1d7e2de954 100644 --- a/src/query/expression/Expressions.java +++ b/src/query/expression/Expressions.java @@ -12,9 +12,13 @@ // see . package net.opentsdb.query.expression; +import java.io.StringReader; +import java.util.ArrayList; import java.util.List; import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.parser.ParseException; +import net.opentsdb.query.expression.parser.SyntaxChecker; /** * Static class with helpers to parse and deal with expressions @@ -64,6 +68,33 @@ public static ExpressionTree parse(final String expression, return root; } + /** + * Parses a list of string expressions into the proper trees, adding the + * metrics to the {@link metric_queries} list. + * @param expressions A list of zero or more expressions (if empty, you get an + * empty tree list back) + * @param ts_query The original query with timestamps + * @param metric_queries The list to fill with metrics to fetch + */ + public static List parseExpressions( + final List expressions, + final TSQuery ts_query, + final List metric_queries) { + final List trees = + new ArrayList(expressions.size()); + for (final String expr: expressions) { + final SyntaxChecker checker = new SyntaxChecker(new StringReader(expr)); + checker.setMetricQueries(metric_queries); + checker.setTSQuery(ts_query); + try { + trees.add(checker.EXPRESSION()); + } catch (ParseException e) { + throw new IllegalArgumentException("Failed to parse " + expr, e); + } + } + return trees; + } + /** * Helper to parse out the function(s) and parameters * @param reader The reader used for iterating over the expression diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 58f1ae38fb..4b5ab81a7b 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -42,6 +42,8 @@ import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSUIDQuery; +import net.opentsdb.query.expression.ExpressionTree; +import net.opentsdb.query.expression.Expressions; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.QueryStats; import net.opentsdb.uid.NoSuchUniqueName; @@ -90,8 +92,10 @@ public void execute(final TSDB tsdb, final HttpQuery query) if (endpoint.toLowerCase().equals("last")) { handleLastDataPointQuery(tsdb, query); + } else if (endpoint.toLowerCase().equals("gexp")){ + handleQuery(tsdb, query, true); } else { - handleQuery(tsdb, query); + handleQuery(tsdb, query, false); } } @@ -99,10 +103,14 @@ public void execute(final TSDB tsdb, final HttpQuery query) * Processing for a data point query * @param tsdb The TSDB to which we belong * @param query The HTTP query to parse/respond + * @param allow_expressions Whether or not expressions should be parsed + * (based on the endpoint) */ - private void handleQuery(final TSDB tsdb, final HttpQuery query) { + private void handleQuery(final TSDB tsdb, final HttpQuery query, + final boolean allow_expressions) { final long start = DateTime.currentTimeMillis(); final TSQuery data_query; + final List expressions; if (query.method() == HttpMethod.POST) { switch (query.apiVersion()) { case 0: @@ -114,8 +122,10 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); } + expressions = null; } else { - data_query = this.parseQuery(tsdb, query); + expressions = new ArrayList(); + data_query = this.parseQuery(tsdb, query, expressions); } if (query.getAPIMethod() == HttpMethod.DELETE && @@ -192,8 +202,20 @@ public Object call(final Exception e) throws Exception { class QueriesCB implements Callback> { public Object call(final ArrayList query_results) throws Exception { - results.addAll(query_results); - + if (allow_expressions) { + // process each of the expressions into a new list, then merge it + // with the original. This avoids possible recursion loops. + final List expression_results = + new ArrayList(expressions.size()); + // let exceptions bubble up + for (final ExpressionTree expression : expressions) { + expression_results.add(expression.evaluate(query_results)); + } + results.addAll(expression_results); + } else { + results.addAll(query_results); + } + /** Simply returns the buffer once serialization is complete and logs it */ class SendIt implements Callback { public Object call(final ChannelBuffer buffer) throws Exception { @@ -432,10 +454,13 @@ public String toString() { * Parses a query string legacy style query from the URI * @param tsdb The TSDB we belong to * @param query The HTTP Query for parsing + * @param expressions A list of parsed expression trees filled from the URI. + * If this is null, it means any expressions in the URI will be skipped. * @return A TSQuery if parsing was successful * @throws BadRequestException if parsing was unsuccessful */ - private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { + private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, + final List expressions) { final TSQuery data_query = new TSQuery(); data_query.setStart(query.getRequiredQueryStringParam("start")); @@ -488,6 +513,30 @@ private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { } } + // TODO - testing out the graphite style expressions here with the "exp" + // param that could stand for experimental or expression ;) + if (expressions != null) { + if (query.hasQueryStringParam("exp")) { + final List uri_expressions = query.getQueryStringParams("exp"); + final List metric_queries = new ArrayList( + uri_expressions.size()); + // parse the expressions into their trees. If one or more expressions + // are improper then it will toss an exception up + expressions.addAll(Expressions.parseExpressions( + uri_expressions, data_query, metric_queries)); + // iterate over each of the parsed metric queries and store it in the + // TSQuery list so that we fetch the data for them. + for (final String mq: metric_queries) { + parseMTypeSubQuery(mq, data_query); + } + } + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Received a request with an expression but at the " + + "wrong endpoint: " + query); + } + } + if (data_query.getQueries() == null || data_query.getQueries().size() < 1) { throw new BadRequestException("Missing sub queries"); } diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index d498b963ff..cce155c244 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -23,12 +23,14 @@ import java.lang.reflect.Method; import java.nio.charset.Charset; +import java.util.List; import net.opentsdb.core.DataPoints; import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; +import net.opentsdb.query.expression.ExpressionTree; import net.opentsdb.query.filter.TagVLiteralOrFilter; import net.opentsdb.query.filter.TagVRegexFilter; import net.opentsdb.query.filter.TagVWildcardFilter; @@ -62,12 +64,13 @@ public final class TestQueryRpc { private QueryRpc rpc; private Query empty_query = mock(Query.class); private Query query_result; + private List expressions; private static final Method parseQuery; static { try { parseQuery = QueryRpc.class.getDeclaredMethod("parseQuery", - TSDB.class, HttpQuery.class); + TSDB.class, HttpQuery.class, List.class); parseQuery.setAccessible(true); } catch (Exception e) { throw new RuntimeException("Failed in static initializer", e); @@ -80,6 +83,7 @@ public void before() throws Exception { empty_query = mock(Query.class); query_result = mock(Query.class); rpc = new QueryRpc(); + expressions = null; when(tsdb.newQuery()).thenReturn(query_result); when(empty_query.run()).thenReturn(new DataPoints[0]); @@ -93,7 +97,7 @@ public void before() throws Exception { public void parseQueryMType() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -107,7 +111,7 @@ public void parseQueryMType() throws Exception { public void parseQueryMTypeWEnd() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&end=5m-ago&m=sum:sys.cpu.0"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertEquals("5m-ago", tsq.getEnd()); } @@ -115,7 +119,7 @@ public void parseQueryMTypeWEnd() throws Exception { public void parseQuery2MType() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0&m=avg:sys.cpu.1"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq.getQueries()); assertEquals(2, tsq.getQueries().size()); TSSubQuery sub1 = tsq.getQueries().get(0); @@ -132,7 +136,7 @@ public void parseQuery2MType() throws Exception { public void parseQueryMTypeWRate() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:rate:sys.cpu.0"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertTrue(sub.getRate()); } @@ -141,7 +145,7 @@ public void parseQueryMTypeWRate() throws Exception { public void parseQueryMTypeWDS() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:1h-avg:sys.cpu.0"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertEquals("1h-avg", sub.getDownsample()); } @@ -150,7 +154,7 @@ public void parseQueryMTypeWDS() throws Exception { public void parseQueryMTypeWDSAndFill() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:1h-avg-lerp:sys.cpu.0"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertEquals("1h-avg-lerp", sub.getDownsample()); } @@ -159,7 +163,7 @@ public void parseQueryMTypeWDSAndFill() throws Exception { public void parseQueryMTypeWRateAndDS() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:1h-avg:rate:sys.cpu.0"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertTrue(sub.getRate()); assertEquals("1h-avg", sub.getDownsample()); @@ -169,7 +173,7 @@ public void parseQueryMTypeWRateAndDS() throws Exception { public void parseQueryMTypeWTag() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=web01}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertNotNull(sub.getTags()); assertEquals("literal_or(web01)", sub.getTags().get("host")); @@ -180,7 +184,7 @@ public void parseQueryMTypeWGroupByRegex() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + TagVRegexFilter.FILTER_NAME + "(something(foo|bar))}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(1, sub.getFilters().size()); @@ -192,7 +196,7 @@ public void parseQueryMTypeWGroupByWildcardExplicit() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + TagVWildcardFilter.FILTER_NAME + "(*quirm)}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(1, sub.getFilters().size()); @@ -203,7 +207,7 @@ public void parseQueryMTypeWGroupByWildcardExplicit() throws Exception { public void parseQueryMTypeWGroupByWildcardImplicit() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=*quirm}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(1, sub.getFilters().size()); @@ -214,7 +218,7 @@ public void parseQueryMTypeWGroupByWildcardImplicit() throws Exception { public void parseQueryMTypeWWildcardFilterExplicit() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{host=wildcard(*quirm)}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(1, sub.getFilters().size()); @@ -225,7 +229,7 @@ public void parseQueryMTypeWWildcardFilterExplicit() throws Exception { public void parseQueryMTypeWWildcardFilterImplicit() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{host=*quirm}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(1, sub.getFilters().size()); @@ -236,7 +240,7 @@ public void parseQueryMTypeWWildcardFilterImplicit() throws Exception { public void parseQueryMTypeWGroupByAndWildcardFilterExplicit() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{colo=lga}{host=wildcard(*quirm)}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); @@ -248,7 +252,7 @@ public void parseQueryMTypeWGroupByAndWildcardFilterSameTagK() throws Exception HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=quirm|tsort}" + "{host=wildcard(*quirm)}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); @@ -261,7 +265,7 @@ public void parseQueryMTypeWGroupByFilterAndWildcardFilterSameTagK() HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + "{host=wildcard(*quirm)}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(2, sub.getFilters().size()); @@ -274,7 +278,7 @@ public void parseQueryMTypeWGroupByFilterMissingClose() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + "{host=wildcard(*quirm)"); - parseQuery.invoke(rpc, tsdb, query); + parseQuery.invoke(rpc, tsdb, query, expressions); } @Test (expected = IllegalArgumentException.class) @@ -282,7 +286,7 @@ public void parseQueryMTypeWGroupByFilterMissingEquals() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + "{hostwildcard(*quirm)}"); - parseQuery.invoke(rpc, tsdb, query); + parseQuery.invoke(rpc, tsdb, query, expressions); } @Test (expected = IllegalArgumentException.class) @@ -290,14 +294,14 @@ public void parseQueryMTypeWGroupByNoSuchFilter() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=nosuchfilter(*tsort)}" + "{host=dummyfilter(*quirm)}"); - parseQuery.invoke(rpc, tsdb, query); + parseQuery.invoke(rpc, tsdb, query, expressions); } @Test public void parseQueryMTypeWEmptyFilterBrackets() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{}"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(0, sub.getFilters().size()); @@ -307,7 +311,7 @@ public void parseQueryMTypeWEmptyFilterBrackets() throws Exception { public void parseQueryTSUIDType() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:010101"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -322,7 +326,7 @@ public void parseQueryTSUIDType() throws Exception { public void parseQueryTSUIDTypeMulti() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:010101,020202"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -338,7 +342,7 @@ public void parseQueryTSUIDTypeMulti() throws Exception { public void parseQuery2TSUIDType() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:010101&tsuid=avg:020202"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -359,7 +363,7 @@ public void parseQuery2TSUIDType() throws Exception { public void parseQueryTSUIDTypeWRate() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:rate:010101"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -375,7 +379,7 @@ public void parseQueryTSUIDTypeWRate() throws Exception { public void parseQueryTSUIDTypeWDS() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:1m-sum:010101"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -391,7 +395,7 @@ public void parseQueryTSUIDTypeWDS() throws Exception { public void parseQueryTSUIDTypeWRateAndDS() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:1m-sum:rate:010101"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); @@ -408,7 +412,7 @@ public void parseQueryTSUIDTypeWRateAndDS() throws Exception { public void parseQueryWPadding() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0&padding"); - TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertTrue(tsq.getPadding()); } @@ -417,14 +421,14 @@ public void parseQueryWPadding() throws Exception { public void parseQueryStartMissing() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?end=1h-ago&m=sum:sys.cpu.0"); - parseQuery.invoke(rpc, tsdb, query); + parseQuery.invoke(rpc, tsdb, query, expressions); } @Test (expected = BadRequestException.class) public void parseQueryNoSubQuery() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago"); - parseQuery.invoke(rpc, tsdb, query); + parseQuery.invoke(rpc, tsdb, query, expressions); } @Test @@ -532,5 +536,37 @@ public void deleteDatapointsBadRequest() throws Exception { assertTrue(json.contains("Deleting data is not enabled")); } + @Test + public void gexp() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/gexp?start=1h-ago&exp=scale(sum:sys.cpu.user,1)"); + rpc.execute(tsdb, query); + assertEquals(query.response().getStatus(), HttpResponseStatus.OK); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); + } + + @Test + public void gexpBadExpression() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query/gexp?start=1h-ago&exp=scale(sum:sys.cpu.user,notanumber)"); + rpc.execute(tsdb, query); + assertEquals(query.response().getStatus(), HttpResponseStatus.BAD_REQUEST); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("factor")); + } + //TODO(cl) add unit tests for the rate options parsing } \ No newline at end of file From 58ca3721c16b562d2e7564ad17204244bb30a460 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 3 Nov 2015 17:18:15 -0800 Subject: [PATCH 294/826] Fix #574 by pulling the metrics randomization setting from the TSD config during CLI UID assignment. Signed-off-by: Chris Larsen --- src/tools/UidManager.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 2885179754..fc51dd6287 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -40,6 +40,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; /** @@ -158,7 +159,7 @@ private static int runCommand(final TSDB tsdb, usage("Wrong number of arguments"); return 2; } - return assign(tsdb.getClient(), table, idwidth, args); + return assign(tsdb, table, idwidth, args); } else if (args[0].equals("rename")) { if (nargs != 4) { usage("Wrong number of arguments"); @@ -349,22 +350,27 @@ private static boolean printResult(final ArrayList row, /** * Implements the {@code assign} subcommand. - * @param client The HBase client to use. + * @param tsdb The TSDB to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param args Command line arguments ({@code assign name [names]}). * @return The exit status of the command (0 means success). */ - private static int assign(final HBaseClient client, + private static int assign(final TSDB tsdb, final byte[] table, final short idwidth, final String[] args) { - final UniqueId uid = new UniqueId(client, table, args[1], (int) idwidth); + boolean randomize = false; + if (UniqueIdType.valueOf(args[1]) == UniqueIdType.METRIC) { + randomize = tsdb.getConfig().getBoolean("tsd.core.uid.random_metrics"); + } + final UniqueId uid = new UniqueId(tsdb.getClient(), table, args[1], + (int) idwidth, randomize); for (int i = 2; i < args.length; i++) { try { uid.getOrCreateId(args[i]); // Lookup again the ID we've just created and print it. - extactLookupName(client, table, idwidth, args[1], args[i]); + extactLookupName(tsdb.getClient(), table, idwidth, args[1], args[i]); } catch (HBaseException e) { LOG.error("error while processing " + args[i], e); return 3; From d24683df68de1f65120c22f1a84afe877d474072 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 3 Nov 2015 17:18:15 -0800 Subject: [PATCH 295/826] Fix #574 by pulling the metrics randomization setting from the TSD config during CLI UID assignment. Signed-off-by: Chris Larsen --- src/tools/UidManager.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 2885179754..fc51dd6287 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -40,6 +40,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; /** @@ -158,7 +159,7 @@ private static int runCommand(final TSDB tsdb, usage("Wrong number of arguments"); return 2; } - return assign(tsdb.getClient(), table, idwidth, args); + return assign(tsdb, table, idwidth, args); } else if (args[0].equals("rename")) { if (nargs != 4) { usage("Wrong number of arguments"); @@ -349,22 +350,27 @@ private static boolean printResult(final ArrayList row, /** * Implements the {@code assign} subcommand. - * @param client The HBase client to use. + * @param tsdb The TSDB to use. * @param table The name of the HBase table to use. * @param idwidth Number of bytes on which the UIDs should be. * @param args Command line arguments ({@code assign name [names]}). * @return The exit status of the command (0 means success). */ - private static int assign(final HBaseClient client, + private static int assign(final TSDB tsdb, final byte[] table, final short idwidth, final String[] args) { - final UniqueId uid = new UniqueId(client, table, args[1], (int) idwidth); + boolean randomize = false; + if (UniqueIdType.valueOf(args[1]) == UniqueIdType.METRIC) { + randomize = tsdb.getConfig().getBoolean("tsd.core.uid.random_metrics"); + } + final UniqueId uid = new UniqueId(tsdb.getClient(), table, args[1], + (int) idwidth, randomize); for (int i = 2; i < args.length; i++) { try { uid.getOrCreateId(args[i]); // Lookup again the ID we've just created and print it. - extactLookupName(client, table, idwidth, args[1], args[i]); + extactLookupName(tsdb.getClient(), table, idwidth, args[1], args[i]); } catch (HBaseException e) { LOG.error("error while processing " + args[i], e); return 3; From ce4660d643cc269da25d79f609b45b56b52bd896 Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Tue, 27 Oct 2015 15:48:46 +0800 Subject: [PATCH 296/826] make OOM handler be able to handle multiple instances of opentsdb server Signed-off-by: Chris Larsen --- tools/opentsdb_restart.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index eaad7537f6..31425750a8 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -8,8 +8,11 @@ import os import subprocess +service_name = "opentsdb" +if 'NAME' in os.environ: + service_name = os.environ['NAME'] -subprocess.call(["service", "opentsdb", "stop"]) +subprocess.call(["service", service_name, "stop"]) # Close any file handles we inherited from our parent JVM. We need # to do this before restarting so that the socket isn't held open. openfiles = [int(f) for f in os.listdir("/proc/self/fd")] @@ -17,4 +20,4 @@ # that there is less chance of errors with those standard streams. # Other files start at fd 3. os.closerange(3, max(openfiles)) -subprocess.call(["service", "opentsdb", "start"]) +subprocess.call(["service", service_name, "start"]) From cc41502925769bd573c7650069301162ba17e40e Mon Sep 17 00:00:00 2001 From: Hong Dai Thanh Date: Fri, 30 Oct 2015 16:37:46 +0700 Subject: [PATCH 297/826] Fix typo TUSID --> TSUID Signed-off-by: Chris Larsen --- src/core/Query.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 519e36ebf5..34553c591e 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -119,7 +119,7 @@ void setTimeSeries(String metric, Map tags, * 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 TUSIDs + * 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 @@ -139,7 +139,7 @@ public void setTimeSeries(final List tsuids, * 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 TUSIDs + * 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 From 074a7e4084681c99cdd47ca61903716826759385 Mon Sep 17 00:00:00 2001 From: Hong Dai Thanh Date: Fri, 30 Oct 2015 16:37:46 +0700 Subject: [PATCH 298/826] Fix typo TUSID --> TSUID Signed-off-by: Chris Larsen --- src/core/Query.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 519e36ebf5..34553c591e 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -119,7 +119,7 @@ void setTimeSeries(String metric, Map tags, * 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 TUSIDs + * 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 @@ -139,7 +139,7 @@ public void setTimeSeries(final List tsuids, * 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 TUSIDs + * 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 From 8dcd77d8907e3a0eaebde0eede81b2f76150dfd7 Mon Sep 17 00:00:00 2001 From: louyl Date: Fri, 9 Oct 2015 17:38:44 +0800 Subject: [PATCH 299/826] Fix up Makefile.am Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 6da1af624e..41db9f8dda 100644 --- a/Makefile.am +++ b/Makefile.am @@ -389,8 +389,8 @@ install-data-local: staticroot install-data-lib install-data-tools \ install-data-bin install-data-etc @$(NORMAL_INSTALL) test -z "$(staticdir)" || $(mkdir_p) "$(DESTDIR)$(staticdir)" - @set -e; pwd; ls -lFh; cd "$(DEV_TSD_STATICROOT)"; \ - list=`find -L . ! -type d`; for p in $$list; do \ + @set -e; pwd; ls -lFh; (cd "$(DEV_TSD_STATICROOT)"; \ + list=`find -L . ! -type d`); for p in $$list; do \ p=$${p#./}; \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ From 65db7d6a9e570144621807b6f45cfe67ce122a06 Mon Sep 17 00:00:00 2001 From: louyl Date: Fri, 9 Oct 2015 17:38:44 +0800 Subject: [PATCH 300/826] Fix up Makefile.am Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index f9395a0504..7c9e486d90 100644 --- a/Makefile.am +++ b/Makefile.am @@ -450,8 +450,8 @@ install-data-local: staticroot install-data-lib install-data-tools \ install-data-bin install-data-etc @$(NORMAL_INSTALL) test -z "$(staticdir)" || $(mkdir_p) "$(DESTDIR)$(staticdir)" - @set -e; pwd; ls -lFh; cd "$(DEV_TSD_STATICROOT)"; \ - list=`find -L . ! -type d`; for p in $$list; do \ + @set -e; pwd; ls -lFh; (cd "$(DEV_TSD_STATICROOT)"; \ + list=`find -L . ! -type d`); for p in $$list; do \ p=$${p#./}; \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ From 825c49eba1ad4d24d728773ff41a8f7f01f182bd Mon Sep 17 00:00:00 2001 From: louyl Date: Fri, 9 Oct 2015 17:38:44 +0800 Subject: [PATCH 301/826] Fix up Makefile.am Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index acc7c609fa..f2549c02c3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -502,8 +502,8 @@ install-data-local: staticroot install-data-lib install-data-tools \ install-data-bin install-data-etc @$(NORMAL_INSTALL) test -z "$(staticdir)" || $(mkdir_p) "$(DESTDIR)$(staticdir)" - @set -e; pwd; ls -lFh; cd "$(DEV_TSD_STATICROOT)"; \ - list=`find -L . ! -type d`; for p in $$list; do \ + @set -e; pwd; ls -lFh; (cd "$(DEV_TSD_STATICROOT)"; \ + list=`find -L . ! -type d`); for p in $$list; do \ p=$${p#./}; \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ From e1d9ead07d689086f745f021436750458e98f598 Mon Sep 17 00:00:00 2001 From: Hari Krishna Dara Date: Wed, 7 Oct 2015 17:25:44 +0530 Subject: [PATCH 302/826] Bug fixes for failing unit tests when UID width is changed from the default 3 (specifically tested with 4) The following tests failed as of 2.1.0: TestTimeSeriesLookup.tagkOnlyMeta:211 expected:<5> but was:<6> TestTimeSeriesLookup.tagkOnlyMetaStar:227 expected:<5> but was:<6> TestTimeSeriesLookup.tagkOnlyData:244 expected:<5> but was:<6> TestTimeSeriesLookup.tagkOnly2Meta:260 expected:<2> but was:<0> TestUniqueIdRpc.tsuidPostByM Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 8 ++++---- src/tsd/UniqueIdRpc.java | 5 +++-- src/uid/.UniqueId.java.swo | Bin 0 -> 16384 bytes 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 src/uid/.UniqueId.java.swo diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index d640644c04..ff66760b0f 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -498,13 +498,13 @@ private String getRowKeyRegex() { } // catch any left over tagk/tag pairs - if (index < pairs.size()){ + if (index < pairs.size()){ // This condition is true whenever the first tagk in the pairs has a null value. buf.setLength(0); buf.append("(?s)^.{").append(query.useMeta() ? TSDB.metrics_width() : TSDB.metrics_width() + Const.SALT_WIDTH()) .append("}"); if (!query.useMeta()) { - buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})"); } ByteArrayPair last_pair = null; @@ -525,7 +525,7 @@ private String getRowKeyRegex() { buf.append(")"); } // moving on to the next tagk set - buf.append("(?:.{6})*"); // catch tag pairs in between + buf.append("(?:.{").append(tagsize).append("})*"); // catch tag pairs in between buf.append("(?:"); if (pairs.get(index).getKey() != null && pairs.get(index).getValue() != null) { @@ -535,7 +535,7 @@ private String getRowKeyRegex() { } else { buf.append("\\Q"); QueryUtil.addId(buf, pairs.get(index).getKey(), true); - buf.append("(?:.{").append(value_width).append("})+"); + buf.append("(?:.{").append(value_width).append("})"); } } last_pair = pairs.get(index); diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 1747e3f188..5f49d0af6c 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -573,8 +573,9 @@ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { try { buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); for (Entry e: sortedTags.entrySet()) { - buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, 3); - buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, 3); + // Fix for net.opentsdb.tsd.TestUniqueIdRpc.tsuidPostByM() + buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, TSDB.tagk_width()); + buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, TSDB.tagv_width()); } } catch (IOException e) { throw new BadRequestException(e); diff --git a/src/uid/.UniqueId.java.swo b/src/uid/.UniqueId.java.swo new file mode 100644 index 0000000000000000000000000000000000000000..20195dc9de84f769e58b2aed50952f2f6a08cb11 GIT binary patch literal 16384 zcmeHNO^h5z74G091hDhxTP}H7BRp%?J-ZHpV8_Pe-P!fX`^U_z9V@}3n(msJ&h~V- zyQ_D1uof;zP2o7A3B9H(P#KHWF1A!yI2S5;s0}yV2&2IvP@V)NpnVs1m+ky+w zBYiX7{i>)aM|H}3v`IDT1oPnHyoPnHyoPnHyoPnHyoPqyY22^mL@j-a^2JPV!`uDbx z-`Dl0t)D+RBLAnBd;0meN93<*`6c~)N;iJS4ZT3)^c0dKXCi7{y%AX z#?R#u`5(2sq3i!(ME(aY&+Iqv7}kG%ME>lE{F;^@()zEB$o1p=CubmMAZH+FAZH+F zAZH+FAZH+FAZH+F;J=gsfdi=t+rP{QUA+I#&d0yF$1pw*d=|I_+z-5Xw_!xUTY(=R zFpQ^xcRf;^%-S@F4IToWY+5eBf=s%Q*9Y0(cU5 z2>2ax48H}w4}1>z5YPg~f&IYGz&Z2&CFcH?K14z2tO+Hl<#@uS?B5ghULZ-~>x4m6 zJaUBUo%Ejk7OhlfD3n1cBmDP5+SKfk>NpWu!a}JdMN}Eq+UzO$#QSK~@$9Yo_&(96 z|FSVbaRhZDf;vf-@5Wt^F2}-kT8<3OJv1xPbO)yL4Xt-%D5)|Nk-%@H{7~8^?WE)E z84<~8*MT!+B|n*wi9|mm<+FLIiY*>SN+`$D^hF&%RJ2ffyir@3n=9Aqm8HcRoup%r zWGWZLhSQC^{>SjSXS9OZ9qrVYyywlxnrg++w3zTAW#0Xkfscq^Z6wXZ>~R3J=4^=}NMF zTF2l*bD%&(k<<1_P$)dx@1g^#DPgCiwzKAVZHj|iv}Aduj>g}a@vv9hLE!dCcqDB{ z{kzR-0us+3zM00g;&;Xg#Zzv26L?nnZZ0sSp$-6P~$4dmY8rh&~eGWH@$Tfso^N84?)^gui6A+fJ*6xQIx{!9CK^ zyhf+dE0&~V<|OwtE7r@xP*>0Uh)N;NFU^62^k!CA(A_iJQl$?H*%D$}+>PjZZDYE? zBoF(hlG6=b*@Zn(|5*iVO4t&x=8AE;76-h}L>ENpF=>S{n-?AnG1vFT#^Ga;W@67n z28p7nM#~L0BPVrAQTP@Melq^<*b&uDm2@Zz> z5kHxPuJ5nLK{6mL%tLmSi=&)rpid10tnoVINo8jLe>A&N2@@;x0)9{ISi(XSHS4vR zGkb^=^05)vqsX-HMe0P~2tRC_k=uI5Q|8cs=qnhN4+%O=EWM_PdY)w_0VDZd5W%jc zYL@Uk8SW}yj#U+1gSB(lBIji9yl`Xe7c`G-;#NzNWxu>($$+WfyI8t!*tN`@yrDTW z(v?cy$moQfU7IN>zw1~P55t8gQ(sx;Atr(uDQr1h8DqKEjL%5K0&I)=YIiO+EnJra z)1c4!ueBTIM%Oi)*ut4J0(&5Y6O0TFaNIjt!(7sgmE)R-epZauJY}MszC4Qwv$eMe z#dm%>?`-MULE(AFTb1@M!rj{_C4sl^{XkzL*sjiVeA7zT0#gTfcp88jOx@wxh<_rV zFc6zK(md8tDmY#&uCK3~Z7()4+=>X=(u-tNJcTJ;%+}_)#TA-Iv_r(2lNjY#rkB$I zKcAMz!t7%Y)DLk8(|$V?UF=FLE%JOd!P<7I%e%cU@1$&m2*u1m zVgqVPXN~vZh@C-Co3>D{V(D9~m(En?EA@*U@MbIZ#d57ivrAPf(Q>I;uS~DZm#Vb9 zQe9rEm7%)^*LlM+aEjHfJ1+K_k{A3|>`qWKR&=3KKf8ooT50hjT_{ztXRKd5kvQo? zngn%L#A}5E+YrpLIaH~2$rK$&tn87CV@yaGfe~gc45;&&L=!}~v@v)>2Mt08_P?yM zgYbz9p~z8e9Y325VR+4NDYSz-Klbb-1W%B}$}(sHi{(_-ak_gZn3@8#_53(weW;5H ztQDqV!>#vV14?%5E1ZFnWi#r)*kpTGtzb*=O%Fg^U#F@Kj9UxBE_WRvGil# z&u+41)9BvNX?GMArpM{nv!fBrV}j^}J9}DONbq@Ul}(>1<&+WiA7O^#|T$ zB~63R|99fd{RM!}|A+KB^eWE#9|5YsQQ$S4`F{`m3iuZANnj1IfD6C^PzLS*zJh$f zGr*I;VcJ%SMS$&k0mwf&133dZ133dZ z133dZ133dZ1OI;vaB4A4FQ(tw9pMwDDHi1K{0ONm&fW6IX;DFo=%JH%f#JI2NYSxq zGkyJmJZ^fwRh#N?rrDG0RE_bLp)31|mBLs#3~@AB@9?1lSv}VmHmCTJ=o-UmtKae_ zNjKtjAh*SzRA*sS;dt^2hzsZ%Y%-23$X5wuH*s9qR%5%;;pERiLe1X8x8$x zZJ?QpqX34s6d{Iol0W8ybYXmps=k`HpG-=p{jjyMVOxgpz;))InRE(LSpkN1fmxaW zZ6h7ZhWBfX&aBM8EU7-wjSPIhR>C7P@CEMG5%sJgZMhZt-Sn+YQf#Cg4***><91=p fu}^YGPbBs9C09u Date: Wed, 7 Oct 2015 17:25:44 +0530 Subject: [PATCH 303/826] Bug fixes for failing unit tests when UID width is changed from the default 3 (specifically tested with 4) The following tests failed as of 2.1.0: TestTimeSeriesLookup.tagkOnlyMeta:211 expected:<5> but was:<6> TestTimeSeriesLookup.tagkOnlyMetaStar:227 expected:<5> but was:<6> TestTimeSeriesLookup.tagkOnlyData:244 expected:<5> but was:<6> TestTimeSeriesLookup.tagkOnly2Meta:260 expected:<2> but was:<0> TestUniqueIdRpc.tsuidPostByM Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 8 ++++---- src/tsd/UniqueIdRpc.java | 5 +++-- src/uid/.UniqueId.java.swo | Bin 0 -> 16384 bytes 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 src/uid/.UniqueId.java.swo diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index d640644c04..ff66760b0f 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -498,13 +498,13 @@ private String getRowKeyRegex() { } // catch any left over tagk/tag pairs - if (index < pairs.size()){ + if (index < pairs.size()){ // This condition is true whenever the first tagk in the pairs has a null value. buf.setLength(0); buf.append("(?s)^.{").append(query.useMeta() ? TSDB.metrics_width() : TSDB.metrics_width() + Const.SALT_WIDTH()) .append("}"); if (!query.useMeta()) { - buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})*"); + buf.append("(?:.{").append(Const.TIMESTAMP_BYTES).append("})"); } ByteArrayPair last_pair = null; @@ -525,7 +525,7 @@ private String getRowKeyRegex() { buf.append(")"); } // moving on to the next tagk set - buf.append("(?:.{6})*"); // catch tag pairs in between + buf.append("(?:.{").append(tagsize).append("})*"); // catch tag pairs in between buf.append("(?:"); if (pairs.get(index).getKey() != null && pairs.get(index).getValue() != null) { @@ -535,7 +535,7 @@ private String getRowKeyRegex() { } else { buf.append("\\Q"); QueryUtil.addId(buf, pairs.get(index).getKey(), true); - buf.append("(?:.{").append(value_width).append("})+"); + buf.append("(?:.{").append(value_width).append("})"); } } last_pair = pairs.get(index); diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 1747e3f188..5f49d0af6c 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -573,8 +573,9 @@ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { try { buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); for (Entry e: sortedTags.entrySet()) { - buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, 3); - buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, 3); + // Fix for net.opentsdb.tsd.TestUniqueIdRpc.tsuidPostByM() + buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, TSDB.tagk_width()); + buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, TSDB.tagv_width()); } } catch (IOException e) { throw new BadRequestException(e); diff --git a/src/uid/.UniqueId.java.swo b/src/uid/.UniqueId.java.swo new file mode 100644 index 0000000000000000000000000000000000000000..20195dc9de84f769e58b2aed50952f2f6a08cb11 GIT binary patch literal 16384 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 fu}^YGPbBs9C09u Date: Wed, 4 Nov 2015 13:53:14 -0800 Subject: [PATCH 304/826] Add the ByteSet utility class for storing unique byte arrays. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/utils/ByteSet.java | 110 ++++++++++++++++++++++++++++++++++++ test/utils/TestByteSet.java | 71 +++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 src/utils/ByteSet.java create mode 100644 test/utils/TestByteSet.java diff --git a/Makefile.am b/Makefile.am index f2549c02c3..1b07e4c556 100644 --- a/Makefile.am +++ b/Makefile.am @@ -153,6 +153,7 @@ tsdb_SRC := \ src/uid/UniqueId.java \ src/uid/UniqueIdInterface.java \ src/utils/ByteArrayPair.java \ + src/utils/ByteSet.java \ src/utils/Config.java \ src/utils/DateTime.java \ src/utils/Exceptions.java \ @@ -296,6 +297,7 @@ test_SRC := \ test/uid/TestRandomUniqueId.java \ test/uid/TestUniqueId.java \ test/utils/TestByteArrayPair.java \ + test/utils/TestByteSet.java \ test/utils/TestConfig.java \ test/utils/TestDateTime.java \ test/utils/TestExceptions.java \ diff --git a/src/utils/ByteSet.java b/src/utils/ByteSet.java new file mode 100644 index 0000000000..6a1bad1d41 --- /dev/null +++ b/src/utils/ByteSet.java @@ -0,0 +1,110 @@ +// 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.utils; + +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Set; + +import org.hbase.async.Bytes.ByteMap; + +/** + * An implementation of a set based on the AsyncHBase ByteMap. This provides + * a unique set implementation of byte arrays, matching on the contents of + * the arrays, not on the hash codes. + */ +public class ByteSet extends AbstractSet + implements Set, Cloneable, java.io.Serializable { + + private static final long serialVersionUID = -496061795957902656L; + + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); + + private transient ByteMap map; + + /** + * Instantiates a unique set of byte arrays based on the array contents. + */ + public ByteSet() { + map = new ByteMap(); + } + + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(final Object key) { + return map.containsKey(key); + } + + @Override + public boolean add(final byte[] key) { + return map.put(key, PRESENT) == null; + } + + @Override + public boolean remove(final Object key) { + return map.remove(key) == PRESENT; + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public ByteSet clone() { + try { + ByteSet new_set = (ByteSet) super.clone(); + new_set.map = (ByteMap) map.clone(); + return new_set; + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + } + + @Override + public String toString() { + final Iterator it = map.keySet().iterator(); + if (!it.hasNext()) { + return "[]"; + } + + final StringBuilder buf = new StringBuilder(); + buf.append('['); + for (;;) { + final byte[] array = it.next(); + buf.append(Arrays.toString(array)); + if (!it.hasNext()) { + return buf.append(']').toString(); + } + buf.append(','); + } + } + + // TODO - writeObject, readObject +} diff --git a/test/utils/TestByteSet.java b/test/utils/TestByteSet.java new file mode 100644 index 0000000000..5cd83a240c --- /dev/null +++ b/test/utils/TestByteSet.java @@ -0,0 +1,71 @@ +// 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.utils; + +import static org.junit.Assert.assertArrayEquals; +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 java.util.Iterator; + +import org.junit.Test; + +public class TestByteSet { + + private static final byte[] V1 = new byte[] { 0, 0, 1 }; + private static final byte[] V2 = new byte[] { 0, 0, 2 }; + private static final byte[] V3 = new byte[] { 0, 0, 3 }; + private static final byte[] V4 = new byte[] { 0, 0, 4 }; + + @Test + public void ctor() { + final ByteSet set = new ByteSet(); + assertNotNull(set); + assertEquals(0, set.size()); + assertTrue(set.isEmpty()); + } + + @Test + public void goodOperations() { + final ByteSet set = new ByteSet(); + set.add(V3); + set.add(V2); + set.add(V1); + + assertEquals(3, set.size()); + assertFalse(set.isEmpty()); + + // should come out in order + final Iterator it = set.iterator(); + assertArrayEquals(V1, it.next()); + assertArrayEquals(V2, it.next()); + assertArrayEquals(V3, it.next()); + assertFalse(it.hasNext()); + + assertEquals("[[0, 0, 1],[0, 0, 2],[0, 0, 3]]", set.toString()); + + assertTrue(set.contains(V1)); + assertFalse(set.contains(V4)); + + assertTrue(set.remove(V1)); + assertFalse(set.contains(V1)); + assertFalse(set.remove(V4)); + + set.clear(); + assertFalse(set.contains(V2)); + assertFalse(set.contains(V3)); + assertTrue(set.isEmpty()); + } +} From b93e54981cf9c669d886b19fc00409efa305e962 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 4 Nov 2015 14:38:47 -0800 Subject: [PATCH 305/826] Add two methods to get the metric and aggregated tag UIDs from the DataPoints interface implementers. Signed-off-by: Chris Larsen --- src/core/BatchedDataPoints.java | 15 ++- src/core/DataPoints.java | 13 ++ src/core/IncomingDataPoints.java | 15 ++- src/core/RowSeq.java | 11 ++ src/core/Span.java | 11 ++ src/core/SpanGroup.java | 22 ++++ .../expression/PostAggregatedDataPoints.java | 16 +++ test/core/TestRowSeq.java | 114 ++++++++++++++++- test/core/TestSpan.java | 119 ++++++++++++++++++ test/core/TestSpanGroup.java | 71 ++++++++++- 10 files changed, 398 insertions(+), 9 deletions(-) diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index c56143f234..e399802345 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -39,8 +39,8 @@ final class BatchedDataPoints implements WritableDataPoints { private final TSDB tsdb; /** - * The row key. 3 bytes for the metric name, 4 bytes for the base timestamp, - * 6 bytes per tag (3 for the name, 3 for the value). + * The row key. Optional salt + 3 bytes for the metric name, 4 bytes for the + * base timestamp, 6 bytes per tag (3 for the name, 3 for the value). */ private byte[] row_key; @@ -309,6 +309,12 @@ public Deferred metricNameAsync() { return tsdb.metrics.getNameAsync(id); } + @Override + public byte[] metricUID() { + return Arrays.copyOfRange(row_key, Const.SALT_WIDTH(), + Const.SALT_WIDTH() + TSDB.metrics_width()); + } + @Override public Map getTags() { try { @@ -343,6 +349,11 @@ public Deferred> getAggregatedTagsAsync() { return Deferred.fromResult(empty); } + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } + @Override public List getTSUIDs() { return Collections.emptyList(); diff --git a/src/core/DataPoints.java b/src/core/DataPoints.java index cef3c69790..c9a4930992 100644 --- a/src/core/DataPoints.java +++ b/src/core/DataPoints.java @@ -38,6 +38,12 @@ public interface DataPoints extends Iterable { * @since 1.2 */ Deferred metricNameAsync(); + + /** + * @return the metric UID + * @since 2.3 + */ + byte[] metricUID(); /** * Returns the tags associated with these data points. @@ -93,6 +99,13 @@ public interface DataPoints extends Iterable { */ Deferred> getAggregatedTagsAsync(); + /** + * 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 + */ + List getAggregatedTagUids(); + /** * Returns a list of unique TSUIDs contained in the results * @return an empty list if there were no results, otherwise a list of TSUIDs diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 20ca6d51ac..4c98e3a596 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -49,8 +49,8 @@ final class IncomingDataPoints implements WritableDataPoints { private final TSDB tsdb; /** - * The row key. 3 bytes for the metric name, 4 bytes for the base timestamp, 6 - * bytes per tag (3 for the name, 3 for the value). + * The row key. Optional salt + 3 bytes for the metric name, 4 bytes for + * the base timestamp, 6 bytes per tag (3 for the name, 3 for the value). */ private byte[] row; @@ -422,6 +422,12 @@ public Deferred metricNameAsync() { return tsdb.metrics.getNameAsync(id); } + @Override + public byte[] metricUID() { + return Arrays.copyOfRange(row, Const.SALT_WIDTH(), + Const.SALT_WIDTH() + TSDB.metrics_width()); + } + public Map getTags() { try { return getTagsAsync().joinUninterruptibly(); @@ -450,6 +456,11 @@ public Deferred> getAggregatedTagsAsync() { return Deferred.fromResult(empty); } + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } + public List getTSUIDs() { return Collections.emptyList(); } diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index 42194aab3b..dcf8d608eb 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -285,6 +285,12 @@ public Deferred metricNameAsync() { return RowKey.metricNameAsync(tsdb, key); } + @Override + public byte[] metricUID() { + return Arrays.copyOfRange(key, Const.SALT_WIDTH(), + Const.SALT_WIDTH() + TSDB.metrics_width()); + } + public Map getTags() { try { return getTagsAsync().joinUninterruptibly(); @@ -314,6 +320,11 @@ public Deferred> getAggregatedTagsAsync() { return Deferred.fromResult(empty); } + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } + public List getTSUIDs() { return Collections.emptyList(); } diff --git a/src/core/Span.java b/src/core/Span.java index c49ee773cb..c9f31ba77f 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -86,6 +86,12 @@ public Deferred metricNameAsync() { return rows.get(0).metricNameAsync(); } + @Override + public byte[] metricUID() { + checkNotEmpty(); + return rows.get(0).metricUID(); + } + /** * @return the list of tag pairs for the rows in this span * @throws IllegalStateException if the span was empty @@ -121,6 +127,11 @@ public Deferred> getAggregatedTagsAsync() { final List empty = Collections.emptyList(); return Deferred.fromResult(empty); } + + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } /** @return the number of data points in this span, O(n) * Unfortunately we must walk the entire array for every row as there may be a diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 026cb0fedc..17625699ef 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -13,6 +13,7 @@ package net.opentsdb.core; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -327,6 +328,11 @@ public Deferred metricNameAsync() { spans.get(0).metricNameAsync(); } + @Override + public byte[] metricUID() { + return spans.isEmpty() ? new byte[] {} : spans.get(0).metricUID(); + } + public Map getTags() { try { return getTagsAsync().joinUninterruptibly(); @@ -388,6 +394,22 @@ public Deferred> getAggregatedTagsAsync() { return resolveAggTags(aggregated_tag_uids); } + + @Override + public List getAggregatedTagUids() { + if (aggregated_tag_uids != null) { + return new ArrayList(aggregated_tag_uids); + } + + if (spans.isEmpty()) { + return Collections.emptyList(); + } + + if (aggregated_tag_uids == null) { + computeTags(); + } + return new ArrayList(aggregated_tag_uids); + } public List getTSUIDs() { List tsuids = new ArrayList(spans.size()); diff --git a/src/query/expression/PostAggregatedDataPoints.java b/src/query/expression/PostAggregatedDataPoints.java index 872def9c49..e0787f62fd 100644 --- a/src/query/expression/PostAggregatedDataPoints.java +++ b/src/query/expression/PostAggregatedDataPoints.java @@ -75,6 +75,14 @@ public Deferred metricNameAsync() { } return base_data_points.metricNameAsync(); } + + @Override + public byte[] metricUID() { + if (alias != null) { + return new byte[] { }; + } + return base_data_points.metricUID(); + } @Override public Map getTags() { @@ -109,6 +117,14 @@ public Deferred> getAggregatedTagsAsync() { return base_data_points.getAggregatedTagsAsync(); } + @Override + public List getAggregatedTagUids() { + if (alias != null) { + return Collections.emptyList(); + } + return base_data_points.getAggregatedTagUids(); + } + @Override public List getTSUIDs() { return base_data_points.getTSUIDs(); diff --git a/test/core/TestRowSeq.java b/test/core/TestRowSeq.java index db9eb1b913..1a3792759b 100644 --- a/test/core/TestRowSeq.java +++ b/test/core/TestRowSeq.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.core; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -27,6 +28,7 @@ import org.hbase.async.Bytes; import org.hbase.async.KeyValue; +import org.hbase.async.Bytes.ByteMap; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -814,8 +816,118 @@ public void seekMsPastLastDp() throws Exception { it.next(); } + @Test + public void metricUID() throws Exception { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + + assertArrayEquals(new byte[] { 0, 0, 1 }, rs.metricUID()); + } + + @Test + public void metricUIDSalted() throws Exception { + setupSalt(); + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + + assertArrayEquals(new byte[] { 0, 0, 1 }, rs.metricUID()); + } + + @Test (expected = NullPointerException.class) + public void metricUIDKeyNotSet() throws Exception { + final RowSeq rs = new RowSeq(tsdb); + rs.metricUID(); + } + + @Test + public void getTagUids() throws Exception { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + + final ByteMap uids = rs.getTagUids(); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1 }, uids.firstKey()); + assertArrayEquals(new byte[] { 0, 0, 2 }, + uids.firstEntry().getValue()); + } + + @Test + public void getTagUidsSalted() throws Exception { + setupSalt(); + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + + final ByteMap uids = rs.getTagUids(); + assertEquals(1, uids.size()); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, + uids.firstEntry().getValue())); + } + + @Test (expected = NullPointerException.class) + public void getTagUidsNotSet() throws Exception { + final RowSeq rs = new RowSeq(tsdb); + rs.getTagUids(); + } + + @Test + public void getAggregatedTagUids() throws Exception { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + final KeyValue kv = makekv(qual12, + MockBase.concatByteArrays(val1, val2, ZERO)); + + final RowSeq rs = new RowSeq(tsdb); + rs.setRow(kv); + + assertEquals(0, rs.getAggregatedTagUids().size()); + } + + /** Shorthand to create a {@link KeyValue}. */ + public static KeyValue makekv(final byte[] qualifier, final byte[] value) { + if (Const.SALT_WIDTH() > 0) { + return new KeyValue(SALTED_KEY, FAMILY, qualifier, value); + } + return new KeyValue(KEY, FAMILY, qualifier, value); + } + /** Shorthand to create a {@link KeyValue}. */ - private static KeyValue makekv(final byte[] key, final byte[] qualifier, + public static KeyValue makekv(final byte[] key, final byte[] qualifier, final byte[] value) { return new KeyValue(key, FAMILY, qualifier, value); } diff --git a/test/core/TestSpan.java b/test/core/TestSpan.java index dbfc247ce5..b9c1b668f3 100644 --- a/test/core/TestSpan.java +++ b/test/core/TestSpan.java @@ -12,8 +12,10 @@ // see . package net.opentsdb.core; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -25,6 +27,7 @@ import org.hbase.async.Bytes; import org.hbase.async.KeyValue; +import org.hbase.async.Bytes.ByteMap; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -408,4 +411,120 @@ public void lastTimestampInRowMs() throws Exception { assertEquals(1356998400008L, Span.lastTimestampInRow((short) 3, kv)); } + + @Test + public void metricUID() throws Exception { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + + final Span span = new Span(tsdb); + span.addRow(new KeyValue(HOUR1, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + + assertEquals(2, span.size()); + + assertArrayEquals(new byte[] { 0, 0, 1 }, span.metricUID()); + } + + @Test + public void metricUIDSalted() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + + final Span span = new Span(tsdb); + final byte[] key = new byte[HOUR1.length + 1]; + System.arraycopy(HOUR1, 0, key, 1, HOUR1.length); + span.addRow(new KeyValue(key, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + + assertEquals(2, span.size()); + + assertArrayEquals(new byte[] { 0, 0, 1 }, span.metricUID()); + } + + @Test + public void getTagUids() { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + + final Span span = new Span(tsdb); + span.addRow(new KeyValue(HOUR1, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + + assertEquals(2, span.size()); + final ByteMap uids = span.getTagUids(); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1 }, uids.firstKey()); + assertArrayEquals(new byte[] { 0, 0, 2 }, + uids.firstEntry().getValue()); + } + + @Test + public void getTagUidsSalted() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + + final Span span = new Span(tsdb); + final byte[] key = new byte[HOUR1.length + 1]; + System.arraycopy(HOUR1, 0, key, 1, HOUR1.length); + span.addRow(new KeyValue(key, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + + assertEquals(2, span.size()); + final ByteMap uids = span.getTagUids(); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1 }, uids.firstKey()); + assertArrayEquals(new byte[] { 0, 0, 2 }, + uids.firstEntry().getValue()); + } + + @Test (expected = IllegalStateException.class) + public void getTagUidsNotSet() { + final Span span = new Span(tsdb); + span.getTagUids(); + } + + @Test + public void getAggregatedTagUids() { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); + + final Span span = new Span(tsdb); + span.addRow(new KeyValue(HOUR1, FAMILY, qual12, + MockBase.concatByteArrays(val1, val2, ZERO))); + + assertEquals(2, span.size()); + final List uids = span.getAggregatedTagUids(); + assertEquals(0, uids.size()); + } + + @Test + public void getAggregatedTagUidsNotSet() { + final Span span = new Span(tsdb); + assertTrue(span.getAggregatedTagUids().isEmpty()); + } + } diff --git a/test/core/TestSpanGroup.java b/test/core/TestSpanGroup.java index 8b0d9d848e..f24458c4c9 100644 --- a/test/core/TestSpanGroup.java +++ b/test/core/TestSpanGroup.java @@ -12,15 +12,16 @@ // see . package net.opentsdb.core; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.List; import net.opentsdb.utils.Config; -import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.HBaseClient; import org.junit.Before; @@ -45,6 +46,19 @@ public void before() { tsdb = PowerMockito.mock(TSDB.class); } + @Test + public void metricUID() throws Exception { + final Span span = mock(Span.class); + when(span.metricUID()).thenReturn(new byte[] { 0, 0, 1 }); + + final SpanGroup group = PowerMockito.spy(new SpanGroup(tsdb, start_ts, + end_ts, null, false, Aggregators.SUM, 0, null)); + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + assertArrayEquals(new byte[] { 0, 0, 1 }, group.metricUID()); + } + @Test public void getTagUids() throws Exception { final ByteMap uids = new ByteMap(); @@ -59,9 +73,9 @@ public void getTagUids() throws Exception { final ByteMap uids_read = group.getTagUids(); assertEquals(1, uids_read.size()); - assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 1 }, uids_read.firstKey())); - assertEquals(0, Bytes.memcmp(new byte[] { 0, 0, 2 }, - uids_read.firstEntry().getValue())); + assertArrayEquals(new byte[] { 0, 0, 1 }, uids_read.firstKey()); + assertArrayEquals(new byte[] { 0, 0, 2 }, + uids_read.firstEntry().getValue()); } @Test @@ -94,4 +108,53 @@ public void getTagUidsNoSpans() throws Exception { final ByteMap uids_read = group.getTagUids(); assertEquals(0, uids_read.size()); } + + @Test + public void getAggregatedTagUidsNotAgged() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 2 }); + final Span span = mock(Span.class); + when(span.getTagUids()).thenReturn(uids); + + final SpanGroup group = PowerMockito.spy(new SpanGroup(tsdb, start_ts, + end_ts, null, false, Aggregators.SUM, 0, null)); + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final List uids_read = group.getAggregatedTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getAggregatedTagUids() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 2 }); + final Span span = mock(Span.class); + when(span.getTagUids()).thenReturn(uids); + + final ByteMap uids2 = new ByteMap(); + uids2.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 0, 3 }); + final Span span2 = mock(Span.class); + when(span2.getTagUids()).thenReturn(uids2); + + final SpanGroup group = PowerMockito.spy(new SpanGroup(tsdb, start_ts, + end_ts, null, false, Aggregators.SUM, 0, null)); + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + spans.add(span2); + + final List uids_read = group.getAggregatedTagUids(); + assertEquals(1, uids_read.size()); + assertArrayEquals(new byte[] { 0, 0, 1 }, uids_read.get(0)); + } + + @Test + public void getAggregatedTagUidsNoSpans() throws Exception { + final SpanGroup group = new SpanGroup(tsdb, start_ts, end_ts, null, + false, Aggregators.SUM, 0, null); + + final List uids_read = group.getAggregatedTagUids(); + assertEquals(0, uids_read.size()); + } + } From cb9d50ad8d86b4ea8a6fb901c22454c4ce690bef Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 4 Nov 2015 14:52:00 -0800 Subject: [PATCH 306/826] Add the SCALAR fill policy and Jacksonify the FillPolicay class for serdes. Add the NumericFillPolicy class for further serdes (that can take scalar values) for use with expressions. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/core/FillPolicy.java | 11 +- src/query/expression/NumericFillPolicy.java | 176 ++++++++++ .../expression/TestNumericFillPolicy.java | 304 ++++++++++++++++++ test/tsd/TestQueryRpc.java | 3 +- 5 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 src/query/expression/NumericFillPolicy.java create mode 100644 test/query/expression/TestNumericFillPolicy.java diff --git a/Makefile.am b/Makefile.am index 1b07e4c556..f207613c96 100644 --- a/Makefile.am +++ b/Makefile.am @@ -82,6 +82,7 @@ tsdb_SRC := \ src/query/expression/ExpressionTree.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ + src/query/expression/NumericFillPolicy.java \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ @@ -248,6 +249,7 @@ test_SRC := \ test/query/expression/TestExpressionTree.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ + test/query/expression/TestNumericFillPolicy.java \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/expression/TestScale.java \ diff --git a/src/core/FillPolicy.java b/src/core/FillPolicy.java index 7cba9bc5d9..09c145f284 100644 --- a/src/core/FillPolicy.java +++ b/src/core/FillPolicy.java @@ -12,6 +12,9 @@ // see . package net.opentsdb.core; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** * Specification of how to deal with missing intervals when downsampling. * @since 2.2 @@ -20,7 +23,8 @@ public enum FillPolicy { NONE("none"), ZERO("zero"), NOT_A_NUMBER("nan"), - NULL("null"); + NULL("null"), + SCALAR("scalar"); // The user-friendly name of this policy. private final String name; @@ -33,6 +37,7 @@ public enum FillPolicy { * Get this fill policy's user-friendly name. * @return this fill policy's user-friendly name. */ + @JsonValue public String getName() { return name; } @@ -42,7 +47,9 @@ public String getName() { * @param name The user-friendly name of a fill policy. * @return an instance of {@link FillPolicy}, or {@code null} if the name * does not match any instance. + * @throws IllegalArgumentException if the name doesn't match a policy */ + @JsonCreator public static FillPolicy fromString(final String name) { for (final FillPolicy policy : FillPolicy.values()) { if (policy.name.equalsIgnoreCase(name)) { @@ -50,7 +57,7 @@ public static FillPolicy fromString(final String name) { } } - return null; + throw new IllegalArgumentException("Unrecognized fill policy: " + name); } } diff --git a/src/query/expression/NumericFillPolicy.java b/src/query/expression/NumericFillPolicy.java new file mode 100644 index 0000000000..cf0f4673d7 --- /dev/null +++ b/src/query/expression/NumericFillPolicy.java @@ -0,0 +1,176 @@ +// 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +import net.opentsdb.core.FillPolicy; + +/** + * POJO for serdes of fill policies. It allows the user to pick either policies + * with default values or a scalar that can be supplied with any number. + * @since 2.3 + */ +@JsonDeserialize(builder = NumericFillPolicy.Builder.class) +public class NumericFillPolicy { + + /** The fill policy to use. This is required */ + private FillPolicy policy; + + /** The value to store with the fill policy */ + private double value; + + /** + * CTor to set the policy. Also calls {@link #validate()} + * @param policy The policy to set. + */ + public NumericFillPolicy(final FillPolicy policy) { + this.policy = policy; + validate(); + } + + /** + * CTor to set the policy and value. Also calls {@link #validate()} + * @param policy The name of the fill policy + * @param value The value to use when filling + * @throws IllegalArgumentException if the policy and value don't gel together + */ + public NumericFillPolicy(final FillPolicy policy, final double value) { + this.policy = policy; + this.value = value; + validate(); + } + + @Override + public String toString() { + return "policy=" + policy + ", value=" + value; + } + + /** @returns a NumericFillPolicy builder */ + public static Builder Builder() { + return new Builder(); + } + + /** + * A builder class for deserialization via Jackson + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private FillPolicy policy; + @JsonProperty + private double value; + + public Builder setPolicy(FillPolicy policy) { + this.policy = policy; + return this; + } + + public Builder setValue(double value) { + this.value = value; + return this; + } + + public NumericFillPolicy build() { + return new NumericFillPolicy(policy, value); + } + } + + @Override + public int hashCode() { + return Objects.hashCode(policy, value); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof NumericFillPolicy)) { + return false; + } + final NumericFillPolicy nfp = (NumericFillPolicy)obj; + return Objects.equal(policy, nfp.policy) && + Objects.equal(value, nfp.value); + } + + /** @return the fill policy */ + public FillPolicy getPolicy() { + return policy; + } + + /** @param policy the fill policy to use */ + public void setPolicy(final FillPolicy policy) { + this.policy = policy; + } + + /** @return the value to use when filling */ + public double getValue() { + return value; + } + + /** @param value the value to use when filling */ + public void setValue(final double value) { + this.value = value; + } + + /** + * Makes sure the policy name and value are a suitable combination. If one + * or the other is missing then we set the other with the proper value. + * @throws IllegalArgumentException if the combination is bad + */ + public void validate() { + if (policy == null) { + if (value == 0) { + policy = FillPolicy.ZERO; + } else if (Double.isNaN(value)) { + policy = FillPolicy.NOT_A_NUMBER; + } else { + policy = FillPolicy.SCALAR; + } + } else { + switch (policy) { + case NONE: + case NOT_A_NUMBER: + if (value != 0 && !Double.isNaN(value)) { + throw new IllegalArgumentException( + "The value for NONE and NAN must be NaN"); + } + value = Double.NaN; + break; + case ZERO: + if (value != 0) { + throw new IllegalArgumentException("The value for ZERO must be 0"); + } + value = 0; + break; + case NULL: + if (value != 0 && !Double.isNaN(value)) { + throw new IllegalArgumentException("The value for NULL must be 0"); + } + value = Double.NaN; + break; + case SCALAR: // it CAN be zero + break; + } + } + } +} diff --git a/test/query/expression/TestNumericFillPolicy.java b/test/query/expression/TestNumericFillPolicy.java new file mode 100644 index 0000000000..2b83d68b5c --- /dev/null +++ b/test/query/expression/TestNumericFillPolicy.java @@ -0,0 +1,304 @@ +// 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.assertTrue; +import static org.junit.Assert.fail; +import net.opentsdb.core.FillPolicy; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +public class TestNumericFillPolicy { + + @Test + public void builder() throws Exception { + NumericFillPolicy nfp = NumericFillPolicy.Builder() + .setPolicy(FillPolicy.NOT_A_NUMBER).build(); + assertEquals(FillPolicy.NOT_A_NUMBER, nfp.getPolicy()); + assertTrue(Double.isNaN((Double)nfp.getValue())); + + nfp = NumericFillPolicy.Builder() + .setPolicy(null).build(); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + assertEquals(0, nfp.getValue(), 0.0001); + } + + @Test + public void policyCtor() throws Exception { + NumericFillPolicy nfp = new NumericFillPolicy(FillPolicy.NONE); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NONE, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NOT_A_NUMBER, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.NULL); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NULL, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.ZERO); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.SCALAR); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + } + + @Test + public void policyAndValueCtor() throws Exception { + NumericFillPolicy nfp = new NumericFillPolicy(FillPolicy.NONE, Double.NaN); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NONE, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER, Double.NaN); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NOT_A_NUMBER, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.NULL, 0); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NULL, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.ZERO, 0); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, 0); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.SCALAR, 42); + assertEquals(42, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.SCALAR, 0); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.SCALAR, Double.NaN); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.SCALAR, 42.5); + assertEquals(42.5, (Double)nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + // defaults from value + nfp = new NumericFillPolicy(null, Double.NaN); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NOT_A_NUMBER, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, 42); + assertEquals(42, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, 42.5); + assertEquals(42.5, (Double)nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, -42.5); + assertEquals(-42.5, (Double)nfp.getValue(), 0.0001); + assertEquals(FillPolicy.SCALAR, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, Double.NaN); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NOT_A_NUMBER, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, 0); + assertEquals(0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, 0.0); + assertEquals(0.0, (Double)nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, -0.0); + assertEquals(-0.0, (Double)nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(null, -0); + assertEquals(-0, nfp.getValue(), 0.0001); + assertEquals(FillPolicy.ZERO, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER, 0); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NOT_A_NUMBER, nfp.getPolicy()); + + nfp = new NumericFillPolicy(FillPolicy.NULL, Double.NaN); + assertTrue(Double.isNaN((Double)nfp.getValue())); + assertEquals(FillPolicy.NULL, nfp.getPolicy()); + + // inappropriate combos + try { + nfp = new NumericFillPolicy(FillPolicy.ZERO, 42); + fail("expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + nfp = new NumericFillPolicy(FillPolicy.NONE, 42); + fail("expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + nfp = new NumericFillPolicy(FillPolicy.NULL, 42); + fail("expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + try { + nfp = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER, 42); + fail("expected an IllegalArgumentException"); + } catch (IllegalArgumentException iae) { } + + } + + @Test + public void serdes() throws Exception { + + NumericFillPolicy ser_nfp = new NumericFillPolicy(FillPolicy.NONE); + String json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"none\"")); + assertTrue(json.contains("\"value\":\"NaN\"")); + NumericFillPolicy des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + ser_nfp = new NumericFillPolicy(FillPolicy.ZERO); + json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"zero\"")); + assertTrue(json.contains("\"value\":0")); + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + ser_nfp = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER); + json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"nan\"")); + assertTrue(json.contains("\"value\":\"NaN\"")); + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + ser_nfp = new NumericFillPolicy(FillPolicy.NULL); + json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"null\"")); + assertTrue(json.contains("\"value\":\"NaN\"")); + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + ser_nfp = new NumericFillPolicy(FillPolicy.SCALAR, 42); + json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"scalar\"")); + assertTrue(json.contains("\"value\":42")); + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + ser_nfp = new NumericFillPolicy(FillPolicy.SCALAR, 42.5); + json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"scalar\"")); + assertTrue(json.contains("\"value\":42.5")); + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + ser_nfp = new NumericFillPolicy(FillPolicy.SCALAR, -42.5); + json = JSON.serializeToString(ser_nfp); + assertTrue(json.contains("\"policy\":\"scalar\"")); + assertTrue(json.contains("\"value\":-42.5")); + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertTrue(des_nfp != ser_nfp); + assertTrue(des_nfp.equals(ser_nfp)); + + json = "{\"policy\":\"zero\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.ZERO, des_nfp.getPolicy()); + assertEquals(0, des_nfp.getValue(), 0.0001); + + json = "{\"policy\":\"nan\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.NOT_A_NUMBER, des_nfp.getPolicy()); + assertTrue(Double.isNaN((Double)des_nfp.getValue())); + + json = "{\"policy\":\"scalar\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.SCALAR, des_nfp.getPolicy()); + assertEquals(0, des_nfp.getValue(), 0.0001); + + json = "{\"policy\":\"none\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.NONE, des_nfp.getPolicy()); + assertTrue(Double.isNaN((Double)des_nfp.getValue())); + + json = "{\"policy\":\"null\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.NULL, des_nfp.getPolicy()); + assertTrue(Double.isNaN((Double)des_nfp.getValue())); + + json = "{\"policy\":\"scalar\",\"value\":42}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.SCALAR, des_nfp.getPolicy()); + assertEquals(42, des_nfp.getValue(), 0.0001); + + json = "{\"policy\":\"scalar\",\"value\":\"42\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.SCALAR, des_nfp.getPolicy()); + assertEquals(42, des_nfp.getValue(), 0.0001); + + json = "{\"policy\":\"scalar\",\"value\":42.5}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.SCALAR, des_nfp.getPolicy()); + assertEquals(42.5, (Double)des_nfp.getValue(), 0.0001); + + json = "{\"policy\":\"nan\",\"value\":NaN}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.NOT_A_NUMBER, des_nfp.getPolicy()); + assertTrue(Double.isNaN((Double)des_nfp.getValue())); + + json = "{\"policy\":\"scalar\",\"value\":0}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.SCALAR, des_nfp.getPolicy()); + assertEquals(0, des_nfp.getValue(), 0.0001); + + json = "{\"policy\":\"scalar\",\"value\":0.0}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + assertEquals(FillPolicy.SCALAR, des_nfp.getPolicy()); + assertEquals(0.0, (Double)des_nfp.getValue(), 0.0001); + + try { + json = "{\"policy\":\"unknown\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + fail("Expected a IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + json = "{\"policy\":\"scalar\",value\":\"foo\"}"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + fail("Expected a IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + json = "{\"policy\":\"badjson"; + des_nfp = JSON.parseToObject(json, NumericFillPolicy.class); + fail("Expected a IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } +} diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index cce155c244..b4f0f5caa1 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -520,8 +520,9 @@ public void executeWithBadDSFill() throws Exception { rpc.execute(tsdb, query); fail("expected BadRequestException"); } catch (final BadRequestException exn) { + System.out.println(exn.getMessage()); assertTrue(exn.getMessage().startsWith( - "No such fill policy: 'badbadbad': must be one of:")); + "Unrecognized fill policy: badbadbad")); } } From 131f00e4cda6e8d6bb93d4d4a403c7c9c2373618 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 4 Nov 2015 15:11:02 -0800 Subject: [PATCH 307/826] Add the TSSubQuery.getFilterTagks() to return the list of tag keys the query deals with. Used for expressions. Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 16 ++++++++++++++++ test/core/TestTSSubQuery.java | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 7438f7d303..faae314fca 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -20,6 +20,7 @@ import java.util.NoSuchElementException; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.utils.ByteSet; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; @@ -273,6 +274,21 @@ public List getFilters() { return filters; } + /** @return the unique set of tagks from the filters. May be null if no filters + * were set. Must make sure to resolve the string tag to UIDs in the filter first. + * @since 2.3 + */ + public ByteSet getFilterTagKs() { + if (filters == null || filters.isEmpty()) { + return null; + } + final ByteSet tagks = new ByteSet(); + for (final TagVFilter filter : filters) { + tagks.add(filter.getTagkBytes()); + } + return tagks; + } + /** @param aggregator the name of an aggregation function */ public void setAggregator(String aggregator) { this.aggregator = aggregator; diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 3336f18827..1b6ac0216e 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.core; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -29,6 +30,7 @@ import net.opentsdb.query.filter.TagVWildcardFilter; import org.junit.Test; +import org.powermock.reflect.Whitebox; public final class TestTSSubQuery { @@ -185,6 +187,19 @@ public void validateWithGroupByFilter() { assertEquals(300000, sub.downsampleInterval()); } + @Test + public void getFilterTagks() { + final TagVFilter filter = TagVFilter.Builder() + .setFilter("*nari").setType("wildcard").setTagk("host").build(); + Whitebox.setInternalState(filter, "tagk_bytes", new byte[] { 0, 0, 1 }); + TSSubQuery sub = getMetricForValidate(); + sub.setFilters(Arrays.asList(filter)); + sub.validateAndSetQuery(); + + assertEquals(1, sub.getFilterTagKs().size()); + assertArrayEquals(new byte[] { 0, 0, 1 }, sub.getFilterTagKs().iterator().next()); + } + // NOTE: Each of the hash and equals tests should make sure that we the code // doesn't change after validation. From 5c6d6a41a090a3429858205fc2da31d7f2ed3e7c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 4 Nov 2015 18:06:39 -0800 Subject: [PATCH 308/826] Add more UIDs to the base TSDB test class, UIDs for A to Z so that we have more to play around with. Signed-off-by: Chris Larsen --- test/core/BaseTsdbTest.java | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index d51efd5601..2f83b61243 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -59,6 +59,25 @@ @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, HashedWheelTimer.class, Scanner.class, Const.class }) public class BaseTsdbTest { + /** A list of UIDs from A to Z for unit testing UIDs values */ + public static final Map METRIC_UIDS = + new HashMap(26); + public static final Map TAGK_UIDS = + new HashMap(26); + public static final Map TAGV_UIDS = + new HashMap(26); + static { + char letter = 'A'; + int uid = 10; + for (int i = 0; i < 26; i++) { + METRIC_UIDS.put(Character.toString(letter), + UniqueId.longToUID(uid, TSDB.metrics_width())); + TAGK_UIDS.put(Character.toString(letter), + UniqueId.longToUID(uid, TSDB.tagk_width())); + TAGV_UIDS.put(Character.toString(letter++), + UniqueId.longToUID(uid++, TSDB.tagv_width())); + } + } public static final String METRIC_STRING = "sys.cpu.user"; public static final byte[] METRIC_BYTES = new byte[] { 0, 0, 1 }; @@ -124,6 +143,7 @@ public void before() throws Exception { tags.put(TAGK_STRING, TAGV_STRING); } + /** Adds the static UIDs to the metrics UID mock object */ void setupMetricMaps() { when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); when(metrics.getIdAsync(METRIC_STRING)) @@ -174,8 +194,32 @@ public Deferred answer(InvocationOnMock invocation) when(metrics.getIdAsync(NSUN_METRIC)) .thenReturn(Deferred.fromError(nsun)); when(metrics.getOrCreateId(NSUN_METRIC)).thenThrow(nsun); + + // Iterate over the metric UIDs and handle both forward and reverse + for (final Map.Entry uid : METRIC_UIDS.entrySet()) { + when(metrics.getId(uid.getKey())).thenReturn(uid.getValue()); + when(metrics.getIdAsync(uid.getKey())) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid.getValue()); + } + }); + when(metrics.getOrCreateId(uid.getKey())) + .thenReturn(uid.getValue()); + when(metrics.getNameAsync(uid.getValue())) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid.getKey()); + } + }); + } } + /** Adds the static UIDs to the tag keys UID mock object */ void setupTagkMaps() { when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); @@ -228,8 +272,32 @@ public Deferred answer(InvocationOnMock invocation) .thenThrow(nsun); when(tag_names.getIdAsync(NSUN_TAGK)) .thenReturn(Deferred.fromError(nsun)); + + // Iterate over the tagk UIDs and handle both forward and reverse + for (final Map.Entry uid : TAGK_UIDS.entrySet()) { + when(tag_names.getId(uid.getKey())).thenReturn(uid.getValue()); + when(tag_names.getIdAsync(uid.getKey())) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid.getValue()); + } + }); + when(tag_names.getOrCreateId(uid.getKey())) + .thenReturn(uid.getValue()); + when(tag_names.getNameAsync(uid.getValue())) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid.getKey()); + } + }); + } } + /** Adds the static UIDs to the tag values UID mock object */ void setupTagvMaps() { when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); @@ -269,6 +337,29 @@ public Deferred answer(InvocationOnMock invocation) when(tag_values.getId(NSUN_TAGV)).thenThrow(nsun); when(tag_values.getIdAsync(NSUN_TAGV)) .thenReturn(Deferred.fromError(nsun)); + + // Iterate over the tagv UIDs and handle both forward and reverse + for (final Map.Entry uid : TAGV_UIDS.entrySet()) { + when(tag_values.getId(uid.getKey())).thenReturn(uid.getValue()); + when(tag_values.getIdAsync(uid.getKey())) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid.getValue()); + } + }); + when(tag_values.getOrCreateId(uid.getKey())) + .thenReturn(uid.getValue()); + when(tag_values.getNameAsync(uid.getValue())) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid.getKey()); + } + }); + } } // ----------------- // From 13a6ad0f02b84443b89b0d72980663adebdf2235 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 4 Nov 2015 14:57:26 -0800 Subject: [PATCH 309/826] Add the ITimeSyncedIterator.java interface for time synchronized iterator implementations. Add the ExpressionDataPoint class to store iterators, values and aggregations of expressions. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/query/expression/ExpressionDataPoint.java | 227 ++++++++++++++++++ src/query/expression/ITimeSyncedIterator.java | 75 ++++++ 3 files changed, 303 insertions(+) create mode 100644 src/query/expression/ExpressionDataPoint.java create mode 100644 src/query/expression/ITimeSyncedIterator.java diff --git a/Makefile.am b/Makefile.am index f207613c96..16698f5269 100644 --- a/Makefile.am +++ b/Makefile.am @@ -82,6 +82,7 @@ tsdb_SRC := \ src/query/expression/ExpressionTree.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ + src/query/expression/ITimeSyncedIterator.java \ src/query/expression/NumericFillPolicy.java \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ diff --git a/src/query/expression/ExpressionDataPoint.java b/src/query/expression/ExpressionDataPoint.java new file mode 100644 index 0000000000..216546869f --- /dev/null +++ b/src/query/expression/ExpressionDataPoint.java @@ -0,0 +1,227 @@ +// 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.HashSet; +import java.util.Set; + +import org.hbase.async.Bytes.ByteMap; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.utils.ByteSet; + +/** + * Contains the information for a series that has been processed through an + * expression iterator. Each time a metric data point series set is added we + * add the metric and compute the tag sets. + *

    + * As the iterator progresses, it will call into the {@link #reset} methods. + * @since 2.3 + */ +public class ExpressionDataPoint implements DataPoint { + + /** A list of metric UIDs wrapped up into this expression result */ + private final ByteSet metric_uids; + + /** The list of tag key/value pairs common to all series in this expression */ + private final ByteMap tags; + + /** The list of aggregated tag keys common to all series in this expression */ + private final ByteSet aggregated_tags; + + /** The list of TSUIDs from all series in this expression */ + private final Set tsuids; + + /** The size of the aggregated results. + * TODO - this is simply the size of the first series added. We need a way + * to compute this properly. + */ + private long size; + + /** The total number of raw data points in all series */ + private long raw_size; + + /** The data point overwritten each time through the iterator */ + private final MutableDataPoint dp; + + /** + * Default ctor that simply sets up new objects for all internal fields. + * TODO - lazily initialize the field to avoid unused objects + */ + public ExpressionDataPoint() { + metric_uids = new ByteSet(); + tags = new ByteMap(); + aggregated_tags = new ByteSet(); + tsuids = new HashSet(); + dp = new MutableDataPoint(); + } + + /** + * Ctor that sets up the meta data maps and initializes an empty dp + * @param dps The data point to pull meta from + */ + @SuppressWarnings("unchecked") + public ExpressionDataPoint(final DataPoints dps) { + metric_uids = new ByteSet(); + metric_uids.add(dps.metricUID()); + tags = (ByteMap) dps.getTagUids().clone(); + aggregated_tags = new ByteSet(); + for (final byte[] tagk : dps.getAggregatedTagUids()) { + aggregated_tags.add(tagk); + } + tsuids = new HashSet(dps.getTSUIDs()); + // TODO - restore when these are faster + //size = dps.size(); + //raw_size = dps.aggregatedSize(); + dp = new MutableDataPoint(); + dp.reset(Long.MAX_VALUE, Double.NaN); + } + + /** + * Ctor that clones the meta data of the existing dps and sets up an empty value + * @param dps The data point to pull meta from + */ + @SuppressWarnings("unchecked") + public ExpressionDataPoint(final ExpressionDataPoint dps) { + metric_uids = new ByteSet(); + metric_uids.addAll(dps.metric_uids); + tags = (ByteMap) dps.tags.clone(); + aggregated_tags = new ByteSet(); + aggregated_tags.addAll(dps.aggregated_tags); + tsuids = new HashSet(dps.tsuids); + size = dps.size; + raw_size = dps.raw_size; + dp = new MutableDataPoint(); + dp.reset(Long.MAX_VALUE, Double.NaN); + } + + /** + * Add another metric series to this collection, computing the tag and + * agg intersections and incrementing the size. + * @param dps The series to add + */ + public void add(final DataPoints dps) { + metric_uids.add(dps.metricUID()); + + // TODO - tags intersection + + for (final byte[] tagk : dps.getAggregatedTagUids()) { + aggregated_tags.add(tagk); + } + + tsuids.addAll(dps.getTSUIDs()); + // TODO - this ain't right. We need to number of dps emitted from HERE. For + // now we'll just take the first dps size. If it's downsampled then this + // will be accurate. + //size += dps.size(); + // TODO - restore when this is faster + //raw_size += dps.aggregatedSize(); + } + + /** + * Add another metric series to this collection, computing the tag and + * agg intersections and incrementing the size. + * @param dps The series to add + */ + public void add(final ExpressionDataPoint dps) { + metric_uids.addAll(dps.metric_uids); + + // TODO - tags intersection + + aggregated_tags.addAll(dps.aggregated_tags); + + tsuids.addAll(dps.tsuids); + // TODO - this ain't right. We need to number of dps emitted from HERE. For + // now we'll just take the first dps size. If it's downsampled then this + // will be accurate. + //size += dps.size(); + raw_size += dps.raw_size; + } + + /** @return the metric UIDs */ + public ByteSet metricUIDs() { + return metric_uids; + } + + /** @return the list of common tag pairs in the series */ + public ByteMap tags() { + return tags; + } + + /** @return the list of aggregated tags */ + public ByteSet aggregatedTags() { + return aggregated_tags; + } + + /** @return the list of TSUIDs aggregated into this series */ + public Set tsuids() { + return tsuids; + } + + /** @return the aggregated number of data points in this series */ + public long size() { + return size; + } + + /** @return the number of raw data points in this series */ + public long rawSize() { + return raw_size; + } + + /** + * Stores a Double data point + * @param timestamp The timestamp + * @param value The value + */ + public void reset(final long timestamp, final double value) { + dp.reset(timestamp, value); + } + + /** + * Stores a data point pulled from the given data point interface + * @param dp the data point to read from + */ + public void reset(final DataPoint dp) { + this.dp.reset(dp); + } + + // DataPoint implementations + + @Override + public long timestamp() { + return dp.timestamp(); + } + + @Override + public boolean isInteger() { + return dp.isInteger(); + } + + @Override + public long longValue() { + return dp.longValue(); + } + + @Override + public double doubleValue() { + return dp.doubleValue(); + } + + @Override + public double toDouble() { + return dp.toDouble(); + } + +} diff --git a/src/query/expression/ITimeSyncedIterator.java b/src/query/expression/ITimeSyncedIterator.java new file mode 100644 index 0000000000..22efbc55ae --- /dev/null +++ b/src/query/expression/ITimeSyncedIterator.java @@ -0,0 +1,75 @@ +// 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 net.opentsdb.utils.ByteSet; + +/** + * An interface for expressions or queries that operate across time series + * and require point-by-point timestamp synchronization. + * @since 2.3 + */ +public interface ITimeSyncedIterator { + + /** @return true if any of the series in the set has another value */ + public boolean hasNext(); + + /** + * @param timestamp The timestamp to fastforward to + * @return The data point array for the given timestamp. Implementations + * may throw an exception if the timestamp is invliad or they may return an + * empty array. + */ + public ExpressionDataPoint[] next(final long timestamp); + + /** + * @return the next timestamp available in this set. + */ + public long nextTimestamp(); + + /** @return the number of series in this set */ + public int size(); + + /** @return an array of the emitters populated during iteration */ + public ExpressionDataPoint[] values(); + + /** @param index the index to null. Nulls the given object so we don't use it + * in timestamps. + */ + public void nullIterator(final int index); + + /** @return the index in the ExpressionIterator */ + public int getIndex(); + + /** @param the index in the ExpressionIterator */ + public void setIndex(final int index); + + /** @return the ID of this set given by the user */ + public String getId(); + + /** @return a set of unique tag key UIDs from the filter list. If no filters + * were defined then the set may be empty. */ + public ByteSet getQueryTagKs(); + + /** @param A fill policy for the iterator. Iterators should implement a default */ + public void setFillPolicy(final NumericFillPolicy policy); + + /** @return the fill policy for the iterator */ + public NumericFillPolicy getFillPolicy(); + + /** @return a copy of the iterator. This should return references to + * underlying data objects but not necessarily copy all of the underlying + * data (to avoid memory explosions). This is useful for creating other + * iterators that operate over the same data. */ + public ITimeSyncedIterator getCopy(); +} From 1d94677443ea2dc42ea6dee91415e343ce6514ec Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 4 Nov 2015 17:46:46 -0800 Subject: [PATCH 310/826] Add the TimeSyncedIterator for working with expressions. Also add a base expression test class that stores data in MockBase and sets up a number of queries for testing Signed-off-by: Chris Larsen --- Makefile.am | 4 + src/query/expression/TimeSyncedIterator.java | 238 +++++++ .../BaseTimeSyncedIteratorTest.java | 649 ++++++++++++++++++ .../expression/TestTimeSyncedIterator.java | 504 ++++++++++++++ 4 files changed, 1395 insertions(+) create mode 100644 src/query/expression/TimeSyncedIterator.java create mode 100644 test/query/expression/BaseTimeSyncedIteratorTest.java create mode 100644 test/query/expression/TestTimeSyncedIterator.java diff --git a/Makefile.am b/Makefile.am index 16698f5269..0ce731dab0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -76,6 +76,7 @@ tsdb_SRC := \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ src/query/expression/Expression.java \ + src/query/expression/ExpressionDataPoint.java \ src/query/expression/ExpressionFactory.java \ src/query/expression/ExpressionReader.java \ src/query/expression/Expressions.java \ @@ -87,6 +88,7 @@ tsdb_SRC := \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ + src/query/expression/TimeSyncedIterator.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ @@ -243,6 +245,7 @@ test_SRC := \ test/meta/TestTSMeta.java \ test/meta/TestTSUIDQuery.java \ test/meta/TestUIDMeta.java \ + test/query/expression/BaseTimeSyncedIteratorTest.java \ test/query/expression/TestAbsolute.java \ test/query/expression/TestExpressionFactory.java \ test/query/expression/TestExpressionReader.java \ @@ -254,6 +257,7 @@ test_SRC := \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/expression/TestScale.java \ + test/query/expression/TestTimeSyncedIterator.java \ test/query/filter/TestTagVFilter.java \ test/query/filter/TestTagVLiteralOrFilter.java \ test/query/filter/TestTagVNotKeyFilter.java \ diff --git a/src/query/expression/TimeSyncedIterator.java b/src/query/expression/TimeSyncedIterator.java new file mode 100644 index 0000000000..58d562b0cd --- /dev/null +++ b/src/query/expression/TimeSyncedIterator.java @@ -0,0 +1,238 @@ +// 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 net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.FillPolicy; +import net.opentsdb.core.SeekableView; +import net.opentsdb.utils.ByteSet; + +/** + * Holds the results of a sub query (a single metric) and iterates over each + * resultant series in lock-step for expression evaluation. + * @since 2.3 + */ +public class TimeSyncedIterator implements ITimeSyncedIterator { + + /** The name of this sub query given by the user */ + private final String id; + + /** The set of tag keys issued with the query */ + private final ByteSet query_tagks; + + /** The data point interfaces fetched from storage */ + private final DataPoints[] dps; + + /** The current value used for iterating */ + private final DataPoint[] current_values; + + /** References to the MutableDataObjects the ExpressionIterator will read */ + private final ExpressionDataPoint[] emitter_values; + + /** A list of the iterators used for fetching the next value */ + private final SeekableView[] iterators; + + /** Set by the ExpressionIterator when it computes the intersection */ + private int index; + + /** A policy to use for emitting values when a timestamp is missing data */ + private NumericFillPolicy fill_policy; + + /** + * Instantiates an iterator based on the results of a TSSubQuery. + * This will setup the emitters so it's safe to call {@link #values()} + * @param id The name of the query. + * @param query_tagks The set of tags used in filters on the query. + * @param dps The data points fetched from storage. + * @throws IllegalArgumentException if one of the parameters is null or the ID + * is empty + */ + public TimeSyncedIterator(final String id, final ByteSet query_tagks, + final DataPoints[] dps) { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("Missing ID string"); + } + if (dps == null) { + // it's ok for these to be empty, but they canna be null ya ken? + throw new IllegalArgumentException("Missing data points"); + } + this.id = id; + this.query_tagks = query_tagks; + this.dps = dps; + // TODO - load from a default or something + fill_policy = new NumericFillPolicy(FillPolicy.ZERO); + current_values = new DataPoint[dps.length]; + emitter_values = new ExpressionDataPoint[dps.length]; + iterators = new SeekableView[dps.length]; + setupEmitters(); + } + + /** + * A copy constructor that loads from an existing iterator. + * @param iterator The iterator to load from + */ + private TimeSyncedIterator(final TimeSyncedIterator iterator) { + id = iterator.id; + query_tagks = iterator.query_tagks; // sharing is ok here + dps = iterator.dps; // TODO ?? OK? + fill_policy = iterator.fill_policy; + current_values = new DataPoint[dps.length]; + emitter_values = new ExpressionDataPoint[dps.length]; + iterators = new SeekableView[dps.length]; + setupEmitters(); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("TimeSyncedIterator(id=") + .append(id) + .append(", index=") + .append(index) + .append(", dpsSize=") + .append(dps.length) + .append(")"); + return buf.toString(); + } + + @Override + public int size() { + return dps.length; + } + + @Override + public boolean hasNext() { + for (final DataPoint dp : current_values) { + if (dp != null) { + return true; + } + } + return false; + } + + @Override + public ExpressionDataPoint[] next(final long timestamp) { + for (int i = 0; i < current_values.length; i++) { + if (current_values[i] == null) { + emitter_values[i].reset(timestamp, fill_policy.getValue()); + continue; + } + + if (current_values[i].timestamp() > timestamp) { + emitter_values[i].reset(timestamp, fill_policy.getValue()); + } else { + emitter_values[i].reset(current_values[i]); + next(i); // move to the next value for this guy + } + } + return emitter_values; + } + + @Override + public long nextTimestamp() { + long ts = Long.MAX_VALUE; + for (final DataPoint dp : current_values) { + if (dp != null) { + long t = dp.timestamp(); + if (t < ts) { + ts = t; + } + } + } + return ts; + } + + /** + * Moves the selected series to the next value. If the iterator has been + * nulled out (no more data) then this is a no-op. + * @param i The iterator index to advance + */ + private void next(final int i) { + if (!iterators[i].hasNext()) { + current_values[i] = null; + return; + } + current_values[i] = iterators[i].next(); + } + + @Override + public int getIndex() { + return index; + } + + @Override + public void setIndex(final int index) { + this.index = index; + } + + @Override + public String getId() { + return id; + } + + /** @return the set of data points */ + public DataPoints[] getDataPoints() { + return dps; + } + + @Override + public void nullIterator(final int index) { + if (index < 0 || index > current_values.length) { + throw new IllegalArgumentException("Index out of range: " + index); + } + current_values[index] = null; + } + + @Override + public ExpressionDataPoint[] values() { + return emitter_values; + } + + @Override + public ByteSet getQueryTagKs() { + return query_tagks; + } + + @Override + public void setFillPolicy(final NumericFillPolicy policy) { + fill_policy = policy; + } + + @Override + public NumericFillPolicy getFillPolicy() { + return fill_policy; + } + + @Override + public ITimeSyncedIterator getCopy() { + return new TimeSyncedIterator(this); + } + + /** + * Iterates over the values and sets up the current and emitter values + */ + private void setupEmitters() { + // set the iterators + for (int i = 0; i < dps.length; i++) { + iterators[i] = dps[i].iterator(); + if (!iterators[i].hasNext()) { + current_values[i] = null; + emitter_values[i] = null; + } else { + current_values[i] = iterators[i].next(); + emitter_values[i] = new ExpressionDataPoint(dps[i]); + } + } + } +} diff --git a/test/query/expression/BaseTimeSyncedIteratorTest.java b/test/query/expression/BaseTimeSyncedIteratorTest.java new file mode 100644 index 0000000000..4d8b0a0cb8 --- /dev/null +++ b/test/query/expression/BaseTimeSyncedIteratorTest.java @@ -0,0 +1,649 @@ +// 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.HashMap; +import java.util.Map; + +import net.opentsdb.core.BaseTsdbTest; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.Query; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.TSSubQuery; +import net.opentsdb.utils.Pair; + +/** + * A class for setting up a number of time series and queries for expression + * testing. + */ +public class BaseTimeSyncedIteratorTest extends BaseTsdbTest { + + /** Start time across all queries */ + protected static final long START_TS = 1388534400; + + /** The query object compiled after calling {@link #runQueries(ArrayList)} */ + protected TSQuery query; + + /** The results of our queries after calling {@link #runQueries(ArrayList)} */ + protected Map> results; + + /** List of iterators */ + protected Map iterators; + + /** + * Queries for metrics A and B with a group by all on the D tag + */ + protected void queryAB_Dstar() throws Exception { + final ArrayList subs = new ArrayList(2); + TSSubQuery sub = new TSSubQuery(); + + HashMap query_tags = new HashMap(1); + query_tags.put("D", "*"); + + sub = new TSSubQuery(); + sub.setMetric("A"); + sub.setTags(query_tags); + sub.setAggregator("sum"); + subs.add(sub); + + sub = new TSSubQuery(); + sub.setMetric("B"); + query_tags = new HashMap(1); + query_tags.put("D", "*"); + sub.setTags(query_tags); + sub.setAggregator("sum"); + subs.add(sub); + + runQueries(subs); + } + + /** + * Queries for A and B but without a tag specifier, thus agging em all + */ + protected void queryAB_AggAll() throws Exception { + final ArrayList subs = new ArrayList(2); + TSSubQuery sub = new TSSubQuery(); + + HashMap query_tags = new HashMap(1); + + sub = new TSSubQuery(); + sub.setMetric("A"); + sub.setTags(query_tags); + sub.setAggregator("sum"); + subs.add(sub); + + sub = new TSSubQuery(); + sub.setMetric("B"); + query_tags = new HashMap(1); + sub.setTags(query_tags); + sub.setAggregator("sum"); + subs.add(sub); + + runQueries(subs); + } + + /** + * Queries for A only with a filter of tag value "D" for tag key "D" + */ + protected void queryA_DD() throws Exception { + final ArrayList subs = new ArrayList(2); + TSSubQuery sub = new TSSubQuery(); + final HashMap query_tags = new HashMap(1); + query_tags.put("D", "D"); + sub = new TSSubQuery(); + sub.setMetric("A"); + sub.setTags(query_tags); + sub.setAggregator("sum"); + subs.add(sub); + + runQueries(subs); + } + + /** + * Executes the queries against MockBase through the regular pipeline and stores + * the results in {@linke #results} + * @param subs The queries to execute + */ + protected void runQueries(final ArrayList subs) throws Exception { + query = new TSQuery(); + query.setStart(Long.toString(START_TS)); + query.setQueries(subs); + query.validateAndSetQuery(); + + final Query[] compiled = query.buildQueries(tsdb); + results = new HashMap>( + compiled.length); + iterators = new HashMap(compiled.length); + + int index = 0; + for (final Query q : compiled) { + final DataPoints[] dps = q.runAsync().join(); + results.put(Integer.toString(index), + new Pair( + query.getQueries().get(index), dps)); + iterators.put(Integer.toString(index), + new TimeSyncedIterator(Integer.toString(index), + query.getQueries().get(index).getFilterTagKs(), dps)); + index++; + } + } + + /** + * A and B, each with two series. Common D values, different E values + */ + protected void twoSeriesAggedE() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "F"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "F"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + } + + /** + * A and B, each with two series. Common D, different E values and the B metric + * series have an extra Z tag with different values. + */ + protected void twoSeriesAggedEandExtraTagK() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "F"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tags.put("Z", "A"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "F"); + tags.put("Z", "B"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + } + + /** + * A and B where A has two series and B only has one. Series A has two D + * values that will be agged. Different D and E values. + */ + protected void oneAggedTheOtherTagged() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "E"); + tags.put("E", "F"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + } + + /** + * A only with three series. Different D values, commong E values. + */ + protected void threeSameENoB() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + } + + /** + * A and B where A has two series, B has three. Different D values, common E. + */ + protected void oneExtraSameE() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + // all by myself...... + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + } + + /** + * A and B with two series each. Different D values, common E. + * A has values at T0 and T1, but then B has values at T2 and T3. Should + * throw NaNs after the intersection. + */ + protected void timeOffset() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561780, 14, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561780, 17, tags).joinUninterruptibly(); + } + + /** + * A and B, each with three series. Different D values, commong E. + */ + protected void threeSameE() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); + } + + /** + * A and B, each with three series. Different D values. Series in A are missing + * the E tag. Common values in E for B. + */ + protected void threeAMissingE() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); + } + + /** + * A and B, each with three series. Different D and E values + */ + protected void threeDifE() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "A"); + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "B"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "C"); + tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "D"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "F"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "G"); + tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); + } + + /** + * A and B, each with 3 series. Different D values, common E. + * Each set has one series that isn't in the other. D=G in A and D=Q in B. + */ + protected void threeDisjointSameE() throws Exception { + setDataPointStorage(); + + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + // not in set 2 + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + // not in set 1 + tags = new HashMap(2); + tags.put("D", "Q"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); + } + + /** + * A and B, each with three series. Different D values, common E values. + * D=G is the only common series between the sets + */ + protected void reduceToOne() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + // not in set 2 + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "P"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + // not in set 1 + tags = new HashMap(2); + tags.put("D", "Q"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); + } + + /** + * A and B, each with three series. Different D values, common E values. + * Each series is "missing" a data point so that we can test time sync. + */ + protected void threeSameEGaps() throws Exception { + setDataPointStorage(); + HashMap tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + + tsdb.addPoint("A", 1431561600, 1, tags).joinUninterruptibly(); + //tsdb.addPoint("A", 1431561660, 2, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 3, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561600, 4, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 5, tags).joinUninterruptibly(); + //tsdb.addPoint("A", 1431561720, 6, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + //tsdb.addPoint("A", 1431561600, 7, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561660, 8, tags).joinUninterruptibly(); + tsdb.addPoint("A", 1431561720, 9, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "D"); + tags.put("E", "E"); + //tsdb.addPoint("B", 1431561600, 11, tags).joinUninterruptibly(); + //tsdb.addPoint("B", 1431561660, 12, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 13, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + //tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); + //tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + //tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + //tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); + } + +} diff --git a/test/query/expression/TestTimeSyncedIterator.java b/test/query/expression/TestTimeSyncedIterator.java new file mode 100644 index 0000000000..70491001f1 --- /dev/null +++ b/test/query/expression/TestTimeSyncedIterator.java @@ -0,0 +1,504 @@ +// 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.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.PowerMockRunner; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.FillPolicy; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +public class TestTimeSyncedIterator extends BaseTimeSyncedIteratorTest { + + @Test + public void ctor() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + assertEquals(3, it.size()); + assertTrue(it.hasNext()); + assertEquals(0, it.getIndex()); + assertEquals("0", it.getId()); + assertEquals(results.get("0").getKey().getFilterTagKs(), it.getQueryTagKs()); + assertTrue(results.get("0").getValue() == it.getDataPoints()); + assertEquals(3, it.values().length); + assertEquals(1, it.getQueryTagKs().size()); + assertEquals(FillPolicy.ZERO, it.getFillPolicy().getPolicy()); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullId() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + new TimeSyncedIterator(null, + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyId() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + new TimeSyncedIterator("", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + } + + @Test + public void ctorNullQueryTags() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", null, + results.get("0").getValue()); + + assertEquals(3, it.size()); + assertTrue(it.hasNext()); + assertEquals(0, it.getIndex()); + assertEquals("0", it.getId()); + assertNull(it.getQueryTagKs()); + assertTrue(results.get("0").getValue() == it.getDataPoints()); + assertEquals(3, it.values().length); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullDPs() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + new TimeSyncedIterator("0", results.get("0").getKey().getFilterTagKs(), + null); + } + + @Test + public void threeSeries() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + double[] values = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + final DataPoint[] dps = it.next(ts); + assertEquals(3, dps.length); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(values[0]++, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + assertEquals(Long.MAX_VALUE, ts); + } + + @Test + public void threeSeriesEmitter() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + final DataPoint[] dps = it.values(); + double[] values = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + it.next(ts); + assertEquals(3, dps.length); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(values[0]++, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + assertEquals(Long.MAX_VALUE, ts); + } + + @Test + public void nullASeries() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + // mimic the intersector kicking out a series + it.nullIterator(0); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + final DataPoint[] dps = it.values(); + double[] values = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + it.next(ts); + assertEquals(3, dps.length); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(0, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + assertEquals(Long.MAX_VALUE, ts); + } + + @Test + public void nullAll() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + // mimic the intersector kicking out all series. + it.nullIterator(0); + it.nullIterator(1); + it.nullIterator(2); + + assertEquals(Long.MAX_VALUE, it.nextTimestamp()); + assertFalse(it.hasNext()); + } + + @Test + public void singleSeries() throws Exception { + threeDisjointSameE(); + queryA_DD(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + double value = 1; + while (it.hasNext()) { + final DataPoint[] dps = it.next(ts); + assertEquals(1, dps.length); + assertEquals(ts, dps[0].timestamp()); + assertEquals(value++, dps[0].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + assertEquals(Long.MAX_VALUE, ts); + } + + @Test + public void noData() throws Exception { + setDataPointStorage(); + queryA_DD(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + assertEquals(0, it.values().length); + assertEquals(Long.MAX_VALUE, it.nextTimestamp()); + assertFalse(it.hasNext()); + } + + @Test + public void threeSeriesMissing() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + it.setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + DataPoint[] dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(dps[2].toDouble())); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertTrue(Double.isNaN(dps[0].toDouble())); + assertEquals(5, dps[1].toDouble(), 0.0001); + assertEquals(8, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(3, dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(dps[1].toDouble())); + assertEquals(9, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + + @Test + public void threeSeriesMissingFillZero() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + it.setFillPolicy(new NumericFillPolicy(FillPolicy.ZERO)); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + DataPoint[] dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + assertEquals(0, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(0, dps[0].toDouble(), 0.0001); + assertEquals(5, dps[1].toDouble(), 0.0001); + assertEquals(8, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(3, dps[0].toDouble(), 0.0001); + assertEquals(0, dps[1].toDouble(), 0.0001); + assertEquals(9, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + + @Test + public void threeSeriesMissingNull() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + it.setFillPolicy(new NumericFillPolicy(FillPolicy.NULL)); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + DataPoint[] dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(dps[2].toDouble())); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertTrue(Double.isNaN(dps[0].toDouble())); + assertEquals(5, dps[1].toDouble(), 0.0001); + assertEquals(8, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(3, dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(dps[1].toDouble())); + assertEquals(9, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + + @Test + public void threeSeriesMissingScalar() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + it.setFillPolicy(new NumericFillPolicy(FillPolicy.SCALAR, 42)); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + DataPoint[] dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + assertEquals(42, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(42, dps[0].toDouble(), 0.0001); + assertEquals(5, dps[1].toDouble(), 0.0001); + assertEquals(8, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + + dps = it.next(ts); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + + assertEquals(3, dps[0].toDouble(), 0.0001); + assertEquals(42, dps[1].toDouble(), 0.0001); + assertEquals(9, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + + @Test + public void failToNextTS() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + int i = 0; + boolean broke = false; + while (it.hasNext()) { + // in this case the caller fails to advance the TS so we keep getting the + // same data points over and over again. + it.next(ts); + ++i; + if (i > 100) { + broke = true; + break; + } + } + if (!broke) { + fail("Expected to iterate over 100 times"); + } + } + + @Test + public void nextTimestampDoesntAdvance() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + assertEquals(1431561600000L, it.nextTimestamp()); + assertEquals(1431561600000L, it.nextTimestamp()); + assertEquals(1431561600000L, it.nextTimestamp()); + assertEquals(1431561600000L, it.nextTimestamp()); + assertEquals(1431561600000L, it.nextTimestamp()); + assertEquals(1431561600000L, it.nextTimestamp()); + assertEquals(1431561600000L, it.nextTimestamp()); + } + + @Test + public void nextExceptionNoException() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + it.next(it.nextTimestamp()); + it.next(it.nextTimestamp()); + it.next(it.nextTimestamp()); + it.next(it.nextTimestamp()); + assertEquals(Long.MAX_VALUE, it.nextTimestamp()); + } + + @Test + public void getCopy() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + final TimeSyncedIterator it = new TimeSyncedIterator("0", + results.get("0").getKey().getFilterTagKs(), + results.get("0").getValue()); + + final ITimeSyncedIterator copy = it.getCopy(); + assertTrue(copy != it); + + long ts = it.nextTimestamp(); + assertEquals(1431561600000L, ts); + + double[] values = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + final DataPoint[] dps = it.next(ts); + assertEquals(3, dps.length); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(values[0]++, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, dps[2].toDouble(), 0.0001); + ts = it.nextTimestamp(); + } + assertEquals(Long.MAX_VALUE, ts); + + ts = copy.nextTimestamp(); + assertEquals(1431561600000L, ts); + + values = new double[] { 1, 4, 7 }; + while (copy.hasNext()) { + final DataPoint[] dps = copy.next(ts); + assertEquals(3, dps.length); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(values[0]++, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, dps[2].toDouble(), 0.0001); + ts = copy.nextTimestamp(); + } + assertEquals(Long.MAX_VALUE, ts); + } +} From a27c81961249746d08e52003382eedbd7934190c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 5 Nov 2015 12:04:46 -0800 Subject: [PATCH 311/826] Add the VariableIterator class that provides an interface for set operations and interation. Signed-off-by: Chris Larsen --- Makefile.am | 3 +- src/query/expression/VariableIterator.java | 101 +++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/query/expression/VariableIterator.java diff --git a/Makefile.am b/Makefile.am index 0ce731dab0..d9fba4272f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -89,12 +89,13 @@ tsdb_SRC := \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ src/query/expression/TimeSyncedIterator.java \ + src/query/expression/TimeSyncedIterator.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ src/query/filter/TagVNotLiteralOrFilter.java \ src/query/filter/TagVRegexFilter.java \ - src/query/filter/TagVWildcardFilter.java \ + src/query/filter/VariableIterator.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ src/search/TimeSeriesLookup.java \ diff --git a/src/query/expression/VariableIterator.java b/src/query/expression/VariableIterator.java new file mode 100644 index 0000000000..0ed7c6dab8 --- /dev/null +++ b/src/query/expression/VariableIterator.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.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * An interface that helps merge different time series sets (e.g. different + * metrics with a group by operator). The implementations handle joining the + * two sets according to the {@link SetOperator}. + * @since 2.3 + */ +public interface VariableIterator { + + /** An operator that determines how to sets of time series are merged via + * expression. */ + public enum SetOperator { + /** A union, meaning results from all sets will appear, using FillPolicies + * for missing series */ + UNION("union"), + + /** Computes the intersection, returning results only for series that appear + * in all sets */ + INTERSECTION("intersection"); + + /** The user-friendly name of this operator. */ + private final String name; + + /** @param the readable name of the operator */ + SetOperator(final String name) { + this.name = name; + } + + /** @return the readable name of the operator */ + @JsonValue + public String getName() { + return name; + } + + /** + * Converts a string to lower case then looks up the operator + * @param name The name to find an operator for + * @return The operator if found. + * @throws IllegalArgumentException if the operator wasn't found + */ + @JsonCreator + public static SetOperator fromString(final String name) { + for (final SetOperator operator : SetOperator.values()) { + if (operator.name.equalsIgnoreCase(name)) { + return operator; + } + } + throw new IllegalArgumentException("Unrecognized set operator: " + name); + } + } + + /** + * Whether or not another set of results are available. Always call this + * before calling next. + * @return True if more results are available, false if not. + */ + public boolean hasNext(); + + /** + * Iterates the {@link getResults()} to the next set of results. If there + * aren't any results left, the implementation may throw an exception. Always + * call {@link hasNext()} first. + */ + public void next(); + + /** + * Returns a map of variable names to result series. You can maintain the + * reference returned without having to call getResults() on every iteration. + * Calling {@link next()} will simply update the ExpressionDataPoint array. + * The implementation may return a null map if there weren't any results + * available. Always all {@link hasNext()} before getting the results. + * @return A map with results to read from. + */ + public Map getResults(); + + /** @return The number of time series after the join. This should match the + * number of entries in the results data point array, not the number of + * variables in the results map. */ + public int getSeriesSize(); + + /** @return the next timestamp for all results without iterating */ + public long nextTimestamp(); +} From f0286ea766bb5ca22d094a8402f8aca6d0174b49 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 5 Nov 2015 12:19:06 -0800 Subject: [PATCH 312/826] Add the IntersectionIterator for computing set unions based on the intersection of sets, i.e. only return results that have series in every set, kicking out any series that are missing in another set. Signed-off-by: Chris Larsen --- Makefile.am | 2 + .../expression/IntersectionIterator.java | 500 +++++++++++ .../expression/TestIntersectionIterator.java | 813 ++++++++++++++++++ 3 files changed, 1315 insertions(+) create mode 100644 src/query/expression/IntersectionIterator.java create mode 100644 test/query/expression/TestIntersectionIterator.java diff --git a/Makefile.am b/Makefile.am index d9fba4272f..84f76540bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -83,6 +83,7 @@ tsdb_SRC := \ src/query/expression/ExpressionTree.java \ src/query/expression/HighestCurrent.java \ src/query/expression/HighestMax.java \ + src/query/expression/IntersectionIterator.java \ src/query/expression/ITimeSyncedIterator.java \ src/query/expression/NumericFillPolicy.java \ src/query/expression/MovingAverage.java \ @@ -254,6 +255,7 @@ test_SRC := \ test/query/expression/TestExpressionTree.java \ test/query/expression/TestHighestCurrent.java \ test/query/expression/TestHighestMax.java \ + test/query/expression/TestIntersectionIterator.java \ test/query/expression/TestNumericFillPolicy.java \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ diff --git a/src/query/expression/IntersectionIterator.java b/src/query/expression/IntersectionIterator.java new file mode 100644 index 0000000000..99e1e3ae7c --- /dev/null +++ b/src/query/expression/IntersectionIterator.java @@ -0,0 +1,500 @@ +// 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.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.ByteSet; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.HBaseClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +/** + * This class handles taking a set of queries and their results and iterates + * over each series in each set with time alignment after computing the + * intersection of all sets. + *

    + * The iterator performs the following: + * - calculates the intersection of all queries based on the tags or query tags + * and optionally the aggregated tags. + * - any series that are not members of ever set are kicked out (and logged). + * - series are aligned across queries so that expressions can operate over them. + * - series are also time aligned and maintain alignment during iteration. + *

    + * The {@link #current_values} map will map the expression "variables" to the + * proper iterator for each serie's array. E.g. + * <"A", [1, 2, 3, 4]> + * <"B", [1, 2, 3, 4]> + *

    + * So to use it's you simply fetch the result map, call {@link #hasNext()} and + * {@link #next()} to iterate and in a for loop, iterate {@link #getSeriesSize()} + * times to get all of the current values. + * For efficiency, call {@link #getResults()} once before iterating, then on + * each call to {@link #next()} you can just iterate over the same result map + * again as the values will be updated. + * @since 2.3 + */ +public class IntersectionIterator implements ITimeSyncedIterator, VariableIterator { + private static final Logger LOG = LoggerFactory.getLogger(IntersectionIterator.class); + + /** The queries compiled and fetched from storage */ + private final Map queries; + + /** A list of the current values for each series post intersection */ + private final Map current_values; + + /** A map of the sub query index to their names for intersection computation */ + private final String[] index_to_names; + + /** Whether or not to intersect on the query tagks instead of the result set + * tagks */ + private final boolean intersect_on_query_tagks; + + /** Whether or not to include the aggregated tags in the result set */ + private final boolean include_agg_tags; + + /** The start/current timestamp for the iterator in ms */ + private long timestamp; + + /** Post intersection number of time series */ + private int series_size; + + /** The ID of this iterator */ + private final String id; + + /** The index of this iterator in a list of iterators */ + private int index; + + /** + * Ctor to create the expression lock-step iterator from a set of query results. + * If the results map is empty, then the ctor will complete but the results map + * will be empty and calls to {@link #hasNext()} will always return false. + * @param results The query results to store + * @param intersect_on_query_tagks Whether or not to include only the query + * specified tags during intersection + * @param include_agg_tags Whether or not to include aggregated tags during + * intersection + * @throws IllegalDataException if, after computing the intersection, no results + * would be left. + */ + public IntersectionIterator(final String id, final Map results, + final boolean intersect_on_query_tagks, final boolean include_agg_tags) { + this.id = id; + this.intersect_on_query_tagks = intersect_on_query_tagks; + this.include_agg_tags = include_agg_tags; + timestamp = Long.MAX_VALUE; + queries = new HashMap(results.size()); + current_values = new HashMap(results.size()); + index_to_names = new String[results.size()]; + + int max_series = 0; + int i = 0; + for (final Map.Entry entry : results.entrySet()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Adding iterator " + entry.getValue()); + } + queries.put(entry.getKey(), entry.getValue()); + entry.getValue().setIndex(i); + index_to_names[i] = entry.getKey(); + if (entry.getValue().values().length > max_series) { + max_series = entry.getValue().values().length; + } + ++i; + } + + if (max_series < 1) { + // we don't want to throw an exception here, just set it up so that the + // call to {@link #hasNext()} will be false. + LOG.debug("No series in the result sets"); + return; + } + + computeIntersection(); + + // calculate the starting timestamp from the various iterators + for (final ITimeSyncedIterator it : queries.values()) { + final long ts = it.nextTimestamp(); + if (ts < timestamp) { + timestamp = ts; + } + } + } + + /** + * A sort of copy constructor that populates the iterator from an existing + * iterator, copying all child iterators. + * @param iterator The iterator to copy from. + */ + private IntersectionIterator(final IntersectionIterator iterator) { + id = iterator.id; + intersect_on_query_tagks = iterator.intersect_on_query_tagks; + include_agg_tags = iterator.include_agg_tags; + timestamp = Long.MAX_VALUE; + queries = new HashMap(iterator.queries.size()); + current_values = new HashMap(queries.size()); + index_to_names = new String[queries.size()]; + + int max_series = 0; + int i = 0; + for (final Entry entry : iterator.queries.entrySet()) { + queries.put(entry.getKey(), entry.getValue().getCopy()); + entry.getValue().setIndex(i); + index_to_names[i] = entry.getKey(); + if (entry.getValue().values().length > max_series) { + max_series = entry.getValue().values().length; + } + ++i; + } + + if (max_series < 1) { + // we don't want to throw an exception here, just set it up so that the + // call to {@link #hasNext()} will be false. + LOG.debug("No series in the result sets"); + return; + } + + computeIntersection(); + + // calculate the starting timestamp from the various iterators + for (final ITimeSyncedIterator it : queries.values()) { + final long ts = it.nextTimestamp(); + if (ts < timestamp) { + timestamp = ts; + } + } + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("IntersectionIterator(id=") + .append(id) + .append(", useQueryTags=") + .append(intersect_on_query_tagks) + .append(", includeAggTags=") + .append(include_agg_tags) + .append(", index=") + .append(index) + .append(", queries=") + .append(queries); + return buf.toString(); + } + + @Override + public boolean hasNext() { + for (final ITimeSyncedIterator sub : queries.values()) { + if (sub.hasNext()) { + return true; + } + } + return false; + } + + /** fetch the next set of time aligned results for all series */ + @Override + public void next() { + if (!hasNext()) { + throw new IllegalDataException("No more data"); + } + for (final ITimeSyncedIterator sub : queries.values()) { + sub.next(timestamp); + } + timestamp = nextTimestamp(); + } + + /** @return a map of values that will change on each iteration */ + @Override + public Map getResults() { + return current_values; + } + + /** @return the number of series in each map of the result set */ + @Override + public int getSeriesSize() { + return series_size; + } + + /** @return the next timestamp calculated from all series in the set */ + public long nextTimestamp() { + long ts = Long.MAX_VALUE; + for (final ITimeSyncedIterator sub : queries.values()) { + if (sub != null) { + final long t = sub.nextTimestamp(); + if (t < ts) { + ts = t; + } + } + } + return ts; + } + + /** + * A super ugly messy way to compute the intersection of the various sets of + * time series returned from the sub queries. + *

    + * The process is: + * - Iterate over each query set + * - For the first set, flatten each series' tag and (optionally) aggregated tag + * set into a single byte array for use as an ID. + * - Populate a map with the IDs and references to the series iterator for the + * first query set. + * - For each additional set, flatten the tags and if the tag set ID isn't in + * the intersection map, kick it out. + * - For each key in the intersection map, if it doesn't appear in the current + * query set, kick it out. + * - Once all sets are finished, align the resulting series iterators in the + * {@link #current_values} map which is then prepped for expression processing. + * @throws IllegalDataException if more than one series was supplied and + * the resulting intersection failed to produce any series + */ + private void computeIntersection() { + final ByteMap ordered_intersection = + new ByteMap(); + final Iterator it = queries.values().iterator(); + + // assume we have at least on query in our set + ITimeSyncedIterator sub = it.next(); + Map> flattened_tags = + new HashMap>(queries.size()); + ByteMap tags = new ByteMap(); + flattened_tags.put(sub.getId(), tags); + ExpressionDataPoint[] dps = sub.values(); + + for (int i = 0; i < sub.size(); i++) { + final byte[] tagks = flattenTags(intersect_on_query_tagks, include_agg_tags, + dps[i].tags(), dps[i].aggregatedTags(), sub); + tags.put(tagks, i); + + final ExpressionDataPoint[] idps = new ExpressionDataPoint[queries.size()]; + idps[sub.getIndex()] = dps[i]; + ordered_intersection.put(tagks, idps); + } + + if (!it.hasNext()) { + setCurrentAndMeta(ordered_intersection); + return; + } + + while (it.hasNext()) { + sub = it.next(); + tags = new ByteMap(); + flattened_tags.put(sub.getId(), tags); + dps = sub.values(); + + // loop through the series in the sub iterator, compute the flattened tag + // ids, then kick out any that are NOT in the existing intersection map. + for (int i = 0; i < sub.size(); i++) { + final byte[] tagks = flattenTags(intersect_on_query_tagks, include_agg_tags, + dps[i].tags(), dps[i].aggregatedTags(), sub); + tags.put(tagks, i); + + final ExpressionDataPoint[] idps = ordered_intersection.get(tagks); + if (idps == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Kicking out " + Bytes.pretty(tagks) + " from " + sub.getId()); + } + sub.nullIterator(i); + continue; + } + idps[sub.getIndex()] = dps[i]; + } + + // gotta go backwards now to complete the intersection by kicking + // any series that appear in other sets but not HERE + final Iterator> reverse_it = + ordered_intersection.iterator(); + while (reverse_it.hasNext()) { + Entry e = reverse_it.next(); + if (!tags.containsKey(e.getKey())) { + if (LOG.isDebugEnabled()) { + LOG.debug("Kicking out " + Bytes.pretty(e.getKey()) + + " from the main list since the query for " + sub.getId() + + " didn't have it"); + } + + // null the iterators for the other sets + for (final Map.Entry> entry : + flattened_tags.entrySet()) { + if (entry.getKey().equals(sub.getId())) { + continue; + } + final Integer index = entry.getValue().get(e.getKey()); + if (index != null) { + queries.get(entry.getKey()).nullIterator(index); + } + } + + reverse_it.remove(); + } + } + } + + // now set our properly condensed and ordered values + if (ordered_intersection.size() < 1) { + // TODO - is it best to toss an exception here or return an empty result? + throw new IllegalDataException("No intersections found: " + this); + } + + setCurrentAndMeta(ordered_intersection); + } + + /** + * Takes the resulting intersection and builds the {@link #current_values} + * and {@link #meta} maps. + * @param ordered_intersection The intersection to build from. + */ + private void setCurrentAndMeta(final ByteMap + ordered_intersection) { + for (final String id : queries.keySet()) { + current_values.put(id, new ExpressionDataPoint[ordered_intersection.size()]); + } + + int i = 0; + for (final ExpressionDataPoint[] idps : ordered_intersection.values()) { + for (int x = 0; x < idps.length; x++) { + final ExpressionDataPoint[] current_dps = + current_values.get(index_to_names[x]); + current_dps[i] = idps[x]; + } + ++i; + } + series_size = ordered_intersection.size(); + } + + /** + * Flattens the appropriate tags into a single byte array + * @param use_query_tags Whether or not to include tags returned with the + * results or just use those group by'd in the query + * @param include_agg_tags Whether or not to include the aggregated tags in + * the identifier + * @param tags The map of tags from the result set + * @param agg_tags The list of aggregated tags + * @param sub The sub query iterator + * @return A byte array with the flattened tag keys and values. Note that + * if the tags set is empty, this may return an empty array (but not a null + * array) + */ + static byte[] flattenTags(final boolean use_query_tags, + final boolean include_agg_tags, final ByteMap tags, + final ByteSet agg_tags, final ITimeSyncedIterator sub) { + if (tags.isEmpty()) { + return HBaseClient.EMPTY_ARRAY; + } + final ByteSet query_tagks; + // NOTE: We MAY need the agg tags but I'm not sure yet + final int tag_size; + if (use_query_tags) { + int i = 0; + if (sub.getQueryTagKs() != null && !sub.getQueryTagKs().isEmpty()) { + query_tagks = sub.getQueryTagKs(); + for (final Map.Entry pair : tags.entrySet()) { + if (query_tagks.contains(pair.getKey())) { + i++; + } + } + } else { + query_tagks = new ByteSet(); + } + tag_size = i; + } else { + query_tagks = new ByteSet(); + tag_size = tags.size(); + } + + int len = (tag_size * (TSDB.tagk_width() + TSDB.tagv_width())) + + (include_agg_tags ? (agg_tags.size() * TSDB.tagk_width()) : 0); + final byte[] tagks = new byte[len]; + int i = 0; + for (final Map.Entry pair : tags.entrySet()) { + if (use_query_tags && !query_tagks.contains(pair.getKey())) { + continue; + } + System.arraycopy(pair.getKey(), 0, tagks, i, TSDB.tagk_width()); + i += TSDB.tagk_width(); + System.arraycopy(pair.getValue(), 0, tagks, i, TSDB.tagv_width()); + i += TSDB.tagv_width(); + } + if (include_agg_tags) { + for (final byte[] tagk : agg_tags) { + System.arraycopy(tagk, 0, tagks, i, TSDB.tagk_width()); + i += TSDB.tagk_width(); + } + } + return tagks; + } + + @Override + public ExpressionDataPoint[] next(long timestamp) { + throw new NotImplementedException(); + } + + @Override + public int size() { + throw new NotImplementedException(); + } + + @Override + public ExpressionDataPoint[] values() { + throw new NotImplementedException(); + } + + @Override + public void nullIterator(int index) { + throw new NotImplementedException(); + } + + @Override + public int getIndex() { + return index; + } + + @Override + public void setIndex(int index) { + this.index = index; + } + + @Override + public String getId() { + return id; + } + + @Override + public ByteSet getQueryTagKs() { + throw new NotImplementedException(); + } + + @Override + public void setFillPolicy(NumericFillPolicy policy) { + throw new NotImplementedException(); + } + + @Override + public NumericFillPolicy getFillPolicy() { + throw new NotImplementedException(); + } + + @Override + public ITimeSyncedIterator getCopy() { + return new IntersectionIterator(this); + } +} diff --git a/test/query/expression/TestIntersectionIterator.java b/test/query/expression/TestIntersectionIterator.java new file mode 100644 index 0000000000..80adf6ca43 --- /dev/null +++ b/test/query/expression/TestIntersectionIterator.java @@ -0,0 +1,813 @@ +// 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.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.hbase.async.HBaseClient; +import org.hbase.async.Bytes.ByteMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.FillPolicy; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.storage.MockBase; +import net.opentsdb.utils.ByteSet; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TimeSyncedIterator.class }) +public class TestIntersectionIterator extends BaseTimeSyncedIteratorTest { + + /** used for the flattenTags tests */ + private static final byte[] UID1 = new byte[] { 0, 0, 1 }; + private static final byte[] UID2 = new byte[] { 0, 0, 2 }; + private static final byte[] UID3 = new byte[] { 0, 0, 3 }; + private ByteMap tags; + private ByteSet agg_tags; + private ITimeSyncedIterator sub; + private ByteSet query_tags; + + @Before + public void beforeLocal() throws Exception { + tags = new ByteMap(); + tags.put(UID1, UID1); + tags.put(UID2, UID2); + + agg_tags = new ByteSet(); + agg_tags.add(UID3); + + sub = mock(ITimeSyncedIterator.class); + query_tags = new ByteSet(); + query_tags.add(UID1); + when(sub.getQueryTagKs()).thenReturn(query_tags); + } + + @Test (expected = NullPointerException.class) + public void ctorNullResults() { + new IntersectionIterator("it", null, true, true); + } + + @Test + public void ctorEmptyResults() { + final IntersectionIterator it = new IntersectionIterator("it", + new HashMap(), true, true); + assertEquals(0, it.getSeriesSize()); + assertFalse(it.hasNext()); + } + + @Test + public void twoAndThreeSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 11, 14 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[2]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void twoAndThreeSeriesExtraDPinKickedSeries() throws Exception { + // in this case we want to make sure the kicked series doesn't cause us + // to dump a bunch of nulls + oneExtraSameE(); + queryAB_Dstar(); + + final HashMap tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561630, 14, tags).joinUninterruptibly(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 11, 14 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[2]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesIntersectToTwo() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 7, 11, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[2]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesIntersectToExtraDPsinKicked() throws Exception { + reduceToOne(); + queryAB_Dstar(); + + HashMap tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561630, 1024, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "Q"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561630, 1024, tags).joinUninterruptibly(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 7, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1]++, set_dps[0].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesIntersectToOne() throws Exception { + reduceToOne(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 7, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1]++, set_dps[0].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesAggedIntoOne() throws Exception { + threeSameE(); + queryAB_AggAll(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 12, 42 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1], set_dps[0].toDouble(), 0.0001); + + values[0] += 3; + values[1] += 3; + + ts += 60000; + } + } + + @Test + public void threeSeriesFullIntersetWithNaNs() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + for (ITimeSyncedIterator iterator : iterators.values()) { + iterator.setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)); + } + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(1, set_dps[0].toDouble(), 0.0001); + assertEquals(4, set_dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[2].toDouble())); + + // whole series is NaN'd + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + assertTrue(Double.isNaN(set_dps[2].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertEquals(5, set_dps[1].toDouble(), 0.0001); + assertEquals(8, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertEquals(15, set_dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[2].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(3, set_dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + assertEquals(9, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(13, set_dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + assertEquals(19, set_dps[2].toDouble(), 0.0001); + + assertFalse(it.hasNext()); + } + + @Test + public void twoSeriesTimeOffset() throws Exception { + timeOffset(); + queryAB_Dstar(); + for (ITimeSyncedIterator iterator : iterators.values()) { + iterator.setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)); + } + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(1, set_dps[0].toDouble(), 0.0001); + assertEquals(4, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(2, set_dps[0].toDouble(), 0.0001); + assertEquals(5, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(13, set_dps[0].toDouble(), 0.0001); + assertEquals(16, set_dps[1].toDouble(), 0.0001); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(14, set_dps[0].toDouble(), 0.0001); + assertEquals(17, set_dps[1].toDouble(), 0.0001); + + assertFalse(it.hasNext()); + } + + @Test (expected = IllegalDataException.class) + public void noIntersectionUsingResultTags() throws Exception { + threeDifE(); + queryAB_Dstar(); + new IntersectionIterator("it", iterators, false, false); + } + + @Test + public void intersectUsingQueryTags() throws Exception { + threeDifE(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[3]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void commonAggregatedTag() throws Exception { + twoSeriesAggedE(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 22 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1], set_dps[0].toDouble(), 0.0001); + + values[0] += 2; + values[1] += 2; + + ts += 60000; + } + } + + @Test + public void extraAggTagIgnored() throws Exception { + twoSeriesAggedEandExtraTagK(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 22 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1], set_dps[0].toDouble(), 0.0001); + + values[0] += 2; + values[1] += 2; + + ts += 60000; + } + } + + @Test (expected = IllegalDataException.class) + public void extraAggTagNoIntersection() throws Exception { + twoSeriesAggedEandExtraTagK(); + queryAB_Dstar(); + new IntersectionIterator("it", iterators, false, true); + } + + @Test (expected = IllegalDataException.class) + public void onlyOneResultSet() throws Exception { + threeSameENoB(); + queryAB_Dstar(); + new IntersectionIterator("it", iterators, false, true); + } + + @Test (expected = IllegalDataException.class) + public void oneAggedOneTaggedNoIntersection() throws Exception { + oneAggedTheOtherTagged(); + queryAB_AggAll(); + new IntersectionIterator("it", iterators, false, true); + } + + @Test + public void oneAggedOneTaggedUseQueryTagsWoutQueryTags() throws Exception { + oneAggedTheOtherTagged(); + queryAB_AggAll(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 11 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1]++, set_dps[0].toDouble(), 0.0001); + + // the first set is agged + values[0] += 2; + + ts += 60000; + } + } + + @Test + public void singleSeries() throws Exception { + oneExtraSameE(); + queryA_DD(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(1, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double value = 1; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(value++, set_dps[0].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test (expected = IllegalDataException.class) + public void setAMissingE() throws Exception { + threeAMissingE(); + queryAB_Dstar(); + new IntersectionIterator("it", iterators, false, false); + } + + @Test + public void setAMissingEQueryTags() throws Exception { + threeAMissingE(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[3]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void noData() throws Exception { + setDataPointStorage(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertEquals(0, dps.size()); + assertFalse(it.hasNext()); + assertEquals(0, it.getSeriesSize()); + } + + @Test (expected = IllegalDataException.class) + public void nextException() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + + final IntersectionIterator it = new IntersectionIterator("it", iterators, false, false); + it.next(); + it.next(); + it.next(); + it.next(); + } + + @Test + public void flattenTags() throws Exception { + final byte[] flat = IntersectionIterator.flattenTags( + false, false, tags, agg_tags, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + @Test + public void flattenTagsWithAgg() throws Exception { + final byte[] flat = IntersectionIterator.flattenTags( + false, true, tags, agg_tags, sub); + assertArrayEquals( + MockBase.concatByteArrays(UID1, UID1, UID2, UID2, UID3), flat); + } + + @Test + public void flattenTagsQueryTags() throws Exception { + final byte[] flat = IntersectionIterator.flattenTags( + true, false, tags, agg_tags, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1), flat); + } + + @Test + public void flattenTagsQueryTagsWithAgg() throws Exception { + final byte[] flat = IntersectionIterator.flattenTags( + true, true, tags, agg_tags, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID3), flat); + } + + @Test + public void flattenEmptyTags() throws Exception { + tags.clear(); + final byte[] flat = IntersectionIterator.flattenTags( + false, false, tags, agg_tags, sub); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, flat); + } + + @Test + public void flattenEmptyTagsWithAggEmpty() throws Exception { + agg_tags.clear(); + final byte[] flat = IntersectionIterator.flattenTags( + false, true, tags, agg_tags, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + // TODO - how will this play out if we choose a default agg of "none" and the + // user hasn't asked for any filtering? + @Test + public void flattenTagsQueryTagsEmpty() throws Exception { + query_tags.clear(); + final byte[] flat = IntersectionIterator.flattenTags( + true, false, tags, agg_tags, sub); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, flat); + } + + @Test + public void flattenTagsQueryTagsEmptyWithAgg() throws Exception { + query_tags.clear(); + final byte[] flat = IntersectionIterator.flattenTags( + true, true, tags, agg_tags, sub); + assertArrayEquals(UID3, flat); + } + + @Test (expected = NullPointerException.class) + public void flattenTagsNullTags() throws Exception { + IntersectionIterator.flattenTags(false, false, null, agg_tags, sub); + } + + @Test + public void flattenTagsNullAggTagsNotRequested() throws Exception { + final byte[] flat = IntersectionIterator.flattenTags( + false, false, tags, null, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + @Test (expected = NullPointerException.class) + public void flattenTagsNullAggTags() throws Exception { + IntersectionIterator.flattenTags(false, true, tags, null, sub); + } + + @Test + public void flattenTagsNullSubNotRequested() throws Exception { + final byte[] flat = IntersectionIterator.flattenTags( + false, false, tags, agg_tags, null); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + @Test (expected = NullPointerException.class) + public void flattenTagsNullSub() throws Exception { + IntersectionIterator.flattenTags(true, false, tags, agg_tags, null); + } + +} From 6d20e2bf3bd8bf53ded41fe873441eabf471e720 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 5 Nov 2015 16:23:23 -0800 Subject: [PATCH 313/826] Add the UnionIterator for computing the union across sets using a fill policy for missing entries in any set. Signed-off-by: Chris Larsen --- Makefile.am | 3 +- src/query/expression/UnionIterator.java | 414 +++++++ .../BaseTimeSyncedIteratorTest.java | 14 +- test/query/expression/TestUnionIterator.java | 1074 +++++++++++++++++ 4 files changed, 1498 insertions(+), 7 deletions(-) create mode 100644 src/query/expression/UnionIterator.java create mode 100644 test/query/expression/TestUnionIterator.java diff --git a/Makefile.am b/Makefile.am index 84f76540bf..2916b67f2a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -90,7 +90,7 @@ tsdb_SRC := \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ src/query/expression/TimeSyncedIterator.java \ - src/query/expression/TimeSyncedIterator.java \ + src/query/expression/UnionIterator.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ @@ -261,6 +261,7 @@ test_SRC := \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/expression/TestScale.java \ test/query/expression/TestTimeSyncedIterator.java \ + test/query/expression/TestUnionIterator.java \ test/query/filter/TestTagVFilter.java \ test/query/filter/TestTagVLiteralOrFilter.java \ test/query/filter/TestTagVNotKeyFilter.java \ diff --git a/src/query/expression/UnionIterator.java b/src/query/expression/UnionIterator.java new file mode 100644 index 0000000000..0ed4514aa1 --- /dev/null +++ b/src/query/expression/UnionIterator.java @@ -0,0 +1,414 @@ +// 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.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import net.opentsdb.core.FillPolicy; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.ByteSet; + +import org.hbase.async.HBaseClient; +import org.hbase.async.Bytes.ByteMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +/** + * An iterator that computes the union of all series in the result sets. This + * means we match every series with it's corresponding series in the other sets. + * If one or more set lacks the matching series, then a {@code null} is stored + * and when the caller iterates over the results, the need to detect the null + * and substitute a fill value. + * @since 2.3 + */ +public class UnionIterator implements ITimeSyncedIterator, VariableIterator { + private static final Logger LOG = LoggerFactory.getLogger(UnionIterator.class); + + /** The queries compiled and fetched from storage */ + private final Map queries; + + /** A list of the current values for each series post intersection */ + private final Map current_values; + + /** A map of the sub query index to their names for intersection computation */ + private final String[] index_to_names; + + /** Whether or not to intersect on the query tagks instead of the result set + * tagks */ + private final boolean union_on_query_tagks; + + /** Whether or not to include the aggregated tags in the result set */ + private final boolean include_agg_tags; + + /** The start/current timestamp for the iterator in ms */ + private long timestamp; + + /** Post intersection number of time series */ + private int series_size; + + /** The ID of this iterator */ + private final String id; + + /** The index of this iterator in a list of iterators */ + private int index; + + /** The fill policy to use when a series is missing from one of the sets. + * Default is zero. */ + private NumericFillPolicy fill_policy; + + /** A data point used for filling missing time series */ + private ExpressionDataPoint fill_dp; + + /** + * Default ctor + * @param id The variable ID for this iterator + * @param results Upstream iterators + * @param union_on_query_tagks Whether or not to flatten and join on only + * the tags from the query or those returned in the results. + * @param include_agg_tags Whether or not to include the flattened aggregated + * tag keys in the join. + */ + public UnionIterator(final String id, final Map results, + final boolean union_on_query_tagks, final boolean include_agg_tags) { + this.id = id; + this.union_on_query_tagks = union_on_query_tagks; + this.include_agg_tags = include_agg_tags; + timestamp = Long.MAX_VALUE; + queries = new HashMap(results.size()); + current_values = new HashMap(results.size()); + index_to_names = new String[results.size()]; + fill_policy = new NumericFillPolicy(FillPolicy.ZERO); + fill_dp = new ExpressionDataPoint(); + + int i = 0; + for (final Map.Entry entry : results.entrySet()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Adding iterator " + entry.getValue()); + } + queries.put(entry.getKey(), entry.getValue()); + entry.getValue().setIndex(i); + index_to_names[i] = entry.getKey(); + ++i; + } + + computeUnion(); + + // calculate the starting timestamp from the various iterators + for (final ITimeSyncedIterator it : queries.values()) { + final long ts = it.nextTimestamp(); + if (ts < timestamp) { + timestamp = ts; + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Computed union: " + this); + } + } + + /** + * Private copy constructor that copies references and sets up new collections + * without copying results. + * @param iterator The iterator to copy from. + */ + private UnionIterator(final UnionIterator iterator) { + id = iterator.id; + union_on_query_tagks = iterator.union_on_query_tagks; + include_agg_tags = iterator.include_agg_tags; + timestamp = Long.MAX_VALUE; + queries = new HashMap(iterator.queries.size()); + current_values = new HashMap(queries.size()); + index_to_names = new String[queries.size()]; + fill_policy = iterator.fill_policy; + + int i = 0; + for (final Map.Entry entry : iterator.queries.entrySet()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Adding iterator " + entry.getValue()); + } + queries.put(entry.getKey(), entry.getValue()); + entry.getValue().setIndex(i); + index_to_names[i] = entry.getKey(); + ++i; + } + + computeUnion(); + + // calculate the starting timestamp from the various iterators + for (final ITimeSyncedIterator it : queries.values()) { + final long ts = it.nextTimestamp(); + if (ts < timestamp) { + timestamp = ts; + } + } + } + + /** + * Computes the union of all sets, matching on tags and optionally the + * aggregated tags across each variable. + */ + private void computeUnion() { + // key = flattened tags, array of queries.size() + final ByteMap ordered_union = + new ByteMap(); + + final Iterator it = queries.values().iterator(); + while (it.hasNext()) { + final ITimeSyncedIterator sub = it.next(); + final ExpressionDataPoint[] dps = sub.values(); + final ByteMap local_tags = new ByteMap(); + + for (int i = 0; i < sub.size(); i++) { + final byte[] key = flattenTags(union_on_query_tagks, include_agg_tags, + dps[i], sub); + local_tags.put(key, i); + ExpressionDataPoint[] udps = ordered_union.get(key); + if (udps == null) { + udps = new ExpressionDataPoint[queries.size()]; + ordered_union.put(key, udps); + } + udps[sub.getIndex()] = dps[i]; + } + } + + if (ordered_union.size() < 1) { + // if no data, just stop here + return; + } + + setCurrentAndMeta(ordered_union); + } + + /** + * Takes the resulting union and builds the {@link #current_values} + * and {@link #meta} maps. + * @param ordered_union The union to build from. + */ + private void setCurrentAndMeta(final ByteMap + ordered_union) { + for (final String id : queries.keySet()) { + current_values.put(id, new ExpressionDataPoint[ordered_union.size()]); + } + + int i = 0; + for (final ExpressionDataPoint[] idps : ordered_union.values()) { + for (int x = 0; x < idps.length; x++) { + final ExpressionDataPoint[] current_dps = + current_values.get(index_to_names[x]); + current_dps[i] = idps[x]; + } + ++i; + } + + // set fills on nulls + for (final ExpressionDataPoint[] idps : current_values.values()) { + for (i = 0; i < idps.length; i++) { + if (idps[i] == null) { + idps[i] = fill_dp; + } + } + } + series_size = ordered_union.size(); + } + + /** + * Creates a key based on the concatenation of the tag pairs then the agg + * tag keys. + * @param use_query_tags Whether or not to include tags returned with the + * results or just use those group by'd in the query + * @param include_agg_tags Whether or not to include the aggregated tags in + * the identifier + * @param dp The current expression data point + * @param sub The sub query iterator + * @return A byte array with the flattened tag keys and values. Note that + * if the tags set is empty, this may return an empty array (but not a null + * array) + */ + static byte[] flattenTags(final boolean use_query_tags, + final boolean include_agg_tags, final ExpressionDataPoint dp, + final ITimeSyncedIterator sub) { + if (dp.tags().isEmpty()) { + return HBaseClient.EMPTY_ARRAY; + } + final int tagk_width = TSDB.tagk_width(); + final int tagv_width = TSDB.tagv_width(); + + final ByteSet query_tagks; + // NOTE: We MAY need the agg tags but I'm not sure yet + final int tag_size; + if (use_query_tags) { + int i = 0; + if (sub.getQueryTagKs() != null && !sub.getQueryTagKs().isEmpty()) { + query_tagks = sub.getQueryTagKs(); + for (final Map.Entry pair : dp.tags().entrySet()) { + if (query_tagks.contains(pair.getKey())) { + i++; + } + } + } else { + query_tagks = new ByteSet(); + } + tag_size = i; + } else { + query_tagks = new ByteSet(); + tag_size = dp.tags().size(); + } + + final int length = (tag_size * (tagk_width + tagv_width)) + + (include_agg_tags ? (dp.aggregatedTags().size() * tagk_width) : 0); + final byte[] key = new byte[length]; + int idx = 0; + for (final Entry pair : dp.tags().entrySet()) { + if (use_query_tags && !query_tagks.contains(pair.getKey())) { + continue; + } + System.arraycopy(pair.getKey(), 0, key, idx, tagk_width); + idx += tagk_width; + System.arraycopy(pair.getValue(), 0, key, idx, tagv_width); + idx += tagv_width; + } + if (include_agg_tags) { + for (final byte[] tagk : dp.aggregatedTags()) { + System.arraycopy(tagk, 0, key, idx, tagk_width); + idx += tagk_width; + } + } + return key; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("UnionIterator(id=") + .append(id) + .append(", useQueryTags=") + .append(union_on_query_tagks) + .append(", includeAggTags=") + .append(include_agg_tags) + .append(", index=") + .append(index) + .append(", queries=") + .append(queries); + return buf.toString(); + } + + // Iterator implementations + + @Override + public boolean hasNext() { + for (final ITimeSyncedIterator sub : queries.values()) { + if (sub.hasNext()) { + return true; + } + } + return false; + } + + @Override + public ExpressionDataPoint[] next(long timestamp) { + throw new NotImplementedException(); + } + + @Override + public long nextTimestamp() { + long ts = Long.MAX_VALUE; + for (final ITimeSyncedIterator sub : queries.values()) { + if (sub != null) { + final long t = sub.nextTimestamp(); + if (t < ts) { + ts = t; + } + } + } + return ts; + } + + @Override + public int size() { + throw new NotImplementedException(); + } + + @Override + public ExpressionDataPoint[] values() { + throw new NotImplementedException(); + } + + @Override + public void nullIterator(int index) { + throw new NotImplementedException(); + } + + @Override + public int getIndex() { + return index; + } + + @Override + public void setIndex(int index) { + this.index = index; + } + + @Override + public String getId() { + return id; + } + + @Override + public ByteSet getQueryTagKs() { + throw new NotImplementedException(); + } + + @Override + public void setFillPolicy(NumericFillPolicy policy) { + this.fill_policy = policy; + } + + @Override + public NumericFillPolicy getFillPolicy() { + return fill_policy; + } + + @Override + public ITimeSyncedIterator getCopy() { + return new UnionIterator(this); + } + + @Override + public void next() { + if (!hasNext()) { + throw new IllegalDataException("No more data"); + } + for (final ITimeSyncedIterator sub : queries.values()) { + sub.next(timestamp); + } + // reset the fill data point + fill_dp.reset(timestamp, fill_policy.getValue()); + timestamp = nextTimestamp(); + } + + @Override + public Map getResults() { + return current_values; + } + + @Override + public int getSeriesSize() { + return series_size; + } +} diff --git a/test/query/expression/BaseTimeSyncedIteratorTest.java b/test/query/expression/BaseTimeSyncedIteratorTest.java index 4d8b0a0cb8..252fbb8796 100644 --- a/test/query/expression/BaseTimeSyncedIteratorTest.java +++ b/test/query/expression/BaseTimeSyncedIteratorTest.java @@ -18,6 +18,7 @@ import net.opentsdb.core.BaseTsdbTest; import net.opentsdb.core.DataPoints; +import net.opentsdb.core.FillPolicy; import net.opentsdb.core.Query; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; @@ -132,9 +133,10 @@ protected void runQueries(final ArrayList subs) throws Exception { results.put(Integer.toString(index), new Pair( query.getQueries().get(index), dps)); - iterators.put(Integer.toString(index), - new TimeSyncedIterator(Integer.toString(index), - query.getQueries().get(index).getFilterTagKs(), dps)); + final ITimeSyncedIterator it = new TimeSyncedIterator(Integer.toString(index), + query.getQueries().get(index).getFilterTagKs(), dps); + it.setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)); + iterators.put(Integer.toString(index), it); index++; } } @@ -311,9 +313,9 @@ protected void oneExtraSameE() throws Exception { tags = new HashMap(2); tags.put("D", "G"); tags.put("E", "E"); - tsdb.addPoint("B", 1431561600, 14, tags).joinUninterruptibly(); - tsdb.addPoint("B", 1431561660, 15, tags).joinUninterruptibly(); - tsdb.addPoint("B", 1431561720, 16, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561600, 17, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561660, 18, tags).joinUninterruptibly(); + tsdb.addPoint("B", 1431561720, 19, tags).joinUninterruptibly(); } /** diff --git a/test/query/expression/TestUnionIterator.java b/test/query/expression/TestUnionIterator.java new file mode 100644 index 0000000000..24083eefc5 --- /dev/null +++ b/test/query/expression/TestUnionIterator.java @@ -0,0 +1,1074 @@ +// 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.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.hbase.async.HBaseClient; +import org.hbase.async.Bytes.ByteMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.FillPolicy; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.storage.MockBase; +import net.opentsdb.utils.ByteSet; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TimeSyncedIterator.class }) +public class TestUnionIterator extends BaseTimeSyncedIteratorTest { + + /** used for the flattenTags tests */ + private static final byte[] UID1 = new byte[] { 0, 0, 1 }; + private static final byte[] UID2 = new byte[] { 0, 0, 2 }; + private static final byte[] UID3 = new byte[] { 0, 0, 3 }; + private ByteMap tags; + private ByteSet agg_tags; + private ITimeSyncedIterator sub; + private ByteSet query_tags; + private NumericFillPolicy fill_policy; + + @Before + public void beforeLocal() throws Exception { + tags = new ByteMap(); + tags.put(UID1, UID1); + tags.put(UID2, UID2); + + agg_tags = new ByteSet(); + agg_tags.add(UID3); + + fill_policy = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER); + sub = mock(ITimeSyncedIterator.class); + query_tags = new ByteSet(); + query_tags.add(UID1); + when(sub.getQueryTagKs()).thenReturn(query_tags); + when(sub.getFillPolicy()).thenReturn(fill_policy); + } + + @Test (expected = NullPointerException.class) + public void ctorNullResults() { + new UnionIterator("it", null, true, true); + } + + @Test + public void ctorEmptyResults() { + final UnionIterator it = new UnionIterator("it", + new HashMap(), true, true); + assertEquals(0, it.getSeriesSize()); + assertFalse(it.hasNext()); + } + + @Test + public void twoAndThreeSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[2]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[2].toDouble(), 0.0001); + ts += 60000; + } + } + + @Test + public void twoAndThreeSeriesExtraDP() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + final HashMap tags = new HashMap(2); + tags.put("D", "G"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561630, 14, tags).joinUninterruptibly(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[2]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesUnionToFour() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(4, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 17, 14 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(4, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(ts, set_dps[3].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + assertEquals(0, set_dps[3].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(4, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(ts, set_dps[3].timestamp()); + assertEquals(values[3]++, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[2].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[3].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesUnionToExtraDPs() throws Exception { + reduceToOne(); // though we won't :) + queryAB_Dstar(); + + HashMap tags = new HashMap(2); + tags.put("D", "F"); + tags.put("E", "E"); + tsdb.addPoint("A", 1431561630, 1024, tags).joinUninterruptibly(); + + tags = new HashMap(2); + tags.put("D", "Q"); + tags.put("E", "E"); + tsdb.addPoint("B", 1431561630, 1024, tags).joinUninterruptibly(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(5, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 17, 11, 14 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(5, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(ts, set_dps[3].timestamp()); + assertEquals(ts, set_dps[4].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + assertEquals(0, set_dps[3].toDouble(), 0.0001); + assertEquals(0, set_dps[4].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(5, set_dps.length); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(ts, set_dps[3].timestamp()); + assertEquals(ts, set_dps[4].timestamp()); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[2].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[3].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[4].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesAgged() throws Exception { + threeSameE(); + queryAB_AggAll(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 12, 42 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1], set_dps[0].toDouble(), 0.0001); + + values[0] += 3; + values[1] += 3; + + ts += 60000; + } + } + + @Test + public void threeSeriesWithNaNs() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + for (ITimeSyncedIterator iterator : iterators.values()) { + iterator.setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)); + } + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(1, set_dps[0].toDouble(), 0.0001); + assertEquals(4, set_dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[2].toDouble())); + + // whole series is NaN'd + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + assertTrue(Double.isNaN(set_dps[2].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertEquals(5, set_dps[1].toDouble(), 0.0001); + assertEquals(8, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertEquals(15, set_dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[2].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(3, set_dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + assertEquals(9, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(13, set_dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + assertEquals(19, set_dps[2].toDouble(), 0.0001); + + assertFalse(it.hasNext()); + } + + @Test + public void twoSeriesTimeOffset() throws Exception { + timeOffset(); + queryAB_Dstar(); + for (ITimeSyncedIterator iterator : iterators.values()) { + iterator.setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)); + } + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(1, set_dps[0].toDouble(), 0.0001); + assertEquals(4, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(2, set_dps[0].toDouble(), 0.0001); + assertEquals(5, set_dps[1].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(13, set_dps[0].toDouble(), 0.0001); + assertEquals(16, set_dps[1].toDouble(), 0.0001); + + ts += 60000; + it.next(); + + set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertTrue(Double.isNaN(set_dps[0].toDouble())); + assertTrue(Double.isNaN(set_dps[1].toDouble())); + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(14, set_dps[0].toDouble(), 0.0001); + assertEquals(17, set_dps[1].toDouble(), 0.0001); + + assertFalse(it.hasNext()); + } + + @Test + public void threeSeriesUsingResultTags() throws Exception { + threeDifE(); + queryAB_Dstar(); + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(6, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(6, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(ts, set_dps[3].timestamp()); + assertEquals(ts, set_dps[4].timestamp()); + assertEquals(ts, set_dps[5].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[2].toDouble(), 0.0001); + assertEquals(0, set_dps[3].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[4].toDouble(), 0.0001); + assertEquals(0, set_dps[5].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(6, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(ts, set_dps[3].timestamp()); + assertEquals(ts, set_dps[4].timestamp()); + assertEquals(ts, set_dps[5].timestamp()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[3].toDouble(), 0.0001); + assertEquals(0, set_dps[4].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[5].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void threeSeriesUsingQueryTags() throws Exception { + threeDifE(); + queryAB_Dstar(); + final UnionIterator it = new UnionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + //assertEquals(0, set_dps[3].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[3]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void commonAggregatedTag() throws Exception { + twoSeriesAggedE(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 22 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1], set_dps[0].toDouble(), 0.0001); + + values[0] += 2; + values[1] += 2; + + ts += 60000; + } + } + + @Test + public void extraAggTagIgnored() throws Exception { + twoSeriesAggedEandExtraTagK(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 22 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1], set_dps[0].toDouble(), 0.0001); + + values[0] += 2; + values[1] += 2; + + ts += 60000; + } + } + + @Test + public void extraAggTag() throws Exception { + twoSeriesAggedEandExtraTagK(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, true); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 22 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + values[0] += 2; + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1], set_dps[1].toDouble(), 0.0001); + values[1] += 2; + + ts += 60000; + } + } + + @Test + public void onlyOneResultSet() throws Exception { + threeSameENoB(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void onlyOneResultSetQueryTags() throws Exception { + threeSameENoB(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void onlyOneResultSetAggTags() throws Exception { + threeSameENoB(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, true); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void oneAggedOneTagged() throws Exception { + oneAggedTheOtherTagged(); + queryAB_AggAll(); + final UnionIterator it = new UnionIterator("it", iterators, false, true); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(2, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 11 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + values[0] += 2; + + set_dps = dps.get("1"); + assertEquals(2, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + System.out.println(set_dps[0].toDouble()); + System.out.println(set_dps[1].toDouble()); + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void oneAggedOneTaggedUseQueryTagsWoutQueryTags() throws Exception { + oneAggedTheOtherTagged(); + queryAB_AggAll(); + + final UnionIterator it = new UnionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 2, 11 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[0], set_dps[0].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(values[1]++, set_dps[0].toDouble(), 0.0001); + + // the first set is agged + values[0] += 2; + + ts += 60000; + } + } + + @Test + public void singleSeries() throws Exception { + oneExtraSameE(); + queryA_DD(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(1, dps.size()); + assertEquals(1, it.getSeriesSize()); + + long ts = 1431561600000L; + double value = 1; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(1, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(value++, set_dps[0].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void setAMissingE() throws Exception { + threeAMissingE(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(6, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(6, set_dps.length); + for (int i = 0; i < set_dps.length; i++) { + assertEquals(ts, set_dps[i].timestamp()); + } + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(0, set_dps[1].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[2].toDouble(), 0.0001); + assertEquals(0, set_dps[3].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[4].toDouble(), 0.0001); + assertEquals(0, set_dps[5].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(6, set_dps.length); + for (int i = 0; i < set_dps.length; i++) { + assertEquals(ts, set_dps[i].timestamp()); + } + assertEquals(0, set_dps[0].toDouble(), 0.0001); + assertEquals(values[3]++, set_dps[1].toDouble(), 0.0001); + assertEquals(0, set_dps[2].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[3].toDouble(), 0.0001); + assertEquals(0, set_dps[4].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[5].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void setAMissingEQueryTags() throws Exception { + threeAMissingE(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, true, false); + final Map dps = it.getResults(); + assertTrue(it.hasNext()); + assertEquals(2, dps.size()); + assertEquals(3, it.getSeriesSize()); + + long ts = 1431561600000L; + double values[] = new double[] { 1, 4, 7, 11, 14, 17 }; + while (it.hasNext()) { + it.next(); + + DataPoint[] set_dps = dps.get("0"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[0]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[2]++, set_dps[2].toDouble(), 0.0001); + + set_dps = dps.get("1"); + assertEquals(3, set_dps.length); + assertEquals(ts, set_dps[0].timestamp()); + assertEquals(ts, set_dps[1].timestamp()); + assertEquals(ts, set_dps[2].timestamp()); + assertEquals(values[3]++, set_dps[0].toDouble(), 0.0001); + assertEquals(values[4]++, set_dps[1].toDouble(), 0.0001); + assertEquals(values[5]++, set_dps[2].toDouble(), 0.0001); + + ts += 60000; + } + } + + @Test + public void noData() throws Exception { + setDataPointStorage(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + final Map dps = it.getResults(); + assertEquals(0, dps.size()); + assertFalse(it.hasNext()); + assertEquals(0, it.getSeriesSize()); + } + + @Test (expected = IllegalDataException.class) + public void nextException() throws Exception { + threeDisjointSameE(); + queryAB_Dstar(); + + final UnionIterator it = new UnionIterator("it", iterators, false, false); + it.next(); + it.next(); + it.next(); + it.next(); + } + + @Test + public void flattenTags() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(false, false, dp, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + @Test + public void flattenTagsWithAgg() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(false, true, dp, sub); + assertArrayEquals( + MockBase.concatByteArrays(UID1, UID1, UID2, UID2, UID3), flat); + } + + @Test + public void flattenTagsQueryTags() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(true, false, dp, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1), flat); + } + + @Test + public void flattenTagsQueryTagsWithAgg() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(true, true, dp, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID3), flat); + } + + @Test + public void flattenEmptyTags() throws Exception { + tags.clear(); + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(false, false, dp, sub); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, flat); + } + + @Test + public void flattenEmptyTagsWithAggEmpty() throws Exception { + agg_tags.clear(); + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(false, true, dp, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + // TODO - how will this play out if we choose a default agg of "none" and the + // user hasn't asked for any filtering? + @Test + public void flattenTagsQueryTagsEmpty() throws Exception { + query_tags.clear(); + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(true, false, dp, sub); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, flat); + } + + @Test + public void flattenTagsQueryTagsEmptyWithAgg() throws Exception { + query_tags.clear(); + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(true, true, dp, sub); + assertArrayEquals(UID3, flat); + } + + @Test (expected = NullPointerException.class) + public void flattenTagsNullTags() throws Exception { + final ExpressionDataPoint dp = getMockDB(null, agg_tags); + UnionIterator.flattenTags(false, false, dp, sub); + } + + @Test + public void flattenTagsNullAggTagsNotRequested() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, null); + final byte[] flat = UnionIterator.flattenTags(false, false, dp, sub); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + @Test (expected = NullPointerException.class) + public void flattenTagsNullAggTags() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, null); + UnionIterator.flattenTags(false, true, dp, sub); + } + + @Test + public void flattenTagsNullSubNotRequested() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + final byte[] flat = UnionIterator.flattenTags(false, false, dp, null); + assertArrayEquals(MockBase.concatByteArrays(UID1, UID1, UID2, UID2), flat); + } + + @Test (expected = NullPointerException.class) + public void flattenTagsNullSub() throws Exception { + final ExpressionDataPoint dp = getMockDB(tags, agg_tags); + UnionIterator.flattenTags(true, false, dp, null); + } + + /** + * A helper to mock out the calls for flatten tags + * @param tags The tags to return + * @param agg_tags The aggregated tags to return + * @return A mocked data point + */ + private ExpressionDataPoint getMockDB(final ByteMap tags, + final ByteSet agg_tags) { + final ExpressionDataPoint dp = mock(ExpressionDataPoint.class); + when(dp.tags()).thenReturn(tags); + when(dp.aggregatedTags()).thenReturn(agg_tags); + return dp; + } +} From e4e712ddd8e83a85998f22984f8be78b58d3f8b9 Mon Sep 17 00:00:00 2001 From: Yulai Fu Date: Thu, 5 Nov 2015 17:06:48 -0800 Subject: [PATCH 314/826] Add pojo classes using the builder pattern for the new query format. Signed-off-by: Chris Larsen --- Makefile.am | 18 ++ src/query/pojo/Downsampler.java | 146 ++++++++++++++ src/query/pojo/Expression.java | 159 ++++++++++++++++ src/query/pojo/Filter.java | 115 +++++++++++ src/query/pojo/Join.java | 132 +++++++++++++ src/query/pojo/Metric.java | 206 ++++++++++++++++++++ src/query/pojo/Output.java | 117 ++++++++++++ src/query/pojo/Query.java | 274 +++++++++++++++++++++++++++ src/query/pojo/Timespan.java | 208 ++++++++++++++++++++ src/query/pojo/Validatable.java | 59 ++++++ test/query/pojo/TestDownsampler.java | 104 ++++++++++ test/query/pojo/TestExpression.java | 96 ++++++++++ test/query/pojo/TestFilter.java | 97 ++++++++++ test/query/pojo/TestJoin.java | 66 +++++++ test/query/pojo/TestMetric.java | 123 ++++++++++++ test/query/pojo/TestOutput.java | 45 +++++ test/query/pojo/TestQuery.java | 223 ++++++++++++++++++++++ test/query/pojo/TestTimeSpan.java | 137 ++++++++++++++ 18 files changed, 2325 insertions(+) create mode 100644 src/query/pojo/Downsampler.java create mode 100644 src/query/pojo/Expression.java create mode 100644 src/query/pojo/Filter.java create mode 100644 src/query/pojo/Join.java create mode 100644 src/query/pojo/Metric.java create mode 100644 src/query/pojo/Output.java create mode 100644 src/query/pojo/Query.java create mode 100644 src/query/pojo/Timespan.java create mode 100644 src/query/pojo/Validatable.java create mode 100644 test/query/pojo/TestDownsampler.java create mode 100644 test/query/pojo/TestExpression.java create mode 100644 test/query/pojo/TestFilter.java create mode 100644 test/query/pojo/TestJoin.java create mode 100644 test/query/pojo/TestMetric.java create mode 100644 test/query/pojo/TestOutput.java create mode 100644 test/query/pojo/TestQuery.java create mode 100644 test/query/pojo/TestTimeSpan.java diff --git a/Makefile.am b/Makefile.am index 2916b67f2a..d9d7e5b29c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -97,6 +97,15 @@ tsdb_SRC := \ src/query/filter/TagVNotLiteralOrFilter.java \ src/query/filter/TagVRegexFilter.java \ src/query/filter/VariableIterator.java \ + src/query/pojo/Downsampler.java \ + src/query/pojo/Expression.java \ + src/query/pojo/Filter.java \ + src/query/pojo/Join.java \ + src/query/pojo/Metric.java \ + src/query/pojo/Output.java \ + src/query/pojo/Query.java \ + src/query/pojo/Timespan.java \ + src/query/pojo/Validatable.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ src/search/TimeSeriesLookup.java \ @@ -268,6 +277,15 @@ test_SRC := \ test/query/filter/TestTagVNotLiteralOrFilter.java \ test/query/filter/TestTagVRegexFilter.java \ test/query/filter/TestTagVWildcardFilter.java \ + test/query/pojo/TestDownsampler.java \ + test/query/pojo/TestExpression.java \ + test/query/pojo/TestFilter.java \ + test/query/pojo/TestJoin.java \ + test/query/pojo/TestMetric.java \ + test/query/pojo/TestOutput.java \ + test/query/pojo/TestQuery.java \ + test/query/pojo/TestTimespan.java \ + test/query/pojo/TestValidatable.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ diff --git a/src/query/pojo/Downsampler.java b/src/query/pojo/Downsampler.java new file mode 100644 index 0000000000..dfda751ffe --- /dev/null +++ b/src/query/pojo/Downsampler.java @@ -0,0 +1,146 @@ +// 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.pojo; + +import java.util.NoSuchElementException; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.utils.DateTime; + +/** + * Pojo builder class used for serdes of the downsampler component of a query + * @since 2.3 + */ +@JsonDeserialize(builder = Downsampler.Builder.class) +public class Downsampler extends Validatable { + /** The relative interval with value and unit, e.g. 60s */ + private String interval; + + /** The aggregator to use for downsampling */ + private String aggregator; + + /** A fill policy for downsampling and working with missing values */ + private NumericFillPolicy fill_policy; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + public Downsampler(Builder builder) { + interval = builder.interval; + aggregator = builder.aggregator; + fill_policy = builder.fillPolicy; + } + + /** @return A new builder for the downsampler */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the downsampler + * @throws IllegalArgumentException if one or more parameters were invalid + */ + public void validate() { + if (interval == null || interval.isEmpty()) { + throw new IllegalArgumentException("Missing or empty interval"); + } + DateTime.parseDuration(interval); + + if (aggregator == null || aggregator.isEmpty()) { + throw new IllegalArgumentException("Missing or empty aggregator"); + } + try { + Aggregators.get(aggregator.toLowerCase()); + } catch (final NoSuchElementException e) { + throw new IllegalArgumentException("Invalid aggregator"); + } + + if (fill_policy != null) { + fill_policy.validate(); + } + } + + /** @return the interval for the downsampler */ + public String getInterval() { + return interval; + } + + /** @return the name of the aggregator to use */ + public String getAggregator() { + return aggregator; + } + + /** @return the fill policy to use */ + public NumericFillPolicy getFillPolicy() { + return fill_policy; + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final Downsampler downsampler = (Downsampler) o; + + return Objects.equal(interval, downsampler.interval) + && Objects.equal(aggregator, downsampler.aggregator) + && Objects.equal(fill_policy, downsampler.fill_policy); + } + + @Override + public int hashCode() { + return Objects.hashCode(interval, aggregator, fill_policy); + } + + /** + * A builder for the downsampler component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String interval; + @JsonProperty + private String aggregator; + @JsonProperty + private NumericFillPolicy fillPolicy; + + public Builder setInterval(String interval) { + this.interval = interval; + return this; + } + + public Builder setAggregator(String aggregator) { + this.aggregator = aggregator; + return this; + } + + public Builder setFillPolicy(NumericFillPolicy fill_policy) { + this.fillPolicy = fill_policy; + return this; + } + + public Downsampler build() { + return new Downsampler(this); + } + } +} diff --git a/src/query/pojo/Expression.java b/src/query/pojo/Expression.java new file mode 100644 index 0000000000..9f5aaebdf5 --- /dev/null +++ b/src/query/pojo/Expression.java @@ -0,0 +1,159 @@ +// 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.pojo; + +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +/** + * Pojo builder class used for serdes of the expression component of a query + * @since 2.3 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(builder = Expression.Builder.class) +public class Expression extends Validatable { + /** An id for this expression for use in output selection or nested expressions */ + private String id; + + /** The raw expression as a string */ + private String expr; + + /** The joiner operator */ + private Join join; + + /** The fill policy to use for ? */ + private NumericFillPolicy fill_policy; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + protected Expression(Builder builder) { + id = builder.id; + expr = builder.expr; + join = builder.join; + fill_policy = builder.fillPolicy; + } + + /** @return the id for this expression for use in output selection or + * nested expressions */ + public String getId() { + return id; + } + + /** @return the raw expression as a string */ + public String getExpr() { + return expr; + } + + /** @return he joiner operator */ + public Join getJoin() { + return join; + } + + /** @return the fill policy to use for ? */ + public NumericFillPolicy getFillPolicy() { + return fill_policy; + } + + /** @return A new builder for the expression */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the expression + * @throws IllegalArgumentException if one or more parameters were invalid + */ + public void validate() { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("missing or empty id"); + } + Query.validateId(id); + + if (expr == null || expr.isEmpty()) { + throw new IllegalArgumentException("missing or empty expr"); + } + + // others are optional + if (join == null) { + join = Join.Builder().setOperator(SetOperator.UNION).build(); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final Expression expression = (Expression) o; + + return Objects.equal(id, expression.id) + && Objects.equal(expr, expression.expr) + && Objects.equal(join, expression.join) + && Objects.equal(fill_policy, expression.fill_policy); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, expr, join, fill_policy); + } + + /** + * A builder for the downsampler component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String id; + @JsonProperty + private String expr; + @JsonProperty + private Join join; + @JsonProperty + private NumericFillPolicy fillPolicy; + + public Builder setId(String id) { + Query.validateId(id); + this.id = id; + return this; + } + + public Builder setExpression(String expr) { + this.expr = expr; + return this; + } + + public Builder setJoin(Join join) { + this.join = join; + return this; + } + + public Builder setFillPolicy(NumericFillPolicy fill_policy) { + this.fillPolicy = fill_policy; + return this; + } + + public Expression build() { + return new Expression(this); + } + } +} diff --git a/src/query/pojo/Filter.java b/src/query/pojo/Filter.java new file mode 100644 index 0000000000..902422da1f --- /dev/null +++ b/src/query/pojo/Filter.java @@ -0,0 +1,115 @@ +// 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.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +import net.opentsdb.query.filter.TagVFilter; + +import java.util.List; + +/** + * Pojo builder class used for serdes of a filter component of a query + * @since 2.3 + */ +@JsonDeserialize(builder = Filter.Builder.class) +public class Filter extends Validatable { + /** The id of the filter set to use in a metric query */ + private String id; + + /** The list of filters in the filter set */ + private List tags; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + private Filter(Builder builder) { + this.id = builder.id; + this.tags = builder.tags; + } + + /** @return the id of the filter set to use in a metric query */ + public String getId() { + return id; + } + + /** @return the list of filters in the filter set */ + public List getTags() { + return tags; + } + + /** @return A new builder for the filter */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the filter set + * @throws IllegalArgumentException if one or more parameters were invalid + */ + public void validate() { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("Missing or empty id"); + } + Query.validateId(id); + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final Filter filter = (Filter) o; + + return Objects.equal(id, filter.id) + && Objects.equal(tags, filter.tags); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, tags); + } + + /** + * A builder for the downsampler component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String id; + @JsonProperty + private List tags; + + public Builder setId(String id) { + Query.validateId(id); + this.id = id; + return this; + } + + public Builder setTags(List tags) { + this.tags = tags; + return this; + } + + public Filter build() { + return new Filter(this); + } + } +} diff --git a/src/query/pojo/Join.java b/src/query/pojo/Join.java new file mode 100644 index 0000000000..ea5c228fa3 --- /dev/null +++ b/src/query/pojo/Join.java @@ -0,0 +1,132 @@ +// 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.pojo; + +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +/** + * Pojo builder class used for serdes of the join component of a query + * @since 2.3 + */ +@JsonDeserialize(builder = Join.Builder.class) +public class Join extends Validatable { + /** The set operator to use for joining sets */ + private SetOperator operator; + + /** Whether or not to use the original query tags instead of the resulting + * series tags when joining. */ + private boolean use_query_tags = false; + + /** Whether or not to use the aggregated tags in the results when joining. */ + private boolean include_agg_tags = true; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + public Join(final Builder builder) { + operator = builder.operator; + use_query_tags = builder.useQueryTags; + include_agg_tags = builder.includeAggTags; + } + + /** @return the set operator to use for joining sets */ + public SetOperator getOperator() { + return operator; + } + + /** @return whether or not to use the original query tags instead of the + * resulting series tags when joining. */ + public boolean getUseQueryTags() { + return use_query_tags; + } + + /** @return Whether or not to use the aggregated tags in the results + * when joining. */ + public boolean getIncludeAggTags() { + return include_agg_tags; + } + + /** @return A new builder for the joiner */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the joiner + * @throws IllegalArgumentException if one or more parameters were invalid + */ + @Override + public void validate() { + if (operator == null) { + throw new IllegalArgumentException("Missing join operator"); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final Join join = (Join) o; + + return Objects.equal(operator, join.operator) + && Objects.equal(use_query_tags, join.use_query_tags) + && Objects.equal(include_agg_tags, join.include_agg_tags); + } + + @Override + public int hashCode() { + return Objects.hashCode(operator, use_query_tags, include_agg_tags); + } + + /** + * A builder for the downsampler component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private SetOperator operator; + @JsonProperty + private boolean useQueryTags = false; + @JsonProperty + private boolean includeAggTags = true; + + public Builder setOperator(final SetOperator operator) { + this.operator = operator; + return this; + } + + public Builder setUseQueryTags(final boolean use_query_tags) { + this.useQueryTags = use_query_tags; + return this; + } + + public Builder setIncludeAggTags(final boolean include_agg_tags) { + this.includeAggTags = include_agg_tags; + return this; + } + + public Join build() { + return new Join(this); + } + } +} diff --git a/src/query/pojo/Metric.java b/src/query/pojo/Metric.java new file mode 100644 index 0000000000..a5e85e0d5d --- /dev/null +++ b/src/query/pojo/Metric.java @@ -0,0 +1,206 @@ +// 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.pojo; + +import java.util.NoSuchElementException; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.utils.DateTime; + +/** + * Pojo builder class used for serdes of a metric component of a query + * @since 2.3 + */ +@JsonDeserialize(builder = Metric.Builder.class) +public class Metric extends Validatable { + /** The name of the metric */ + private String metric; + + /** An ID for the metric */ + private String id; + + /** The ID of a filter set */ + private String filter; + + /** An optional time offset for time over time expressions */ + private String time_offset; + + /** An optional aggregation override for the metric */ + private String aggregator; + + /** A fill policy for dealing with missing values in the metric */ + private NumericFillPolicy fill_policy; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + public Metric(Builder builder) { + metric = builder.metric; + id = builder.id; + filter = builder.filter; + time_offset = builder.timeOffset; + aggregator = builder.aggregator; + fill_policy = builder.fillPolicy; + } + + /** @return the name of the metric */ + public String getMetric() { + return metric; + } + + /** @return an ID for the metric */ + public String getId() { + return id; + } + + /** @return the ID of a filter set */ + public String getFilter() { + return filter; + } + + /** @return an optional time offset for time over time expressions */ + public String getTimeOffset() { + return time_offset; + } + + /** @return an optional aggregation override for the metric */ + public String getAggregator() { + return aggregator; + } + + /** @return a fill policy for dealing with missing values in the metric */ + public NumericFillPolicy getFillPolicy() { + return fill_policy; + } + + /** @return A new builder for the metric */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the metric + * @throws IllegalArgumentException if one or more parameters were invalid + */ + public void validate() { + if (metric == null || metric.isEmpty()) { + throw new IllegalArgumentException("missing or empty metric"); + } + + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("missing or empty id"); + } + Query.validateId(id); + + if (time_offset != null) { + DateTime.parseDateTimeString(time_offset, null); + } + + if (aggregator != null && !aggregator.isEmpty()) { + try { + Aggregators.get(aggregator.toLowerCase()); + } catch (final NoSuchElementException e) { + throw new IllegalArgumentException("Invalid aggregator"); + } + } + + if (fill_policy != null) { + fill_policy.validate(); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + final Metric that = (Metric) o; + + return Objects.equal(that.filter, filter) + && Objects.equal(that.id, id) + && Objects.equal(that.metric, metric) + && Objects.equal(that.time_offset, time_offset) + && Objects.equal(that.aggregator, aggregator) + && Objects.equal(that.fill_policy, fill_policy); + } + + @Override + public int hashCode() { + return Objects.hashCode(metric, id, filter, time_offset, aggregator, + fill_policy); + } + + /** + * A builder for a metric component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String metric; + @JsonProperty + private String id; + @JsonProperty + private String filter; + @JsonProperty + private String timeOffset; + @JsonProperty + private String aggregator; + @JsonProperty + private NumericFillPolicy fillPolicy; + + public Builder setMetric(String metric) { + this.metric = metric; + return this; + } + + public Builder setId(String id) { + Query.validateId(id); + this.id = id; + return this; + } + + public Builder setFilter(String filter) { + this.filter = filter; + return this; + } + + public Builder setTimeOffset(String time_offset) { + this.timeOffset = time_offset; + return this; + } + + public Builder setAggregator(String aggregator) { + this.aggregator = aggregator; + return this; + } + + public Builder setFillPolicy(NumericFillPolicy fill_policy) { + this.fillPolicy = fill_policy; + return this; + } + + public Metric build() { + return new Metric(this); + } + } +} diff --git a/src/query/pojo/Output.java b/src/query/pojo/Output.java new file mode 100644 index 0000000000..8b996cf9ab --- /dev/null +++ b/src/query/pojo/Output.java @@ -0,0 +1,117 @@ +// 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.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +/** + * Pojo builder class used for serdes of the output component of a query + * @since 2.3 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(builder = Output.Builder.class) +public class Output extends Validatable { + /** The ID of a metric or expression to emit */ + private String id; + + /** An alias to use as the metric name for the output */ + private String alias; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + public Output(Builder builder) { + this.id = builder.id; + this.alias = builder.alias; + } + + /** @return the ID of a metric or expression to emit */ + public String getId() { + return id; + } + + /** @return an alias to use as the metric name for the output */ + public String getAlias() { + return alias; + } + + /** @return A new builder for the output */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the output + * @throws IllegalArgumentException if one or more parameters were invalid + */ + @Override public void validate() { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("missing or empty id"); + } + Query.validateId(id); + } + + @Override + public String toString() { + return "var=" + id + ", alias=" + alias; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Output output = (Output) o; + + return Objects.equal(output.alias, alias) + && Objects.equal(output.id, id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, alias); + } + + /** + * A builder for the downsampler component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String id; + @JsonProperty + private String alias; + + public Builder setId(String id) { + Query.validateId(id); + this.id = id; + return this; + } + + public Builder setAlias(String alias) { + this.alias = alias; + return this; + } + + public Output build() { + return new Output(this); + } + } +} diff --git a/src/query/pojo/Query.java b/src/query/pojo/Query.java new file mode 100644 index 0000000000..09f62dcf78 --- /dev/null +++ b/src/query/pojo/Query.java @@ -0,0 +1,274 @@ +// 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.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Pojo builder class used for serdes of the expression query + * @since 2.3 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(builder = Query.Builder.class) +public class Query extends Validatable { + /** An optional name for the query */ + private String name; + + /** The timespan component of the query */ + private Timespan time; + + /** A list of filters */ + private List filters; + + /** A list of metrics */ + private List metrics; + + /** A list of expressions */ + private List expressions; + + /** A list of outputs */ + private List outputs; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + public Query(Builder builder) { + this.name = builder.name; + this.time = builder.time; + this.filters = builder.filters; + this.metrics = builder.metrics; + this.expressions = builder.expressions; + this.outputs = builder.outputs; + } + + /** @return an optional name for the query */ + public String getName() { + return name; + } + + /** @return the timespan component of the query */ + public Timespan getTime() { + return time; + } + + /** @return a list of filters */ + public List getFilters() { + return filters; + } + + /** @return a list of metrics */ + public List getMetrics() { + return metrics; + } + + /** @return a list of expressions */ + public List getExpressions() { + return expressions; + } + + /** @return a list of outputs */ + public List getOutputs() { + return outputs; + } + + /** @return A new builder for the query */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the query + * @throws IllegalArgumentException if one or more parameters were invalid + */ + public void validate() { + if (time == null) { + throw new IllegalArgumentException("missing time"); + } + + validatePOJO(time, "time"); + + if (metrics == null || metrics.isEmpty()) { + throw new IllegalArgumentException("missing or empty metrics"); + } + + final Set metric_ids = new HashSet(); + + for (Metric metric : metrics) { + if (metric_ids.contains(metric.getId())) { + throw new IllegalArgumentException("duplicated metric id: " + + metric.getId()); + } + metric_ids.add(metric.getId()); + } + + final Set filter_ids = new HashSet(); + + for (Filter filter : filters) { + if (filter_ids.contains(filter.getId())) { + throw new IllegalArgumentException("duplicated filter id: " + + filter.getId()); + } + filter_ids.add(filter.getId()); + } + + final Set expression_ids = new HashSet(); + + for (Expression expression : expressions) { + if (expression_ids.contains(expression.getId())) { + throw new IllegalArgumentException("duplicated expression id: " + + expression.getId()); + } + expression_ids.add(expression.getId()); + } + + validateCollection(metrics, "metric"); + + if (filters != null) { + validateCollection(filters, "filter"); + } + + if (expressions != null) { + validateCollection(expressions, "expression"); + } + + validateFilters(); + } + + /** Validates the filters, making sure each metric has a filter + * @throws IllegalArgumentException if one or more parameters were invalid + */ + private void validateFilters() { + final Set ids = new HashSet(); + for (Filter filter : filters) { + ids.add(filter.getId()); + } + + for (Metric metric : metrics) { + if (!ids.contains(metric.getFilter())) { + throw new IllegalArgumentException( + String.format("unrecognized filter id %s in metric %s", + metric.getFilter(), metric.getId())); + } + } + } + + /** + * Makes sure the ID has only letters and characters + * @param id The ID to parse + * @throws IllegalArgumentException if the ID is invalid + */ + public static void validateId(final String id) { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("The ID cannot be null or empty"); + } + for (int i = 0; i < id.length(); i++) { + final char c = id.charAt(i); + if (!(Character.isLetterOrDigit(c))) { + throw new IllegalArgumentException("Invalid id (\"" + id + + "\"): illegal character: " + c); + } + } + if (id.length() == 1) { + if (Character.isDigit(id.charAt(0))) { + throw new IllegalArgumentException("The ID cannot be an integer"); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Query query = (Query) o; + + return Objects.equal(query.expressions, expressions) + && Objects.equal(query.filters, filters) + && Objects.equal(query.metrics, metrics) + && Objects.equal(query.name, name) + && Objects.equal(query.outputs, outputs) + && Objects.equal(query.time, time); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, time, filters, metrics, expressions, outputs); + } + + /** + * A builder for the query component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String name; + @JsonProperty + private Timespan time; + @JsonProperty + private List filters; + @JsonProperty + private List metrics; + @JsonProperty + private List expressions; + @JsonProperty + private List outputs; + + public Builder() { } + + public Builder setName(final String name) { + this.name = name; + return this; + } + + public Builder setTime(final Timespan time) { + this.time = time; + return this; + } + + public Builder setFilters(final List filters) { + this.filters = filters; + return this; + } + + public Builder setMetrics(final List metrics) { + this.metrics = metrics; + return this; + } + + public Builder setExpressions(final List expressions) { + this.expressions = expressions; + return this; + } + + public Builder setOutputs(final List outputs) { + this.outputs = outputs; + return this; + } + + public Query build() { + return new Query(this); + } + } + +} diff --git a/src/query/pojo/Timespan.java b/src/query/pojo/Timespan.java new file mode 100644 index 0000000000..96e56ba255 --- /dev/null +++ b/src/query/pojo/Timespan.java @@ -0,0 +1,208 @@ +// 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.pojo; + +import java.util.NoSuchElementException; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Objects; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.utils.DateTime; + +/** + * Pojo builder class used for serdes of the timespan component of a query + * @since 2.3 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(builder = Timespan.Builder.class) +public class Timespan extends Validatable { + /** User given start date/time, could be relative or absolute */ + private String start; + + /** User given end date/time, could be relative, absolute or empty */ + private String end; + + /** User's timezone used for converting absolute human readable dates */ + private String timezone; + + /** An optional downsampler for all queries */ + private Downsampler downsampler; + + /** The global aggregator to use */ + private String aggregator; + + /** Whether or not to compute a rate */ + private boolean rate; + + /** + * Default ctor + * @param builder The builder to pull values from + */ + public Timespan(Builder builder) { + start = builder.start; + end = builder.end; + timezone = builder.timezone; + downsampler = builder.downsampler; + aggregator = builder.aggregator; + rate = builder.rate; + } + + /** @return user given start date/time, could be relative or absolute */ + public String getStart() { + return start; + } + + /** @return user given end date/time, could be relative, absolute or empty */ + public String getEnd() { + return end; + } + + /** @return user's timezone used for converting absolute human readable dates */ + public String getTimezone() { + return timezone; + } + + /** @return an optional downsampler for all queries */ + public Downsampler getDownsampler() { + return downsampler; + } + + /** @return the global aggregator to use */ + public String getAggregator() { + return aggregator; + } + + /** @return whether or not to compute a rate */ + public boolean isRate() { + return rate; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Timespan timespan = (Timespan) o; + + return Objects.equal(timespan.downsampler, downsampler) + && Objects.equal(timespan.end, end) + && Objects.equal(timespan.start, start) + && Objects.equal(timespan.timezone, timezone) + && Objects.equal(timespan.aggregator, aggregator) + && Objects.equal(timespan.rate, rate); + } + + @Override + public int hashCode() { + return Objects.hashCode(start, end, timezone, downsampler, aggregator, rate); + } + + /** @return A new builder for the downsampler */ + public static Builder Builder() { + return new Builder(); + } + + /** Validates the timespan + * @throws IllegalArgumentException if one or more parameters were invalid + */ + public void validate() { + if (start == null || start.isEmpty()) { + throw new IllegalArgumentException("missing or empty start"); + } + DateTime.parseDateTimeString(start, timezone); + + if (end != null && !end.isEmpty()) { + DateTime.parseDateTimeString(end, timezone); + } + + if (downsampler != null) { + downsampler.validate(); + } + + if (aggregator == null || aggregator.isEmpty()) { + throw new IllegalArgumentException("Missing or empty aggregator"); + } + + try { + Aggregators.get(aggregator.toLowerCase()); + } catch (final NoSuchElementException e) { + throw new IllegalArgumentException("Invalid aggregator"); + } + } + + /** + * A builder for the downsampler component of a query + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static final class Builder { + @JsonProperty + private String start; + + @JsonProperty + private String end; + + @JsonProperty + private String timezone; + + @JsonProperty + private Downsampler downsampler; + + @JsonProperty + private String aggregator; + + @JsonProperty + private boolean rate; + + public Builder setStart(final String start) { + this.start = start; + return this; + } + + public Builder setEnd(final String end) { + this.end = end; + return this; + } + + public Builder setTimezone(final String timezone) { + this.timezone = timezone; + return this; + } + + public Builder setDownsampler(final Downsampler downsample) { + this.downsampler = downsample; + return this; + } + + public Builder setAggregator(final String aggregator) { + this.aggregator = aggregator; + return this; + } + + public Builder setRate(final boolean rate) { + this.rate = rate; + return this; + } + + public Timespan build() { + return new Timespan(this); + } + } + +} diff --git a/src/query/pojo/Validatable.java b/src/query/pojo/Validatable.java new file mode 100644 index 0000000000..77b53af650 --- /dev/null +++ b/src/query/pojo/Validatable.java @@ -0,0 +1,59 @@ +// 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.pojo; + +import java.util.Collection; +import java.util.Iterator; + +/** + * An interface for the pojos to implement to make sure all the bits of the + * expression queries are there + * @since 2.3 + */ +public abstract class Validatable { + abstract public void validate(); + + /** + * Iterate through a field that is a collection of POJOs and validate each of + * them. Inherit member POJO's error message. + * @param collection the validatable POJO collection + * @param name name of the field + */ + void validateCollection(final Collection collection, + final String name) { + Iterator iterator = collection.iterator(); + int i = 0; + while (iterator.hasNext()) { + try { + iterator.next().validate(); + } catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid " + name + + " at index " + i, e); + } + i++; + } + } + + /** + * Validate a single POJO validate + * @param pojo The POJO object to validate + * @param name name of the field + */ + void validatePOJO(final T pojo, final String name) { + try { + pojo.validate(); + } catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid " + name, e); + } + } +} diff --git a/test/query/pojo/TestDownsampler.java b/test/query/pojo/TestDownsampler.java new file mode 100644 index 0000000000..9613999238 --- /dev/null +++ b/test/query/pojo/TestDownsampler.java @@ -0,0 +1,104 @@ +// 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.pojo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import net.opentsdb.core.FillPolicy; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +public class TestDownsampler { + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIntervalIsNull() throws Exception { + String json = "{\"aggregator\":\"sum\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIntervalIsEmpty() throws Exception { + String json = "{\"interval\":\"\",\"aggregator\":\"sum\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIntervalIsInvalid() throws Exception { + String json = "{\"interval\":\"45foo\",\"aggregator\":\"sum\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenAggregatorIsNull() throws Exception { + String json = "{\"interval\":\"1h\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenAggregatorIsEmpty() throws Exception { + String json = "{\"interval\":\"1h\",\"aggregator\":\"\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenAggregatorIsInvalid() throws Exception { + String json = "{\"interval\":\"1h\",\"aggregator\":\"no such agg\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + } + + @Test + public void deserialize() throws Exception { + String json = "{\"interval\":\"1h\",\"aggregator\":\"zimsum\"}"; + Downsampler downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + Downsampler expected = Downsampler.Builder() + .setInterval("1h").setAggregator("zimsum").build(); + assertEquals(expected, downsampler); + + json = "{\"interval\":\"1h\",\"aggregator\":\"zimsum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"},\"junkfield\":true}"; + downsampler = JSON.parseToObject(json, Downsampler.class); + downsampler.validate(); + expected = Downsampler.Builder() + .setInterval("1h").setAggregator("zimsum") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)).build(); + assertEquals(expected, downsampler); + } + + @Test + public void serialize() throws Exception { + Downsampler downsampler = Downsampler.Builder() + .setInterval("1h").setAggregator("zimsum").build(); + String json = JSON.serializeToString(downsampler); + assertTrue(json.contains("\"interval\":\"1h\"")); + assertTrue(json.contains("\"aggregator\":\"zimsum\"")); + assertTrue(json.contains("\"fillPolicy\":null")); + + downsampler = Downsampler.Builder() + .setInterval("15m").setAggregator("max") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)).build(); + json = JSON.serializeToString(downsampler); + assertTrue(json.contains("\"interval\":\"15m\"")); + assertTrue(json.contains("\"aggregator\":\"max\"")); + assertTrue(json.contains("\"fillPolicy\":{")); + assertTrue(json.contains("\"policy\":\"nan\"")); + } +} diff --git a/test/query/pojo/TestExpression.java b/test/query/pojo/TestExpression.java new file mode 100644 index 0000000000..6d1f5afd4e --- /dev/null +++ b/test/query/pojo/TestExpression.java @@ -0,0 +1,96 @@ +// 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.pojo; + +import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TestExpression { + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIdIsNull() throws Exception { + String json = "{\"expr\":\"a + b + c\"}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIdIsEmpty() throws Exception { + String json = "{\"expr\":\"a + b + c\",\"id\":\"\"}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIdIsInvalid() throws Exception { + String json = "{\"expr\":\"a + b + c\",\"id\":\"system.busy\"}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenExprIsNull() throws Exception { + String json = "{\"id\":\"1\"}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenExprIsEmpty() throws Exception { + String json = "{\"id\":\"1\",\"expr\":\"\"}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenJoinIsInvalid() throws Exception { + String json = "{\"expr\":\"a + b + c\",\"id\":\"system.busy\"," + + "\"join\":{\"operator\":\"nosuchjoin\"}}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + } + + @Test + public void deserialize() throws Exception { + String json = "{\"id\":\"e\",\"expr\":\"a + b + c\"}"; + Expression expression = JSON.parseToObject(json, Expression.class); + expression.validate(); + Expression expected = Expression.Builder().setId("e") + .setExpression("a + b + c").setJoin( + Join.Builder().setOperator(SetOperator.UNION).build()).build(); + assertEquals(expected, expression); + } + + @Test + public void serialize() throws Exception { + Expression expression = Expression.Builder().setId("e1") + .setJoin(Join.Builder().setOperator(SetOperator.UNION).build()) + .setExpression("a + b + c").build(); + String actual = JSON.serializeToString(expression); + assertTrue(actual.contains("\"id\":\"e1\"")); + assertTrue(actual.contains("\"expr\":\"a + b + c\"")); + assertTrue(actual.contains("\"join\":{\"operator\":\"union\"")); + + } + + @Test + public void unknownShouldBeIgnored() throws Exception { + String json = "{\"id\":\"1\",\"expr\":\"a + b + c\",\"unknown\":\"yo\"}"; + JSON.parseToObject(json, Expression.class); + // pass if no unexpected exception + } +} diff --git a/test/query/pojo/TestFilter.java b/test/query/pojo/TestFilter.java new file mode 100644 index 0000000000..e38088c09b --- /dev/null +++ b/test/query/pojo/TestFilter.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.query.pojo; + +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TestFilter { + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIdIsNull() throws Exception { + String json = "{\"id\":null}"; + Filter filter = JSON.parseToObject(json, Filter.class); + filter.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationBadId() throws Exception { + String json = "{\"id\":\"bad.Id\",\"tags\":[]}"; + Filter filter = JSON.parseToObject(json, Filter.class); + filter.validate(); + } + + @Test + public void deserialize() throws Exception { + String json = "{\"id\":\"f1\",\"tags\":[{\"tagk\":\"host\"," + + "\"filter\":\"*\",\"type\":\"iwildcard\",\"groupBy\":false}]}"; + + TagVFilter tag = new TagVFilter.Builder().setFilter("*").setGroupBy( + false) + .setTagk("host").setType("iwildcard").build(); + + Filter expectedFilter = Filter.Builder().setId("f1") + .setTags(Arrays.asList(tag)).build(); + + Filter filter = JSON.parseToObject(json, Filter.class); + filter.validate(); + assertEquals(expectedFilter, filter); + } + + @Test + public void serialize() throws Exception { + TagVFilter tag = new TagVFilter.Builder().setFilter("*").setGroupBy(false) + .setTagk("host").setType("iwildcard").build(); + + Filter filter = Filter.Builder().setId("f1") + .setTags(Arrays.asList(tag)).build(); + + String actual = JSON.serializeToString(filter); + assertTrue(actual.contains("\"id\":\"f1\"")); + assertTrue(actual.contains("\"tags\":[")); + assertTrue(actual.contains("\"tagk\":\"host\"")); + } + + @Test + public void unknownShouldBeIgnored() throws Exception { + String json = "{\"id\":\"1\",\"unknown\":\"yo\"}"; + JSON.parseToObject(json, Filter.class); + // pass if no unexpected exception + } + + @Test(expected = IllegalArgumentException.class) + public void invalidTags() throws Exception { + String json = "{\"id\":\"1\",\"tags\":[{\"tagk\":\"\"," + + "\"filter\":\"*\",\"type\":\"iwildcard\",\"group_by\":false}]," + + "\"aggregation\":{\"tags\":[\"appid\"],\"aggregator\":\"sum\"}}"; + + Filter filter = JSON.parseToObject(json, Filter.class); + filter.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidAggregation() throws Exception { + String json = "{\"id\":\"1\",\"tags\":[{\"tagk\":\"\"," + + "\"filter\":\"*\",\"type\":\"iwildcard\",\"group_by\":false}]," + + "\"aggregator\":\"what\"}"; + Filter filter = JSON.parseToObject(json, Filter.class); + filter.validate(); + } +} diff --git a/test/query/pojo/TestJoin.java b/test/query/pojo/TestJoin.java new file mode 100644 index 0000000000..8b27a5d3ec --- /dev/null +++ b/test/query/pojo/TestJoin.java @@ -0,0 +1,66 @@ +// 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.pojo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +public class TestJoin { + + @Test + public void deserialize() throws Exception { + final String json = "{\"operator\":\"union\"}"; + final Join join = Join.Builder().setOperator(SetOperator.UNION).build(); + final Join deserialized = JSON.parseToObject(json, Join.class); + assertEquals(join, deserialized); + } + + @Test + public void serialize() throws Exception { + final Join join = Join.Builder().setOperator(SetOperator.UNION).build(); + final String json = JSON.serializeToString(join); + assertTrue(json.contains("\"operator\":\"union\"")); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenOperatorIsNull() throws Exception { + final String json = "{\"operator\":null}"; + final Join join = JSON.parseToObject(json, Join.class); + join.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenOperatorIsEmpty() throws Exception { + final String json = "{\"operator\":\"\"}"; + final Join join = JSON.parseToObject(json, Join.class); + join.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenOperatorIsInvalid() throws Exception { + final String json = "{\"operator\":\"nosuchop\"}"; + final Join join = JSON.parseToObject(json, Join.class); + join.validate(); + } + + @Test + public void unknownShouldBeIgnored() throws Exception { + String json = "{\"operator\":\"intersection\",\"unknown\":\"yo\"}"; + JSON.parseToObject(json, Filter.class); + // pass if no unexpected exception + } +} diff --git a/test/query/pojo/TestMetric.java b/test/query/pojo/TestMetric.java new file mode 100644 index 0000000000..c312fc1b97 --- /dev/null +++ b/test/query/pojo/TestMetric.java @@ -0,0 +1,123 @@ +// 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.pojo; + +import net.opentsdb.core.FillPolicy; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TestMetric { + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenMetricIsNull() throws Exception { + String json = "{\"id\":\"1\",\"filter\":\"2\"," + + "\"timeOffset\":\"1h-ago\",\"aggregator\":\"sum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenMetricIsEmpty() throws Exception { + String json = "{\"metric\":\"\",\"id\":\"1\",\"filter\":\"2\"," + + "\"timeOffset\":\"1h-ago\",\"aggregator\":\"sum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIDIsNull() throws Exception { + String json = "{\"metric\":\"system.cpu\",\"id\":null,\"filter\":\"2\"," + + "\"timeOffset\":\"1h-ago\",\"aggregator\":\"sum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIDIsEmpty() throws Exception { + String json = "{\"metric\":\"system.cpu\",\"id\":\"\",\"filter\":\"2\"," + + "\"timeOffset\":\"1h-ago\",\"aggregator\":\"sum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenIDIsInvalid() throws Exception { + String json = "{\"metric\":\"system.cpu\",\"id\":\"system.cpu\",\"filter\":\"2\"," + + "\"timeOffset\":\"1h-ago\",\"aggregator\":\"sum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } + + @Test + public void deserializeAllFields() throws Exception { + String json = "{\"metric\":\"YAMAS.cpu.idle\",\"id\":\"e1\",\"filter\":\"f2\"," + + "\"timeOffset\":\"1h-ago\",\"aggregator\":\"sum\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + Metric expectedMetric = Metric.Builder().setMetric("YAMAS.cpu.idle") + .setId("e1").setFilter("f2").setTimeOffset("1h-ago") + .setAggregator("sum") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)) + .build(); + + assertEquals(expectedMetric, metric); + } + + @Test + public void serialize() throws Exception { + Metric metric = Metric.Builder().setMetric("YAMAS.cpu.idle") + .setId("e1").setFilter("f2").setTimeOffset("1h-ago") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)) + .build(); + + String actual = JSON.serializeToString(metric); + assertTrue(actual.contains("\"metric\":\"YAMAS.cpu.idle\"")); + assertTrue(actual.contains("\"id\":\"e1\"")); + assertTrue(actual.contains("\"filter\":\"f2\"")); + assertTrue(actual.contains("\"timeOffset\":\"1h-ago\"")); + assertTrue(actual.contains("\"fillPolicy\":{")); + } + + @Test + public void unknownShouldBeIgnored() throws Exception { + String json = "{\"aggregator\":\"sum\",\"tags\":[\"foo\",\"bar\"],\"unknown\":\"garbage\"}"; + JSON.parseToObject(json, Metric.class); + // pass if no unexpected exception + } + + @Test(expected = IllegalArgumentException.class) + public void validationtErrorWhenTimeOffsetIsInvalid() throws Exception { + String json = "{\"metric\":\"YAMAS.cpu.idle\",\"id\":\"1\",\"filter\":\"2\"," + + "\"timeOffset\":\"what?\"}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationtErrorBadFill() throws Exception { + String json = "{\"metric\":\"YAMAS.cpu.idle\",\"id\":\"1\",\"filter\":\"2\"," + + "\"fillPolicy\":{\"policy\":\"zero\",\"value\":42}}"; + Metric metric = JSON.parseToObject(json, Metric.class); + metric.validate(); + } +} diff --git a/test/query/pojo/TestOutput.java b/test/query/pojo/TestOutput.java new file mode 100644 index 0000000000..b4084e4b3e --- /dev/null +++ b/test/query/pojo/TestOutput.java @@ -0,0 +1,45 @@ +// 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.pojo; + +import net.opentsdb.utils.JSON; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestOutput { + @Test + public void deserializeAllFields() throws Exception { + String json = "{\"id\":\"m1\",\"alias\":\"CPU OK\"}"; + Output output = JSON.parseToObject(json, Output.class); + Output expectedOutput = Output.Builder().setId("m1").setAlias("CPU OK") + .build(); + assertEquals(expectedOutput, output); + } + + @Test + public void serialize() throws Exception { + Output output = Output.Builder().setId("m1").setAlias("CPU OK") + .build(); + String actual = JSON.serializeToString(output); + String expected = "{\"id\":\"m1\",\"alias\":\"CPU OK\"}"; + assertEquals(expected, actual); + } + + @Test + public void unknownFieldShouldBeIgnored() throws Exception { + String json = "{\"id\":\"m1\",\"unknown\":\"yo\"}"; + JSON.parseToObject(json, Filter.class); + // pass if no unexpected exception + } +} diff --git a/test/query/pojo/TestQuery.java b/test/query/pojo/TestQuery.java new file mode 100644 index 0000000000..ff8585d9c8 --- /dev/null +++ b/test/query/pojo/TestQuery.java @@ -0,0 +1,223 @@ +// 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.pojo; + +import net.opentsdb.core.FillPolicy; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.utils.JSON; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TestQuery { + Timespan time; + TagVFilter tag; + Filter filter; + Metric metric; + Expression expression; + Output output; + + String json = "{" + + " \"time\":{" + + " \"start\":\"3h-ago\"," + + " \"end\":\"1h-ago\"," + + " \"timezone\":\"UTC\"," + + " \"aggregator\":\"avg\"," + + " \"downsampler\":{\"interval\":\"15m\"," + + " \"aggregator\":\"avg\"," + + " \"fillPolicy\":{\"policy\":\"nan\"}}" + + " }," + + " \"filters\":[" + + " {" + + " \"id\":\"f1\"," + + " \"tags\":[" + + " {" + + " \"tagk\":\"host\"," + + " \"filter\":\"*\"," + + " \"type\":\"iwildcard\"," + + " \"groupBy\":false" + + " }" + + " ]" + + " }" + + " ]," + + " \"metrics\":[" + + " {" + + " \"metric\":\"YAMAS.cpu.idle\"," + + " \"id\":\"m1\"," + + " \"filter\":\"f1\"," + + " \"aggregator\":\"sum\"," + + " \"timeOffset\":\"0\"" + + " }" + + " ]," + + " \"expressions\":[" + + " {" + + " \"id\":\"e1\"," + + " \"expr\":\"a + b + c\"" + + " }" + + " ]," + + " \"outputs\":[" + + " {" + + " \"id\":\"m1\"," + + " \"alias\":\"CPU Idle EAST DC\"" + + " }" + + " ]" + + "}"; + + @Before + public void setup() { + time = Timespan.Builder().setStart("3h-ago").setAggregator("avg") + .setEnd("1h-ago").setTimezone("UTC").setDownsampler( + Downsampler.Builder().setInterval("15m").setAggregator("avg") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)).build()) + .build(); + TagVFilter tag = new TagVFilter.Builder().setFilter("*").setGroupBy( + false) + .setTagk("host").setType("iwildcard").build(); + filter = Filter.Builder().setId("f1").setTags(Arrays.asList(tag)).build(); + metric = Metric.Builder().setMetric("YAMAS.cpu.idle") + .setId("m1").setFilter("f1").setTimeOffset("0") + .setAggregator("sum").build(); + expression = Expression.Builder().setId("e1") + .setExpression("a + b + c").setJoin( + Join.Builder().setOperator(SetOperator.UNION).build()).build(); + output = Output.Builder().setId("m1").setAlias("CPU Idle EAST DC") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void validationErrorWhenTimeIsNull() throws Exception { + Query query = getDefaultQueryBuilder().setTime(null).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidTime() throws Exception { + Timespan invalidTime = Timespan.Builder().build(); + Query query = getDefaultQueryBuilder().setTime(invalidTime).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void metricsIsNull() throws Exception { + Query query = getDefaultQueryBuilder().setMetrics(null).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void metricsIsEmpty() throws Exception { + Query query = getDefaultQueryBuilder().setMetrics( + Collections.emptyList()).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidMetric() throws Exception { + Metric invalidMetric = Metric.Builder().build(); + Query query = getDefaultQueryBuilder() + .setMetrics(Arrays.asList(invalidMetric)).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidFilter() throws Exception { + Filter invalidFilter = Filter.Builder().build(); + Query query = getDefaultQueryBuilder() + .setFilters(Arrays.asList(invalidFilter)).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidExpression() throws Exception { + Expression invalidExpression = Expression.Builder().build(); + Query query = getDefaultQueryBuilder() + .setExpressions(Arrays.asList(invalidExpression)).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void noSuchFilterIdInMetric() throws Exception { + Metric invalid_metric = Metric.Builder().setMetric("YAMAS.cpu.idle") + .setId("m2").setFilter("f2").setTimeOffset("0").build(); + Query query = getDefaultQueryBuilder().setMetrics( + Arrays.asList(invalid_metric, metric)).build(); + query.validate(); + } + + @Test + public void deserialize() throws Exception { + Query query = JSON.parseToObject(json, Query.class); + query.validate(); + Query expected = Query.Builder().setExpressions(Arrays.asList(expression)) + .setFilters(Arrays.asList(filter)).setMetrics(Arrays.asList(metric)) + .setTime(time).setOutputs(Arrays.asList(output)).build(); + assertEquals(expected, query); + } + + @Test(expected = IllegalArgumentException.class) + public void duplicatedFilterId() throws Exception { + Query query = getDefaultQueryBuilder().setFilters( + Arrays.asList(filter, filter)).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void duplicatedExpressionId() throws Exception { + Query query = getDefaultQueryBuilder().setExpressions( + Arrays.asList(expression, expression)).build(); + query.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void duplicatedMetricId() throws Exception { + Query query = getDefaultQueryBuilder().setMetrics( + Arrays.asList(metric, metric)).build(); + query.validate(); + } + + @Test + public void serialize() throws Exception { + Query query = Query.Builder().setExpressions(Arrays.asList(expression)) + .setFilters(Arrays.asList(filter)).setMetrics(Arrays.asList(metric)) + .setName("q1").setTime(time).setOutputs(Arrays.asList(output)).build(); + + String actual = JSON.serializeToString(query); +// String expected = "{\"name\":\"q1\",\"time\":{\"start\":\"3h-ago\"," +// + "\"end\":\"1h-ago\",\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"," +// + "\"interpolation\":\"LERP\"},\"filters\":[{\"id\":\"f1\"," +// + "\"tags\":[{\"tagk\":\"host\",\"filter\":\"*\",\"group_by\":false," +// + "\"type\":\"iwildcard\"}],\"aggregator\":\"sum\"}]," +// + "\"metrics\":[{\"metric\":\"YAMAS.cpu.idle\"," +// + "\"id\":\"m1\",\"filter\":\"f1\",\"time_offset\":\"0\"}]," +// + "\"expressions\":[{\"id\":\"e1\",\"expr\":\"a + b + c\"}]," +// + "\"outputs\":[{\"var\":\"q1.m1\",\"alias\":\"CPU Idle EAST DC\"}]}"; + assertTrue(actual.contains("\"name\":\"q1\"")); + assertTrue(actual.contains("\"start\":\"3h-ago\"")); + assertTrue(actual.contains("\"end\":\"1h-ago\"")); + assertTrue(actual.contains("\"timezone\":\"UTC\"")); + // TODO - finish the assertions + } + + private Query.Builder getDefaultQueryBuilder() { + return Query.Builder().setExpressions(Arrays.asList(expression)) + .setFilters(Arrays.asList(filter)).setMetrics(Arrays.asList(metric)) + .setName("q1").setTime(time).setOutputs(Arrays.asList(output)); + } +} diff --git a/test/query/pojo/TestTimeSpan.java b/test/query/pojo/TestTimeSpan.java new file mode 100644 index 0000000000..ecd1450d72 --- /dev/null +++ b/test/query/pojo/TestTimeSpan.java @@ -0,0 +1,137 @@ +// 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.pojo; + +import net.opentsdb.core.FillPolicy; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.utils.JSON; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TestTimeSpan { + @Test(expected = IllegalArgumentException.class) + public void startIsNull() { + String json = "{\"start\":null,\"end\":\"2015/05/05\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"," + + ",\"aggregator\":\"sum\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void startIsEmpty() { + String json = "{\"start\":\"\",\"end\":\"2015/05/05\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"" + + ",\"aggregator\":\"sum\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test + public void endIsNull() { + String json = "{\"start\":\"2015/05/05\",\"end\":null," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"" + + ",\"aggregator\":\"sum\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test + public void endIsEmpty() { + String json = "{\"start\":\"1h-ago\",\"end\":\"\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"" + + ",\"aggregator\":\"sum\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void aggregatorIsNull() { + String json = "{\"start\":\"1h-ago\",\"end\":\"2015/05/05\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"," + + "}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void aggregatorIsEmpty() { + String json = "{\"start\":\"1h-ago\",\"end\":\"2015/05/05\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"" + + ",\"aggregator\":\"\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void idIsNull() { + String json = "{\"start\":\"-1h\",\"end\":\"2015/05/05\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"" + + ",\"interpolation\":\"LERP\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void idIsEmpty() { + String json = "{\"start\":\"-1h\",\"end\":\"2015/05/05\"," + + "\"timezone\":\"UTC\",\"downsample\":\"15m-avg-nan\"" + + ",\"interpolation\":\"LERP\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidDownsample() { + String json = "{\"start\":\"1h-ago\",\"end\":\"2015/05/05\",\"timezone\":\"UTC\"," + + "\"downsampler\":\"xxx\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + timespan.validate(); + } + + @Test + public void deserialize() { + String json = "{\"start\":\"1h-ago\",\"end\":\"2015/05/05\",\"timezone\":\"UTC\"," + + "\"downsampler\":{\"interval\":\"15m\",\"aggregator\":\"avg\"," + + "\"fillPolicy\":{\"policy\":\"nan\"}},\"aggregator\":\"sum\"," + + "\"unknownfield\":\"boo\"}"; + Timespan timespan = JSON.parseToObject(json, Timespan.class); + Timespan expected = Timespan.Builder().setStart("1h-ago") + .setEnd("2015/05/05").setTimezone("UTC").setAggregator("sum") + .setDownsampler( + Downsampler.Builder().setInterval("15m").setAggregator("avg") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)).build()) + .build(); + timespan.validate(); + assertEquals(expected, timespan); + } + + @Test + public void serialize() { + Timespan timespan = Timespan.Builder().setStart("1h-ago") + .setEnd("2015/05/05").setTimezone("UTC").setAggregator("sum").setDownsampler( + Downsampler.Builder().setInterval("15m").setAggregator("avg") + .setFillPolicy(new NumericFillPolicy(FillPolicy.NOT_A_NUMBER)).build()) + .build(); + String actual = JSON.serializeToString(timespan); + assertTrue(actual.contains("\"start\":\"1h-ago\"")); + assertTrue(actual.contains("\"end\":\"2015/05/05\"")); + assertTrue(actual.contains("\"aggregator\":\"sum\"")); + assertTrue(actual.contains("\"timezone\":\"UTC\"")); + assertTrue(actual.contains("\"downsampler\":{")); + assertTrue(actual.contains("\"interval\":\"15m\"")); + } +} From 35717b127ef9cee78918bbdf04558c9564c9555d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 5 Nov 2015 18:42:48 -0800 Subject: [PATCH 315/826] Add the ExpressionIterator. For now it uses Apache Jexl as the expression engine for simple operations like + - / *. We'll likely toss it with a real parser that can also handle functions. And add JGrapht for DAG creation used in the next commit. Also fix up the make file. Signed-off-by: Chris Larsen --- Makefile.am | 11 +- pom.xml.in | 12 + src/query/expression/ExpressionIterator.java | 417 +++++++ .../expression/TestExpressionIterator.java | 1095 +++++++++++++++++ third_party/include.mk | 2 + third_party/jexl/commons-jexl-2.1.1.jar.md5 | 1 + .../jexl/commons-logging-1.1.1.jar.md5 | 1 + third_party/jexl/include.mk | 33 + third_party/jgrapht/include.mk | 23 + .../jgrapht/jgrapht-core-0.9.1.jar.md5 | 1 + 10 files changed, 1594 insertions(+), 2 deletions(-) create mode 100644 src/query/expression/ExpressionIterator.java create mode 100644 test/query/expression/TestExpressionIterator.java create mode 100644 third_party/jexl/commons-jexl-2.1.1.jar.md5 create mode 100644 third_party/jexl/commons-logging-1.1.1.jar.md5 create mode 100644 third_party/jexl/include.mk create mode 100644 third_party/jgrapht/include.mk create mode 100644 third_party/jgrapht/jgrapht-core-0.9.1.jar.md5 diff --git a/Makefile.am b/Makefile.am index d9d7e5b29c..113cb60954 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,6 +78,7 @@ tsdb_SRC := \ src/query/expression/Expression.java \ src/query/expression/ExpressionDataPoint.java \ src/query/expression/ExpressionFactory.java \ + src/query/expression/ExpressionIterator.java \ src/query/expression/ExpressionReader.java \ src/query/expression/Expressions.java \ src/query/expression/ExpressionTree.java \ @@ -91,12 +92,13 @@ tsdb_SRC := \ src/query/expression/Scale.java \ src/query/expression/TimeSyncedIterator.java \ src/query/expression/UnionIterator.java \ + src/query/expression/VariableIterator.java \ src/query/filter/TagVFilter.java \ src/query/filter/TagVLiteralOrFilter.java \ src/query/filter/TagVNotKeyFilter.java \ src/query/filter/TagVNotLiteralOrFilter.java \ src/query/filter/TagVRegexFilter.java \ - src/query/filter/VariableIterator.java \ + src/query/filter/TagVWildcardFilter.java \ src/query/pojo/Downsampler.java \ src/query/pojo/Expression.java \ src/query/pojo/Filter.java \ @@ -180,6 +182,7 @@ tsdb_SRC := \ src/utils/Threads.java tsdb_DEPS = \ + $(COMMONS_LOGGING) \ $(GUAVA) \ $(LOG4J_OVER_SLF4J) \ $(LOGBACK_CLASSIC) \ @@ -188,6 +191,8 @@ tsdb_DEPS = \ $(JACKSON_CORE) \ $(JACKSON_DATABIND) \ $(JAVACC) \ + $(JEXL) \ + $(JGRAPHT) \ $(NETTY) \ $(SLF4J_API) \ $(SUASYNC) \ @@ -259,6 +264,7 @@ test_SRC := \ test/query/expression/BaseTimeSyncedIteratorTest.java \ test/query/expression/TestAbsolute.java \ test/query/expression/TestExpressionFactory.java \ + test/query/expression/TestExpressionIterator.java \ test/query/expression/TestExpressionReader.java \ test/query/expression/TestExpressions.java \ test/query/expression/TestExpressionTree.java \ @@ -285,7 +291,6 @@ test_SRC := \ test/query/pojo/TestOutput.java \ test/query/pojo/TestQuery.java \ test/query/pojo/TestTimespan.java \ - test/query/pojo/TestValidatable.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ @@ -769,6 +774,8 @@ pom.xml: pom.xml.in Makefile -e 's/@SUASYNC_VERSION@/$(SUASYNC_VERSION)/' \ -e 's/@ZOOKEEPER_VERSION@/$(ZOOKEEPER_VERSION)/' \ -e 's/@APACHE_MATH_VERSION@/$(APACHE_MATH_VERSION)/' \ + -e 's/@JEXL_VERSION@/$(JEXL_VERSION)/' \ + -e 's/@JGRAPHT_VERSION@/$(JGRAPHT_VERSION)/' \ -e 's/@spec_title@/$(spec_title)/' \ -e 's/@spec_vendor@/$(spec_vendor)/' \ -e 's/@spec_version@/$(PACKAGE_VERSION)/' \ diff --git a/pom.xml.in b/pom.xml.in index 8e37d22217..e5914cd031 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -388,6 +388,18 @@ commons-math3 @APACHE_MATH_VERSION@ + + + org.apache.commons + commons-jexl + @JEXL_VERSION@ + + + + org.jgrapht + jgrapht-core + @JGRAPHT_VERSION@ + diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java new file mode 100644 index 0000000000..c7d12c78ba --- /dev/null +++ b/src/query/expression/ExpressionIterator.java @@ -0,0 +1,417 @@ +// 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.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import net.opentsdb.core.FillPolicy; +import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.utils.ByteSet; + +import org.apache.commons.jexl2.JexlContext; +import org.apache.commons.jexl2.JexlEngine; +import org.apache.commons.jexl2.MapContext; +import org.apache.commons.jexl2.Script; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSet; + +/** + * A iterator that applies an expression to the results of multiple sub queries. + * To use this class: + * - Instantiate with a valid expression + * - Call {@link #getVariableNames()} and iterate over a set of TSSubQueries and + * their results. For each query that matches a variable name, call + * {@link #addResults()} with the result set. + * - Call {@link #compile()} to setup the meta data, fills and compute the + * intersection of the series. + * - Call {@link #values()} and store the reference. Results for each + * series will be written here as you iterate. + * - Call {@link #hasNext()} and {@link #next()} to iterate over results. + * - At each iteration, fetch the timestamp and value from the data points array. + *

    + * Iteration is performed across all series supplied to the iterator, synchronizing + * on the timestamps and substituting fill values where appropriate. + *

    + * WARNING: You MUST supply a result set and associated sub query to match each + * of the variable names in the expression. If you fail to do so, when you call + * {@link #compile()} you'll get an exception. + *

    + * NOTE: Right now this class only supports intersection on the series so that + * each metric result must contain series with the same tags based on the flags + * provided in the ctor. + * NOTE: If a result set doesn't include a fill policy, we default to ZERO for + * "missing" data points. + */ +public class ExpressionIterator implements ITimeSyncedIterator { + private static final Logger LOG = LoggerFactory.getLogger(ExpressionIterator.class); + + /** Docs don't say whether this is thread safe or not. SOME methods are marked + * as not thread safe, so I assume it's ok to instantiate one of these guys + * and keep creating scripts from it. + */ + private final static JexlEngine JEXL_ENGINE = new JexlEngine(); + + /** Whether or not to intersect on the query tagks instead of the result set + * tagks */ + private final boolean intersect_on_query_tagks; + + /** Whether or not to include the aggregated tags in the result set */ + private final boolean include_agg_tags; + + /** List of iterators and their IDs */ + private final Map results; + + /** The compiled expression */ + private final Script expression; + + /** The context where we'll dump results for processing through the expression */ + private final JexlContext context = new MapContext(); + + /** A list of unique variable names pulled from the expression */ + private final Set names; + + /** The intersection iterator we'll use for processing */ + // TODO - write an interface to allow other set operators, e.g. union, disjoint + private VariableIterator iterator; + + /** A map of results from the intersection iterator to pass to the expression */ + private Map iteration_results; + + /** The results of processing the expressions */ + private ExpressionDataPoint[] dps; + + /** The ID of this iterator */ + private final String id; + + /** The index of this iterator in expressions */ + private int index; + + /** A fill policy for this expression if data is missing */ + private NumericFillPolicy fill_policy; + + /** The set operator to use for joining sets */ + private SetOperator set_operator; + + // NOTE - if the query is set to NONE for the aggregation and the query has + // no tagk filters then we shouldn't set the II's intersect_on_query_tagks + /** + * Default Ctor that compiles the expression for use with this iterator. + * @param expression The expression to compile and use + * @param set_operator The type of set operator to use + * @param intersect_on_query_tagks Whether or not to include only the query + * specified tags during intersection + * @param include_agg_tags Whether or not to include aggregated tags during + * intersection + * @throws IllegalArgumentException if the expression is null or empty or doesn't + * contain any variables. + * @throws JexlException if the expression isn't valid + */ + public ExpressionIterator(final String id, final String expression, + final SetOperator set_operator, + final boolean intersect_on_query_tagks, final boolean include_agg_tags) { + if (expression == null || expression.isEmpty()) { + throw new IllegalArgumentException("The expression cannot be null"); + } + if (set_operator == null) { + throw new IllegalArgumentException("The set operator cannot be null"); + } + this.id = id; + this.intersect_on_query_tagks = intersect_on_query_tagks; + this.include_agg_tags = include_agg_tags; + results = new HashMap(); + this.expression = JEXL_ENGINE.createScript(expression); + names = new HashSet(); + extractVariableNames(); + if (names.size() < 1) { + throw new IllegalArgumentException( + "The expression didn't appear to have any variables"); + } + this.set_operator = set_operator; + fill_policy = new NumericFillPolicy(FillPolicy.NOT_A_NUMBER); + } + + /** + * Copy constructor that setups up a dupe of this iterator with fresh sub + * iterator objects for use in a nested expression. + * @param iterator The expression to copy from. + */ + private ExpressionIterator(final ExpressionIterator iterator) { + id = iterator.id; + // need to recompile, don't know if we'll run into threading issues + expression = JEXL_ENGINE.createScript(iterator.expression.toString()); + intersect_on_query_tagks = iterator.intersect_on_query_tagks; + include_agg_tags = iterator.include_agg_tags; + set_operator = iterator.set_operator; + + results = new HashMap(); + for (Entry entry : iterator.results.entrySet()) { + results.put(entry.getKey(), entry.getValue().getCopy()); + } + + names = new HashSet(); + extractVariableNames(); + if (names.size() < 1) { + throw new IllegalArgumentException( + "The expression didn't appear to have any variables"); + } + } + + @Override + public String toString() { + final StringBuffer buf = new StringBuffer(); + buf.append("ExpressionIterator(id=") + .append(id) + .append(", expression=\"") + .append(expression.toString()) + .append("\", VariableIterator=") + .append(iterator) + .append(", dps=") + .append(dps) + .append(", results=") + .append(results) + .append(")"); + return buf.toString(); + } + + /** + * Adds a sub query result object to the iterator. + * TODO - accept a proper object, not a map + * @param results The results to store. + * @throws IllegalArgumentException if the object is missing required data + */ + public void addResults(final String id, final ITimeSyncedIterator iterator) { + if (id == null) { + throw new IllegalArgumentException("Missing ID"); + } + if (iterator == null) { + throw new IllegalArgumentException("Iterator cannot be null"); + } + results.put(id, iterator); + } + + /** + * Builds the iterator by computing the intersection of all series in all sets + * and sets up the output. + * @throws IllegalArgumentException if there aren't any results, or we don't + * have a result for each variable, or something else is wrong. + * @throws IllegalDataException if no series were left after computing the + * intersection. + */ + public void compile() { + if (LOG.isDebugEnabled()) { + LOG.debug("Compiling " + this); + } + if (results.size() < 1) { + throw new IllegalArgumentException("Missing query results."); + } + if (results.size() < names.size()) { + throw new IllegalArgumentException("Not enough query results [" + + results.size() + "] for the expression variables [" + + names.size() + "]"); + } + + // don't care if we have extra results, but we had darned well better make + // sure we have a result set for each variable + for (final String variable : names) { + // validation + final ITimeSyncedIterator it = results.get(variable.toLowerCase()); + if (it == null) { + throw new IllegalArgumentException("Missing results for variable " + variable); + } + + if (it instanceof ExpressionIterator) { + ((ExpressionIterator)it).compile(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Matched variable " + variable + " to " + it); + } + } + + // TODO implement other set functions + switch (set_operator) { + case INTERSECTION: + iterator = new IntersectionIterator(id, results, intersect_on_query_tagks, + include_agg_tags); + break; + case UNION: + iterator = new UnionIterator(id, results, intersect_on_query_tagks, + include_agg_tags); + } + iteration_results = iterator.getResults(); + + dps = new ExpressionDataPoint[iterator.getSeriesSize()]; + for (int i = 0; i < iterator.getSeriesSize(); i++) { + final Iterator> it = + iteration_results.entrySet().iterator(); + Entry entry = it.next(); + + if (entry.getValue() == null || entry.getValue()[i] == null) { + dps[i] = new ExpressionDataPoint(); + } else { + dps[i] = new ExpressionDataPoint(entry.getValue()[i]); + } + while (it.hasNext()) { + entry = it.next(); + if (entry.getValue() != null && entry.getValue()[i] != null) { + dps[i].add(entry.getValue()[i]); + } + } + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Finished compiling " + this); + } + } + + /** + * Checks to see if we have another value in any of the series. + * Make sure to call {@link #compile()} first. + * @return True if there is more data to process, false if not + */ + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + /** + * Fetches the next set of data and computes a value for the expression. + * Make sure to call {@link #compile()} first. + * And make sure to call {@link #hasNext()} before calling this. + * @return A link to the data points for this result set + * @throws IllegalDataException if there wasn't any data left in any of the + * series. + * @throws JexlException if something went pear shaped processing the expression + */ + public ExpressionDataPoint[] next(final long timestamp) { + + // fetch the timestamp ONCE to save some cycles. + // final long timestamp = iterator.nextTimestamp(); + iterator.next(); + + // set aside a couple of addresses for the variables + double val; + double result; + for (int i = 0; i < iterator.getSeriesSize(); i++) { + // this here is why life sucks. there MUST be a better way to bind variables + for (final String variable : names) { + if (iteration_results.get(variable)[i] == null) { + context.set(variable, results.get(variable).getFillPolicy().getValue()); + } else { + val = iteration_results.get(variable)[i].toDouble(); + if (Double.isNaN(val)) { + context.set(variable, results.get(variable).getFillPolicy().getValue()); + } else { + context.set(variable, val); + } + } + } + result = (Double)expression.execute(context); + dps[i].reset(timestamp, result); + } + return dps; + } + + /** @return a list of expression results. You can keep this list and check the + * results on each call to {@link #next()} */ + @Override + public ExpressionDataPoint[] values() { + return dps; + } + + /** + * Pulls the variable names from the expression and stores them in {@link #names} + */ + private void extractVariableNames() { + if (expression == null) { + throw new IllegalArgumentException("The expression was null"); + } + + for (final List exp_list : JEXL_ENGINE.getVariables(expression)) { + for (final String variable : exp_list) { + names.add(variable); + } + } + } + + /** @return an immutable set of the variable IDs used in the expression. Case + * sensitive. */ + public Set getVariableNames() { + return ImmutableSet.copyOf(names); + } + + public void setSetOperator(final SetOperator set_operator) { + this.set_operator = set_operator; + } + + @Override + public long nextTimestamp() { + return iterator.nextTimestamp(); + } + + @Override + public int size() { + return dps.length; + } + + @Override + public void nullIterator(int index) { + if (index < 0 || index >= dps.length) { + throw new IllegalArgumentException("Index out of bounds"); + } + // TODO - do it + } + + @Override + public int getIndex() { + return index; + } + + @Override + public void setIndex(int index) { + this.index = index; + } + + @Override + public String getId() { + return id; + } + + @Override + public ByteSet getQueryTagKs() { + return null; + } + + @Override + public void setFillPolicy(NumericFillPolicy policy) { + fill_policy = policy; + } + + @Override + public NumericFillPolicy getFillPolicy() { + return fill_policy; + } + + @Override + public ITimeSyncedIterator getCopy() { + final ExpressionIterator ei = new ExpressionIterator(this); + return ei; + } +} diff --git a/test/query/expression/TestExpressionIterator.java b/test/query/expression/TestExpressionIterator.java new file mode 100644 index 0000000000..9c98a5071c --- /dev/null +++ b/test/query/expression/TestExpressionIterator.java @@ -0,0 +1,1095 @@ +// 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.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import net.opentsdb.core.FillPolicy; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +import org.apache.commons.jexl2.JexlException; +import org.hbase.async.Bytes; +import org.junit.Test; + +public class TestExpressionIterator extends BaseTimeSyncedIteratorTest { + + @Test + public void ctor() throws Exception { + final ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + assertEquals(2, exp.getVariableNames().size()); + assertTrue(exp.getVariableNames().contains("a")); + assertTrue(exp.getVariableNames().contains("b")); + assertFalse(exp.getVariableNames().contains("+")); // I'm not a variable :( + assertNull(exp.values()); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNoVariables() throws Exception { + new ExpressionIterator("ei", "1 + 1", SetOperator.INTERSECTION, false, false); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullExpression() throws Exception { + new ExpressionIterator("ei", null, SetOperator.INTERSECTION, false, false); + } + + @Test (expected = JexlException.class) + public void ctorBadExpression() throws Exception { + new ExpressionIterator("ei", " a / ", SetOperator.INTERSECTION, false, false); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorEmptyExpression() throws Exception { + new ExpressionIterator("ei", "", SetOperator.INTERSECTION, false, false); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNullOperator() throws Exception { + new ExpressionIterator("ei", "a + b", null, false, false); + } + + @Test + public void aPlusBWithTwoSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 12, 18 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0], dps[0].toDouble(), 0.0001); + assertEquals(values[1], dps[1].toDouble(), 0.0001); + + values[0] += 2; + values[1] += 2; + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aMinusBWithTwoSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a - b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(-10, dps[0].toDouble(), 0.0001); + assertEquals(-10, dps[1].toDouble(), 0.0001); + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aTimesBWithTwoSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a * b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(11, dps[0].toDouble(), 0.0001); + assertEquals(56, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(24, dps[0].toDouble(), 0.0001); + assertEquals(75, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(39, dps[0].toDouble(), 0.0001); + assertEquals(96, dps[1].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aDivideBWithTwoSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a / b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(0.0909, dps[0].toDouble(), 0.0001); + assertEquals(0.2857, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(0.1666, dps[0].toDouble(), 0.0001); + assertEquals(0.3333, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(0.2307, dps[0].toDouble(), 0.0001); + assertEquals(0.375, dps[1].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aModBWithTwoSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a % b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + double[] values = new double[] { 1, 4 }; + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0]++, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aDivideByZeroWithTwoSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + // Jexl apparently happily allows this, just emits a zero + ExpressionIterator exp = new ExpressionIterator("ei", "a / 0", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(0, dps[0].toDouble(), 0.0001); + assertEquals(0, dps[1].toDouble(), 0.0001); + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void doubleVariableAndPrecedence() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + (b * b)", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(122, dps[0].toDouble(), 0.0001); + assertEquals(200, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(146, dps[0].toDouble(), 0.0001); + assertEquals(230, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(172, dps[0].toDouble(), 0.0001); + assertEquals(262, dps[1].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void doubleVariableAndPrecedenceChanged() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "(a + b) * b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(132, dps[0].toDouble(), 0.0001); + assertEquals(252, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(168, dps[0].toDouble(), 0.0001); + assertEquals(300, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(208, dps[0].toDouble(), 0.0001); + assertEquals(352, dps[1].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aPlusScalarDropB() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + 1", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 2, 5 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0]++, dps[0].toDouble(), 0.0001); + assertEquals(values[1]++, dps[1].toDouble(), 0.0001); + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test (expected = IllegalArgumentException.class) + public void missingRequiredVariable() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b + c", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + } + + @Test + public void aPlusBMissingPointsDefaultFillZero() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(3, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + assertEquals(0, dps[2].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(0, dps[0].toDouble(), 0.0001); + assertEquals(20, dps[1].toDouble(), 0.0001); + assertEquals(8, dps[2].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(16, dps[0].toDouble(), 0.0001); + assertEquals(0, dps[1].toDouble(), 0.0001); + assertEquals(28, dps[2].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("G"), dps[2].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aPlusBMissingPointsFillOne() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + iterators.get("a").setFillPolicy(new NumericFillPolicy(FillPolicy.SCALAR, 1)); + iterators.get("b").setFillPolicy(new NumericFillPolicy(FillPolicy.SCALAR, 1)); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(3, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(2, dps[0].toDouble(), 0.0001); + assertEquals(5, dps[1].toDouble(), 0.0001); + assertEquals(2, dps[2].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(2, dps[0].toDouble(), 0.0001); + assertEquals(20, dps[1].toDouble(), 0.0001); + assertEquals(9, dps[2].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(16, dps[0].toDouble(), 0.0001); + assertEquals(2, dps[1].toDouble(), 0.0001); + assertEquals(28, dps[2].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("G"), dps[2].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aPlusBMissingPointsFillInfectiousNaN() throws Exception { + threeSameEGaps(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + iterators.get("a").setFillPolicy( + new NumericFillPolicy(FillPolicy.NOT_A_NUMBER, Double.NaN)); + iterators.get("b").setFillPolicy( + new NumericFillPolicy(FillPolicy.NOT_A_NUMBER, Double.NaN)); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(3, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertTrue(Double.isNaN(dps[0].toDouble())); + assertTrue(Double.isNaN(dps[1].toDouble())); + assertTrue(Double.isNaN(dps[2].toDouble())); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertTrue(Double.isNaN(dps[0].toDouble())); + assertEquals(20, dps[1].toDouble(), 0.0001); + assertTrue(Double.isNaN(dps[2].toDouble())); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(16, dps[0].toDouble(), 0.0001); + assertTrue(Double.isNaN(dps[1].toDouble())); + assertEquals(28, dps[2].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("G"), dps[2].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aPlusBResultsOffsetDefaultFill() throws Exception { + timeOffset(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(2, dps[0].toDouble(), 0.0001); + assertEquals(5, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(13, dps[0].toDouble(), 0.0001); + assertEquals(16, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(14, dps[0].toDouble(), 0.0001); + assertEquals(17, dps[1].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void aPlusBOneAggedOneTaggedUseQueryTagsWoutQueryTags() throws Exception { + oneAggedTheOtherTagged(); + queryAB_AggAll(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, true, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(1, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double value = 13; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(value, dps[0].toDouble(), 0.0001); + + value += 3; + ts += 60000; + its = exp.nextTimestamp(); + } + + assertEquals(2, dps[0].tags().size()); + assertEquals(2, dps[0].aggregatedTags().size()); + assertTrue(dps[0].aggregatedTags().contains(TAGV_UIDS.get("D"))); + assertTrue(dps[0].aggregatedTags().contains(TAGV_UIDS.get("E"))); + // TODO - make sure the tags are empty once the expression data does it's + // thing + //assertTrue(dps[0].tags().isEmpty()); + } + + @Test + public void singleNestedExpression() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator ei = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + ei.addResults("a", iterators.get("a")); + ei.addResults("b", iterators.get("b")); + ei.compile(); + + ExpressionIterator exp = new ExpressionIterator("ei", "x * 2", + SetOperator.INTERSECTION, false, false); + exp.addResults("x", ei); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 24, 36 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0], dps[0].toDouble(), 0.0001); + assertEquals(values[1], dps[1].toDouble(), 0.0001); + + values[0] += 4; + values[1] += 4; + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void doubleNestedExpression() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator e1 = new ExpressionIterator("e1", "a + b", + SetOperator.INTERSECTION, false, false); + e1.addResults("a", iterators.get("a")); + e1.addResults("b", iterators.get("b")); + e1.compile(); + + ExpressionIterator e2 = new ExpressionIterator("e2", "e1 * 2", + SetOperator.INTERSECTION, false, false); + e2.addResults("e1", e1); + e2.compile(); + + ExpressionIterator e3 = new ExpressionIterator("e3", "e2 * 2", + SetOperator.INTERSECTION, false, false); + e3.addResults("e2", e2); + + e3.compile(); + final ExpressionDataPoint[] dps = e3.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 48, 72 }; + long its = e3.nextTimestamp(); + while (e3.hasNext()) { + e3.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0], dps[0].toDouble(), 0.0001); + assertEquals(values[1], dps[1].toDouble(), 0.0001); + + values[0] += 8; + values[1] += 8; + ts += 60000; + its = e3.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test (expected = IllegalDataException.class) + public void noIntersectionFound() throws Exception { + threeDifE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + } + + @Test (expected = IllegalArgumentException.class) + public void addResultsMissingId() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b + c", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + } + + @Test (expected = IllegalArgumentException.class) + public void addResultsMissingSubQuery() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b + c", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + } + + @Test (expected = IllegalArgumentException.class) + public void addResultsMissingResults() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b + c", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + } + + @Test + public void unionOneExtraSeries() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.UNION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(3, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 12, 18, 17 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(ts, dps[2].timestamp()); + assertEquals(values[0], dps[0].toDouble(), 0.0001); + assertEquals(values[1], dps[1].toDouble(), 0.0001); + assertEquals(values[2], dps[2].toDouble(), 0.0001); + + values[0] += 2; + values[1] += 2; + values[2] += 1; + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void unionOffset() throws Exception { + timeOffset(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.UNION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + long its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(1, dps[0].toDouble(), 0.0001); + assertEquals(4, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(2, dps[0].toDouble(), 0.0001); + assertEquals(5, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(13, dps[0].toDouble(), 0.0001); + assertEquals(16, dps[1].toDouble(), 0.0001); + ts += 60000; + + its = exp.nextTimestamp(); + exp.next(its); + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(14, dps[0].toDouble(), 0.0001); + assertEquals(17, dps[1].toDouble(), 0.0001); + + assertFalse(exp.hasNext()); + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + } + + @Test + public void unionNoIntersection() throws Exception { + threeDifE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.UNION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(6, dps.length); + validateMeta(dps, false); + + long ts = 1431561600000L; + double[] values = new double[] { 1, 11, 4, 14, 7, 17 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + for (int i = 0; i < values.length; i++) { + assertEquals(ts, dps[i].timestamp()); + assertEquals(values[i], dps[i].toDouble(), 0.0001); + ++values[i]; + } + + ts += 60000; + its = exp.nextTimestamp(); + } + } + + @Test + public void scratch() throws Exception { + threeDifE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.UNION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(6, dps.length); + //validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 1, 11, 4, 14, 7, 17 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + for (int i = 0; i < dps.length; i++) { + System.out.println(dps[i].timestamp() + " " + dps[i].toDouble()); + } + + for (int i = 0; i < values.length; i++) { + assertEquals(ts, dps[i].timestamp()); + assertEquals(values[i], dps[i].toDouble(), 0.0001); + ++values[i]; + } + + ts += 60000; + its = exp.nextTimestamp(); + } + validateMeta(dps, false); + } + + /** + * Makes sure the series contain both metrics + * @param dps The results to validate + * @param common_e The common e + */ + private void validateMeta(final ExpressionDataPoint[] dps, + final boolean common_e) { + for (int i = 0; i < dps.length; i++) { + // TODO - change this guy to a byteset :( Since it's a bloody list we + // can't do a "contains" because it checks for the address of the byte + // arrays + boolean found = false; + for (final byte[] metric : dps[i].metricUIDs()) { + if (Bytes.memcmp(TAGV_UIDS.get("A"), metric) == 0) { + found = true; + } else if (Bytes.memcmp(TAGV_UIDS.get("B"), metric) == 0) { + found = true; + break; + } + } + if (!found) { + fail("Missing a metric"); + } + + if (common_e) { + assertArrayEquals(TAGV_UIDS.get("E"), dps[i].tags().get(TAGV_UIDS.get("E"))); + } + } + } + + private void remapResults() { + iterators.clear(); + iterators.put("a", new TimeSyncedIterator("a", + query.getQueries().get(0).getFilterTagKs(), results.get("0").getValue())); + iterators.put("b", new TimeSyncedIterator("b", + query.getQueries().get(1).getFilterTagKs(), results.get("1").getValue())); + } +} diff --git a/third_party/include.mk b/third_party/include.mk index 3a25a241a9..dc2f22d20b 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -24,6 +24,8 @@ include third_party/hamcrest/include.mk include third_party/jackson/include.mk include third_party/javacc/include.mk include third_party/javassist/include.mk +include third_party/jexl/include.mk +include third_party/jgrapht/include.mk include third_party/junit/include.mk include third_party/logback/include.mk include third_party/mockito/include.mk diff --git a/third_party/jexl/commons-jexl-2.1.1.jar.md5 b/third_party/jexl/commons-jexl-2.1.1.jar.md5 new file mode 100644 index 0000000000..866f0e175a --- /dev/null +++ b/third_party/jexl/commons-jexl-2.1.1.jar.md5 @@ -0,0 +1 @@ +4ad8f5c161dd3a50e190334555675db9 diff --git a/third_party/jexl/commons-logging-1.1.1.jar.md5 b/third_party/jexl/commons-logging-1.1.1.jar.md5 new file mode 100644 index 0000000000..00979c8fe9 --- /dev/null +++ b/third_party/jexl/commons-logging-1.1.1.jar.md5 @@ -0,0 +1 @@ +ed448347fc0104034aa14c8189bf37de diff --git a/third_party/jexl/include.mk b/third_party/jexl/include.mk new file mode 100644 index 0000000000..b78ce1e8ee --- /dev/null +++ b/third_party/jexl/include.mk @@ -0,0 +1,33 @@ +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +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): $(JEXL).md5 + set dummy "$(JEXL_BASE_URL)" "$(JEXL)"; shift; $(FETCH_DEPENDENCY) + +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): $(COMMONS_LOGGING).md5 + set dummy "$(COMMONS_LOGGING_BASE_URL)" "$(COMMONS_LOGGING)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(COMMONS_LOGGING) \ No newline at end of file diff --git a/third_party/jgrapht/include.mk b/third_party/jgrapht/include.mk new file mode 100644 index 0000000000..11647e3bcc --- /dev/null +++ b/third_party/jgrapht/include.mk @@ -0,0 +1,23 @@ +# Copyright (C) 2015 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +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): $(JGRAPHT).md5 + set dummy "$(JGRAPHT_BASE_URL)" "$(JGRAPHT)"; shift; $(FETCH_DEPENDENCY) + +THIRD_PARTY += $(JGRAPHT) diff --git a/third_party/jgrapht/jgrapht-core-0.9.1.jar.md5 b/third_party/jgrapht/jgrapht-core-0.9.1.jar.md5 new file mode 100644 index 0000000000..a0089aa304 --- /dev/null +++ b/third_party/jgrapht/jgrapht-core-0.9.1.jar.md5 @@ -0,0 +1 @@ +86e15da146c96430aef3e1de36df52c8 From a4d47dc8b8221d442289edb3575622667744e33a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 9 Nov 2015 11:23:01 -0800 Subject: [PATCH 316/826] Fix a bug where an HBase exception wasn't propagated to the end user at query time. Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 16 +++++++++++++++- test/core/TestTsdbQuery.java | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index cef3748f15..7b060a44ed 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -566,6 +566,20 @@ final class ScannerCB implements Callback skips = new HashSet(); private final Set keepers = new HashSet(); + + /** Error callback that will capture an exception from AsyncHBase and store + * it so we can bubble it up to the caller. + */ + class ErrorCB implements Callback { + @Override + public Object call(final Exception e) throws Exception { + LOG.error("Scanner " + scanner + " threw an exception", e); + scanner.close(); + results.callback(e); + return null; + } + } + /** * Starts the scanner and is called recursively to fetch the next set of * rows from the scanner. @@ -574,7 +588,7 @@ final class ScannerCB implements Callback Date: Mon, 9 Nov 2015 11:27:12 -0800 Subject: [PATCH 317/826] Add the QueryExecutor TSD class (WIP) for handling expressions via the /query/exp endpoint. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/tsd/QueryExecutor.java | 877 ++++++++++++++++++++++++++++++++ src/tsd/QueryRpc.java | 18 + test/tsd/TestQueryExecutor.java | 607 ++++++++++++++++++++++ 4 files changed, 1504 insertions(+) create mode 100644 src/tsd/QueryExecutor.java create mode 100644 test/tsd/TestQueryExecutor.java diff --git a/Makefile.am b/Makefile.am index 113cb60954..edcaececad 100644 --- a/Makefile.am +++ b/Makefile.am @@ -149,6 +149,7 @@ tsdb_SRC := \ src/tsd/LogsRpc.java \ src/tsd/PipelineFactory.java \ src/tsd/PutDataPointRpc.java \ + src/tsd/QueryExecutor.java \ src/tsd/QueryRpc.java \ src/tsd/RpcHandler.java \ src/tsd/RpcPlugin.java \ @@ -316,6 +317,7 @@ test_SRC := \ test/tsd/TestHttpQuery.java \ test/tsd/TestHttpRpcPluginQuery.java \ test/tsd/TestPutRpc.java \ + test/tsd/TestQueryExecutor.java \ test/tsd/TestQueryRpc.java \ test/tsd/TestQueryRpcLastDataPoint.java \ test/tsd/TestRpcHandler.java \ diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java new file mode 100644 index 0000000000..9a5683f01b --- /dev/null +++ b/src/tsd/QueryExecutor.java @@ -0,0 +1,877 @@ +// 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.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBufferOutputStream; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jgrapht.experimental.dag.DirectedAcyclicGraph; +import org.jgrapht.experimental.dag.DirectedAcyclicGraph.CycleFoundException; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.traverse.TopologicalOrderIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.QueryException; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.TSSubQuery; +import net.opentsdb.core.Tags; +import net.opentsdb.query.expression.ExpressionDataPoint; +import net.opentsdb.query.expression.ExpressionIterator; +import net.opentsdb.query.expression.NumericFillPolicy; +import net.opentsdb.query.expression.TimeSyncedIterator; +import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.query.pojo.Expression; +import net.opentsdb.query.pojo.Filter; +import net.opentsdb.query.pojo.Metric; +import net.opentsdb.query.pojo.Output; +import net.opentsdb.query.pojo.Query; +import net.opentsdb.query.pojo.Timespan; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; + +/** + * TEMP class for handling V2 queries with expression support. So far we ONLY + * support expressions and this will be pipelined better. For now it's functioning + * fairly well. + * + * So far this sucker allows for expressions and nested expressions with the + * ability to determine the output. If no output fields are specified, all + * expressions are dumped to the output. If one or more outputs are given then + * only those outputs will be emitted. + * + * TODO + * - handle/add output flags to determine whats emitted + * - allow for queries only, no expressions + * - possibly other set operations + * - time over time queries + * - skip querying for data that isn't going to be emitted + */ +public class QueryExecutor { + private static final Logger LOG = LoggerFactory.getLogger(QueryExecutor.class); + + /** The TSDB to which we belong (and will use for fetching data) */ + private final TSDB tsdb; + + /** The user's query */ + private final Query query; + + /** TEMP A v1 TSQuery that we use for fetching the data from HBase */ + private final TSQuery ts_query; + + /** A map of the sub queries to their Metric ids */ + private final Map sub_queries; + + /** A map of the sub query results to their Metric ids */ + private final Map sub_query_results; + + /** A map of expression iterators to their IDs */ + private final Map expressions; + + /** A map of Metric fill policies to the metric IDs */ + private final Map fills; + + /** The HTTP query from the user */ + private HttpQuery http_query; + + /** + * Default Ctor that constructs a TSQuery and TSSubQueries from the new + * Query POJO class. + * @param tsdb The TSDB to which we belong + * @param query The raw query to parse and use for output + * @throws IllegalArgumentException if we were unable to parse the Query into + * a TSQuery. + */ + public QueryExecutor(final TSDB tsdb, final Query query) { + this.tsdb = tsdb; + this.query = query; + + // if metrics is null, this is a bad query + sub_queries = new HashMap(query.getMetrics().size()); + sub_query_results = new HashMap( + query.getMetrics().size()); + + if (query.getExpressions() != null) { + expressions = new HashMap( + query.getExpressions().size()); + } else { + expressions = null; + } + + final Timespan timespan = query.getTime(); + + // compile the ts_query + ts_query = new TSQuery(); + ts_query.setStart(timespan.getStart()); + ts_query.setTimezone(timespan.getTimezone()); + + if (timespan.getEnd() != null && !timespan.getEnd().isEmpty()) { + ts_query.setEnd(timespan.getEnd()); + } + + fills = new HashMap(query.getMetrics().size()); + for (final Metric mq : query.getMetrics()) { + if (mq.getFillPolicy() != null) { + fills.put(mq.getId(), mq.getFillPolicy()); + } + final TSSubQuery sub = new TSSubQuery(); + sub_queries.put(mq.getId(), sub); + + sub.setMetric(mq.getMetric()); + + if (timespan.getDownsampler() != null) { + sub.setDownsample(timespan.getDownsampler().getInterval() + "-" + + timespan.getDownsampler().getAggregator()); + } + + // filters + if (mq.getFilter() != null && !mq.getFilter().isEmpty()) { + Filter filters = null; + if (query.getFilters() == null || query.getFilters().isEmpty()) { + throw new IllegalArgumentException("No filter defined: " + mq.getFilter()); + } + for (final Filter filter : query.getFilters()) { + if (filter.getId().equals(mq.getFilter())) { + filters = filter; + break; + } + } + sub.setRate(timespan.isRate()); + sub.setFilters(filters.getTags()); + sub.setAggregator( + mq.getAggregator() != null ? mq.getAggregator() : timespan.getAggregator()); + } + } + + final ArrayList subs = + new ArrayList(sub_queries.values()); + ts_query.setQueries(subs); + + // setup expressions + for (final Expression expression : query.getExpressions()) { + // TODO - flags + + // TODO - get a default from the configs + final SetOperator operator = expression.getJoin() != null ? + expression.getJoin().getOperator() : SetOperator.UNION; + final boolean qts = expression.getJoin() == null ? false : expression.getJoin().getUseQueryTags(); + final boolean ats = expression.getJoin() == null ? true : expression.getJoin().getIncludeAggTags(); + final ExpressionIterator iterator = + new ExpressionIterator(expression.getId(), expression.getExpr(), + operator, qts, ats); + if (expression.getFillPolicy() != null) { + iterator.setFillPolicy(expression.getFillPolicy()); + } + expressions.put(expression.getId(), iterator); + + } + + ts_query.validateAndSetQuery(); + } + + /** + * Execute the RPC and serialize the response + * @param query The HTTP query to parse and and return results to + */ + public void execute(final HttpQuery query) { + http_query = query; + final QueryStats query_stats = + new QueryStats(query.getRemoteAddress(), ts_query); + ts_query.setQueryStats(query_stats); + + final long start = DateTime.currentTimeMillis(); + + /** + * Sends the serialized results to the caller. This should be the very + * last callback executed. + */ + class CompleteCB implements Callback { + @Override + public Object call(final ChannelBuffer cb) throws Exception { + query.sendReply(cb); + return null; + } + } + + /** + * After all of the queries have run and we have data (or not) then we + * need to compile the iterators. + * This class could probably be improved: + * First we iterate over the results AND for each result, iterate over + * the expressions, giving a time synced iterator to each expression that + * needs the result set. + * THEN we iterate over the expressions again and build a DAG to determine + * if any of the expressions require the output of an expression. If so + * then we add the expressions to the proper parent and compile them in + * order. + * After all of that we're ready to start serializing and iterating + * over the results. + */ + class QueriesCB implements Callback> { + public Object call(final ArrayList query_results) + throws Exception { + + query_stats.setTimeStorage(DateTime.currentTimeMillis() - start); + for (int i = 0; i < query_results.size(); i++) { + final TSSubQuery sub = ts_query.getQueries().get(i); + + Iterator> it = sub_queries.entrySet().iterator(); + while (it.hasNext()) { + final Entry entry = it.next(); + if (entry.getValue().equals(sub)) { + sub_query_results.put(entry.getKey(), query_results.get(i)); + for (final ExpressionIterator ei : expressions.values()) { + if (ei.getVariableNames().contains(entry.getKey())) { + final TimeSyncedIterator tsi = new TimeSyncedIterator( + entry.getKey(), sub.getFilterTagKs(), + query_results.get(i)); + final NumericFillPolicy fill = fills.get(entry.getKey()); + if (fill != null) { + tsi.setFillPolicy(fill); + } + ei.addResults(entry.getKey(), tsi); + LOG.debug("Added results for " + entry.getKey() + + " to " + ei.getId()); + } + } + } + } + } + + // handle nested expressions + DirectedAcyclicGraph graph = null; + for (final Entry eii : expressions.entrySet()) { + for (final String var : eii.getValue().getVariableNames()) { + final ExpressionIterator ei = expressions.get(var); + if (ei != null) { + // TODO - really ought to calculate this earlier + if (eii.getKey().equals(var)) { + throw new IllegalArgumentException( + "Self referencing expression found: " + eii.getKey()); + } + LOG.debug("Nested expression detected. " + eii.getKey() + + " depends on " + var); + + if (graph == null) { + graph = new DirectedAcyclicGraph(DefaultEdge.class); + } + if (!graph.containsVertex(eii.getKey())) { + graph.addVertex(eii.getKey()); + } + if (!graph.containsVertex(var)) { + graph.addVertex(var); + } + try { + graph.addDagEdge(eii.getKey(), var); + } catch (CycleFoundException cfe) { + throw new IllegalArgumentException("Circular reference found: " + + eii.getKey(), cfe); + } + } + } + } + + // compile all of the expressions + final long intersect_start = DateTime.currentTimeMillis(); + if (graph != null) { + final ExpressionIterator[] compile_stack = + new ExpressionIterator[expressions.size()]; + final TopologicalOrderIterator it = + new TopologicalOrderIterator(graph); + int i = 0; + while (it.hasNext()) { + compile_stack[i++] = expressions.get(it.next()); + } + for (int x = compile_stack.length - 1; x >= 0; x--) { + // look for and add expressions + for (final String var : compile_stack[x].getVariableNames()) { + ExpressionIterator source = expressions.get(var); + if (source != null) { + compile_stack[x].addResults(var, source.getCopy()); + LOG.debug("Adding expression " + source.getId() + " to " + + compile_stack[x].getId()); + } + } + + compile_stack[x].compile(); + LOG.debug("Successfully compiled " + compile_stack[x]); + } + } else { + for (final ExpressionIterator ei : expressions.values()) { + ei.compile(); + LOG.debug("Successfully compiled " + ei); + } + } + LOG.debug("Finished compilations in " + + (DateTime.currentTimeMillis() - intersect_start) + " ms"); + + return serialize().addCallback(new CompleteCB()).addErrback(new ErrorCB()); + } + } + + /** + * Callback executed after we have resolved the metric, tag names and tag + * values to their respective UIDs. This callback then runs the actual + * queries and fetches their results. + */ + class BuildCB implements Callback, net.opentsdb.core.Query[]> { + @Override + public Deferred call(final net.opentsdb.core.Query[] queries) { + final ArrayList> deferreds = + new ArrayList>(queries.length); + + for (final net.opentsdb.core.Query query : queries) { + deferreds.add(query.runAsync()); + } + return Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()) + .addErrback(new ErrorCB()); + } + } + + // TODO - only run the ones that will be involved in an output. Folks WILL + // ask for stuff they don't need.... *sigh* + ts_query.buildQueriesAsync(tsdb).addCallback(new BuildCB()) + .addErrback(new ErrorCB()); + } + + /** + * Writes the results to a ChannelBuffer to return to the caller. This will + * iterate over all of the outputs and drop in meta data where appropriate. + * @throws Exception if something went pear shaped + */ + private Deferred serialize() throws Exception { + final long start = System.currentTimeMillis(); + // buffers and an array list to stored the deferreds + final ChannelBuffer response = ChannelBuffers.dynamicBuffer(); + final OutputStream output_stream = new ChannelBufferOutputStream(response); + + final JsonGenerator json = JSON.getFactory().createGenerator(output_stream); + json.writeStartObject(); + json.writeFieldName("outputs"); + json.writeStartArray(); + + // We want the serializer to execute serially so we need to create a callback + // chain so that when one DPsResolver is finished, it triggers the next to + // start serializing. + final Deferred cb_chain = new Deferred(); + + // default to the expressions if there, or fall back to the metrics + final List outputs; + if (query.getOutputs() == null || query.getOutputs().isEmpty()) { + if (query.getExpressions() != null && !query.getExpressions().isEmpty()) { + outputs = new ArrayList(query.getExpressions().size()); + for (final Expression exp : query.getExpressions()) { + outputs.add(Output.Builder().setId(exp.getId()).build()); + } + } else if (query.getMetrics() != null && !query.getMetrics().isEmpty()) { + outputs = new ArrayList(query.getMetrics().size()); + for (final Metric metric : query.getMetrics()) { + outputs.add(Output.Builder().setId(metric.getId()).build()); + } + } else { + throw new IllegalArgumentException( + "How did we get here?? No metrics or expressions??"); + } + } else { + outputs = query.getOutputs(); + } + + for (final Output output : outputs) { + if (expressions != null) { + final ExpressionIterator it = expressions.get(output.getId()); + if (it != null) { + cb_chain.addCallback(new SerializeExpressionIterator(tsdb, json, + output, it, ts_query)); + continue; + } + } + + if (query.getMetrics() != null && !query.getMetrics().isEmpty()) { + final TSSubQuery sub = sub_queries.get(output.getId()); + if (sub != null) { + final TimeSyncedIterator it = new TimeSyncedIterator(output.getId(), + sub.getFilterTagKs(), sub_query_results.get(output.getId())); + cb_chain.addCallback(new SerializeSubIterator(tsdb, json, output, it)); + continue; + } + } else { + LOG.warn("Couldn't find a variable matching: " + output.getId() + + " in query " + query); + } + } + + /** Final callback to close out the JSON array and return our results */ + class FinalCB implements Callback { + public ChannelBuffer call(final Object obj) + throws Exception { + json.writeEndArray(); + + ts_query.getQueryStats().setTimeSerialization( + DateTime.currentTimeMillis() - start); + ts_query.getQueryStats().markComplete(); + + // dump overall stats as an extra object in the array + if (true) { + final QueryStats stats = ts_query.getQueryStats(); + json.writeFieldName("statsSummary"); + json.writeStartObject(); + //json.writeStringField("hostname", TSDB.getHostname()); + //json.writeNumberField("runningQueries", stats.getNumRunningQueries()); + json.writeNumberField("datapoints", stats.getAggregatedSize()); + json.writeNumberField("rawDatapoints", stats.getSize()); + //json.writeNumberField("rowsFetched", stats.getRowsFetched()); + json.writeNumberField("aggregationTime", stats.getTimeAggregation()); + json.writeNumberField("serializationTime", stats.getTimeSerialization()); + json.writeNumberField("storageTime", stats.getTimeStorage()); + json.writeNumberField("timeTotal", + ((double)stats.getTimeTotal() / (double)1000000)); + json.writeEndObject(); + } + + // dump the original query + if (true) { + json.writeFieldName("query"); + json.writeObject(QueryExecutor.this.query); + } + // IMPORTANT Make sure the close the JSON array and the generator + json.writeEndObject(); + json.close(); + return response; + } + } + + // trigger the callback chain here + cb_chain.callback(null); + return cb_chain.addCallback(new FinalCB()); + } + + /** This has to be attached to callbacks or we may never respond to clients */ + class ErrorCB implements Callback { + public Object call(final Exception e) throws Exception { + try { + LOG.error("Query exception: ", e); + if (e instanceof DeferredGroupException) { + Throwable ex = e.getCause(); + while (ex != null && ex instanceof DeferredGroupException) { + ex = ex.getCause(); + } + if (ex != null) { + LOG.error("Unexpected exception: ", ex); + // TODO - find a better way to determine the real error + QueryExecutor.this.ts_query.getQueryStats() + .markComplete(HttpResponseStatus.BAD_REQUEST, ex); + QueryExecutor.this.http_query.badRequest(new BadRequestException(ex)); + } else { + LOG.error("The deferred group exception didn't have a cause???"); + QueryExecutor.this.ts_query.getQueryStats() + .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); + QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); + } + } else if (e.getClass() == QueryException.class) { + QueryExecutor.this.ts_query.getQueryStats() + .markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); + QueryExecutor.this.http_query.badRequest(new BadRequestException((QueryException)e)); + } else { + QueryExecutor.this.ts_query.getQueryStats() + .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); + QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); + } + return null; + } catch (RuntimeException ex) { + LOG.error("Exception thrown during exception handling", ex); + QueryExecutor.this.ts_query.getQueryStats() + .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); + QueryExecutor.this.http_query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, + ex.getMessage().getBytes()); + return null; + } + } + } + + /** + * Handles serializing the output of an expression iterator + */ + private class SerializeExpressionIterator + implements Callback, Object> { + final TSDB tsdb; + final JsonGenerator json; + final Output output; + final ExpressionIterator iterator; + final ExpressionDataPoint[] dps; + final TSQuery query; + + // WARNING: Make sure to write an endObject() before triggering this guy + final Deferred completed; + + /** + * The default ctor to setup the serializer + * @param tsdb The TSDB to use for name resolution + * @param json The JSON generator to write to + * @param output The Output spec associated with this expression + * @param iterator The iterator to run through + * @param query The original TSQuery + */ + public SerializeExpressionIterator(final TSDB tsdb, final JsonGenerator json, + final Output output, final ExpressionIterator iterator, final TSQuery query) { + this.tsdb = tsdb; + this.json = json; + this.output = output; + this.iterator = iterator; + this.query = query; + dps = iterator.values(); + completed = new Deferred(); + } + + /** Super simple closer that tells the upstream chain we're done with this */ + class MetaCB implements Callback { + @Override + public Object call(final Object ignored) throws Exception { + completed.callback(null); + return completed; + } + } + + @Override + public Deferred call(final Object ignored) throws Exception { + //result set opening + json.writeStartObject(); + + json.writeStringField("id", output.getId()); + if (output.getAlias() != null) { + json.writeStringField("alias", output.getAlias()); + } + json.writeFieldName("dps"); + json.writeStartArray(); + + long first_ts = Long.MIN_VALUE; + long last_ts = 0; + long count = 0; + long ts = iterator.nextTimestamp(); + long qs = query.startTime(); + long qe = query.endTime(); + while (iterator.hasNext()) { + iterator.next(ts); + + long timestamp = dps[0].timestamp(); + if (timestamp >= qs && timestamp <= qe) { + json.writeStartArray(); + if (dps.length > 0) { + json.writeNumber(timestamp); + if (first_ts == Long.MIN_VALUE) { + first_ts = timestamp; + } else { + last_ts = timestamp; + } + ++count; + } + for (int i = 0; i < dps.length; i++) { + json.writeNumber(dps[i].toDouble()); + } + + json.writeEndArray(); + } + ts = iterator.nextTimestamp(); + } + json.writeEndArray(); + + // data points meta + json.writeFieldName("dpsMeta"); + json.writeStartObject(); + json.writeNumberField("firstTimestamp", first_ts < 0 ? 0 : first_ts); + json.writeNumberField("lastTimestamp", last_ts); + json.writeNumberField("setCount", count); + json.writeNumberField("series", dps.length); + json.writeEndObject(); + + // resolve meta LAST since we may not even need it + if (dps.length > 0) { + final MetaSerializer meta_serializer = + new MetaSerializer(tsdb, json, iterator.values()); + meta_serializer.call(null).addCallback(new MetaCB()) + .addErrback(QueryExecutor.this.new ErrorCB()); + } else { + // done, not dumping any more info + json.writeEndObject(); + //json.writeEndArray(); + completed.callback(null); + } + + return completed; + } + + } + + /** + * Serializes a raw, non expression result set. + */ + private class SerializeSubIterator implements + Callback, Object> { + final TSDB tsdb; + final JsonGenerator json; + final Output output; + final TimeSyncedIterator iterator; + + // WARNING: Make sure to write an endObject() before triggering this guy + final Deferred completed; + + public SerializeSubIterator(final TSDB tsdb, final JsonGenerator json, + final Output output, final TimeSyncedIterator iterator) { + this.tsdb = tsdb; + this.json = json; + this.output = output; + this.iterator = iterator; + completed = new Deferred(); + } + + class MetaCB implements Callback { + @Override + public Object call(final Object ignored) throws Exception { + completed.callback(null); + return completed; + } + } + + @Override + public Deferred call(final Object ignored) throws Exception { + //result set opening + json.writeStartObject(); + + json.writeStringField("id", output.getId()); + if (output.getAlias() != null) { + json.writeStringField("alias", output.getAlias()); + } + json.writeFieldName("dps"); + json.writeStartArray(); + + final long first_ts = iterator.nextTimestamp(); + long ts = first_ts; + long last_ts = 0; + long count = 0; + final DataPoint[] dps = iterator.values(); + while (iterator.hasNext()) { + iterator.next(ts); + json.writeStartArray(); + + if (dps.length > 0) { + json.writeNumber(dps[0].timestamp()); + last_ts = dps[0].timestamp(); + ++count; + } + for (int i = 0; i < dps.length; i++) { + json.writeNumber(dps[i].toDouble()); + } + + json.writeEndArray(); + ts = iterator.nextTimestamp(); + } + json.writeEndArray(); + + // data points meta + json.writeFieldName("dpsMeta"); + json.writeStartObject(); + json.writeNumberField("firstTimestamp", first_ts); + json.writeNumberField("lastTimestamp", last_ts); + json.writeNumberField("setCount", count); + json.writeNumberField("series", dps.length); + json.writeEndObject(); + + // resolve meta LAST since we may not even need it + if (dps.length > 0) { + final DataPoints[] odps = iterator.getDataPoints(); + final ExpressionDataPoint[] edps = new ExpressionDataPoint[dps.length]; + for (int i = 0; i < dps.length; i++) { + edps[i] = new ExpressionDataPoint(odps[i]); + } + final MetaSerializer meta_serializer = + new MetaSerializer(tsdb, json, edps); + meta_serializer.call(null).addCallback(new MetaCB()); + } else { + // done, not dumping any more info + json.writeEndObject(); + completed.callback(null); + } + + return completed; + } + + } + + /** + * Handles resolving metrics, tags, aggregated tags and other meta data + * associated with a result set. + */ + private class MetaSerializer implements Callback, Object> { + final TSDB tsdb; + final JsonGenerator json; + final ExpressionDataPoint[] dps; + final List metrics; + final Map[] tags; + final List[] agg_tags; + + final Deferred completed; + + @SuppressWarnings("unchecked") + public MetaSerializer(final TSDB tsdb, final JsonGenerator json, + final ExpressionDataPoint[] dps) { + this.tsdb = tsdb; + this.json = json; + this.dps = dps; + completed = new Deferred(); + metrics = new ArrayList(); + tags = new Map[dps.length]; + agg_tags = new List[dps.length]; + } + + class MetricsCB implements Callback> { + @Override + public Object call(final ArrayList names) throws Exception { + metrics.addAll(names); + Collections.sort(metrics); + return null; + } + } + + class AggTagsCB implements Callback> { + final int index; + public AggTagsCB(final int index) { + this.index = index; + } + @Override + public Object call(final ArrayList tags) throws Exception { + agg_tags[index] = tags; + return null; + } + } + + class TagsCB implements Callback> { + final int index; + public TagsCB(final int index) { + this.index = index; + } + @Override + public Object call(final Map tags) throws Exception { + MetaSerializer.this.tags[index] = tags; + return null; + } + } + + class MetaCB implements Callback> { + @Override + public Object call(final ArrayList ignored) throws Exception { + json.writeFieldName("meta"); + json.writeStartArray(); + + // first field is the timestamp + json.writeStartObject(); + json.writeNumberField("index", 0); + json.writeFieldName("metrics"); + json.writeStartArray(); + json.writeString("timestamp"); + json.writeEndArray(); + json.writeEndObject(); + + for (int i = 0; i < dps.length; i++) { + json.writeStartObject(); + + json.writeNumberField("index", i + 1); + json.writeFieldName("metrics"); + json.writeObject(metrics); + + json.writeFieldName("commonTags"); + if (tags[i] == null) { + json.writeObject(Collections.emptyMap()); + } else { + json.writeObject(tags[i]); + } + + json.writeFieldName("aggregatedTags"); + if (agg_tags[i] == null) { + json.writeObject(Collections.emptyList()); + } else { + json.writeObject(agg_tags[i]); + } + + // TODO restore when we can calculate size efficiently + //json.writeNumberField("dps", dps[i].size()); + //json.writeNumberField("rawDps", dps[i].rawSize()); + + json.writeEndObject(); + } + + json.writeEndArray(); + + // all done with this series of results + json.writeEndObject(); + completed.callback(null); + return null; + } + } + + @Override + public Deferred call(final Object ignored) throws Exception { + final List> deferreds = + new ArrayList>(); + + final List> metric_deferreds = + new ArrayList>(dps[0].metricUIDs().size()); + + for (final byte[] uid : dps[0].metricUIDs()) { + metric_deferreds.add(tsdb.getUidName(UniqueIdType.METRIC, uid)); + } + + deferreds.add(Deferred.group(metric_deferreds) + .addCallback(new MetricsCB())); + + for (int i = 0; i < dps.length; i++) { + if (dps[i].aggregatedTags().size() > 0) { + final List> agg_deferreds = + new ArrayList>(dps[i].aggregatedTags().size()); + for (final byte[] uid : dps[i].aggregatedTags()) { + agg_deferreds.add(tsdb.getUidName(UniqueIdType.TAGK, uid)); + } + deferreds.add(Deferred.group(agg_deferreds) + .addCallback(new AggTagsCB(i))); + } + + deferreds.add(Tags.getTagsAsync(tsdb, dps[i].tags()) + .addCallback(new TagsCB(i))); + } + + Deferred.groupInOrder(deferreds).addCallback(new MetaCB()) + .addErrback(QueryExecutor.this.new ErrorCB()); + return completed; + } + + } + +} diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 4b5ab81a7b..554e586b9a 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -49,6 +49,7 @@ import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; /** * Handles queries for timeseries datapoints. Each request is parsed into a @@ -94,6 +95,9 @@ public void execute(final TSDB tsdb, final HttpQuery query) handleLastDataPointQuery(tsdb, query); } else if (endpoint.toLowerCase().equals("gexp")){ handleQuery(tsdb, query, true); + } else if (endpoint.toLowerCase().equals("exp")) { + handleExpressionQuery(tsdb, query); + return; } else { handleQuery(tsdb, query, false); } @@ -278,6 +282,20 @@ public Object call(final List annotations) throws Exception { } } + /** + * Handles an expression query + * @param tsdb The TSDB to which we belong + * @param query The HTTP query to parse/respond + * @since 2.3 + */ + private void handleExpressionQuery(final TSDB tsdb, final HttpQuery query) { + final net.opentsdb.query.pojo.Query v2_query = + JSON.parseToObject(query.getContent(), net.opentsdb.query.pojo.Query.class); + v2_query.validate(); + final QueryExecutor executor = new QueryExecutor(tsdb, v2_query); + executor.execute(query); + } + /** * Processes a last data point query * @param tsdb The TSDB to which we belong diff --git a/test/tsd/TestQueryExecutor.java b/test/tsd/TestQueryExecutor.java new file mode 100644 index 0000000000..a1a021156b --- /dev/null +++ b/test/tsd/TestQueryExecutor.java @@ -0,0 +1,607 @@ +// 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 static org.junit.Assert.assertTrue; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.BaseTimeSyncedIteratorTest; +import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.pojo.Expression; +import net.opentsdb.query.pojo.Filter; +import net.opentsdb.query.pojo.Join; +import net.opentsdb.query.pojo.Metric; +import net.opentsdb.query.pojo.Output; +import net.opentsdb.query.pojo.Query; +import net.opentsdb.query.pojo.Timespan; +import net.opentsdb.storage.MockBase; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; + +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; + +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({TSDB.class, Config.class, HttpQuery.class, + Deferred.class, TSQuery.class, DateTime.class, DeferredGroupException.class }) +public class TestQueryExecutor extends BaseTimeSyncedIteratorTest { + + private Timespan time; + private List tags; + private List filters; + private List metrics; + private List expressions; + private List outputs; + private Join intersection; + + @Before + public void setup() { + intersection = Join.Builder().setOperator(SetOperator.INTERSECTION).build(); + time = Timespan.Builder().setStart("1431561600") + .setAggregator("sum").build(); + + tags = Arrays.asList(new TagVFilter.Builder().setFilter("*").setGroupBy(true) + .setTagk("D").setType("wildcard").build()); + + filters = Arrays.asList(Filter.Builder().setId("f1") + .setTags(tags).build()); + final Metric metric1 = Metric.Builder().setMetric("A").setId("a") + .setFilter("f1").build(); + final Metric metric2 = Metric.Builder().setMetric("B").setId("b") + .setFilter("f1").build(); + metrics = Arrays.asList(metric1, metric2); + expressions = Arrays.asList(Expression.Builder().setId("e") + .setExpression("a + b").setJoin(intersection).build()); + outputs = Arrays.asList(Output.Builder().setId("e").setAlias("A plus B") + .build()); + } + + @Test + public void oneExpressionWithOutputAlias() throws Exception { + oneExtraSameE(); + final String json = JSON.serializeToString(getDefaultQueryBuilder().build()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"alias\":\"A plus B\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + } + + @Test + public void oneExpressionDefaultOutput() throws Exception { + oneExtraSameE(); + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"id\":\"e\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + // TODO - more asserts once we settle on names + } + + @Test + public void oneExpressionOutputAndBAlso() throws Exception { + oneExtraSameE(); + + outputs = new ArrayList(3); + outputs.add(Output.Builder().setId("e").setAlias("A plus B").build()); + outputs.add(Output.Builder().setId("a").build()); + outputs.add(Output.Builder().setId("b").build()); + + final String json = JSON.serializeToString(getDefaultQueryBuilder().build()); + + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"alias\":\"A plus B\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + + assertTrue(response.contains("\"id\":\"a\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,1.0,4.0]")); + assertTrue(response.contains("\"metrics\":[\"A\"]")); + assertTrue(response.contains("\"id\":\"b\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,11.0,14.0,17.0]")); + assertTrue(response.contains("\"metrics\":[\"B\"]")); + } + + @Test + public void oneExpressionDefaultFill() throws Exception { + threeSameEGaps(); + String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"alias\":\"A plus B\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,1.0,4.0,0.0]")); + assertTrue(response.contains("[1431561660000,0.0,20.0,8.0]")); + assertTrue(response.contains("[1431561720000,16.0,0.0,28.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"index\":3")); + } + + @Test + public void twoExpressionsDefaultOutput() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b") + .setJoin(intersection).build(), + Expression.Builder().setId("e2").setExpression("a * b") + .setJoin(intersection).build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"id\":\"e\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"id\":\"e2\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,11.0,56.0]")); + assertTrue(response.contains("[1431561660000,24.0,75.0]")); + assertTrue(response.contains("[1431561720000,39.0,96.0]")); + } + + @Test + public void twoExpressionsOneWithoutResultsDefaultOutput() throws Exception { + oneExtraSameE(); + final Metric metric1 = Metric.Builder().setMetric("A").setId("a") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric2 = Metric.Builder().setMetric("B").setId("b") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric3 = Metric.Builder().setMetric("D").setId("d") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric4 = Metric.Builder().setMetric("F").setId("f") + .setFilter("f1").setAggregator("sum").build(); + metrics = Arrays.asList(metric1, metric2, metric3, metric4); + + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b") + .setJoin(intersection).build(), + Expression.Builder().setId("x").setExpression("d + f") + .setJoin(intersection).build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"id\":\"e\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"id\":\"x\"")); + assertTrue(response.contains("\"dps\":[]")); + assertTrue(response.contains("\"firstTimestamp\":0")); + assertTrue(response.contains("\"series\":0")); + } + + @Test + public void multiExpressionsOneOutput() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b").setJoin(intersection).build(), + Expression.Builder().setId("e2").setExpression("e * 2").setJoin(intersection).build(), + Expression.Builder().setId("e3").setExpression("e * 2").setJoin(intersection).build(), + Expression.Builder().setId("e4").setExpression("e2 + e3").setJoin(intersection).build()); + + final String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + } + + @Test + public void nestedExpressionsOneLevelDefaultOutput() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b") + .setJoin(intersection).build(), + Expression.Builder().setId("e2").setExpression("e * 2") + .setJoin(intersection).build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"id\":\"e\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"id\":\"e2\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,24.0,36.0]")); + assertTrue(response.contains("[1431561660000,28.0,40.0]")); + assertTrue(response.contains("[1431561720000,32.0,44.0]")); + } + + @Test + public void nestedExpressionsTwoLevelsDefaultOutput() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b").setJoin(intersection).build(), + Expression.Builder().setId("e2").setExpression("e * 2").setJoin(intersection).build(), + Expression.Builder().setId("e3").setExpression("e * 2").setJoin(intersection).build(), + Expression.Builder().setId("e4").setExpression("e2 + e3").setJoin(intersection).build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"id\":\"e\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"id\":\"e2\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,24.0,36.0]")); + assertTrue(response.contains("[1431561660000,28.0,40.0]")); + assertTrue(response.contains("[1431561720000,32.0,44.0]")); + assertTrue(response.contains("\"id\":\"e3\"")); + assertTrue(response.contains("\"id\":\"e4\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,48.0,72.0]")); + assertTrue(response.contains("[1431561660000,56.0,80.0]")); + assertTrue(response.contains("[1431561720000,64.0,88.0]")); + } + + @Test + public void nestedExpressionsTwoLevelsDefaultOutputOrdering() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e2").setExpression("e * 2").setJoin(intersection).build(), + Expression.Builder().setId("e4").setExpression("e2 + e3").setJoin(intersection).build(), + Expression.Builder().setId("e3").setExpression("e * 2").setJoin(intersection).build(), + Expression.Builder().setId("e").setExpression("a + b").setJoin(intersection).build() + ); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"id\":\"e\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,12.0,18.0]")); + assertTrue(response.contains("[1431561660000,14.0,20.0]")); + assertTrue(response.contains("[1431561720000,16.0,22.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"id\":\"e2\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,24.0,36.0]")); + assertTrue(response.contains("[1431561660000,28.0,40.0]")); + assertTrue(response.contains("[1431561720000,32.0,44.0]")); + assertTrue(response.contains("\"id\":\"e3\"")); + assertTrue(response.contains("\"id\":\"e4\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,48.0,72.0]")); + assertTrue(response.contains("[1431561660000,56.0,80.0]")); + assertTrue(response.contains("[1431561720000,64.0,88.0]")); + } + + @Test + public void emptyResultSet() throws Exception { + setDataPointStorage(); + String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"dps\":[]")); + assertTrue(response.contains("\"firstTimestamp\":0")); + assertTrue(response.contains("\"series\":0")); + } + + @Test + public void scannerException() throws Exception { + oneExtraSameE(); + storage.throwException(MockBase.stringToBytes( + "00000B5553E58000000D00000F00000E00000E"), + new RuntimeException("Boo!"), true); + final String json = JSON.serializeToString(getDefaultQueryBuilder().build()); + + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"Boo!\"")); + } + + @Test + public void nsunMetric() throws Exception { + oneExtraSameE(); + final Metric metric1 = Metric.Builder().setMetric("A").setId("a") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric2 = Metric.Builder().setMetric(NSUN_METRIC).setId("b") + .setFilter("f1").setAggregator("sum").build(); + metrics = Arrays.asList(metric1, metric2); + final String json = JSON.serializeToString(getDefaultQueryBuilder().build()); + + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"No such name for '" + + NSUN_METRIC + "'")); + } + + @Test + public void selfReferencingExpression() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b").build(), + Expression.Builder().setId("e2").setExpression("e * 2").build(), + Expression.Builder().setId("e3").setExpression("e * 2").build(), + Expression.Builder().setId("e4").setExpression("e2 + e4").build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"Self referencing")); + } + + @Test + public void circularReferenceExpression() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + e4").build(), + Expression.Builder().setId("e2").setExpression("e * 2").build(), + Expression.Builder().setId("e3").setExpression("e * 2").build(), + Expression.Builder().setId("e4").setExpression("e2 + e3").build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + final String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"Circular reference found:")); + } + + @Test + public void noIntersectionsFound() throws Exception { + threeDifE(); + + String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"No intersections found")); + } + + @Test + public void noIntersectionsFoundNestedExpression() throws Exception { + oneExtraSameE(); + + final Metric metric1 = Metric.Builder().setMetric("A").setId("a") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric2 = Metric.Builder().setMetric("B").setId("b") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric3 = Metric.Builder().setMetric("D").setId("d") + .setFilter("f1").setAggregator("sum").build(); + metrics = Arrays.asList(metric1, metric2, metric3); + + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b").setJoin(intersection).build(), + Expression.Builder().setId("x").setExpression("d + e").setJoin(intersection).build()); + + final Query q = Query.Builder().setExpressions(expressions) + .setFilters(filters).setMetrics(metrics).setName("q1") + .setTime(time).build(); + String json = JSON.serializeToString(q); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"No intersections found")); + } + + @Test + public void noIntersectionsFoundOneMetricEmpty() throws Exception { + oneExtraSameE(); + final Metric metric1 = Metric.Builder().setMetric("A").setId("a") + .setFilter("f1").setAggregator("sum").build(); + final Metric metric2 = Metric.Builder().setMetric("D").setId("b") + .setFilter("f1").setAggregator("sum").build(); + metrics = Arrays.asList(metric1, metric2); + String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"No intersections found")); + } + + @Test + public void notEnoughMetrics() throws Exception { + oneExtraSameE(); + expressions = Arrays.asList( + Expression.Builder().setId("e").setExpression("a + b + c").build()); + String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"code\":400")); + assertTrue(response.contains("\"message\":\"Not enough query results")); + } + + protected Query.Builder getDefaultQueryBuilder() { + return Query.Builder().setExpressions(expressions).setFilters(filters) + .setMetrics(metrics).setName("q1").setTime(time).setOutputs(outputs); + } +} From 42c2723782955a4909f7357ffef3e019c499a016 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 9 Nov 2015 11:23:01 -0800 Subject: [PATCH 318/826] Fix a bug where an HBase exception wasn't propagated to the end user at query time. Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 16 +++++++++++++++- test/core/TestTsdbQuery.java | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index cef3748f15..7b060a44ed 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -566,6 +566,20 @@ final class ScannerCB implements Callback skips = new HashSet(); private final Set keepers = new HashSet(); + + /** Error callback that will capture an exception from AsyncHBase and store + * it so we can bubble it up to the caller. + */ + class ErrorCB implements Callback { + @Override + public Object call(final Exception e) throws Exception { + LOG.error("Scanner " + scanner + " threw an exception", e); + scanner.close(); + results.callback(e); + return null; + } + } + /** * Starts the scanner and is called recursively to fetch the next set of * rows from the scanner. @@ -574,7 +588,7 @@ final class ScannerCB implements Callback Date: Thu, 5 Nov 2015 18:55:04 -0800 Subject: [PATCH 319/826] Add an Tags.getTagsAsync() override that accepts a ByteMap. --- src/core/Tags.java | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/core/Tags.java b/src/core/Tags.java index 83ec687824..462196e152 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -462,6 +462,49 @@ public Map call(final ArrayList names) return Deferred.groupInOrder(deferreds).addCallback(new NameCB()); } + /** + * Returns the names mapped to tag key/value UIDs + * @param tsdb The TSDB instance to use for Unique ID lookups. + * @param tags The map of tag key to tag value pairs + * @return A map of tag names (keys), tag values (values). If the tags list + * was null or empty, the result will be an empty map + * @throws NoSuchUniqueId if the row key contained an invalid ID. + * @since 2.3 + */ + public static Deferred> getTagsAsync(final TSDB tsdb, + final ByteMap tags) { + if (tags == null || tags.isEmpty()) { + return Deferred.fromResult(Collections.emptyMap()); + } + + final ArrayList> deferreds = + new ArrayList>(); + + for (final Map.Entry pair : tags) { + deferreds.add(tsdb.tag_names.getNameAsync(pair.getKey())); + deferreds.add(tsdb.tag_values.getNameAsync(pair.getValue())); + } + + class NameCB implements Callback, ArrayList> { + public Map call(final ArrayList names) + throws Exception { + final HashMap result = new HashMap(); + String tagk = ""; + for (String name : names) { + if (tagk.isEmpty()) { + tagk = name; + } else { + result.put(tagk, name); + tagk = ""; + } + } + return result; + } + } + + return Deferred.groupInOrder(deferreds).addCallback(new NameCB()); + } + /** * Returns the tag key and value pairs as a byte map given a row key * @param row The row key to parse the UIDs from From b7d9a481854cc98055b9bc52eba920873d5c7595 Mon Sep 17 00:00:00 2001 From: louyl Date: Mon, 9 Nov 2015 16:53:31 +0800 Subject: [PATCH 320/826] Fix IncomingDataPoint toString, separate tags with space Signed-off-by: Chris Larsen --- src/core/IncomingDataPoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/IncomingDataPoint.java b/src/core/IncomingDataPoint.java index 0a7c70970d..dced8077ef 100644 --- a/src/core/IncomingDataPoint.java +++ b/src/core/IncomingDataPoint.java @@ -94,10 +94,10 @@ public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("metric=").append(this.metric); buf.append(" ts=").append(this.timestamp); - buf.append(" value=").append(this.value).append(" "); + buf.append(" value=").append(this.value); if (this.tags != null) { for (Map.Entry entry : this.tags.entrySet()) { - buf.append(entry.getKey()).append("=").append(entry.getValue()); + buf.append(" ").append(entry.getKey()).append("=").append(entry.getValue()); } } return buf.toString(); From 3866e8c1b045ae900599bf09fffb7713ca881520 Mon Sep 17 00:00:00 2001 From: louyl Date: Mon, 9 Nov 2015 16:53:31 +0800 Subject: [PATCH 321/826] Fix IncomingDataPoint toString, separate tags with space Signed-off-by: Chris Larsen --- src/core/IncomingDataPoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/IncomingDataPoint.java b/src/core/IncomingDataPoint.java index 0a7c70970d..dced8077ef 100644 --- a/src/core/IncomingDataPoint.java +++ b/src/core/IncomingDataPoint.java @@ -94,10 +94,10 @@ public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("metric=").append(this.metric); buf.append(" ts=").append(this.timestamp); - buf.append(" value=").append(this.value).append(" "); + buf.append(" value=").append(this.value); if (this.tags != null) { for (Map.Entry entry : this.tags.entrySet()) { - buf.append(entry.getKey()).append("=").append(entry.getValue()); + buf.append(" ").append(entry.getKey()).append("=").append(entry.getValue()); } } return buf.toString(); From 56d2cd5b0dbeee693e495e9353e0fdff1155d41d Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Tue, 27 Oct 2015 15:48:46 +0800 Subject: [PATCH 322/826] make OOM handler be able to handle multiple instances of opentsdb server Signed-off-by: Chris Larsen --- tools/opentsdb_restart.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index eaad7537f6..31425750a8 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -8,8 +8,11 @@ import os import subprocess +service_name = "opentsdb" +if 'NAME' in os.environ: + service_name = os.environ['NAME'] -subprocess.call(["service", "opentsdb", "stop"]) +subprocess.call(["service", service_name, "stop"]) # Close any file handles we inherited from our parent JVM. We need # to do this before restarting so that the socket isn't held open. openfiles = [int(f) for f in os.listdir("/proc/self/fd")] @@ -17,4 +20,4 @@ # that there is less chance of errors with those standard streams. # Other files start at fd 3. os.closerange(3, max(openfiles)) -subprocess.call(["service", "opentsdb", "start"]) +subprocess.call(["service", service_name, "start"]) From 37b1705f64db6a800b7c5a76d80290b3d8d942bd Mon Sep 17 00:00:00 2001 From: Johannes Meixner Date: Mon, 9 Nov 2015 20:36:05 +0200 Subject: [PATCH 323/826] Fix staging and build on FreeBSD Make sure that the filesystem outside of DESTDIR isn't touched, hence set java.util.prefs.userRoot to $(HOME) and make sure etc/ config files go to $(sysconfdir)/etc/opentsdb instead of $(pkgdatadir)/etc/opentsdb. Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 7c9e486d90..0f299fd7e3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -354,7 +354,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(pkgdatadir)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp @@ -411,7 +411,7 @@ gwtc: .gwtc-stamp @$(mkdir_p) gwt { cd $(srcdir) && cat $(httpui_SRC); } | $(MD5) >"$@-t" cmp -s "$@" "$@-t" && exit 0; \ - $(JAVA) $(GWTC_JVM_ARGS) -cp $(GWT_CLASSPATH) com.google.gwt.dev.Compiler \ + $(JAVA) -Djava.util.prefs.userRoot=$(HOME) $(GWTC_JVM_ARGS) -cp $(GWT_CLASSPATH) com.google.gwt.dev.Compiler \ $(GWTC_ARGS) -war gwt tsd.QueryUi @mv "$@-t" "$@" From fd32bed6f17f24c987fcae9255f5750a84e280de Mon Sep 17 00:00:00 2001 From: Johannes Meixner Date: Mon, 9 Nov 2015 20:36:05 +0200 Subject: [PATCH 324/826] Fix staging and build on FreeBSD Make sure that the filesystem outside of DESTDIR isn't touched, hence set java.util.prefs.userRoot to $(HOME) and make sure etc/ config files go to $(sysconfdir)/etc/opentsdb instead of $(pkgdatadir)/etc/opentsdb. Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index edcaececad..d829bbfd13 100644 --- a/Makefile.am +++ b/Makefile.am @@ -441,7 +441,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(pkgdatadir)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp @@ -501,7 +501,7 @@ gwtc: .gwtc-stamp @$(mkdir_p) gwt { cd $(srcdir) && cat $(httpui_SRC); } | $(MD5) >"$@-t" cmp -s "$@" "$@-t" && exit 0; \ - $(JAVA) $(GWTC_JVM_ARGS) -cp $(GWT_CLASSPATH) com.google.gwt.dev.Compiler \ + $(JAVA) -Djava.util.prefs.userRoot=$(HOME) $(GWTC_JVM_ARGS) -cp $(GWT_CLASSPATH) com.google.gwt.dev.Compiler \ $(GWTC_ARGS) -war gwt tsd.QueryUi @mv "$@-t" "$@" From e6273f79fafaf6fb4d941787d9020cbf50a27768 Mon Sep 17 00:00:00 2001 From: Jim Westfall Date: Mon, 19 Oct 2015 17:05:58 -0700 Subject: [PATCH 325/826] QueryUi: URL.decode() url/query string before use Some browsers (aka firefox) like to encode { and } as %7B and %7D. This causes problem when parsing the query string since its using { and } to figure out the metric and tags. Without this the UI thows an error like the following: Request failed: Bad Request: No such name for 'metrics': 'server.nic.usage.mbit%7Bhost=host1%7D' Signed-off-by: Chris Larsen --- src/tsd/client/QueryUi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 25de12556c..919357f072 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -743,7 +743,7 @@ private static QueryString getQueryString(final String qs) { } private void refreshFromQueryString() { - final QueryString qs = getQueryString(History.getToken()); + final QueryString qs = getQueryString(URL.decode(History.getToken())); maybeSetTextbox(qs, "start", start_datebox.getTextBox()); maybeSetTextbox(qs, "end", end_datebox.getTextBox()); @@ -909,7 +909,7 @@ public void got(final JSONValue json) { if (autoreload.getValue()) { history += "&autoreload=" + autoreoload_interval.getText(); } - if (!history.equals(History.getToken())) { + if (!history.equals(URL.decode(History.getToken()))) { History.newItem(history, false); } From 9d07ff766373d50950a96f9141ea7595ff40e088 Mon Sep 17 00:00:00 2001 From: Hong Dai Thanh Date: Fri, 30 Oct 2015 16:37:46 +0700 Subject: [PATCH 326/826] Fix typo TUSID --> TSUID Signed-off-by: Chris Larsen --- src/core/Query.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Query.java b/src/core/Query.java index 01e08b969c..c33983f486 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -105,7 +105,7 @@ void setTimeSeries(String metric, Map tags, * 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 TUSIDs + * 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 @@ -125,7 +125,7 @@ public void setTimeSeries(final List tsuids, * 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 TUSIDs + * 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 From 6956da7513a9ada9fd35e5bd71c51dbf35865944 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 27 Oct 2015 16:46:00 -0700 Subject: [PATCH 327/826] Fix test dependencies where DeleteRequest was pulled from Zookeeper instead of AsyncHBase. Signed-off-by: Chris Larsen --- test/tools/TestDumpSeries.java | 2 +- test/tools/TestTextImporter.java | 2 +- test/tools/TestUID.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/tools/TestDumpSeries.java b/test/tools/TestDumpSeries.java index d6366378fa..6c17ba02ac 100644 --- a/test/tools/TestDumpSeries.java +++ b/test/tools/TestDumpSeries.java @@ -31,8 +31,8 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; diff --git a/test/tools/TestTextImporter.java b/test/tools/TestTextImporter.java index bf703a4578..25709983e8 100644 --- a/test/tools/TestTextImporter.java +++ b/test/tools/TestTextImporter.java @@ -37,8 +37,8 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index 2da55c3303..5ed4f386ad 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -23,8 +23,8 @@ import net.opentsdb.storage.MockBase; import net.opentsdb.utils.Config; -import org.apache.zookeeper.proto.DeleteRequest; import org.hbase.async.Bytes; +import org.hbase.async.DeleteRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; From e1b125b8fe0cfe3a02c1ae191c53d0a05953fa6f Mon Sep 17 00:00:00 2001 From: Johannes Meixner Date: Mon, 9 Nov 2015 20:36:05 +0200 Subject: [PATCH 328/826] Fix staging and build on FreeBSD Make sure that the filesystem outside of DESTDIR isn't touched, hence set java.util.prefs.userRoot to $(HOME) and make sure etc/ config files go to $(sysconfdir)/etc/opentsdb instead of $(pkgdatadir)/etc/opentsdb. Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 41db9f8dda..20f9509a46 100644 --- a/Makefile.am +++ b/Makefile.am @@ -293,7 +293,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(pkgdatadir)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp @@ -350,7 +350,7 @@ gwtc: .gwtc-stamp @$(mkdir_p) gwt { cd $(srcdir) && cat $(httpui_SRC); } | $(MD5) >"$@-t" cmp -s "$@" "$@-t" && exit 0; \ - $(JAVA) $(GWTC_JVM_ARGS) -cp $(GWT_CLASSPATH) com.google.gwt.dev.Compiler \ + $(JAVA) -Djava.util.prefs.userRoot=$(HOME) $(GWTC_JVM_ARGS) -cp $(GWT_CLASSPATH) com.google.gwt.dev.Compiler \ $(GWTC_ARGS) -war gwt tsd.QueryUi @mv "$@-t" "$@" From 5bff570a04156706bb8f0ec86124a5d669480da0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 9 Oct 2015 17:38:44 +0800 Subject: [PATCH 329/826] Release 2.1.2. Thanks for the bug fixes! Signed-off-by: Chris Larsen --- NEWS | 9 +++++++++ THANKS | 4 ++++ configure.ac | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 8013e6daf1..3948ef925a 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,14 @@ OpenTSDB - User visible changes. +* Version 2.1.2 (2015-11-09) + +Bug Fixes: + - Fix the built-in UI to handle query parameter parsing properly (found when Firefox + changed their URI behavior) + - Fix comments about the Zookeeper quorum setting in various config files. + - Fix quoting in the Makefile when installing. + - Make sure builds write files in the proper location on FreeBSD. + * Version 2.1.1 (2015-09-12) Bug Fixes: diff --git a/THANKS b/THANKS index c53931ddea..e4d3c6071c 100644 --- a/THANKS +++ b/THANKS @@ -21,12 +21,15 @@ Christophe Furmaniak Dave Barr Filippo Giunchedi Guenther Schmuelling +Hong Dai Thanh Hugo Trippaers Jacek Masiulaniec Jari Takkala Jan Mangs Jesse Chang +Jim Westfall Johan Zeeck +Johannes Meixner Jonathan Works Josh Thomas Kieren Hynd @@ -34,6 +37,7 @@ Kimoon Kim Kris Beevers Lex Herbert Liangliang He +Lou Yunlong Matt Jibson Mark Smith Martin Jansen diff --git a/configure.ac b/configure.ac index 082b2d07b9..36e407eb9f 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.1.1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.1.2], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From e2521391c8f7824018d4fdbc7f28edd7611258b9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 9 Oct 2015 17:38:44 +0800 Subject: [PATCH 330/826] Release 2.2.0RC2. Thanks for the bug fixes! Signed-off-by: Chris Larsen --- NEWS | 23 +++++++++++++++++++++++ THANKS | 4 ++++ configure.ac | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index ac24f3fd81..04393452ea 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,19 @@ OpenTSDB - User visible changes. +* Version 2.2.0 RC2 (2015-11-09) + +Noteworthy Changes: + - Allow overriding the metric and tag UID widths via config file instead of + having to modify the source code. + +Bug Fixes: + - OOM handling script now handles multiple TSDs installed on the same host. + - Fix a bug where queries never return if an exception is thrown from the + storage layer. + - Fix random metric UID assignment in the CLI tool. + - Fix for meta data sync when salting is enabled. + - + * Version 2.2.0 RC1 (2015-09-12) Noteworthy Changes: @@ -49,6 +63,15 @@ Bug Fixes: - Avoid OOM issues over Telnet when the sending client isn't reading errors off it's socket fast enough by blocking writes. +* Version 2.1.2 (2015-11-09) + +Bug Fixes: + - Fix the built-in UI to handle query parameter parsing properly (found when Firefox + changed their URI behavior) + - Fix comments about the Zookeeper quorum setting in various config files. + - Fix quoting in the Makefile when installing. + - Make sure builds write files in the proper location on FreeBSD. + * Version 2.1.1 (2015-09-12) Bug Fixes: diff --git a/THANKS b/THANKS index 33898d5163..4761912619 100644 --- a/THANKS +++ b/THANKS @@ -25,6 +25,7 @@ Dave Barr Filippo Giunchedi Gabriel Nicolas Avellaneda Guenther Schmuelling +Hong Dai Thanh Hugo Trippaers Jacek Masiulaniec Jari Takkala @@ -33,7 +34,9 @@ Jan Mangs Jason Harvey Jim Scott Jesse Chang +Jim Westfall Johan Zeeck +Johannes Meixner Jonathan Works Josh Thomas Kieren Hynd @@ -42,6 +45,7 @@ Kris Beevers Lex Herbert Liangliang He Loïs Burg +Lou Yunlong Matt Jibson Matt Schallert Marc Tamsky diff --git a/configure.ac b/configure.ac index b9d6c136f9..41c5cac7d3 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.2.0RC2-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.0RC2], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 755f0768a7e6aa004dbc6ea1dee012c7bbd9bcf9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 9 Nov 2015 11:38:43 -0800 Subject: [PATCH 331/826] Fix a typo regarding TestTimeSpan.java class in the Makefile. Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index d829bbfd13..b525dce11b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -291,7 +291,7 @@ test_SRC := \ test/query/pojo/TestMetric.java \ test/query/pojo/TestOutput.java \ test/query/pojo/TestQuery.java \ - test/query/pojo/TestTimespan.java \ + test/query/pojo/TestTimeSpan.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ From bd9f13609c900a1161c8fda04f8c07f64d66b61d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 11 Nov 2015 12:43:42 -0800 Subject: [PATCH 332/826] Release 2.1.3 to fix the static file bug copying introduced in 8dcd77d8907e3a0eaebde0eede81b2f76150dfd7 Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- NEWS | 5 +++++ configure.ac | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index 20f9509a46..5d13365f50 100644 --- a/Makefile.am +++ b/Makefile.am @@ -389,8 +389,8 @@ install-data-local: staticroot install-data-lib install-data-tools \ install-data-bin install-data-etc @$(NORMAL_INSTALL) test -z "$(staticdir)" || $(mkdir_p) "$(DESTDIR)$(staticdir)" - @set -e; pwd; ls -lFh; (cd "$(DEV_TSD_STATICROOT)"; \ - list=`find -L . ! -type d`); for p in $$list; do \ + @set -e; pwd; ls -lFh; cd "$(DEV_TSD_STATICROOT)"; \ + list=`find -L . ! -type d`; for p in $$list; do \ p=$${p#./}; \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ diff --git a/NEWS b/NEWS index 3948ef925a..029a4d509a 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,10 @@ OpenTSDB - User visible changes. +* Version 2.1.3 (2015-11-11) + +Bug Fixes: + - Fix build issues where the static files were not copied into the proper location. + * Version 2.1.2 (2015-11-09) Bug Fixes: diff --git a/configure.ac b/configure.ac index 36e407eb9f..e953a43527 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.1.2], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.1.3], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 6d2e8dd754464c5c97446b4eae2efdc3997fc490 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 11 Nov 2015 12:57:36 -0800 Subject: [PATCH 333/826] Release 2.2.0RC3 to fix the static file bug copying introduced in 8dcd77d --- Makefile.am | 4 ++-- NEWS | 11 +++++++++++ configure.ac | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index 0f299fd7e3..d5e8334f10 100644 --- a/Makefile.am +++ b/Makefile.am @@ -450,8 +450,8 @@ install-data-local: staticroot install-data-lib install-data-tools \ install-data-bin install-data-etc @$(NORMAL_INSTALL) test -z "$(staticdir)" || $(mkdir_p) "$(DESTDIR)$(staticdir)" - @set -e; pwd; ls -lFh; (cd "$(DEV_TSD_STATICROOT)"; \ - list=`find -L . ! -type d`); for p in $$list; do \ + @set -e; pwd; ls -lFh; cd "$(DEV_TSD_STATICROOT)"; \ + list=`find -L . ! -type d`; for p in $$list; do \ p=$${p#./}; \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ diff --git a/NEWS b/NEWS index 04393452ea..1a7515e1e9 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,10 @@ OpenTSDB - User visible changes. +* Version 2.2.0 RC3 (2015-11-11) + +Bug Fixes: + - Fix build issues where the static files were not copied into the proper location. + * Version 2.2.0 RC2 (2015-11-09) Noteworthy Changes: @@ -63,6 +68,12 @@ Bug Fixes: - Avoid OOM issues over Telnet when the sending client isn't reading errors off it's socket fast enough by blocking writes. +* Version 2.1.3 (2015-11-11) + +Bug Fixes: + + - Fix build issues where the static files were not copied into the proper location. + * Version 2.1.2 (2015-11-09) Bug Fixes: diff --git a/configure.ac b/configure.ac index 41c5cac7d3..ad2b2b4893 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.2.0RC2], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.0RC3], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 65289cd7c621a6c21472c93a1fef6681f5be4951 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 11 Nov 2015 14:27:44 -0800 Subject: [PATCH 334/826] Make sure the JavaCC parser is compiled when creating the dist tarball or packages. Signed-off-by: Chris Larsen --- Makefile.am | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Makefile.am b/Makefile.am index b525dce11b..e1567acf92 100644 --- a/Makefile.am +++ b/Makefile.am @@ -387,14 +387,15 @@ httpui_SRC := \ httpui_DEPS = src/tsd/QueryUi.gwt.xml # TODO(CL) - There is likely a MUCH better way to compile and add the expression sources and jars. +expr_grammar = $(srcdir)/src/parser.jj expr_package = net/opentsdb/query/expression/parser -expr_src_dir = $(abs_builddir)/src/$(expr_package) +expr_src_dir = $(builddir)/src/$(expr_package) get_expr_classes = `classes=''; for f in $(packagedir)$(expr_package)/*.class; do classes="$$classes $$f"; done; echo $$classes;` #dist_pkgdata_DATA = src/logback.xml dist_static_DATA = src/tsd/static/favicon.ico -EXTRA_DIST = tsdb.in $(tsdb_SRC) $(test_SRC) \ +EXTRA_DIST = tsdb.in $(tsdb_SRC) $(test_SRC) $(expr_grammar) \ $(test_plugin_SRC) $(test_plugin_MF) $(test_plugin_SVCS:%=test/%) \ $(THIRD_PARTY) $(THIRD_PARTY:=.md5) \ $(httpui_SRC) $(httpui_DEPS) \ @@ -449,7 +450,7 @@ install-exec-hook: $(builddata_SRC): .git/HEAD $(tsdb_SRC) $(top_srcdir)/build-aux/gen_build_data.sh $(srcdir)/build-aux/gen_build_data.sh $(builddata_SRC) $(package).$(builddata_subpackage) $(PACKAGE_VERSION) -jar: runjavacc $(jar) .javac-unittests-stamp .gwtc-stamp +jar: $(jar) .javac-unittests-stamp .gwtc-stamp JAVA_COMPILE := $(JAVAC) $(AM_JAVACFLAGS) -d . @@ -483,9 +484,9 @@ $(tsdb_SRC): $(tsdb_DEPS) find_jar = test -f "$$jar" && echo "$$jar" || echo "$(srcdir)/$$jar" get_dep_classpath = `for jar in $(tsdb_DEPS); do $(find_jar); done | tr '\n' ':'` -.javac-stamp: $(tsdb_SRC) $(builddata_SRC) +.javac-stamp: $(tsdb_SRC) $(builddata_SRC) runjavacc @$(filter_src); cp=$(get_dep_classpath); \ - echo "DO THA COMPILE!!! $(JAVA_COMPILE) -cp $$cp $$src"; \ + echo "$(JAVA_COMPILE) -cp $$cp $$src"; \ $(JAVA_COMPILE) -cp $$cp $$src @touch "$@" @@ -725,7 +726,7 @@ $(JAVADOC_DIR)/index.html: $(tsdb_SRC) $? $(builddata_SRC) runjavacc: - $(JAVA) -cp $(JAVACC) javacc -OUTPUT_DIRECTORY:$(expr_src_dir) $(abs_srcdir)/src/parser.jj; echo PWD: `pwd`; + $(JAVA) -cp $(JAVACC) javacc -OUTPUT_DIRECTORY:$(expr_src_dir) $(expr_grammar); echo PWD: `pwd`; dist-hook: $(mkdir_p) $(distdir)/.git From 4d2fb7c44f1bb99c5cba8e097c5b31335f455804 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 11 Nov 2015 15:13:15 -0800 Subject: [PATCH 335/826] Rollback to fix the static file bug copying introduced in 8dcd77d --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index e1567acf92..521e0d238e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -541,8 +541,8 @@ install-data-local: staticroot install-data-lib install-data-tools \ install-data-bin install-data-etc @$(NORMAL_INSTALL) test -z "$(staticdir)" || $(mkdir_p) "$(DESTDIR)$(staticdir)" - @set -e; pwd; ls -lFh; (cd "$(DEV_TSD_STATICROOT)"; \ - list=`find -L . ! -type d`); for p in $$list; do \ + @set -e; pwd; ls -lFh; cd "$(DEV_TSD_STATICROOT)"; \ + list=`find -L . ! -type d`; for p in $$list; do \ p=$${p#./}; \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ From 9aa14a37700e7f9b5a7d7b50fdc51653f89c5889 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 13 Nov 2015 12:36:01 -0800 Subject: [PATCH 336/826] Add the EDPtoDPS class for converting from an ExpressionDataPoint to a DataPoints object. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/query/expression/EDPtoDPS.java | 246 +++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 src/query/expression/EDPtoDPS.java diff --git a/Makefile.am b/Makefile.am index 521e0d238e..c68f7186be 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,6 +75,7 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ + src/query/expression/EDPtoDPS.java \ src/query/expression/Expression.java \ src/query/expression/ExpressionDataPoint.java \ src/query/expression/ExpressionFactory.java \ diff --git a/src/query/expression/EDPtoDPS.java b/src/query/expression/EDPtoDPS.java new file mode 100644 index 0000000000..4c17ff70e6 --- /dev/null +++ b/src/query/expression/EDPtoDPS.java @@ -0,0 +1,246 @@ +// 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.Collections; +import java.util.List; +import java.util.Map; + +import org.hbase.async.Bytes.ByteMap; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.meta.Annotation; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.ByteSet; + +/** + * An ugly temporary class for converting from an expression datapoint to a + * standard data point for serialization in the default query format. + */ +public class EDPtoDPS implements DataPoints { + /** The TSDB used for UID to name lookups */ + private final TSDB tsdb; + + /** The index of this data point in the iterator */ + private final int index; + + /** The iterator that contains the results for this data point */ + private final ExpressionIterator iterator; + + /** The list of data points from the iterator from which we read */ + private final ExpressionDataPoint[] edps; + + /** + * Default ctor + * @param tsdb The TSDB used for UID to name lookups + * @param index The index of this data point in the iterator + * @param iterator The iterator that contains the results for this data point + */ + public EDPtoDPS(final TSDB tsdb, final int index, + final ExpressionIterator iterator) { + this.tsdb = tsdb; + this.index = index; + this.iterator = iterator; + edps = iterator.values(); + } + + @Override + public String metricName() { + try { + return metricNameAsync().joinUninterruptibly(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred metricNameAsync() { + if (edps[index].metricUIDs() == null) { + throw new IllegalStateException("Iterator UID was null for index " + + index + " and iterator " + iterator); + } + final byte[] uid = edps[index].metricUIDs().iterator().next(); + return tsdb.getUidName(UniqueIdType.METRIC, uid); + } + + @Override + public byte[] metricUID() { + if (edps[index].metricUIDs() == null) { + throw new IllegalStateException("Iterator UID was null for index " + + index + " and iterator " + iterator); + } + return edps[index].metricUIDs().iterator().next(); + } + + @Override + public Map getTags() { + try { + return getTagsAsync().joinUninterruptibly(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred> getTagsAsync() { + return Tags.getTagsAsync(tsdb, edps[index].tags()); + } + + @Override + public ByteMap getTagUids() { + return edps[index].tags(); + } + + @Override + public List getAggregatedTags() { + try { + return getAggregatedTagsAsync().joinUninterruptibly(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred> getAggregatedTagsAsync() { + final ByteSet tagks = edps[index].aggregatedTags(); + final List aggregated_tags = new ArrayList(tagks.size()); + + final List> names = + new ArrayList>(tagks.size()); + for (final byte[] tagk : tagks) { + names.add(tsdb.getUidName(UniqueIdType.TAGK, tagk)); + } + + /** Adds the names to the aggregated_tags list */ + final class ResolveCB implements Callback, ArrayList> { + @Override + public List call(final ArrayList names) throws Exception { + for (final String name : names) { + aggregated_tags.add(name); + } + return aggregated_tags; + } + } + + return Deferred.group(names).addCallback(new ResolveCB()); + } + + @Override + public List getAggregatedTagUids() { + final List agg_tags = new ArrayList( + edps[index].aggregatedTags()); + return agg_tags; + } + + @Override + public List getTSUIDs() { + // TODO Fix it up + return Collections.emptyList(); + } + + @Override + public List getAnnotations() { + // TODO Fix it up + return Collections.emptyList(); + } + + @Override + public int size() { + // TODO Estimate + return -1; + } + + @Override + public int aggregatedSize() { + // TODO Estimate + return -1; + } + + @Override + public SeekableView iterator() { + return new Iterator(); + } + + @Override + public long timestamp(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInteger(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public long longValue(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public double doubleValue(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public int getQueryIndex() { + // TODO Fix it up + return 0; + } + + /** + * Simple class that fills the local data point while iterating through the + * expression data points at the proper index. + */ + private class Iterator implements SeekableView { + /** A data pont to mutate as we iterate */ + final MutableDataPoint dp = new MutableDataPoint(); + + @Override + public boolean hasNext() { + return iterator.hasNext(index); + } + + @Override + public DataPoint next() { + iterator.next(index); + dp.reset(edps[index].timestamp(), edps[index].toDouble()); + return dp; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void seek(long timestamp) { + throw new UnsupportedOperationException(); + } + + } +} From ef3e911c548814100634eba452171ef0eaffa651 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 13 Nov 2015 12:38:32 -0800 Subject: [PATCH 337/826] Add the DivideSeries function. Also modify the ExpressionFactory to load some functions after the TSDB object has been initialized and call that load method in the TSDB ctor. Signed-off-by: Chris Larsen --- Makefile.am | 4 +- src/core/TSDB.java | 5 + src/query/expression/DivideSeries.java | 86 +++++++++ src/query/expression/ExpressionFactory.java | 11 ++ test/query/expression/TestDivideSeries.java | 197 ++++++++++++++++++++ 5 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 src/query/expression/DivideSeries.java create mode 100644 test/query/expression/TestDivideSeries.java diff --git a/Makefile.am b/Makefile.am index c68f7186be..58e6c3c405 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,7 +75,8 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ - src/query/expression/EDPtoDPS.java \ + src/query/expression/Expression.java \ + src/query/expression/DivideSeries.java \ src/query/expression/Expression.java \ src/query/expression/ExpressionDataPoint.java \ src/query/expression/ExpressionFactory.java \ @@ -265,6 +266,7 @@ test_SRC := \ test/meta/TestUIDMeta.java \ test/query/expression/BaseTimeSyncedIteratorTest.java \ test/query/expression/TestAbsolute.java \ + test/query/expression/TestDivideSeries.java \ test/query/expression/TestExpressionFactory.java \ test/query/expression/TestExpressionIterator.java \ test/query/expression/TestExpressionReader.java \ diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 64cf75c779..900f038128 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -54,6 +54,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; +import net.opentsdb.query.expression.ExpressionFactory; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.search.SearchPlugin; import net.opentsdb.search.SearchQuery; @@ -208,6 +209,10 @@ public TSDB(final HBaseClient client, final Config config) { uid_cache_map.put(TAG_VALUE_QUAL.getBytes(CHARSET), tag_values); UniqueId.preloadUidCache(this, uid_cache_map); } + + // load up the functions that require the TSDB object + ExpressionFactory.addTSDBFunctions(this); + LOG.debug(config.dumpConfiguration()); } diff --git a/src/query/expression/DivideSeries.java b/src/query/expression/DivideSeries.java new file mode 100644 index 0000000000..cda975cd3f --- /dev/null +++ b/src/query/expression/DivideSeries.java @@ -0,0 +1,86 @@ +// 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.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +/** + * Performs a UNION set join on up to 26 metric query results and returns the + * quotient. + */ +public class DivideSeries implements Expression { + /** The TSDB used for UID to name lookups */ + final TSDB tsdb; + + /** + * Default ctor. + * @param tsdb The TSDB used for UID to name lookups + */ + public DivideSeries(final TSDB tsdb) { + this.tsdb = tsdb; + } + + @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[]{}; + } + + if (query_results.size() < 2 || query_results.size() > 26) { + throw new IllegalArgumentException("Must have 2 to 26 series, got " + + query_results.size() + " instead"); + } + + final StringBuilder buf = new StringBuilder(); + char v = 'a'; + for (int i = 0; i < query_results.size(); i++) { + buf.append(v++); + if (i < query_results.size() - 1) { + buf.append(" / "); + } + } + + final ExpressionIterator expression = new ExpressionIterator("divideSeries", + buf.toString(), SetOperator.UNION, false, false); + v = 'a'; + + for (final DataPoints[] dps : query_results) { + final TimeSyncedIterator it = new TimeSyncedIterator( + Character.toString(v++), null, dps); + expression.addResults(it.getId(), it); + } + expression.compile(); + + final DataPoints[] results = new DataPoints[expression.values().length]; + for (int i = 0; i < expression.values().length; i++) { + results[i] = new EDPtoDPS(tsdb, i, expression); + } + return results; + } + + @Override + public String writeStringField(final List query_params, + final String inner_expression) { + return "divideSeries(" + inner_expression + ")"; + } + +} diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index 3c61528dea..31b876b525 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -15,6 +15,8 @@ import java.util.HashMap; import java.util.Map; +import net.opentsdb.core.TSDB; + /** * A static class that stores and instantiates a static map of the available * functions. @@ -37,6 +39,15 @@ public final class ExpressionFactory { /** Don't instantiate me! */ private ExpressionFactory() { } + /** + * Adds more functions to the map that depend on an instantiated TSDB object. + * Only call this once please. + * @param tsdb The TSDB object to initialize with + */ + public static void addTSDBFunctions(final TSDB tsdb) { + available_functions.put("divideSeries", new DivideSeries(tsdb)); + } + /** * Add an expression to the map. * WARNING: The map is not thread safe so don't use this to dynamically diff --git a/test/query/expression/TestDivideSeries.java b/test/query/expression/TestDivideSeries.java new file mode 100644 index 0000000000..e880a6c31e --- /dev/null +++ b/test/query/expression/TestDivideSeries.java @@ -0,0 +1,197 @@ +// 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.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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class }) +public class TestDivideSeries extends BaseTimeSyncedIteratorTest { + + private static long START_TIME = 1356998400000L; + private static int INTERVAL = 60000; + private static int NUM_POINTS = 5; + + private TSQuery data_query; + private SeekableView view; + private DataPoints dps; + private DataPoints[] group_bys; + private List query_results; + private List params; + private DivideSeries func; + + @Before + public void beforeLocal() 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.metricName()).thenReturn(METRIC_STRING); + when(dps.metricUID()).thenReturn(new byte[] {0,0,1}); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new DivideSeries(tsdb); + } + + @Test + public void divideOneSeriesEach() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC_STRING, results[0].metricName()); + + double[] vals= new double[] { 0.1, 0.181, 0.25, 0.307, 0.357 }; + + long ts = START_TIME; + int i = 0; + for (final DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertEquals(vals[i++], dp.toDouble(), 0.001); + ts += INTERVAL; + } + } + + @Test + public void divideMultipleSeriesEach() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + query_results.clear(); + query_results.add(results.get("1").getValue()); + query_results.add(results.get("0").getValue()); + final DataPoints[] results = func.evaluate(data_query, + query_results, params); + + assertEquals(3, results.length); + + final int vals[][] = new int[2][]; + vals[0] = new int[] { 11, 1 }; + vals[1] = new int[] { 14, 4 }; + for (int i = 0; i < results.length; i++) { + long ts = 1431561600000l; + final SeekableView it = results[i].iterator(); + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + if (i < 2) { + assertEquals(((double)vals[i][0]++ / (double)vals[i][1]++), + dp.toDouble(), 0.001); + } else { + assertEquals(0, dp.toDouble(), 0.0001); + } + ts += INTERVAL; + } + } + } + + @Test (expected = IllegalArgumentException.class) + public void divideOneResultSet() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void divideTooManyResultSets() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + query_results.add(group_bys); + // doesn't matter what they are + for (int i = 0; i < 100; i++) { + query_results.add(group_bys); + } + + func.evaluate(data_query, query_results, params); + } + + @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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 writeStringField() throws Exception { + params.add("1"); + assertEquals("divideSeries(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("divideSeries(null)", func.writeStringField(params, null)); + assertEquals("divideSeries()", func.writeStringField(params, "")); + assertEquals("divideSeries(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From 5a5da04eee2b5477cbc51e487c4edeb4e78fc27c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 13 Nov 2015 13:02:17 -0800 Subject: [PATCH 338/826] Add the SumSeries function for the Graphite endpoint. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/ExpressionFactory.java | 1 + src/query/expression/SumSeries.java | 85 +++++++++ test/query/expression/TestSumSeries.java | 195 ++++++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 src/query/expression/SumSeries.java create mode 100644 test/query/expression/TestSumSeries.java diff --git a/Makefile.am b/Makefile.am index 58e6c3c405..09db54d3c0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -92,6 +92,7 @@ tsdb_SRC := \ src/query/expression/MovingAverage.java \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ + src/query/expression/SumSeries.java \ src/query/expression/TimeSyncedIterator.java \ src/query/expression/UnionIterator.java \ src/query/expression/VariableIterator.java \ @@ -279,6 +280,7 @@ test_SRC := \ test/query/expression/TestMovingAverage.java \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/expression/TestScale.java \ + test/query/expression/TestSumSeries.java \ test/query/expression/TestTimeSyncedIterator.java \ test/query/expression/TestUnionIterator.java \ test/query/filter/TestTagVFilter.java \ diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index 31b876b525..49585bae62 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -46,6 +46,7 @@ private ExpressionFactory() { } */ public static void addTSDBFunctions(final TSDB tsdb) { available_functions.put("divideSeries", new DivideSeries(tsdb)); + available_functions.put("sumSeries", new SumSeries(tsdb)); } /** diff --git a/src/query/expression/SumSeries.java b/src/query/expression/SumSeries.java new file mode 100644 index 0000000000..3241901bcc --- /dev/null +++ b/src/query/expression/SumSeries.java @@ -0,0 +1,85 @@ +// 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.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +/** + * Performs a UNION set join on x metric query results and returns the results. + */ +public class SumSeries implements Expression { + /** The TSDB used for UID to name lookups */ + final TSDB tsdb; + + /** + * Default ctor. + * @param tsdb The TSDB used for UID to name lookups + */ + public SumSeries(final TSDB tsdb) { + this.tsdb = tsdb; + } + + @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[]{}; + } + + if (query_results.size() < 2 || query_results.size() > 26) { + throw new IllegalArgumentException("Must have 2 to 26 series, got " + + query_results.size() + " instead"); + } + + final StringBuilder buf = new StringBuilder(); + char v = 'a'; + for (int i = 0; i < query_results.size(); i++) { + buf.append(v++); + if (i < query_results.size() - 1) { + buf.append(" + "); + } + } + System.out.println("Expression: [" + buf.toString() + "]"); + final ExpressionIterator expression = new ExpressionIterator("sumSeries", + buf.toString(), SetOperator.UNION, false, false); + v = 'a'; + + for (final DataPoints[] dps : query_results) { + final TimeSyncedIterator it = new TimeSyncedIterator( + Character.toString(v++), null, dps); + expression.addResults(it.getId(), it); + } + expression.compile(); + + final DataPoints[] results = new DataPoints[expression.values().length]; + for (int i = 0; i < expression.values().length; i++) { + results[i] = new EDPtoDPS(tsdb, i, expression); + } + return results; + } + + @Override + public String writeStringField(final List query_params, + final String inner_expression) { + return "sumSeries(" + inner_expression + ")"; + } + +} diff --git a/test/query/expression/TestSumSeries.java b/test/query/expression/TestSumSeries.java new file mode 100644 index 0000000000..77a22b56a3 --- /dev/null +++ b/test/query/expression/TestSumSeries.java @@ -0,0 +1,195 @@ +// 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.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.sun.java_cup.internal.runtime.Scanner; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class, Scanner.class }) +public class TestSumSeries extends BaseTimeSyncedIteratorTest { + + private static long START_TIME = 1356998400000L; + private static int INTERVAL = 60000; + private static int NUM_POINTS = 5; + + private TSQuery data_query; + private SeekableView view; + private DataPoints dps; + private DataPoints[] group_bys; + private List query_results; + private List params; + private SumSeries func; + + @Before + public void beforeLocal() 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.metricName()).thenReturn(METRIC_STRING); + when(dps.metricUID()).thenReturn(new byte[] {0,0,1}); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new SumSeries(tsdb); + } + + @Test + public void sumOneSeriesEach() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC_STRING, results[0].metricName()); + + long ts = START_TIME; + double v = 11; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertEquals(v, dp.toDouble(), 0.001); + v += 2; + ts += INTERVAL; + } + } + + @Test + public void sumMultipleSeriesEach() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + query_results.clear(); + query_results.add(results.get("1").getValue()); + query_results.add(results.get("0").getValue()); + final DataPoints[] results = func.evaluate(data_query, + query_results, params); + + assertEquals(3, results.length); + + final int vals[] = new int[] { 12, 18, 17 }; + for (int i = 0; i < results.length; i++) { + long ts = 1431561600000l; + final SeekableView it = results[i].iterator(); + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(vals[i], dp.toDouble(), 0.0001); + if (i < 2) { + vals[i] += 2; + } else { + vals[i]++; + } + ts += INTERVAL; + } + } + } + + @Test (expected = IllegalArgumentException.class) + public void sumOneResultSet() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void sumTooManyResultSets() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + // doesn't matter what they are + for (int i = 0; i < 100; i++) { + query_results.add(group_bys); + } + + func.evaluate(data_query, query_results, params); + } + + @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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 writeStringField() throws Exception { + params.add("1"); + assertEquals("sumSeries(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("sumSeries(null)", func.writeStringField(params, null)); + assertEquals("sumSeries()", func.writeStringField(params, "")); + assertEquals("sumSeries(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From d045bcf429de50d2a06a26b15c3f68f78aca85c9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 13 Nov 2015 13:15:25 -0800 Subject: [PATCH 339/826] Add the DiffSeries graphte function Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/DiffSeries.java | 85 +++++++++ src/query/expression/ExpressionFactory.java | 1 + test/query/expression/TestDiffSeries.java | 192 ++++++++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 src/query/expression/DiffSeries.java create mode 100644 test/query/expression/TestDiffSeries.java diff --git a/Makefile.am b/Makefile.am index 09db54d3c0..dd7a2925b8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -76,6 +76,7 @@ tsdb_SRC := \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ src/query/expression/Expression.java \ + src/query/expression/DiffSeries.java \ src/query/expression/DivideSeries.java \ src/query/expression/Expression.java \ src/query/expression/ExpressionDataPoint.java \ @@ -267,6 +268,7 @@ test_SRC := \ test/meta/TestUIDMeta.java \ test/query/expression/BaseTimeSyncedIteratorTest.java \ test/query/expression/TestAbsolute.java \ + test/query/expression/TestDiffSeries.java \ test/query/expression/TestDivideSeries.java \ test/query/expression/TestExpressionFactory.java \ test/query/expression/TestExpressionIterator.java \ diff --git a/src/query/expression/DiffSeries.java b/src/query/expression/DiffSeries.java new file mode 100644 index 0000000000..be8634b628 --- /dev/null +++ b/src/query/expression/DiffSeries.java @@ -0,0 +1,85 @@ +// 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.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +/** + * Performs a UNION set join on up to 26 metric query results and returns the + * difference. + */ +public class DiffSeries implements Expression { + /** The TSDB used for UID to name lookups */ + final TSDB tsdb; + + /** + * Default ctor. + * @param tsdb The TSDB used for UID to name lookups + */ + public DiffSeries(final TSDB tsdb) { + this.tsdb = tsdb; + } + + @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[]{}; + } + + if (query_results.size() < 2 || query_results.size() > 26) { + throw new IllegalArgumentException("Must have 2 to 26 series, got " + + query_results.size() + " instead"); + } + + final StringBuilder buf = new StringBuilder(); + char v = 'a'; + for (int i = 0; i < query_results.size(); i++) { + buf.append(v++); + if (i < query_results.size() - 1) { + buf.append(" - "); + } + } + + final ExpressionIterator expression = new ExpressionIterator("diffSeries", + buf.toString(), SetOperator.UNION, false, false); + v = 'a'; + for (final DataPoints[] dps : query_results) { + final TimeSyncedIterator it = new TimeSyncedIterator( + Character.toString(v++), null, dps); + expression.addResults(it.getId(), it); + } + expression.compile(); + + final DataPoints[] results = new DataPoints[expression.values().length]; + for (int i = 0; i < expression.values().length; i++) { + results[i] = new EDPtoDPS(tsdb, i, expression); + } + return results; + } + + @Override + public String writeStringField(final List query_params, + final String inner_expression) { + return "diffSeries(" + inner_expression + ")"; + } + +} diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index 49585bae62..432f1f72db 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -47,6 +47,7 @@ private ExpressionFactory() { } public static void addTSDBFunctions(final TSDB tsdb) { available_functions.put("divideSeries", new DivideSeries(tsdb)); available_functions.put("sumSeries", new SumSeries(tsdb)); + available_functions.put("diffSeries", new DiffSeries(tsdb)); } /** diff --git a/test/query/expression/TestDiffSeries.java b/test/query/expression/TestDiffSeries.java new file mode 100644 index 0000000000..3c36e234c1 --- /dev/null +++ b/test/query/expression/TestDiffSeries.java @@ -0,0 +1,192 @@ +// 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.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.sun.java_cup.internal.runtime.Scanner; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class, Scanner.class }) +public class TestDiffSeries extends BaseTimeSyncedIteratorTest { + + private static long START_TIME = 1356998400000L; + private static int INTERVAL = 60000; + private static int NUM_POINTS = 5; + + private TSQuery data_query; + private SeekableView view; + private DataPoints dps; + private DataPoints[] group_bys; + private List query_results; + private List params; + private DiffSeries func; + + @Before + public void beforeLocal() 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.metricName()).thenReturn(METRIC_STRING); + when(dps.metricUID()).thenReturn(new byte[] {0,0,1}); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new DiffSeries(tsdb); + } + + @Test + public void diffOneSeriesEach() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC_STRING, results[0].metricName()); + + long ts = START_TIME; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertEquals(-9, dp.toDouble(), 0.001); + ts += INTERVAL; + } + } + + @Test + public void diffMultipleSeriesEach() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + query_results.clear(); + query_results.add(results.get("1").getValue()); + query_results.add(results.get("0").getValue()); + final DataPoints[] results = func.evaluate(data_query, + query_results, params); + + assertEquals(3, results.length); + + double val = 17; + for (int i = 0; i < results.length; i++) { + long ts = 1431561600000l; + final SeekableView it = results[i].iterator(); + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + if (i < 2) { + assertEquals(10, dp.toDouble(), 0.0001); + } else { + assertEquals(val++, dp.toDouble(), 0.0001); + } + ts += INTERVAL; + } + } + } + + @Test (expected = IllegalArgumentException.class) + public void diffOneResultSet() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void diffTooManyResultSets() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + // doesn't matter what they are + for (int i = 0; i < 100; i++) { + query_results.add(group_bys); + } + + func.evaluate(data_query, query_results, params); + } + + @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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 writeStringField() throws Exception { + params.add("1"); + assertEquals("diffSeries(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("diffSeries(null)", func.writeStringField(params, null)); + assertEquals("diffSeries()", func.writeStringField(params, "")); + assertEquals("diffSeries(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From 74931051380142a8b272a4977d327be706713d2b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 13 Nov 2015 15:25:19 -0800 Subject: [PATCH 340/826] Add the MultiplySeries function for Graphite. Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/query/expression/ExpressionFactory.java | 1 + src/query/expression/MultiplySeries.java | 86 ++++++++ test/query/expression/TestMultiplySeries.java | 196 ++++++++++++++++++ 4 files changed, 285 insertions(+) create mode 100644 src/query/expression/MultiplySeries.java create mode 100644 test/query/expression/TestMultiplySeries.java diff --git a/Makefile.am b/Makefile.am index dd7a2925b8..20b57ea29c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -91,6 +91,7 @@ tsdb_SRC := \ src/query/expression/ITimeSyncedIterator.java \ src/query/expression/NumericFillPolicy.java \ src/query/expression/MovingAverage.java \ + src/query/expression/MultiplySeries.java \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ src/query/expression/SumSeries.java \ @@ -280,6 +281,7 @@ test_SRC := \ test/query/expression/TestIntersectionIterator.java \ test/query/expression/TestNumericFillPolicy.java \ test/query/expression/TestMovingAverage.java \ + test/query/expression/TestMultiplySeries.java \ test/query/expression/TestPostAggregatedDataPoints.java \ test/query/expression/TestScale.java \ test/query/expression/TestSumSeries.java \ diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index 432f1f72db..c2d9ecd246 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -48,6 +48,7 @@ public static void addTSDBFunctions(final TSDB tsdb) { available_functions.put("divideSeries", new DivideSeries(tsdb)); available_functions.put("sumSeries", new SumSeries(tsdb)); available_functions.put("diffSeries", new DiffSeries(tsdb)); + available_functions.put("multiplySeries", new MultiplySeries(tsdb)); } /** diff --git a/src/query/expression/MultiplySeries.java b/src/query/expression/MultiplySeries.java new file mode 100644 index 0000000000..34cbb251d8 --- /dev/null +++ b/src/query/expression/MultiplySeries.java @@ -0,0 +1,86 @@ +// 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.List; + +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.VariableIterator.SetOperator; + +/** + * Performs a UNION set join on up to 26 metric query results and returns the + * product. + */ +public class MultiplySeries implements Expression { + /** The TSDB used for UID to name lookups */ + final TSDB tsdb; + + /** + * Default ctor. + * @param tsdb The TSDB used for UID to name lookups + */ + public MultiplySeries(final TSDB tsdb) { + this.tsdb = tsdb; + } + + @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[]{}; + } + + if (query_results.size() < 2 || query_results.size() > 26) { + throw new IllegalArgumentException("Must have 2 to 26 series, got " + + query_results.size() + " instead"); + } + + final StringBuilder buf = new StringBuilder(); + char v = 'a'; + for (int i = 0; i < query_results.size(); i++) { + buf.append(v++); + if (i < query_results.size() - 1) { + buf.append(" * "); + } + } + + final ExpressionIterator expression = new ExpressionIterator("multiplySeries", + buf.toString(), SetOperator.UNION, false, false); + v = 'a'; + + for (final DataPoints[] dps : query_results) { + final TimeSyncedIterator it = new TimeSyncedIterator( + Character.toString(v++), null, dps); + expression.addResults(it.getId(), it); + } + expression.compile(); + + final DataPoints[] results = new DataPoints[expression.values().length]; + for (int i = 0; i < expression.values().length; i++) { + results[i] = new EDPtoDPS(tsdb, i, expression); + } + return results; + } + + @Override + public String writeStringField(final List query_params, + final String inner_expression) { + return "multiplySeries(" + inner_expression + ")"; + } + +} diff --git a/test/query/expression/TestMultiplySeries.java b/test/query/expression/TestMultiplySeries.java new file mode 100644 index 0000000000..292f975f47 --- /dev/null +++ b/test/query/expression/TestMultiplySeries.java @@ -0,0 +1,196 @@ +// 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.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.sun.java_cup.internal.runtime.Scanner; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSQuery.class, Scanner.class }) +public class TestMultiplySeries extends BaseTimeSyncedIteratorTest { + + private static long START_TIME = 1356998400000L; + private static int INTERVAL = 60000; + private static int NUM_POINTS = 5; + + private TSQuery data_query; + private SeekableView view; + private DataPoints dps; + private DataPoints[] group_bys; + private List query_results; + private List params; + private MultiplySeries func; + + @Before + public void beforeLocal() 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.metricName()).thenReturn(METRIC_STRING); + when(dps.metricUID()).thenReturn(new byte[] {0,0,1}); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new MultiplySeries(tsdb); + } + + @Test + public void multiplyOneSeriesEach() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + query_results.add(group_bys); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(1, results.length); + assertEquals(METRIC_STRING, results[0].metricName()); + final int[] vals = new int[] { 10, 22, 36, 52, 70 }; + int i = 0; + long ts = START_TIME; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertEquals(vals[i++], dp.toDouble(), 0.001); + ts += INTERVAL; + } + } + + @Test + public void multiplyMultipleSeriesEach() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + + query_results.clear(); + query_results.add(results.get("1").getValue()); + query_results.add(results.get("0").getValue()); + final DataPoints[] results = func.evaluate(data_query, + query_results, params); + + assertEquals(3, results.length); + + double[][] vals = new double[2][]; + vals[0] = new double[] { 11, 24, 39 }; + vals[1] = new double[] { 56, 75, 96 }; + for (int i = 0; i < results.length; i++) { + long ts = 1431561600000l; + final SeekableView it = results[i].iterator(); + int x = 0; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + if (i < 2) { + assertEquals(vals[i][x++], dp.toDouble(), 0.0001); + } else { + assertEquals(0, dp.toDouble(), 0.0001); + } + ts += INTERVAL; + } + } + } + + @Test (expected = IllegalArgumentException.class) + public void multiplyOneResultSet() throws Exception { + func.evaluate(data_query, query_results, params); + } + + @Test (expected = IllegalArgumentException.class) + public void multiplyTooManyResultSets() 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.metricName()).thenReturn("sys.mem"); + when(dps2.metricUID()).thenReturn(new byte[] {0,0,2}); + group_bys = new DataPoints[] { dps2 }; + // doesn't matter what they are + for (int i = 0; i < 100; i++) { + query_results.add(group_bys); + } + + func.evaluate(data_query, query_results, params); + } + + @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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + 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 writeStringField() throws Exception { + params.add("1"); + assertEquals("multiplySeries(inner_expression)", + func.writeStringField(params, "inner_expression")); + assertEquals("multiplySeries(null)", func.writeStringField(params, null)); + assertEquals("multiplySeries()", func.writeStringField(params, "")); + assertEquals("multiplySeries(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From 3a268f7cb6e7f5a5c91be00a9b76e85b58df8631 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 12 Nov 2015 13:38:55 -0800 Subject: [PATCH 341/826] Add single series iteration interfaces so we can use expressions but still output in the TSD 2.0 format where we display one series at a time instead of iterating over them all at once in lock-step. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionDataPoint.java | 12 ++++ src/query/expression/ExpressionIterator.java | 34 +++++++++++ src/query/expression/ITimeSyncedIterator.java | 16 ++++- .../expression/IntersectionIterator.java | 21 +++++++ src/query/expression/TimeSyncedIterator.java | 34 +++++++---- src/query/expression/UnionIterator.java | 43 ++++++++++++- src/query/expression/VariableIterator.java | 14 +++++ .../expression/TestExpressionIterator.java | 61 +++++++++++++------ 8 files changed, 202 insertions(+), 33 deletions(-) diff --git a/src/query/expression/ExpressionDataPoint.java b/src/query/expression/ExpressionDataPoint.java index 216546869f..ea85cc3652 100644 --- a/src/query/expression/ExpressionDataPoint.java +++ b/src/query/expression/ExpressionDataPoint.java @@ -56,6 +56,9 @@ public class ExpressionDataPoint implements DataPoint { /** The data point overwritten each time through the iterator */ private final MutableDataPoint dp; + /** An index in the original {@link TimeSyncedIterator} iterator array */ + private int index; + /** * Default ctor that simply sets up new objects for all internal fields. * TODO - lazily initialize the field to avoid unused objects @@ -224,4 +227,13 @@ public double toDouble() { return dp.toDouble(); } + /** @param index The index in the {@link TimeSyncedIterator} array */ + public void setIndex(final int index) { + this.index = index; + } + + /** @return the index in the {@link TimeSyncedIterator} array */ + public int getIndex() { + return index; + } } diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index c7d12c78ba..6684c60088 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -414,4 +414,38 @@ public ITimeSyncedIterator getCopy() { final ExpressionIterator ei = new ExpressionIterator(this); return ei; } + + @Override + public boolean hasNext(final int i) { + return iterator.hasNext(i); + } + + @Override + public void next(final int i) { + iterator.next(i); + + // set aside a couple of addresses for the variables + double val; + double result; + // this here is why life sucks. there MUST be a better way to bind variables + long ts = Long.MAX_VALUE; + for (final String variable : names) { + if (iteration_results.get(variable)[i] == null) { + context.set(variable, results.get(variable).getFillPolicy().getValue()); + } else { + if (iteration_results.get(variable)[i].timestamp() < ts) { + ts = iteration_results.get(variable)[i].timestamp(); + } + val = iteration_results.get(variable)[i].toDouble(); + if (Double.isNaN(val)) { + context.set(variable, results.get(variable).getFillPolicy().getValue()); + } else { + context.set(variable, val); + } + } + } + result = (Double)expression.execute(context); + dps[i].reset(ts, result); + } + } diff --git a/src/query/expression/ITimeSyncedIterator.java b/src/query/expression/ITimeSyncedIterator.java index 22efbc55ae..b72a0df401 100644 --- a/src/query/expression/ITimeSyncedIterator.java +++ b/src/query/expression/ITimeSyncedIterator.java @@ -32,11 +32,25 @@ public interface ITimeSyncedIterator { */ public ExpressionDataPoint[] next(final long timestamp); + /** + * Determines whether the individual series in the {@link values} array has + * another value. This may be used for non-synchronous iteration. + * @param index The index of the series in the values array to check for + * @return True if the series has another value, false if not + */ + public boolean hasNext(final int index); + + /** + * Fetches the next value for an individual series in the {@link values} array. + * @param index The index of the series in the values array to advance + */ + public void next(final int index); + /** * @return the next timestamp available in this set. */ public long nextTimestamp(); - + /** @return the number of series in this set */ public int size(); diff --git a/src/query/expression/IntersectionIterator.java b/src/query/expression/IntersectionIterator.java index 99e1e3ae7c..40d01068a0 100644 --- a/src/query/expression/IntersectionIterator.java +++ b/src/query/expression/IntersectionIterator.java @@ -497,4 +497,25 @@ public NumericFillPolicy getFillPolicy() { public ITimeSyncedIterator getCopy() { return new IntersectionIterator(this); } + + @Override + public boolean hasNext(int index) { + for (final ITimeSyncedIterator sub : queries.values()) { + if (sub.hasNext(index)) { + return true; + } + } + return false; + } + + @Override + public void next(int index) { + if (!hasNext()) { + throw new IllegalDataException("No more data"); + } + for (final ITimeSyncedIterator sub : queries.values()) { + sub.next(index); + } + } + } diff --git a/src/query/expression/TimeSyncedIterator.java b/src/query/expression/TimeSyncedIterator.java index 58d562b0cd..451e7b96c2 100644 --- a/src/query/expression/TimeSyncedIterator.java +++ b/src/query/expression/TimeSyncedIterator.java @@ -133,7 +133,11 @@ public ExpressionDataPoint[] next(final long timestamp) { emitter_values[i].reset(timestamp, fill_policy.getValue()); } else { emitter_values[i].reset(current_values[i]); - next(i); // move to the next value for this guy + if (!iterators[i].hasNext()) { + current_values[i] = null; + } else { + current_values[i] = iterators[i].next(); + } } } return emitter_values; @@ -152,20 +156,25 @@ public long nextTimestamp() { } return ts; } - - /** - * Moves the selected series to the next value. If the iterator has been - * nulled out (no more data) then this is a no-op. - * @param i The iterator index to advance - */ - private void next(final int i) { - if (!iterators[i].hasNext()) { + + @Override + public void next(final int i) { + if (current_values[i] == null) { + throw new RuntimeException("No more elements"); + } + emitter_values[i].reset(current_values[i]); + if (iterators[i].hasNext()) { + current_values[i] = iterators[i].next(); + } else { current_values[i] = null; - return; } - current_values[i] = iterators[i].next(); } - + + @Override + public boolean hasNext(final int i) { + return current_values[i] != null; + } + @Override public int getIndex() { return index; @@ -232,6 +241,7 @@ private void setupEmitters() { } else { current_values[i] = iterators[i].next(); emitter_values[i] = new ExpressionDataPoint(dps[i]); + emitter_values[i].setIndex(i); } } } diff --git a/src/query/expression/UnionIterator.java b/src/query/expression/UnionIterator.java index 0ed4514aa1..6453791c06 100644 --- a/src/query/expression/UnionIterator.java +++ b/src/query/expression/UnionIterator.java @@ -46,6 +46,9 @@ public class UnionIterator implements ITimeSyncedIterator, VariableIterator { /** A list of the current values for each series post intersection */ private final Map current_values; + /** A map used for single series iteration where the array is the index */ + private final Map single_series_matrix; + /** A map of the sub query index to their names for intersection computation */ private final String[] index_to_names; @@ -92,6 +95,7 @@ public UnionIterator(final String id, final Map res timestamp = Long.MAX_VALUE; queries = new HashMap(results.size()); current_values = new HashMap(results.size()); + single_series_matrix = new HashMap(results.size()); index_to_names = new String[results.size()]; fill_policy = new NumericFillPolicy(FillPolicy.ZERO); fill_dp = new ExpressionDataPoint(); @@ -134,6 +138,7 @@ private UnionIterator(final UnionIterator iterator) { timestamp = Long.MAX_VALUE; queries = new HashMap(iterator.queries.size()); current_values = new HashMap(queries.size()); + single_series_matrix = new HashMap(queries.size()); index_to_names = new String[queries.size()]; fill_policy = iterator.fill_policy; @@ -204,14 +209,25 @@ private void setCurrentAndMeta(final ByteMap ordered_union) { for (final String id : queries.keySet()) { current_values.put(id, new ExpressionDataPoint[ordered_union.size()]); + // TODO - blech. Fill with a sentinel value to reflect "no data here!" + final int[] m = new int[ordered_union.size()]; + for (int i = 0; i < m.length; i++) { + m[i] = -1; + } + single_series_matrix.put(id, m); } int i = 0; - for (final ExpressionDataPoint[] idps : ordered_union.values()) { + for (final Entry entry : ordered_union.entrySet()) { + final ExpressionDataPoint[] idps = entry.getValue(); for (int x = 0; x < idps.length; x++) { final ExpressionDataPoint[] current_dps = current_values.get(index_to_names[x]); current_dps[i] = idps[x]; + final int[] m = single_series_matrix.get(index_to_names[x]); + if (idps[x] != null) { + m[i] = idps[x].getIndex(); + } } ++i; } @@ -411,4 +427,29 @@ public Map getResults() { public int getSeriesSize() { return series_size; } + + @Override + public boolean hasNext(int index) { + for (final Entry entry : single_series_matrix.entrySet()) { + final int idx = entry.getValue()[index]; + if (idx >= 0 && queries.get(entry.getKey()).hasNext(idx)) { + return true; + } + } + return false; + } + + @Override + public void next(int index) { + if (!hasNext()) { + throw new IllegalDataException("No more data"); + } + for (final Entry entry : single_series_matrix.entrySet()) { + final int idx = entry.getValue()[index]; + if (idx >= 0) { + queries.get(entry.getKey()).next(idx); + } + } + } + } diff --git a/src/query/expression/VariableIterator.java b/src/query/expression/VariableIterator.java index 0ed7c6dab8..9bfba0b889 100644 --- a/src/query/expression/VariableIterator.java +++ b/src/query/expression/VariableIterator.java @@ -80,6 +80,20 @@ public static SetOperator fromString(final String name) { * call {@link hasNext()} first. */ public void next(); + + /** + * Determines whether the individual series in the {@link values} array has + * another value. This may be used for non-synchronous iteration. + * @param index The index of the series in the values array to check for + * @return True if the series has another value, false if not + */ + public boolean hasNext(final int index); + + /** + * Fetches the next value for an individual series in the {@link values} array. + * @param index The index of the series in the values array to advance + */ + public void next(final int index); /** * Returns a map of variable names to result series. You can maintain the diff --git a/test/query/expression/TestExpressionIterator.java b/test/query/expression/TestExpressionIterator.java index 9c98a5071c..cd90c88d31 100644 --- a/test/query/expression/TestExpressionIterator.java +++ b/test/query/expression/TestExpressionIterator.java @@ -1019,8 +1019,8 @@ public void unionNoIntersection() throws Exception { } @Test - public void scratch() throws Exception { - threeDifE(); + public void unionSingleSeriesIteration() throws Exception { + oneExtraSameE(); queryAB_Dstar(); remapResults(); @@ -1031,28 +1031,51 @@ public void scratch() throws Exception { exp.compile(); final ExpressionDataPoint[] dps = exp.values(); - assertEquals(6, dps.length); - //validateMeta(dps, true); + double[] values = new double[] { 12, 18, 17 }; - long ts = 1431561600000L; - double[] values = new double[] { 1, 11, 4, 14, 7, 17 }; - long its = exp.nextTimestamp(); - while (exp.hasNext()) { - exp.next(its); - for (int i = 0; i < dps.length; i++) { - System.out.println(dps[i].timestamp() + " " + dps[i].toDouble()); + for (int i = 0; i < dps.length; i++) { + long ts = 1431561600000L; + while (exp.hasNext(i)) { + exp.next(i); + assertEquals(ts, dps[i].timestamp()); + assertEquals(values[i], dps[i].toDouble(), 0.001); + + ts += 60000; + if (i < dps.length - 1) { + values[i] += 2; + } else { + values[i]++; + } } - - for (int i = 0; i < values.length; i++) { + } + } + + @Test + public void intersectionSingleSeriesIteration() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a + b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + double[] values = new double[] { 12, 18 }; + + for (int i = 0; i < dps.length; i++) { + long ts = 1431561600000L; + while (exp.hasNext(i)) { + exp.next(i); assertEquals(ts, dps[i].timestamp()); - assertEquals(values[i], dps[i].toDouble(), 0.0001); - ++values[i]; + assertEquals(values[i], dps[i].toDouble(), 0.001); + ts += 60000; + values[i] += 2; } - - ts += 60000; - its = exp.nextTimestamp(); } - validateMeta(dps, false); + } /** From 956d63b23bec6d737f842058b7f47c22b2108e8c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 12 Nov 2015 15:03:43 -0800 Subject: [PATCH 342/826] Null checks in the ExpressionDataPoint ctor for unit testing. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionDataPoint.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/query/expression/ExpressionDataPoint.java b/src/query/expression/ExpressionDataPoint.java index ea85cc3652..35c7807af1 100644 --- a/src/query/expression/ExpressionDataPoint.java +++ b/src/query/expression/ExpressionDataPoint.java @@ -79,10 +79,13 @@ public ExpressionDataPoint() { public ExpressionDataPoint(final DataPoints dps) { metric_uids = new ByteSet(); metric_uids.add(dps.metricUID()); - tags = (ByteMap) dps.getTagUids().clone(); + tags = dps.getTagUids() != null ? + (ByteMap) dps.getTagUids().clone() : new ByteMap(); aggregated_tags = new ByteSet(); - for (final byte[] tagk : dps.getAggregatedTagUids()) { - aggregated_tags.add(tagk); + if (dps.getAggregatedTagUids() != null) { + for (final byte[] tagk : dps.getAggregatedTagUids()) { + aggregated_tags.add(tagk); + } } tsuids = new HashSet(dps.getTSUIDs()); // TODO - restore when these are faster From bfa40b1bd8d5a02c9c6e57541aeb797d33742b1a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 15 Nov 2015 12:43:13 -0800 Subject: [PATCH 343/826] Add the EDPtoDPS.java class to the makefile (whoops!) Signed-off-by: Chris Larsen --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index 20b57ea29c..bcb4733c35 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,6 +78,7 @@ tsdb_SRC := \ src/query/expression/Expression.java \ src/query/expression/DiffSeries.java \ src/query/expression/DivideSeries.java \ + src/query/expression/EDPtoDPS.java \ src/query/expression/Expression.java \ src/query/expression/ExpressionDataPoint.java \ src/query/expression/ExpressionFactory.java \ From 682a32a18920857898b44b98e7ec0953ea098651 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 18 Nov 2015 15:02:24 -0600 Subject: [PATCH 344/826] Expression.java was included twice in Makefile Signed-off-by: Chris Larsen --- Makefile.am | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index bcb4733c35..a65db199f7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,7 +75,6 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ - src/query/expression/Expression.java \ src/query/expression/DiffSeries.java \ src/query/expression/DivideSeries.java \ src/query/expression/EDPtoDPS.java \ From 2588419f5724b38be8868e9c5e8d8fe2de9bc6e8 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 18 Nov 2015 14:49:05 -0600 Subject: [PATCH 345/826] A few changes to the expression support. * Added convienience aliases for SumSeries, DiffSeries, MultiplySeries, and DivideSeries * Added Alias function Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/query/expression/Alias.java | 89 +++++++++++++++++++++ src/query/expression/ExpressionFactory.java | 5 ++ 3 files changed, 95 insertions(+) create mode 100644 src/query/expression/Alias.java diff --git a/Makefile.am b/Makefile.am index a65db199f7..89a4b656c8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -75,6 +75,7 @@ tsdb_SRC := \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ src/query/expression/Absolute.java \ + src/query/expression/Alias.java \ src/query/expression/DiffSeries.java \ src/query/expression/DivideSeries.java \ src/query/expression/EDPtoDPS.java \ diff --git a/src/query/expression/Alias.java b/src/query/expression/Alias.java new file mode 100644 index 0000000000..410d409fd2 --- /dev/null +++ b/src/query/expression/Alias.java @@ -0,0 +1,89 @@ +// 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.List; +import java.util.Map; +import com.google.common.base.Joiner; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.TSQuery; + +/** + * Returns an alias if provided or the original metric name if not. + * @since 2.3 + */ +public class Alias implements Expression { + + static Joiner COMMA_JOINER = Joiner.on(',').skipNulls(); + + @Override + public DataPoints[] evaluate(TSQuery data_query, List queryResults, + List queryParams) { + if (queryResults == null || queryResults.size() == 0) { + throw new NullPointerException("No query results"); + } + + String aliasTemplate = "__default"; + + if (queryParams != null && queryParams.size() >= 0) { + aliasTemplate = COMMA_JOINER.join(queryParams); + } + + DataPoints[] inputPoints = queryResults.get(0); + + DataPoint[][] dps = new DataPoint[inputPoints.length][]; + + for (int j = 0; j < dps.length; j++) { + DataPoints base = inputPoints[j]; + dps[j] = new DataPoint[base.size()]; + int i = 0; + + for (DataPoint pt : base) { + if (pt.isInteger()) { + dps[j][i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), pt.longValue()); + } else { + dps[j][i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), pt.doubleValue()); + } + i++; + } + } + + DataPoints[] resultArray = new DataPoints[queryResults.get(0).length]; + for (int i = 0; i < resultArray.length; i++) { + PostAggregatedDataPoints result = new PostAggregatedDataPoints(inputPoints[i], + dps[i]); + + String alias = aliasTemplate; + for (Map.Entry e : inputPoints[i].getTags().entrySet()) { + alias = alias.replace("@" + e.getKey(), e.getValue()); + } + + result.setAlias(alias); + resultArray[i] = result; + } + + return resultArray; + } + + @Override + public String writeStringField(List queryParams, String innerExpression) { + if (queryParams == null || queryParams.size() == 0) { + return "NULL"; + } + + return queryParams.get(0); + } +} \ No newline at end of file diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index c2d9ecd246..23bd39d1dd 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -29,6 +29,7 @@ public final class ExpressionFactory { new HashMap(); static { + available_functions.put("alias", new Alias()); available_functions.put("scale", new Scale()); available_functions.put("absolute", new Absolute()); available_functions.put("movingAverage", new MovingAverage()); @@ -46,9 +47,13 @@ private ExpressionFactory() { } */ public static void addTSDBFunctions(final TSDB tsdb) { available_functions.put("divideSeries", new DivideSeries(tsdb)); + available_functions.put("divide", new DivideSeries(tsdb)); available_functions.put("sumSeries", new SumSeries(tsdb)); + available_functions.put("sum", new SumSeries(tsdb)); available_functions.put("diffSeries", new DiffSeries(tsdb)); + available_functions.put("difference", new DiffSeries(tsdb)); available_functions.put("multiplySeries", new MultiplySeries(tsdb)); + available_functions.put("multiply", new MultiplySeries(tsdb)); } /** From c434dc1c3dd94b3ed07516b98070b672e8eb32fe Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 20 Nov 2015 16:40:41 -0800 Subject: [PATCH 346/826] Cleanup Alias and fix it so that it handles tag resolution in the alias template to resolve asynchronously. Also add unit tests. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/query/expression/Alias.java | 125 +++---- .../expression/PostAggregatedDataPoints.java | 28 +- test/query/expression/TestAlias.java | 321 ++++++++++++++++++ 4 files changed, 414 insertions(+), 61 deletions(-) create mode 100644 test/query/expression/TestAlias.java diff --git a/Makefile.am b/Makefile.am index 89a4b656c8..7ce950db64 100644 --- a/Makefile.am +++ b/Makefile.am @@ -270,6 +270,7 @@ test_SRC := \ test/meta/TestUIDMeta.java \ test/query/expression/BaseTimeSyncedIteratorTest.java \ test/query/expression/TestAbsolute.java \ + test/query/expression/TestAlias.java \ test/query/expression/TestDiffSeries.java \ test/query/expression/TestDivideSeries.java \ test/query/expression/TestExpressionFactory.java \ diff --git a/src/query/expression/Alias.java b/src/query/expression/Alias.java index 410d409fd2..737d1c46ba 100644 --- a/src/query/expression/Alias.java +++ b/src/query/expression/Alias.java @@ -12,78 +12,89 @@ // see . package net.opentsdb.query.expression; +import java.util.ArrayList; import java.util.List; -import java.util.Map; + import com.google.common.base.Joiner; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; import net.opentsdb.core.TSQuery; /** - * Returns an alias if provided or the original metric name if not. + * Returns an alias if provided or the original metric name if not. The alias + * may optionally contain a template for tag replacement so that tags are + * advanced to the metric name for systems that require it. (e.g. flatten + * a name for Graphite). * @since 2.3 */ public class Alias implements Expression { - static Joiner COMMA_JOINER = Joiner.on(',').skipNulls(); - - @Override - public DataPoints[] evaluate(TSQuery data_query, List queryResults, - List queryParams) { - if (queryResults == null || queryResults.size() == 0) { - throw new NullPointerException("No query results"); - } - - String aliasTemplate = "__default"; - - if (queryParams != null && queryParams.size() >= 0) { - aliasTemplate = COMMA_JOINER.join(queryParams); - } - - DataPoints[] inputPoints = queryResults.get(0); - - DataPoint[][] dps = new DataPoint[inputPoints.length][]; - - for (int j = 0; j < dps.length; j++) { - DataPoints base = inputPoints[j]; - dps[j] = new DataPoint[base.size()]; - int i = 0; - - for (DataPoint pt : base) { - if (pt.isInteger()) { - dps[j][i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), pt.longValue()); - } else { - dps[j][i] = MutableDataPoint.ofDoubleValue(pt.timestamp(), pt.doubleValue()); - } - i++; - } - } - - DataPoints[] resultArray = new DataPoints[queryResults.get(0).length]; - for (int i = 0; i < resultArray.length; i++) { - PostAggregatedDataPoints result = new PostAggregatedDataPoints(inputPoints[i], - dps[i]); - - String alias = aliasTemplate; - for (Map.Entry e : inputPoints[i].getTags().entrySet()) { - alias = alias.replace("@" + e.getKey(), e.getValue()); - } - - result.setAlias(alias); - resultArray[i] = result; - } + static Joiner COMMA_JOINER = Joiner.on(',').skipNulls(); - return resultArray; + @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"); } - - @Override - public String writeStringField(List queryParams, String innerExpression) { - if (queryParams == null || queryParams.size() == 0) { - return "NULL"; + if (query_results == null || query_results.isEmpty()) { + return new DataPoints[]{}; + } + if (params == null || params.isEmpty()) { + throw new IllegalArgumentException("Missing the alias"); + } + final String alias_template = COMMA_JOINER.join(params); + + int num_results = 0; + for (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) { + // TODO(cl) - Using an array as the size function may not return the exact + // results and we should figure a way to avoid copying data anyway. + final List new_dps_list = new ArrayList(); + final SeekableView view = dps.iterator(); + while (view.hasNext()) { + DataPoint pt = view.next(); + if (pt.isInteger()) { + new_dps_list.add(MutableDataPoint.ofLongValue( + pt.timestamp(), Math.abs(pt.longValue()))); + } else { + new_dps_list.add(MutableDataPoint.ofDoubleValue( + pt.timestamp(), Math.abs(pt.doubleValue()))); + } } - - return queryParams.get(0); + + final DataPoint[] new_dps = new DataPoint[dps.size()]; + new_dps_list.toArray(new_dps); + final PostAggregatedDataPoints padps = new PostAggregatedDataPoints( + dps, new_dps); + + padps.setAlias(alias_template); + results[ix++] = padps; + } } + return results; + } + + @Override + public String writeStringField(final List query_params, + final String innerExpression) { + final StringBuilder buf = new StringBuilder(); + buf.append("alias(") + .append(innerExpression) + .append(query_params == null || query_params.isEmpty() + ? "" : "," + COMMA_JOINER.join(query_params)) + .append(")"); + return buf.toString(); + } } \ No newline at end of file diff --git a/src/query/expression/PostAggregatedDataPoints.java b/src/query/expression/PostAggregatedDataPoints.java index e0787f62fd..2af4bf47ff 100644 --- a/src/query/expression/PostAggregatedDataPoints.java +++ b/src/query/expression/PostAggregatedDataPoints.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; import org.hbase.async.Bytes.ByteMap; @@ -24,6 +25,7 @@ import net.opentsdb.core.SeekableView; import net.opentsdb.meta.Annotation; +import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; /** @@ -61,16 +63,34 @@ public PostAggregatedDataPoints(final DataPoints base_data_points, @Override public String metricName() { - if (alias != null) { - return alias; - } else { - return base_data_points.metricName(); + try { + return metricNameAsync().join(); + } catch (Exception e) { + throw new RuntimeException("Unexpected exception waiting for " + + "name resolution", e); } } @Override public Deferred metricNameAsync() { if (alias != null) { + if (alias.contains("@") && getTagUids().size() > 0) { + // need to resolve the tag UIDs for the templating feature + + class TemplateFill implements Callback, + Map> { + @Override + public Deferred call(final Map tags) + throws Exception { + for (final Entry pair : tags.entrySet()) { + alias = alias.replace("@" + pair.getKey(), pair.getValue()); + } + return Deferred.fromResult(alias); + } + } + return base_data_points.getTagsAsync() + .addCallbackDeferring(new TemplateFill()); + } return Deferred.fromResult(alias); } return base_data_points.metricNameAsync(); diff --git a/test/query/expression/TestAlias.java b/test/query/expression/TestAlias.java new file mode 100644 index 0000000000..744287e50f --- /dev/null +++ b/test/query/expression/TestAlias.java @@ -0,0 +1,321 @@ +// 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.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.hbase.async.Bytes.ByteMap; +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 TestAlias { + + 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 Alias func; + private Map tags; + private ByteMap tag_uids; + + @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)); + + tags = new HashMap(2); + tags.put("host", "web01"); + tags.put("dc", "lga"); + tag_uids = new ByteMap(); + tag_uids.put(new byte[] { 0, 0, 1 }, new byte[] { 0, 0, 1 }); + tag_uids.put(new byte[] { 0, 0, 2 }, new byte[] { 0, 0, 2 }); + + dps = PowerMockito.mock(DataPoints.class); + when(dps.iterator()).thenReturn(view); + when(dps.metricName()).thenReturn(METRIC); + when(dps.getTagsAsync()).thenReturn(Deferred.fromResult(tags)); + when(dps.getTagUids()).thenReturn(tag_uids); + + group_bys = new DataPoints[] { dps }; + + query_results = new ArrayList(1); + query_results.add(group_bys); + + params = new ArrayList(1); + func = new Alias(); + } + + @Test + public void evaluateGroupByLong() 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.metricName()).thenReturn("sys.mem"); + group_bys = new DataPoints[] { dps, dps2 }; + query_results.clear(); + query_results.add(group_bys); + params.add("My Alias"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("My Alias", results[0].metricName()); + assertEquals("My Alias", results[1].metricName()); + + long ts = START_TIME; + long v = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateGroupByDouble() 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.metricName()).thenReturn("sys.mem"); + group_bys = new DataPoints[] { dps, dps2 }; + query_results.clear(); + query_results.add(group_bys); + params.add("My Alias"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("My Alias", results[0].metricName()); + assertEquals("My Alias", results[1].metricName()); + + long ts = START_TIME; + double v = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals((long)v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + 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 evaluateSubQuerySeries() 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.metricName()).thenReturn("sys.mem"); + group_bys = new DataPoints[] { dps, dps2 }; + query_results.clear(); + query_results.add(group_bys); + params.add("My Alias"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("My Alias", results[0].metricName()); + assertEquals("My Alias", results[1].metricName()); + + long ts = START_TIME; + long v = 1; + for (DataPoint dp : results[0]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + ts = START_TIME; + v = 10; + for (DataPoint dp : results[1]) { + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(v, dp.longValue()); + ts += INTERVAL; + v += 1; + } + } + + @Test + public void evaluateWithTags() 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.metricName()).thenReturn("sys.mem"); + when(dps2.getTagsAsync()).thenReturn(Deferred.fromResult(tags)); + when(dps2.getTagUids()).thenReturn(tag_uids); + group_bys = new DataPoints[] { dps, dps2 }; + query_results.clear(); + query_results.add(group_bys); + params.add("My Alias.@host.@dc"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("My Alias.web01.lga", results[0].metricName()); + assertEquals("My Alias.web01.lga", results[1].metricName()); + } + + @Test + public void evaluateWithATag() 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.metricName()).thenReturn("sys.mem"); + when(dps2.getTagsAsync()).thenReturn(Deferred.fromResult(tags)); + when(dps2.getTagUids()).thenReturn(tag_uids); + group_bys = new DataPoints[] { dps, dps2 }; + query_results.clear(); + query_results.add(group_bys); + params.add("My Alias.@dc"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("My Alias.lga", results[0].metricName()); + assertEquals("My Alias.lga", results[1].metricName()); + } + + @Test + public void evaluateWithTagsJoined() 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.metricName()).thenReturn("sys.mem"); + when(dps2.getTagsAsync()).thenReturn(Deferred.fromResult(tags)); + when(dps2.getTagUids()).thenReturn(tag_uids); + group_bys = new DataPoints[] { dps, dps2 }; + query_results.clear(); + query_results.add(group_bys); + params.add("My Alias"); + params.add("@host"); + params.add("@dc"); + params.add("@none"); + + final DataPoints[] results = func.evaluate(data_query, query_results, params); + + assertEquals(2, results.length); + assertEquals("My Alias,web01,lga,@none", results[0].metricName()); + assertEquals("My Alias,web01,lga,@none", results[1].metricName()); + } + + @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 (expected = IllegalArgumentException.class) + public void evaluateNullParams() throws Exception { + func.evaluate(data_query, query_results, null); + } + + @Test + public void evaluateEmptyResults() throws Exception { + final DataPoints[] results = func.evaluate(data_query, + Collections.emptyList(), params); + assertEquals(0, results.length); + } + + @Test (expected = IllegalArgumentException.class) + public void evaluateEmptyParams() throws Exception { + func.evaluate(data_query, query_results, null); + } + + @Test + public void writeStringField() throws Exception { + assertEquals("alias(m)", func.writeStringField(params, "m")); + params.add("MyAlias"); + assertEquals("alias(m,MyAlias)", func.writeStringField(params, "m")); + params.clear(); + params.add("Alias"); + params.add("@host"); + assertEquals("alias(m,Alias,@host)", func.writeStringField(params, "m")); + params.clear(); + assertEquals("alias(null)", func.writeStringField(params, null)); + assertEquals("alias()", func.writeStringField(params, "")); + assertEquals("alias(inner_expression)", + func.writeStringField(null, "inner_expression")); + } +} From 90265791efe4824dba012d485b6e8cea8e844c8e Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Wed, 25 Nov 2015 00:40:01 +0800 Subject: [PATCH 347/826] "tsdb fsck --fix-all" wrongly clears data points when salting enabled Const.SALT_WIDTH() is actually not constant value, it mustn't be called on class initialization. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 9f006b7b19..247dd9818a 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -114,11 +114,11 @@ final class Fsck { final AtomicLong vle_fixed = new AtomicLong(); /** Length of the metric + timestamp for key validation */ - private static int key_prefix_length = Const.SALT_WIDTH() + + private int key_prefix_length = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; /** Length of a tagk + tagv pair for key validation */ - private static int key_tags_length = TSDB.tagk_width() + TSDB.tagv_width(); + private int key_tags_length = TSDB.tagk_width() + TSDB.tagv_width(); /** How often to report progress */ private static long report_rows = 10000; From 37ca54e7aff0a2f121cfdb29809200d36fe731fe Mon Sep 17 00:00:00 2001 From: Vitaliy Fuks Date: Sat, 21 Nov 2015 16:40:09 -0500 Subject: [PATCH 348/826] Silence stray logging output in TSMeta.call() This line was added in ab276c823ee082bc216951e371062d1d019652d6 and based on indentation is likely an accidental commit. It causes continuous spamming in our logs after upgrade. Changing log level to "debug" to quiet it down. Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 9abc89e463..a9d6dd2a69 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -525,7 +525,7 @@ final class TSMetaCB implements Callback, Long> { @Override public Deferred call(final Long incremented_value) throws Exception { -LOG.info("Value: " + incremented_value); + LOG.debug("Value: " + incremented_value); if (incremented_value > 1) { // TODO - maybe update the search index every X number of increments? // Otherwise the search engine would only get last_updated/count From 7db3bb8803e5e11cdc1690e9ad84069543269653 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 27 Nov 2015 18:50:39 -0800 Subject: [PATCH 349/826] Add code to properly handle append data points in the FSCK. More work can be done but at least it won't throw errors. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 15 ++++++ test/tools/TestFsck.java | 114 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 9f006b7b19..e8a197cc17 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -34,6 +34,7 @@ import com.stumbleupon.async.Deferred; +import net.opentsdb.core.AppendDataPoints; import net.opentsdb.core.Const; import net.opentsdb.core.IllegalDataException; import net.opentsdb.core.Internal; @@ -92,6 +93,8 @@ final class Fsck { final AtomicLong rows_processed = new AtomicLong(); final AtomicLong valid_datapoints = new AtomicLong(); final AtomicLong annotations = new AtomicLong(); + final AtomicLong append_dps = new AtomicLong(); + final AtomicLong append_dps_fixed = new AtomicLong(); final AtomicLong bad_key = new AtomicLong(); final AtomicLong bad_key_fixed = new AtomicLong(); final AtomicLong duplicates = new AtomicLong(); @@ -382,6 +385,18 @@ private void fsckRow(final ArrayList row, if (qual[0] == Annotation.PREFIX()) { annotations.getAndIncrement(); continue; + } else if (qual[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + append_dps.getAndIncrement(); + try { + final AppendDataPoints adps = new AppendDataPoints(); + adps.parseKeyValue(tsdb, kv); + if (adps.repairedDeferred() != null) { + append_dps_fixed.incrementAndGet(); + } + } catch (RuntimeException e) { + LOG.error("Unexpected exception processing append data point: " + kv, e); + } + continue; } LOG.warn("Found an object possibly from a future version of OpenTSDB\n\t" + kv); diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 58cbd374b9..9eea0aa6a3 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -1474,6 +1474,120 @@ public void badCompactTooLongFix() throws Exception { assertEquals(-1, storage.numColumns(ROW)); } + @Test + public void appendOK() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual1, val1, qual2, val2)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(0, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + + @Test + public void appendOutOfOrder() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual2, val2, qual1, val1)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(0, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual2, val2, qual1, val1), + storage.getColumn(ROW, appendq)); + } + + @Test + public void appendOutOfOrderFixed() throws Exception { + config.overrideConfig("tsd.storage.repair_appends", "true"); + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual2, val2, qual1, val1)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(1, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + + @Test + public void appendDupe() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual1, val1, qual1, val1, qual2, val2)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(0, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + /* + * TODO - Fix dupes in the appends by re-writing the data. Right now it just + * resolves them at query time but leaves the values in storage. + @Test + public void appendDupeFix() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual1, val1, qual1, val1, qual2, val2)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(1, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + */ + // VLE -------------------------------------------- @Test From b5be7f3424f3cb8074c31a4b283f051425e35236 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 27 Nov 2015 18:50:39 -0800 Subject: [PATCH 350/826] Add code to properly handle append data points in the FSCK. More work can be done but at least it won't throw errors. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 15 ++++++ test/tools/TestFsck.java | 114 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 247dd9818a..45dde4539f 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -34,6 +34,7 @@ import com.stumbleupon.async.Deferred; +import net.opentsdb.core.AppendDataPoints; import net.opentsdb.core.Const; import net.opentsdb.core.IllegalDataException; import net.opentsdb.core.Internal; @@ -92,6 +93,8 @@ final class Fsck { final AtomicLong rows_processed = new AtomicLong(); final AtomicLong valid_datapoints = new AtomicLong(); final AtomicLong annotations = new AtomicLong(); + final AtomicLong append_dps = new AtomicLong(); + final AtomicLong append_dps_fixed = new AtomicLong(); final AtomicLong bad_key = new AtomicLong(); final AtomicLong bad_key_fixed = new AtomicLong(); final AtomicLong duplicates = new AtomicLong(); @@ -382,6 +385,18 @@ private void fsckRow(final ArrayList row, if (qual[0] == Annotation.PREFIX()) { annotations.getAndIncrement(); continue; + } else if (qual[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + append_dps.getAndIncrement(); + try { + final AppendDataPoints adps = new AppendDataPoints(); + adps.parseKeyValue(tsdb, kv); + if (adps.repairedDeferred() != null) { + append_dps_fixed.incrementAndGet(); + } + } catch (RuntimeException e) { + LOG.error("Unexpected exception processing append data point: " + kv, e); + } + continue; } LOG.warn("Found an object possibly from a future version of OpenTSDB\n\t" + kv); diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 58cbd374b9..9eea0aa6a3 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -1474,6 +1474,120 @@ public void badCompactTooLongFix() throws Exception { assertEquals(-1, storage.numColumns(ROW)); } + @Test + public void appendOK() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual1, val1, qual2, val2)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(0, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + + @Test + public void appendOutOfOrder() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual2, val2, qual1, val1)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(0, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual2, val2, qual1, val1), + storage.getColumn(ROW, appendq)); + } + + @Test + public void appendOutOfOrderFixed() throws Exception { + config.overrideConfig("tsd.storage.repair_appends", "true"); + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual2, val2, qual1, val1)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(1, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + + @Test + public void appendDupe() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual1, val1, qual1, val1, qual2, val2)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(0, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + /* + * TODO - Fix dupes in the appends by re-writing the data. Right now it just + * resolves them at query time but leaves the values in storage. + @Test + public void appendDupeFix() throws Exception { + final byte[] qual1 = { 0x0, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x0, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] appendq = { 0x05, 0x0, 0x0 }; + storage.addColumn(ROW, appendq, + MockBase.concatByteArrays(qual1, val1, qual1, val1, qual2, val2)); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(1, fsck.kvs_processed.get()); + assertEquals(0, fsck.bad_compacted_columns.get()); + assertEquals(1, fsck.append_dps.get()); + assertEquals(1, fsck.append_dps_fixed.get()); + assertEquals(0, fsck.totalErrors()); + assertEquals(0, fsck.correctable()); + assertArrayEquals( MockBase.concatByteArrays(qual1, val1, qual2, val2), + storage.getColumn(ROW, appendq)); + } + */ + // VLE -------------------------------------------- @Test From 2c14cd45e524e6540a7632f98009a4a2700d78ab Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 20 Nov 2015 18:21:37 -0800 Subject: [PATCH 351/826] Fix up Expression method UTs for async metric lookups. Signed-off-by: Chris Larsen --- test/query/expression/TestAbsolute.java | 14 ++++++++------ test/query/expression/TestHighestCurrent.java | 18 ++++++++++-------- test/query/expression/TestHighestMax.java | 16 +++++++++------- test/query/expression/TestMovingAverage.java | 8 +++++--- test/query/expression/TestScale.java | 18 ++++++++++-------- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/test/query/expression/TestAbsolute.java b/test/query/expression/TestAbsolute.java index 3d675ee0d6..7d822fe4f4 100644 --- a/test/query/expression/TestAbsolute.java +++ b/test/query/expression/TestAbsolute.java @@ -37,6 +37,8 @@ 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.*", @@ -67,7 +69,7 @@ public void before() throws Exception { dps = PowerMockito.mock(DataPoints.class); when(dps.iterator()).thenReturn(view); - when(dps.metricName()).thenReturn(METRIC); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(METRIC)); group_bys = new DataPoints[] { dps }; @@ -84,7 +86,7 @@ public void evaluatePositiveGroupByLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -121,7 +123,7 @@ public void evaluatePositiveGroupByDouble() throws Exception { NUM_POINTS, false, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -158,7 +160,7 @@ public void evaluateFactorNegativeGroupByLong() throws Exception { NUM_POINTS, true, -10, -1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -195,7 +197,7 @@ public void evaluateNegativeGroupByDouble() throws Exception { NUM_POINTS, false, -10, -1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -233,7 +235,7 @@ public void evaluateNegativeSubQuerySeries() throws Exception { NUM_POINTS, true, -10, -1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); diff --git a/test/query/expression/TestHighestCurrent.java b/test/query/expression/TestHighestCurrent.java index 145c81f41d..876ee6356d 100644 --- a/test/query/expression/TestHighestCurrent.java +++ b/test/query/expression/TestHighestCurrent.java @@ -36,6 +36,8 @@ 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.*", @@ -66,7 +68,7 @@ public void before() throws Exception { dps = PowerMockito.mock(DataPoints.class); when(dps.iterator()).thenReturn(view); - when(dps.metricName()).thenReturn(METRIC); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(METRIC)); group_bys = new DataPoints[] { dps }; @@ -84,7 +86,7 @@ public void evaluateTopN1with2SeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -112,7 +114,7 @@ public void evaluateTopN2with2SeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -151,7 +153,7 @@ public void evaluateTopN100with2SeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); DataPoints[] group_bys2 = new DataPoints[] { dps2 }; query_results.add(group_bys2); @@ -189,7 +191,7 @@ public void evaluateTopN100with2SubQuerySeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -228,7 +230,7 @@ public void evaluateTopN2with2SeriesDouble() throws Exception { NUM_POINTS, false, 10, 1.5); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -267,7 +269,7 @@ public void evaluateTopN1with2SeriesLongDoubleMixed() throws Exception { NUM_POINTS, false, 10, 1.5, true); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -304,7 +306,7 @@ public void evaluateTopN1with2SeriesDiffSpan() throws Exception { 3, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); diff --git a/test/query/expression/TestHighestMax.java b/test/query/expression/TestHighestMax.java index 2426204bc5..66e38689a7 100644 --- a/test/query/expression/TestHighestMax.java +++ b/test/query/expression/TestHighestMax.java @@ -36,6 +36,8 @@ 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.*", @@ -66,7 +68,7 @@ public void before() throws Exception { dps = PowerMockito.mock(DataPoints.class); when(dps.iterator()).thenReturn(view); - when(dps.metricName()).thenReturn(METRIC); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(METRIC)); group_bys = new DataPoints[] { dps }; @@ -84,7 +86,7 @@ public void evaluateTopN1with2SeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -112,7 +114,7 @@ public void evaluateTopN2with2SeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -151,7 +153,7 @@ public void evaluateTopN100with2SeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); DataPoints[] group_bys2 = new DataPoints[] { dps2 }; query_results.add(group_bys2); @@ -189,7 +191,7 @@ public void evaluateTopN100with2SubQuerySeriesLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -228,7 +230,7 @@ public void evaluateTopN2with2SeriesDouble() throws Exception { NUM_POINTS, false, 10, 1.5); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -267,7 +269,7 @@ public void evaluateTopN1with2SeriesLongDoubleMixed() throws Exception { NUM_POINTS, false, 10, 1.5, true); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); diff --git a/test/query/expression/TestMovingAverage.java b/test/query/expression/TestMovingAverage.java index 4a59cf38a1..3d2be90d99 100644 --- a/test/query/expression/TestMovingAverage.java +++ b/test/query/expression/TestMovingAverage.java @@ -36,6 +36,8 @@ 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.*", @@ -65,7 +67,7 @@ public void before() throws Exception { dps = PowerMockito.mock(DataPoints.class); when(dps.iterator()).thenReturn(view); - when(dps.metricName()).thenReturn(METRIC); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(METRIC)); group_bys = new DataPoints[] { dps }; @@ -282,7 +284,7 @@ public void evaluateGroupBy() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -322,7 +324,7 @@ public void evaluateSubQuery() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); DataPoints[] group_bys2 = new DataPoints[] { dps2 }; query_results.add(group_bys2); diff --git a/test/query/expression/TestScale.java b/test/query/expression/TestScale.java index f7f35dd687..d34469d7c0 100644 --- a/test/query/expression/TestScale.java +++ b/test/query/expression/TestScale.java @@ -36,6 +36,8 @@ 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.*", @@ -66,7 +68,7 @@ public void before() throws Exception { dps = PowerMockito.mock(DataPoints.class); when(dps.iterator()).thenReturn(view); - when(dps.metricName()).thenReturn(METRIC); + when(dps.metricNameAsync()).thenReturn(Deferred.fromResult(METRIC)); group_bys = new DataPoints[] { dps }; @@ -84,7 +86,7 @@ public void evaluateFactor1GroupByLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -122,7 +124,7 @@ public void evaluateFactor1GroupByDouble() throws Exception { NUM_POINTS, false, 10, 1.5); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -160,7 +162,7 @@ public void evaluateFactor1point5GroupBy() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -199,7 +201,7 @@ public void evaluateFactor1024GroupBy() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -237,7 +239,7 @@ public void evaluateFactor1SubQuerySeries() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -275,7 +277,7 @@ public void evaluateFactor0GroupByLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); @@ -309,7 +311,7 @@ public void evaluateFactorNegative1GroupByLong() throws Exception { NUM_POINTS, true, 10, 1); DataPoints dps2 = PowerMockito.mock(DataPoints.class); when(dps2.iterator()).thenReturn(view2); - when(dps2.metricName()).thenReturn("sys.mem"); + when(dps2.metricNameAsync()).thenReturn(Deferred.fromResult("sys.mem")); group_bys = new DataPoints[] { dps, dps2 }; query_results.clear(); query_results.add(group_bys); From fb255884470597b3b4e8220b4250d75630099cdb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 20 Nov 2015 17:22:10 -0800 Subject: [PATCH 352/826] Fix for #639 thanks to @rluta! Signed-off-by: Chris Larsen --- src/tools/UidManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index fc51dd6287..5dccc5837e 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -361,7 +361,7 @@ private static int assign(final TSDB tsdb, final short idwidth, final String[] args) { boolean randomize = false; - if (UniqueIdType.valueOf(args[1]) == UniqueIdType.METRIC) { + if (UniqueId.stringToUniqueIdType(args[1]) == UniqueIdType.METRIC) { randomize = tsdb.getConfig().getBoolean("tsd.core.uid.random_metrics"); } final UniqueId uid = new UniqueId(tsdb.getClient(), table, args[1], From 73cd9cda87781ba8abb4f5030242d308e702b1d7 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 20 Nov 2015 17:22:10 -0800 Subject: [PATCH 353/826] Fix for #639 thanks to @rluta! Signed-off-by: Chris Larsen --- src/tools/UidManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index fc51dd6287..5dccc5837e 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -361,7 +361,7 @@ private static int assign(final TSDB tsdb, final short idwidth, final String[] args) { boolean randomize = false; - if (UniqueIdType.valueOf(args[1]) == UniqueIdType.METRIC) { + if (UniqueId.stringToUniqueIdType(args[1]) == UniqueIdType.METRIC) { randomize = tsdb.getConfig().getBoolean("tsd.core.uid.random_metrics"); } final UniqueId uid = new UniqueId(tsdb.getClient(), table, args[1], From f6c5fbbfee489c1d8f1639a64b81e2ea2028c7d5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 29 Nov 2015 12:55:10 -0800 Subject: [PATCH 354/826] Fix the IncomingDataPoints class for the text importer CLI where, with salting enabled, the row key was corrupted as it wrote the timestamp in the wrong position. Fixes #623. Thanks @rgazaryants Signed-off-by: Chris Larsen --- src/core/IncomingDataPoints.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 20ca6d51ac..b253b6f536 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -156,8 +156,8 @@ static Deferred rowKeyTemplateAsync(final TSDB tsdb, final short tag_value_width = tsdb.tag_values.width(); final short num_tags = (short) tags.size(); - int row_size = (metric_width + Const.TIMESTAMP_BYTES + tag_name_width - * num_tags + tag_value_width * num_tags); + 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. @@ -171,7 +171,7 @@ static Deferred rowKeyTemplateAsync(final TSDB tsdb, // Copy the metric ID at the beginning of the row key. class CopyMetricInRowKeyCB implements Callback { public byte[] call(final byte[] metricid) { - copyInRowKey(row, (short) 0, metricid); + copyInRowKey(row, (short) Const.SALT_WIDTH(), metricid); return row; } } @@ -180,7 +180,7 @@ public byte[] call(final byte[] metricid) { class CopyTagsInRowKeyCB implements Callback, ArrayList> { public Deferred call(final ArrayList tags) { - short pos = metric_width; + short pos = (short) (Const.SALT_WIDTH() + metric_width); pos += Const.TIMESTAMP_BYTES; for (final byte[] tag : tags) { copyInRowKey(row, pos, tag); @@ -242,7 +242,7 @@ private long updateBaseTime(final long timestamp) { // because the HBase client may still hold a reference to it in its // internal datastructures. row = Arrays.copyOf(row, row.length); - Bytes.setInt(row, (int) base_time, tsdb.metrics.width()); + Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + tsdb.metrics.width()); RowKey.prefixKeyWithSalt(row); // in case the timestamp will be involved in // salting later tsdb.scheduleForCompaction(row, (int) base_time); From a997aded698778164d1be8f52cf2ffd13edf276a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 29 Nov 2015 12:55:10 -0800 Subject: [PATCH 355/826] Fix the IncomingDataPoints class for the text importer CLI where, with salting enabled, the row key was corrupted as it wrote the timestamp in the wrong position. Fixes #623. Thanks @rgazaryants Signed-off-by: Chris Larsen --- src/core/IncomingDataPoints.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 4c98e3a596..6fb9c9ba23 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -156,8 +156,8 @@ static Deferred rowKeyTemplateAsync(final TSDB tsdb, final short tag_value_width = tsdb.tag_values.width(); final short num_tags = (short) tags.size(); - int row_size = (metric_width + Const.TIMESTAMP_BYTES + tag_name_width - * num_tags + tag_value_width * num_tags); + 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. @@ -171,7 +171,7 @@ static Deferred rowKeyTemplateAsync(final TSDB tsdb, // Copy the metric ID at the beginning of the row key. class CopyMetricInRowKeyCB implements Callback { public byte[] call(final byte[] metricid) { - copyInRowKey(row, (short) 0, metricid); + copyInRowKey(row, (short) Const.SALT_WIDTH(), metricid); return row; } } @@ -180,7 +180,7 @@ public byte[] call(final byte[] metricid) { class CopyTagsInRowKeyCB implements Callback, ArrayList> { public Deferred call(final ArrayList tags) { - short pos = metric_width; + short pos = (short) (Const.SALT_WIDTH() + metric_width); pos += Const.TIMESTAMP_BYTES; for (final byte[] tag : tags) { copyInRowKey(row, pos, tag); @@ -242,7 +242,7 @@ private long updateBaseTime(final long timestamp) { // because the HBase client may still hold a reference to it in its // internal datastructures. row = Arrays.copyOf(row, row.length); - Bytes.setInt(row, (int) base_time, tsdb.metrics.width()); + Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + tsdb.metrics.width()); RowKey.prefixKeyWithSalt(row); // in case the timestamp will be involved in // salting later tsdb.scheduleForCompaction(row, (int) base_time); From 31332d7077193010584c613241d000f679f7ffda Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 29 Nov 2015 13:17:06 -0800 Subject: [PATCH 356/826] Output the index of the query with the results Signed-off-by: Chris Larsen --- src/core/TSQuery.java | 2 ++ src/core/TSSubQuery.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 5310d9a4cf..ccdd48abf2 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -174,8 +174,10 @@ public void validateAndSetQuery() { } // validate queries + int i = 0; for (TSSubQuery sub : queries) { sub.validateAndSetQuery(); + sub.setIndex(i++); } } diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index faae314fca..a2aee06e07 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -71,6 +71,9 @@ public final class TSSubQuery { * tags map. In the future we'll have special JSON objects for them. */ private List filters; + /** Index of the sub query */ + private int index; + /** * Default constructor necessary for POJO de/serialization */ @@ -289,6 +292,12 @@ public ByteSet getFilterTagKs() { return tagks; } + /** @return the index of the sub query + * @since 2.3 */ + public int getIndex() { + return index; + } + /** @param aggregator the name of an aggregation function */ public void setAggregator(String aggregator) { this.aggregator = aggregator; @@ -337,4 +346,10 @@ public void setFilters(List filters) { this.filters = filters; } + /** @param index the index of the sub query + * @since 2.3 */ + public void setIndex(final int index) { + this.index = index; + } + } From 538f7ee5461083f265bd1f1f320481e84aed204e Mon Sep 17 00:00:00 2001 From: Clement Laforet Date: Sat, 28 Nov 2015 23:54:32 +0100 Subject: [PATCH 357/826] - Update asynccassandra to fix build Signed-off-by: Chris Larsen --- ...ndra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 | 1 - ...ndra-0.0.1-20151104.191228-3-jar-with-dependencies.jar.md5 | 1 + third_party/asynccassandra/include.mk | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 create mode 100644 third_party/asynccassandra/asynccassandra-0.0.1-20151104.191228-3-jar-with-dependencies.jar.md5 diff --git a/third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 b/third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 deleted file mode 100644 index 10cf1edf3e..0000000000 --- a/third_party/asynccassandra/asynccassandra-0.0.1-20151102.192826-2-jar-with-dependencies.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -cce1a4b5736fcdcc3ced33982c3069d1 \ No newline at end of file diff --git a/third_party/asynccassandra/asynccassandra-0.0.1-20151104.191228-3-jar-with-dependencies.jar.md5 b/third_party/asynccassandra/asynccassandra-0.0.1-20151104.191228-3-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..6469b18da4 --- /dev/null +++ b/third_party/asynccassandra/asynccassandra-0.0.1-20151104.191228-3-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +0dd29195cdb9ca4467d0fc32bfffb98c \ No newline at end of file diff --git a/third_party/asynccassandra/include.mk b/third_party/asynccassandra/include.mk index 4d8658a262..7eb8f99e96 100644 --- a/third_party/asynccassandra/include.mk +++ b/third_party/asynccassandra/include.mk @@ -13,11 +13,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCCASSANDRA_VERSION := 0.0.1-20151102.192826-2 +ASYNCCASSANDRA_VERSION := 0.0.1-20151104.191228-3 ASYNCCASSANDRA := third_party/asynccassandra/asynccassandra-$(ASYNCCASSANDRA_VERSION)-jar-with-dependencies.jar ASYNCCASSANDRA_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/net/opentsdb/asynccassandra/0.0.1-SNAPSHOT/ $(ASYNCCASSANDRA): $(ASYNCCASSANDRA).md5 set dummy "$(ASYNCCASSANDRA_BASE_URL)" "$(ASYNCCASSANDRA)"; shift; $(FETCH_DEPENDENCY) -THIRD_PARTY += $(ASYNCCASSANDRA) \ No newline at end of file +THIRD_PARTY += $(ASYNCCASSANDRA) From 2b66c77a1f3024e73cb1ebe3cd4ca431d9676aa8 Mon Sep 17 00:00:00 2001 From: Yubao Liu Date: Wed, 25 Nov 2015 00:40:01 +0800 Subject: [PATCH 358/826] "tsdb fsck --fix-all" wrongly clears data points when salting enabled Const.SALT_WIDTH() is actually not constant value, it mustn't be called on class initialization. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index e8a197cc17..45dde4539f 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -117,11 +117,11 @@ final class Fsck { final AtomicLong vle_fixed = new AtomicLong(); /** Length of the metric + timestamp for key validation */ - private static int key_prefix_length = Const.SALT_WIDTH() + + private int key_prefix_length = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; /** Length of a tagk + tagv pair for key validation */ - private static int key_tags_length = TSDB.tagk_width() + TSDB.tagv_width(); + private int key_tags_length = TSDB.tagk_width() + TSDB.tagv_width(); /** How often to report progress */ private static long report_rows = 10000; From 1f6af3b235851369dbf088d9bd9153a6047f9420 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 9 Dec 2015 19:21:56 -0800 Subject: [PATCH 359/826] Attempt a fix at #612 by returning a copy of the list of filters in the sub query to avoid hash issues. Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 7438f7d303..462ca6950c 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -270,7 +270,8 @@ public List getFilters() { if (filters == null) { filters = new ArrayList(); } - return filters; + // send a copy so ordering doesn't mess up the hash code + return new ArrayList(filters); } /** @param aggregator the name of an aggregation function */ From 6e705d867b10beaa425ccb121dd9a34288c3130b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 9 Dec 2015 19:21:56 -0800 Subject: [PATCH 360/826] Attempt a fix at #612 by returning a copy of the list of filters in the sub query to avoid hash issues. Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index a2aee06e07..8ed1c23bce 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -274,7 +274,8 @@ public List getFilters() { if (filters == null) { filters = new ArrayList(); } - return filters; + // send a copy so ordering doesn't mess up the hash code + return new ArrayList(filters); } /** @return the unique set of tagks from the filters. May be null if no filters From 1fccdc3d430c52d68ff5492da01a19e854054ca4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 14 Dec 2015 20:13:15 -0800 Subject: [PATCH 361/826] A couple of additional UTs for TSSubQuery Signed-off-by: Chris Larsen --- test/core/TestTSSubQuery.java | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 3336f18827..743a47fbaa 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -185,6 +185,46 @@ public void validateWithGroupByFilter() { assertEquals(300000, sub.downsampleInterval()); } + @Test + public void validateWithFilterAndGroupByFilter() { + TSSubQuery sub = getMetricForValidate(); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("colo", "lga*")); + sub.setFilters(filters); + Map tags = new HashMap(); + tags.put("host", TagVWildcardFilter.FILTER_NAME + "(*nari)"); + sub.setTags(tags); + + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(TagVWildcardFilter.FILTER_NAME + "(*nari)", + sub.getTags().get("host")); + assertEquals(1, sub.getFilters().size()); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + + @Test + public void validateWithFilterAndGroupByFilterSameTag() { + TSSubQuery sub = getMetricForValidate(); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "veti*")); + sub.setFilters(filters); + Map tags = new HashMap(); + tags.put("host", TagVWildcardFilter.FILTER_NAME + "(*nari)"); + sub.setTags(tags); + + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(TagVWildcardFilter.FILTER_NAME + "(*nari)", + sub.getTags().get("host")); + assertEquals(1, sub.getFilters().size()); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + // NOTE: Each of the hash and equals tests should make sure that we the code // doesn't change after validation. From 3cd59be7f336a025f940558255bea46ac27a5922 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 14 Dec 2015 21:18:47 -0800 Subject: [PATCH 362/826] Fix #642 by sorting the tags properly on the bytes, NOT the string values. Sheesh. Thanks @wuxuehong214 Signed-off-by: Chris Larsen --- .gitignore | 1 + src/tsd/UniqueIdRpc.java | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 8fdaa6b007..24afdb80df 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ guava-rpm-maker/\.project src-main src-test plugin_test.jar +/bin/ diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 4c5487abad..7e4ff2c220 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -22,6 +22,7 @@ import java.util.TreeMap; import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; import org.hbase.async.PutRequest; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -549,6 +550,7 @@ private TSMeta parseTSMetaQS(final HttpQuery query) { * @param data_query The query we're building * @throws BadRequestException if we are unable to parse the query or it is * missing components + * @todo - make this asynchronous */ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { if (query_string == null || query_string.isEmpty()) { @@ -565,16 +567,23 @@ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { } catch (IllegalArgumentException e) { throw new BadRequestException(e); } - final TreeMap sortedTags = new TreeMap(tags); + + // sort the UIDs on tagk values + final ByteMap tag_uids = new ByteMap(); + for (final Entry pair : tags.entrySet()) { + tag_uids.put(tsdb.getUID(UniqueIdType.TAGK, pair.getKey()), + tsdb.getUID(UniqueIdType.TAGV, pair.getValue())); + } + // Byte Buffer to generate TSUID, pre allocated to the size of the TSUID final ByteArrayOutputStream buf = new ByteArrayOutputStream( - TSDB.metrics_width() + sortedTags.size() * + TSDB.metrics_width() + tag_uids.size() * (TSDB.tagk_width() + TSDB.tagv_width())); try { - buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); - for (Entry e: sortedTags.entrySet()) { - buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, 3); - buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, 3); + buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); + for (final Entry uids: tag_uids.entrySet()) { + buf.write(uids.getKey()); + buf.write(uids.getValue()); } } catch (IOException e) { throw new BadRequestException(e); From 99d24644d7c01fb2ad3cd9a940a1462c44a62910 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 14 Dec 2015 21:18:47 -0800 Subject: [PATCH 363/826] Fix #642 by sorting the tags properly on the bytes, NOT the string values. Sheesh. Thanks @wuxuehong214 Signed-off-by: Chris Larsen --- src/tsd/UniqueIdRpc.java | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 5f49d0af6c..b1c2ddaf67 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -22,6 +22,7 @@ import java.util.TreeMap; import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; import org.hbase.async.PutRequest; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -549,6 +550,7 @@ private TSMeta parseTSMetaQS(final HttpQuery query) { * @param data_query The query we're building * @throws BadRequestException if we are unable to parse the query or it is * missing components + * @todo - make this asynchronous */ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { if (query_string == null || query_string.isEmpty()) { @@ -565,17 +567,23 @@ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { } catch (IllegalArgumentException e) { throw new BadRequestException(e); } - final TreeMap sortedTags = new TreeMap(tags); + + // sort the UIDs on tagk values + final ByteMap tag_uids = new ByteMap(); + for (final Entry pair : tags.entrySet()) { + tag_uids.put(tsdb.getUID(UniqueIdType.TAGK, pair.getKey()), + tsdb.getUID(UniqueIdType.TAGV, pair.getValue())); + } + // Byte Buffer to generate TSUID, pre allocated to the size of the TSUID final ByteArrayOutputStream buf = new ByteArrayOutputStream( - TSDB.metrics_width() + sortedTags.size() * + TSDB.metrics_width() + tag_uids.size() * (TSDB.tagk_width() + TSDB.tagv_width())); try { - buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); - for (Entry e: sortedTags.entrySet()) { - // Fix for net.opentsdb.tsd.TestUniqueIdRpc.tsuidPostByM() - buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, TSDB.tagk_width()); - buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, TSDB.tagv_width()); + buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); + for (final Entry uids: tag_uids.entrySet()) { + buf.write(uids.getKey()); + buf.write(uids.getValue()); } } catch (IOException e) { throw new BadRequestException(e); From 98b2b79f98cbfb63479e3ee22627019baf60f1fe Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 14 Dec 2015 21:18:47 -0800 Subject: [PATCH 364/826] Fix #642 by sorting the tags properly on the bytes, NOT the string values. Sheesh. Thanks @wuxuehong214 Signed-off-by: Chris Larsen --- src/tsd/UniqueIdRpc.java | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 5f49d0af6c..b1c2ddaf67 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -22,6 +22,7 @@ import java.util.TreeMap; import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; import org.hbase.async.PutRequest; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -549,6 +550,7 @@ private TSMeta parseTSMetaQS(final HttpQuery query) { * @param data_query The query we're building * @throws BadRequestException if we are unable to parse the query or it is * missing components + * @todo - make this asynchronous */ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { if (query_string == null || query_string.isEmpty()) { @@ -565,17 +567,23 @@ private String getTSUIDForMetric(final String query_string, TSDB tsdb) { } catch (IllegalArgumentException e) { throw new BadRequestException(e); } - final TreeMap sortedTags = new TreeMap(tags); + + // sort the UIDs on tagk values + final ByteMap tag_uids = new ByteMap(); + for (final Entry pair : tags.entrySet()) { + tag_uids.put(tsdb.getUID(UniqueIdType.TAGK, pair.getKey()), + tsdb.getUID(UniqueIdType.TAGV, pair.getValue())); + } + // Byte Buffer to generate TSUID, pre allocated to the size of the TSUID final ByteArrayOutputStream buf = new ByteArrayOutputStream( - TSDB.metrics_width() + sortedTags.size() * + TSDB.metrics_width() + tag_uids.size() * (TSDB.tagk_width() + TSDB.tagv_width())); try { - buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); - for (Entry e: sortedTags.entrySet()) { - // Fix for net.opentsdb.tsd.TestUniqueIdRpc.tsuidPostByM() - buf.write(tsdb.getUID(UniqueIdType.TAGK, e.getKey()), 0, TSDB.tagk_width()); - buf.write(tsdb.getUID(UniqueIdType.TAGV, e.getValue()), 0, TSDB.tagv_width()); + buf.write(tsdb.getUID(UniqueIdType.METRIC, metric)); + for (final Entry uids: tag_uids.entrySet()) { + buf.write(uids.getKey()); + buf.write(uids.getValue()); } } catch (IOException e) { throw new BadRequestException(e); From cc1fcd9c73e8328c735acee3643330c541358493 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 14 Dec 2015 20:13:15 -0800 Subject: [PATCH 365/826] A couple of additional UTs for TSSubQuery Signed-off-by: Chris Larsen --- test/core/TestTSSubQuery.java | 41 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 1b6ac0216e..675dc1ceb1 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -188,16 +188,43 @@ public void validateWithGroupByFilter() { } @Test - public void getFilterTagks() { - final TagVFilter filter = TagVFilter.Builder() - .setFilter("*nari").setType("wildcard").setTagk("host").build(); - Whitebox.setInternalState(filter, "tagk_bytes", new byte[] { 0, 0, 1 }); + public void validateWithFilterAndGroupByFilter() { TSSubQuery sub = getMetricForValidate(); - sub.setFilters(Arrays.asList(filter)); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("colo", "lga*")); + sub.setFilters(filters); + Map tags = new HashMap(); + tags.put("host", TagVWildcardFilter.FILTER_NAME + "(*nari)"); + sub.setTags(tags); + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(TagVWildcardFilter.FILTER_NAME + "(*nari)", + sub.getTags().get("host")); + assertEquals(1, sub.getFilters().size()); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); + } + + @Test + public void validateWithFilterAndGroupByFilterSameTag() { + TSSubQuery sub = getMetricForValidate(); + final List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "veti*")); + sub.setFilters(filters); + Map tags = new HashMap(); + tags.put("host", TagVWildcardFilter.FILTER_NAME + "(*nari)"); + sub.setTags(tags); - assertEquals(1, sub.getFilterTagKs().size()); - assertArrayEquals(new byte[] { 0, 0, 1 }, sub.getFilterTagKs().iterator().next()); + sub.validateAndSetQuery(); + assertEquals("sys.cpu.0", sub.getMetric()); + assertEquals(TagVWildcardFilter.FILTER_NAME + "(*nari)", + sub.getTags().get("host")); + assertEquals(1, sub.getFilters().size()); + assertEquals(Aggregators.SUM, sub.aggregator()); + assertEquals(Aggregators.AVG, sub.downsampler()); + assertEquals(300000, sub.downsampleInterval()); } // NOTE: Each of the hash and equals tests should make sure that we the code From 7cc20d83172c1cfbdd5756f752ee81676e653eea Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 15 Dec 2015 12:49:26 -0800 Subject: [PATCH 366/826] Fix #615 by adding a checkbox to the UI to allow group by or non group by operations. Also modify the URI parsing params to be static in QueryRpc.java so they can be used by the GraphHandler class. Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 74 ++--------------- src/tsd/QueryRpc.java | 12 +-- src/tsd/client/MetricForm.java | 141 ++++++++++++++++++++++++++++++--- 3 files changed, 140 insertions(+), 87 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index a69bb7f693..6f02526a3b 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -24,7 +24,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; @@ -39,20 +38,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.core.Aggregator; -import net.opentsdb.core.Aggregators; import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; -import net.opentsdb.core.DownsamplingSpecification; import net.opentsdb.core.Query; -import net.opentsdb.core.RateOptions; import net.opentsdb.core.TSDB; -import net.opentsdb.core.Tags; +import net.opentsdb.core.TSQuery; import net.opentsdb.graph.Plot; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.StatsCollector; -import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; @@ -857,69 +851,11 @@ private static void printMetricHeader(final PrintWriter writer, final String met * @throws IllegalArgumentException if the metric or tags were malformed. */ private static Query[] parseQuery(final TSDB tsdb, final HttpQuery query) { - final List ms = query.getQueryStringParams("m"); - if (ms == null) { - throw BadRequestException.missingParameter("m"); - } - final Query[] tsdbqueries = new Query[ms.size()]; - int nqueries = 0; - for (final String m : ms) { - // m is of the following forms: - // agg:[interval-agg:][rate[{counter[,[countermax][,resetvalue]]}]:] - // metric[{tag=value,...}] - // Where the parts in square brackets `[' .. `]' are optional. - final String[] parts = Tags.splitString(m, ':'); - int i = parts.length; - if (i < 2 || i > 4) { - throw new BadRequestException("Invalid parameter m=" + m + " (" - + (i < 2 ? "not enough" : "too many") + " :-separated parts)"); - } - final Aggregator agg = getAggregator(parts[0]); - i--; // Move to the last part (the metric name). - final HashMap parsedtags = new HashMap(); - final String metric = Tags.parseWithMetric(parts[i], parsedtags); - final boolean rate = parts[--i].startsWith("rate"); - final RateOptions rate_options = QueryRpc.parseRateOptions(rate, parts[i]); - if (rate) { - i--; // Move to the next part. - } - final Query tsdbquery = tsdb.newQuery(); - try { - tsdbquery.setTimeSeries(metric, parsedtags, agg, rate, rate_options); - } catch (NoSuchUniqueName e) { - throw new BadRequestException(e.getMessage()); - } - // downsampling function & interval. - if (i > 0) { - // downsampler given, so parse it - final DownsamplingSpecification ds_spec = - new DownsamplingSpecification(parts[1]); - - tsdbquery.downsample(ds_spec.getInterval(), ds_spec.getFunction(), - ds_spec.getFillPolicy()); - } else { - // no downsampler - tsdbquery.downsample(1000, agg, - DownsamplingSpecification.DEFAULT_FILL_POLICY); - } - tsdbqueries[nqueries++] = tsdbquery; - } - return tsdbqueries; + final TSQuery q = QueryRpc.parseQuery(tsdb, query); + q.validateAndSetQuery(); + return q.buildQueries(tsdb); } - - /** - * Returns the aggregator with the given name. - * @param name Name of the aggregator to get. - * @throws BadRequestException if there's no aggregator with this name. - */ - private static final Aggregator getAggregator(final String name) { - try { - return Aggregators.get(name); - } catch (NoSuchElementException e) { - throw new BadRequestException("No such aggregation function: " + name); - } - } - + private static final PlotThdFactory thread_factory = new PlotThdFactory(); private static final class PlotThdFactory implements ThreadFactory { diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 58f1ae38fb..8baa487ea3 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -115,7 +115,7 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { query.apiVersion() + " is not implemented"); } } else { - data_query = this.parseQuery(tsdb, query); + data_query = parseQuery(tsdb, query); } if (query.getAPIMethod() == HttpMethod.DELETE && @@ -435,7 +435,7 @@ public String toString() { * @return A TSQuery if parsing was successful * @throws BadRequestException if parsing was unsuccessful */ - private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { + public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { final TSQuery data_query = new TSQuery(); data_query.setStart(query.getRequiredQueryStringParam("start")); @@ -477,14 +477,14 @@ private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { if (query.hasQueryStringParam("tsuid")) { final List tsuids = query.getQueryStringParams("tsuid"); for (String q : tsuids) { - this.parseTsuidTypeSubQuery(q, data_query); + parseTsuidTypeSubQuery(q, data_query); } } if (query.hasQueryStringParam("m")) { final List legacy_queries = query.getQueryStringParams("m"); for (String q : legacy_queries) { - this.parseMTypeSubQuery(q, data_query); + parseMTypeSubQuery(q, data_query); } } @@ -503,7 +503,7 @@ private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { * @throws BadRequestException if we are unable to parse the query or it is * missing components */ - private void parseMTypeSubQuery(final String query_string, + private static void parseMTypeSubQuery(final String query_string, TSQuery data_query) { if (query_string == null || query_string.isEmpty()) { throw new BadRequestException("The query string was empty"); @@ -556,7 +556,7 @@ private void parseMTypeSubQuery(final String query_string, * @throws BadRequestException if we are unable to parse the query or it is * missing components */ - private void parseTsuidTypeSubQuery(final String query_string, + private static void parseTsuidTypeSubQuery(final String query_string, TSQuery data_query) { if (query_string == null || query_string.isEmpty()) { throw new BadRequestException("The tsuid query string was empty"); diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index 9a06606412..fd51faeef2 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -119,17 +119,82 @@ private String parseWithMetric(final String metric) { clearTags(); return metric.substring(0, len - 2); } + final int num_tags_before = getNumTags(); + + final List filters = new ArrayList(); + final int close = metric.indexOf('}'); + int i = 0; + if (close != metric.length() - 1) { // "foo{...}{tagk=filter}" + final int filter_bracket = metric.lastIndexOf('{'); + for (final String filter : metric.substring(filter_bracket + 1, + metric.length() - 1).split(",")) { + if (filter.isEmpty()) { + break; + } + final String[] kv = filter.split("="); + if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) { + continue; // Invalid tag. + } + final Filter f = new Filter(); + f.tagk = kv[0]; + f.tagv = kv[1]; + f.is_groupby = false; + filters.add(f); + i++; + } + } + + i = 0; + for (final String tag : metric.substring(curly + 1, close).split(",")) { + if (tag.isEmpty() && close != metric.length() - 1){ + break; + } + final String[] kv = tag.split("="); + if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) { + continue; // Invalid tag. + } + final Filter f = new Filter(); + f.tagk = kv[0]; + f.tagv = kv[1]; + f.is_groupby = true; + filters.add(f); + i++; + } + + i = 0; + for (int x = filters.size() - 1; x >= 0; x--) { + final Filter filter = filters.get(x); + if (i < num_tags_before) { + setTag(i++, filter.tagk, filter.tagv, filter.is_groupby); + } else { + addTag(filter.tagk, filter.tagv, filter.is_groupby); + } + } + + if (i < num_tags_before) { + setTag(i, "", "", true); + } else { + addTag(); + } + // Remove extra tags. + for (i++; i < num_tags_before; i++) { + tagtable.removeRow(i + 1); + } + // Return the "foo" part of "foo{a=b,...,x=y}" + return metric.substring(0, curly); + + /* // substring the tags out of "foo{a=b,...,x=y}" and parse them. int i = 0; // Tag index. final int num_tags_before = getNumTags(); for (final String tag : metric.substring(curly + 1, len - 1).split(",")) { final String[] kv = tag.split("="); if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) { - setTag(i, "", ""); + setTag(i, "", "", true); continue; // Invalid tag. } if (i < num_tags_before) { - setTag(i, kv[0], kv[1]); + setTag(i, kv[0], kv[1], true); } else { addTag(kv[0], kv[1]); } @@ -137,7 +202,7 @@ private String parseWithMetric(final String metric) { } // Leave an empty line at the end. if (i < num_tags_before) { - setTag(i, "", ""); + setTag(i, "", "", true); } else { addTag(); } @@ -146,7 +211,7 @@ private String parseWithMetric(final String metric) { tagtable.removeRow(i + 1); } // Return the "foo" part of "foo{a=b,...,x=y}" - return metric.substring(0, curly); + return metric.substring(0, curly); */ } public void updateFromQueryString(final String m, final String o) { @@ -353,13 +418,42 @@ public boolean buildQueryString(final StringBuilder url) { } } url.append(':').append(metric); + boolean non_groupbys = false; + int groupby_tags = 0; { final int ntags = getNumTags(); url.append('{'); for (int tag = 0; tag < ntags; tag++) { final String tagname = getTagName(tag); final String tagvalue = getTagValue(tag); - if (tagname.isEmpty() || tagvalue.isEmpty()) { + if (tagname.isEmpty() || tagvalue.isEmpty() || !isTagGroupby(tag)) { + if (!isTagGroupby(tag)) { + non_groupbys = true; + } + continue; + } + url.append(tagname).append('=').append(tagvalue) + .append(','); + ++groupby_tags; + } + final int last = url.length() - 1; + if (url.charAt(last) == '{') { // There was no tag. + url.setLength(last); // So remove the `{'. + } else { // Need to replace the last `,' with a `}'. + url.setCharAt(url.length() - 1, '}'); + } + } + if (non_groupbys) { + if (groupby_tags == 0) { + // need this to shift group by to non-group by + url.append("{}"); + } + final int ntags = getNumTags(); + url.append('{'); + for (int tag = 0; tag < ntags; tag++) { + final String tagname = getTagName(tag); + final String tagvalue = getTagValue(tag); + if (tagname.isEmpty() || tagvalue.isEmpty() || isTagGroupby(tag)) { continue; } url.append(tagname).append('=').append(tagvalue) @@ -390,6 +484,10 @@ private String getTagName(final int i) { private String getTagValue(final int i) { return ((SuggestBox) tagtable.getWidget(i + 1, 2)).getValue(); } + + private boolean isTagGroupby(final int i) { + return ((CheckBox) tagtable.getWidget(i + 1, 3)).getValue(); + } private void setTagName(final int i, final String value) { ((SuggestBox) tagtable.getWidget(i + 1, 1)).setValue(value); @@ -399,6 +497,10 @@ private void setTagValue(final int i, final String value) { ((SuggestBox) tagtable.getWidget(i + 1, 2)).setValue(value); } + private void isTagGroupby(final int i, final boolean groupby) { + ((CheckBox) tagtable.getWidget(i + 1, 3)).setValue(groupby); + } + /** * Changes the name/value of an existing tag. * @param i The index of the tag to change. @@ -406,27 +508,34 @@ private void setTagValue(final int i, final String value) { * @param value The new value of the tag. * Requires: {@code i < getNumTags()}. */ - private void setTag(final int i, final String name, final String value) { + private void setTag(final int i, final String name, final String value, + final boolean groupby) { setTagName(i, name); setTagValue(i, value); + isTagGroupby(i, groupby); } private void addTag() { - addTag(null, null); + addTag(null, null, true); } private void addTag(final String default_tagname) { - addTag(default_tagname, null); + addTag(default_tagname, null, true); } private void addTag(final String default_tagname, - final String default_value) { + final String default_value, + final boolean is_groupby) { final int row = tagtable.getRowCount(); final ValidatedTextBox tagname = new ValidatedTextBox(); final SuggestBox suggesttagk = RemoteOracle.newSuggestBox("tagk", tagname); final ValidatedTextBox tagvalue = new ValidatedTextBox(); final SuggestBox suggesttagv = RemoteOracle.newSuggestBox("tagv", tagvalue); + final CheckBox groupby = new CheckBox(); + groupby.setValue(is_groupby); + groupby.setTitle("Group by"); + groupby.addClickHandler(events_handler); tagname.setValidationRegexp(TSDB_ID_RE); tagvalue.setValidationRegexp(TSDB_TAGVALUE_RE); tagname.setWidth("100%"); @@ -440,6 +549,7 @@ private void addTag(final String default_tagname, tagtable.setWidget(row, 1, suggesttagk); tagtable.setWidget(row, 2, suggesttagv); + tagtable.setWidget(row, 3, groupby); if (row > 2) { final Button remove = new Button("x"); remove.addClickHandler(removetag); @@ -457,7 +567,7 @@ private void addTag(final String default_tagname, } private void clearTags() { - setTag(0, "", ""); + setTag(0, "", "", true); for (int i = getNumTags() - 1; i > 1; i++) { tagtable.removeRow(i + 1); } @@ -493,9 +603,10 @@ public void onBlur(final BlurEvent event) { for (int tag = 1; tag < ntags; tag++) { final String tagname = getTagName(tag); final String tagvalue = getTagValue(tag); - setTag(tag - 1, tagname, tagvalue); + // todo - groupby + setTag(tag - 1, tagname, tagvalue, isTagGroupby(tag)); } - setTag(ntags - 1, "", ""); + setTag(ntags - 1, "", "", true); } // Try to remove empty lines from the tag table (but never remove the // first line or last line, even if they're empty). Walk the table @@ -620,6 +731,12 @@ static final public LocalRateOptions parseRateOptions(boolean rate, String spec) } } + private static class Filter { + String tagk; + String tagv; + boolean is_groupby; + } + // ------------------- // // Focusable interface // // ------------------- // From 99a989fb2b43ef1511dc82e88b4ecf94e8b16e1a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 15 Dec 2015 12:49:26 -0800 Subject: [PATCH 367/826] Fix #615 by adding a checkbox to the UI to allow group by or non group by operations. Also modify the URI parsing params to be static in QueryRpc.java so they can be used by the GraphHandler class. Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 74 ++--------------- src/tsd/QueryRpc.java | 12 +-- src/tsd/client/MetricForm.java | 141 ++++++++++++++++++++++++++++++--- 3 files changed, 140 insertions(+), 87 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index a69bb7f693..6f02526a3b 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -24,7 +24,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; @@ -39,20 +38,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.core.Aggregator; -import net.opentsdb.core.Aggregators; import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; -import net.opentsdb.core.DownsamplingSpecification; import net.opentsdb.core.Query; -import net.opentsdb.core.RateOptions; import net.opentsdb.core.TSDB; -import net.opentsdb.core.Tags; +import net.opentsdb.core.TSQuery; import net.opentsdb.graph.Plot; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.StatsCollector; -import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; @@ -857,69 +851,11 @@ private static void printMetricHeader(final PrintWriter writer, final String met * @throws IllegalArgumentException if the metric or tags were malformed. */ private static Query[] parseQuery(final TSDB tsdb, final HttpQuery query) { - final List ms = query.getQueryStringParams("m"); - if (ms == null) { - throw BadRequestException.missingParameter("m"); - } - final Query[] tsdbqueries = new Query[ms.size()]; - int nqueries = 0; - for (final String m : ms) { - // m is of the following forms: - // agg:[interval-agg:][rate[{counter[,[countermax][,resetvalue]]}]:] - // metric[{tag=value,...}] - // Where the parts in square brackets `[' .. `]' are optional. - final String[] parts = Tags.splitString(m, ':'); - int i = parts.length; - if (i < 2 || i > 4) { - throw new BadRequestException("Invalid parameter m=" + m + " (" - + (i < 2 ? "not enough" : "too many") + " :-separated parts)"); - } - final Aggregator agg = getAggregator(parts[0]); - i--; // Move to the last part (the metric name). - final HashMap parsedtags = new HashMap(); - final String metric = Tags.parseWithMetric(parts[i], parsedtags); - final boolean rate = parts[--i].startsWith("rate"); - final RateOptions rate_options = QueryRpc.parseRateOptions(rate, parts[i]); - if (rate) { - i--; // Move to the next part. - } - final Query tsdbquery = tsdb.newQuery(); - try { - tsdbquery.setTimeSeries(metric, parsedtags, agg, rate, rate_options); - } catch (NoSuchUniqueName e) { - throw new BadRequestException(e.getMessage()); - } - // downsampling function & interval. - if (i > 0) { - // downsampler given, so parse it - final DownsamplingSpecification ds_spec = - new DownsamplingSpecification(parts[1]); - - tsdbquery.downsample(ds_spec.getInterval(), ds_spec.getFunction(), - ds_spec.getFillPolicy()); - } else { - // no downsampler - tsdbquery.downsample(1000, agg, - DownsamplingSpecification.DEFAULT_FILL_POLICY); - } - tsdbqueries[nqueries++] = tsdbquery; - } - return tsdbqueries; + final TSQuery q = QueryRpc.parseQuery(tsdb, query); + q.validateAndSetQuery(); + return q.buildQueries(tsdb); } - - /** - * Returns the aggregator with the given name. - * @param name Name of the aggregator to get. - * @throws BadRequestException if there's no aggregator with this name. - */ - private static final Aggregator getAggregator(final String name) { - try { - return Aggregators.get(name); - } catch (NoSuchElementException e) { - throw new BadRequestException("No such aggregation function: " + name); - } - } - + private static final PlotThdFactory thread_factory = new PlotThdFactory(); private static final class PlotThdFactory implements ThreadFactory { diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 554e586b9a..1346352a0f 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -129,7 +129,7 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, expressions = null; } else { expressions = new ArrayList(); - data_query = this.parseQuery(tsdb, query, expressions); + data_query = parseQuery(tsdb, query); } if (query.getAPIMethod() == HttpMethod.DELETE && @@ -477,7 +477,7 @@ public String toString() { * @return A TSQuery if parsing was successful * @throws BadRequestException if parsing was unsuccessful */ - private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, + public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { final List expressions) { final TSQuery data_query = new TSQuery(); @@ -520,14 +520,14 @@ private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, if (query.hasQueryStringParam("tsuid")) { final List tsuids = query.getQueryStringParams("tsuid"); for (String q : tsuids) { - this.parseTsuidTypeSubQuery(q, data_query); + parseTsuidTypeSubQuery(q, data_query); } } if (query.hasQueryStringParam("m")) { final List legacy_queries = query.getQueryStringParams("m"); for (String q : legacy_queries) { - this.parseMTypeSubQuery(q, data_query); + parseMTypeSubQuery(q, data_query); } } @@ -570,7 +570,7 @@ private TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, * @throws BadRequestException if we are unable to parse the query or it is * missing components */ - private void parseMTypeSubQuery(final String query_string, + private static void parseMTypeSubQuery(final String query_string, TSQuery data_query) { if (query_string == null || query_string.isEmpty()) { throw new BadRequestException("The query string was empty"); @@ -623,7 +623,7 @@ private void parseMTypeSubQuery(final String query_string, * @throws BadRequestException if we are unable to parse the query or it is * missing components */ - private void parseTsuidTypeSubQuery(final String query_string, + private static void parseTsuidTypeSubQuery(final String query_string, TSQuery data_query) { if (query_string == null || query_string.isEmpty()) { throw new BadRequestException("The tsuid query string was empty"); diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index 9a06606412..fd51faeef2 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -119,17 +119,82 @@ private String parseWithMetric(final String metric) { clearTags(); return metric.substring(0, len - 2); } + final int num_tags_before = getNumTags(); + + final List filters = new ArrayList(); + final int close = metric.indexOf('}'); + int i = 0; + if (close != metric.length() - 1) { // "foo{...}{tagk=filter}" + final int filter_bracket = metric.lastIndexOf('{'); + for (final String filter : metric.substring(filter_bracket + 1, + metric.length() - 1).split(",")) { + if (filter.isEmpty()) { + break; + } + final String[] kv = filter.split("="); + if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) { + continue; // Invalid tag. + } + final Filter f = new Filter(); + f.tagk = kv[0]; + f.tagv = kv[1]; + f.is_groupby = false; + filters.add(f); + i++; + } + } + + i = 0; + for (final String tag : metric.substring(curly + 1, close).split(",")) { + if (tag.isEmpty() && close != metric.length() - 1){ + break; + } + final String[] kv = tag.split("="); + if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) { + continue; // Invalid tag. + } + final Filter f = new Filter(); + f.tagk = kv[0]; + f.tagv = kv[1]; + f.is_groupby = true; + filters.add(f); + i++; + } + + i = 0; + for (int x = filters.size() - 1; x >= 0; x--) { + final Filter filter = filters.get(x); + if (i < num_tags_before) { + setTag(i++, filter.tagk, filter.tagv, filter.is_groupby); + } else { + addTag(filter.tagk, filter.tagv, filter.is_groupby); + } + } + + if (i < num_tags_before) { + setTag(i, "", "", true); + } else { + addTag(); + } + // Remove extra tags. + for (i++; i < num_tags_before; i++) { + tagtable.removeRow(i + 1); + } + // Return the "foo" part of "foo{a=b,...,x=y}" + return metric.substring(0, curly); + + /* // substring the tags out of "foo{a=b,...,x=y}" and parse them. int i = 0; // Tag index. final int num_tags_before = getNumTags(); for (final String tag : metric.substring(curly + 1, len - 1).split(",")) { final String[] kv = tag.split("="); if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) { - setTag(i, "", ""); + setTag(i, "", "", true); continue; // Invalid tag. } if (i < num_tags_before) { - setTag(i, kv[0], kv[1]); + setTag(i, kv[0], kv[1], true); } else { addTag(kv[0], kv[1]); } @@ -137,7 +202,7 @@ private String parseWithMetric(final String metric) { } // Leave an empty line at the end. if (i < num_tags_before) { - setTag(i, "", ""); + setTag(i, "", "", true); } else { addTag(); } @@ -146,7 +211,7 @@ private String parseWithMetric(final String metric) { tagtable.removeRow(i + 1); } // Return the "foo" part of "foo{a=b,...,x=y}" - return metric.substring(0, curly); + return metric.substring(0, curly); */ } public void updateFromQueryString(final String m, final String o) { @@ -353,13 +418,42 @@ public boolean buildQueryString(final StringBuilder url) { } } url.append(':').append(metric); + boolean non_groupbys = false; + int groupby_tags = 0; { final int ntags = getNumTags(); url.append('{'); for (int tag = 0; tag < ntags; tag++) { final String tagname = getTagName(tag); final String tagvalue = getTagValue(tag); - if (tagname.isEmpty() || tagvalue.isEmpty()) { + if (tagname.isEmpty() || tagvalue.isEmpty() || !isTagGroupby(tag)) { + if (!isTagGroupby(tag)) { + non_groupbys = true; + } + continue; + } + url.append(tagname).append('=').append(tagvalue) + .append(','); + ++groupby_tags; + } + final int last = url.length() - 1; + if (url.charAt(last) == '{') { // There was no tag. + url.setLength(last); // So remove the `{'. + } else { // Need to replace the last `,' with a `}'. + url.setCharAt(url.length() - 1, '}'); + } + } + if (non_groupbys) { + if (groupby_tags == 0) { + // need this to shift group by to non-group by + url.append("{}"); + } + final int ntags = getNumTags(); + url.append('{'); + for (int tag = 0; tag < ntags; tag++) { + final String tagname = getTagName(tag); + final String tagvalue = getTagValue(tag); + if (tagname.isEmpty() || tagvalue.isEmpty() || isTagGroupby(tag)) { continue; } url.append(tagname).append('=').append(tagvalue) @@ -390,6 +484,10 @@ private String getTagName(final int i) { private String getTagValue(final int i) { return ((SuggestBox) tagtable.getWidget(i + 1, 2)).getValue(); } + + private boolean isTagGroupby(final int i) { + return ((CheckBox) tagtable.getWidget(i + 1, 3)).getValue(); + } private void setTagName(final int i, final String value) { ((SuggestBox) tagtable.getWidget(i + 1, 1)).setValue(value); @@ -399,6 +497,10 @@ private void setTagValue(final int i, final String value) { ((SuggestBox) tagtable.getWidget(i + 1, 2)).setValue(value); } + private void isTagGroupby(final int i, final boolean groupby) { + ((CheckBox) tagtable.getWidget(i + 1, 3)).setValue(groupby); + } + /** * Changes the name/value of an existing tag. * @param i The index of the tag to change. @@ -406,27 +508,34 @@ private void setTagValue(final int i, final String value) { * @param value The new value of the tag. * Requires: {@code i < getNumTags()}. */ - private void setTag(final int i, final String name, final String value) { + private void setTag(final int i, final String name, final String value, + final boolean groupby) { setTagName(i, name); setTagValue(i, value); + isTagGroupby(i, groupby); } private void addTag() { - addTag(null, null); + addTag(null, null, true); } private void addTag(final String default_tagname) { - addTag(default_tagname, null); + addTag(default_tagname, null, true); } private void addTag(final String default_tagname, - final String default_value) { + final String default_value, + final boolean is_groupby) { final int row = tagtable.getRowCount(); final ValidatedTextBox tagname = new ValidatedTextBox(); final SuggestBox suggesttagk = RemoteOracle.newSuggestBox("tagk", tagname); final ValidatedTextBox tagvalue = new ValidatedTextBox(); final SuggestBox suggesttagv = RemoteOracle.newSuggestBox("tagv", tagvalue); + final CheckBox groupby = new CheckBox(); + groupby.setValue(is_groupby); + groupby.setTitle("Group by"); + groupby.addClickHandler(events_handler); tagname.setValidationRegexp(TSDB_ID_RE); tagvalue.setValidationRegexp(TSDB_TAGVALUE_RE); tagname.setWidth("100%"); @@ -440,6 +549,7 @@ private void addTag(final String default_tagname, tagtable.setWidget(row, 1, suggesttagk); tagtable.setWidget(row, 2, suggesttagv); + tagtable.setWidget(row, 3, groupby); if (row > 2) { final Button remove = new Button("x"); remove.addClickHandler(removetag); @@ -457,7 +567,7 @@ private void addTag(final String default_tagname, } private void clearTags() { - setTag(0, "", ""); + setTag(0, "", "", true); for (int i = getNumTags() - 1; i > 1; i++) { tagtable.removeRow(i + 1); } @@ -493,9 +603,10 @@ public void onBlur(final BlurEvent event) { for (int tag = 1; tag < ntags; tag++) { final String tagname = getTagName(tag); final String tagvalue = getTagValue(tag); - setTag(tag - 1, tagname, tagvalue); + // todo - groupby + setTag(tag - 1, tagname, tagvalue, isTagGroupby(tag)); } - setTag(ntags - 1, "", ""); + setTag(ntags - 1, "", "", true); } // Try to remove empty lines from the tag table (but never remove the // first line or last line, even if they're empty). Walk the table @@ -620,6 +731,12 @@ static final public LocalRateOptions parseRateOptions(boolean rate, String spec) } } + private static class Filter { + String tagk; + String tagv; + boolean is_groupby; + } + // ------------------- // // Focusable interface // // ------------------- // From 6355773624a289202ac139eb83d794adaf6fe289 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 15 Dec 2015 16:36:58 -0800 Subject: [PATCH 368/826] Add miss merge fixes... doh Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 2 +- src/tsd/QueryRpc.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 6f02526a3b..d6ce433e13 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -851,7 +851,7 @@ private static void printMetricHeader(final PrintWriter writer, final String met * @throws IllegalArgumentException if the metric or tags were malformed. */ private static Query[] parseQuery(final TSDB tsdb, final HttpQuery query) { - final TSQuery q = QueryRpc.parseQuery(tsdb, query); + final TSQuery q = QueryRpc.parseQuery(tsdb, query, null); q.validateAndSetQuery(); return q.buildQueries(tsdb); } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 1346352a0f..2c93eeef1e 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -129,7 +129,7 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, expressions = null; } else { expressions = new ArrayList(); - data_query = parseQuery(tsdb, query); + data_query = parseQuery(tsdb, query, expressions); } if (query.getAPIMethod() == HttpMethod.DELETE && @@ -477,7 +477,7 @@ public String toString() { * @return A TSQuery if parsing was successful * @throws BadRequestException if parsing was unsuccessful */ - public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { + public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, final List expressions) { final TSQuery data_query = new TSQuery(); From 6ffe3cbae1eb67ce3a500c68c4923723c5232694 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 15 Dec 2015 16:49:35 -0800 Subject: [PATCH 369/826] Close #658, an ugly oversight around case insensitivity in the iwildcard filter. Signed-off-by: Chris Larsen --- src/query/filter/TagVWildcardFilter.java | 2 +- test/query/filter/TestTagVLiteralOrFilter.java | 9 +++++++++ test/query/filter/TestTagVWildcardFilter.java | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/query/filter/TagVWildcardFilter.java b/src/query/filter/TagVWildcardFilter.java index 2fae7a099f..82d8da97ab 100644 --- a/src/query/filter/TagVWildcardFilter.java +++ b/src/query/filter/TagVWildcardFilter.java @@ -119,7 +119,7 @@ public Deferred match(final Map tags) { // match all return Deferred.fromResult(true); } else if (case_insensitive) { - tags.get(tagk).toLowerCase(); + tagv = tags.get(tagk).toLowerCase(); } if (has_postfix && !has_prefix && !tagv.endsWith(components[components.length-1])) { diff --git a/test/query/filter/TestTagVLiteralOrFilter.java b/test/query/filter/TestTagVLiteralOrFilter.java index 2208657bc7..fbc14bfc23 100644 --- a/test/query/filter/TestTagVLiteralOrFilter.java +++ b/test/query/filter/TestTagVLiteralOrFilter.java @@ -85,6 +85,15 @@ public void matchCaseInsensitive() throws Exception { assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } + @Test + public void matchCaseInsensitiveValue() throws Exception { + tags.put(TAGK, "CMTDIBBLER"); + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|CMtDibbler|Slant", true); + assertTrue(filter.match(tags).join()); + assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + @Test public void matchCaseInsensitiveFail() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, diff --git a/test/query/filter/TestTagVWildcardFilter.java b/test/query/filter/TestTagVWildcardFilter.java index 3270d49149..2c1daf1c52 100644 --- a/test/query/filter/TestTagVWildcardFilter.java +++ b/test/query/filter/TestTagVWildcardFilter.java @@ -259,6 +259,14 @@ public void matchPostfixCaseInsensitive() throws Exception { assertTrue(filter.match(tags).join()); } + @Test + public void matchPostfixCaseInsensitiveValue() throws Exception { + tags.put(TAGK, "ogg-01.ops.ankh.MORPORK.com"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*.MorPork.com", true); + assertTrue(filter.match(tags).join()); + } + @Test public void matchPrefixCaseInsensitive() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, From 53b1959773359a19683e8d4de94ec40915a88b4f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 15 Dec 2015 16:49:35 -0800 Subject: [PATCH 370/826] Close #658, an ugly oversight around case insensitivity in the iwildcard filter. Signed-off-by: Chris Larsen --- src/query/filter/TagVWildcardFilter.java | 2 +- test/query/filter/TestTagVLiteralOrFilter.java | 9 +++++++++ test/query/filter/TestTagVWildcardFilter.java | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/query/filter/TagVWildcardFilter.java b/src/query/filter/TagVWildcardFilter.java index 2fae7a099f..82d8da97ab 100644 --- a/src/query/filter/TagVWildcardFilter.java +++ b/src/query/filter/TagVWildcardFilter.java @@ -119,7 +119,7 @@ public Deferred match(final Map tags) { // match all return Deferred.fromResult(true); } else if (case_insensitive) { - tags.get(tagk).toLowerCase(); + tagv = tags.get(tagk).toLowerCase(); } if (has_postfix && !has_prefix && !tagv.endsWith(components[components.length-1])) { diff --git a/test/query/filter/TestTagVLiteralOrFilter.java b/test/query/filter/TestTagVLiteralOrFilter.java index 2208657bc7..fbc14bfc23 100644 --- a/test/query/filter/TestTagVLiteralOrFilter.java +++ b/test/query/filter/TestTagVLiteralOrFilter.java @@ -85,6 +85,15 @@ public void matchCaseInsensitive() throws Exception { assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); } + @Test + public void matchCaseInsensitiveValue() throws Exception { + tags.put(TAGK, "CMTDIBBLER"); + TagVFilter filter = new TagVLiteralOrFilter(TAGK, + "LutZe|CMtDibbler|Slant", true); + assertTrue(filter.match(tags).join()); + assertTrue(((TagVLiteralOrFilter)filter).isCaseInsensitive()); + } + @Test public void matchCaseInsensitiveFail() throws Exception { TagVFilter filter = new TagVLiteralOrFilter(TAGK, diff --git a/test/query/filter/TestTagVWildcardFilter.java b/test/query/filter/TestTagVWildcardFilter.java index 3270d49149..2c1daf1c52 100644 --- a/test/query/filter/TestTagVWildcardFilter.java +++ b/test/query/filter/TestTagVWildcardFilter.java @@ -259,6 +259,14 @@ public void matchPostfixCaseInsensitive() throws Exception { assertTrue(filter.match(tags).join()); } + @Test + public void matchPostfixCaseInsensitiveValue() throws Exception { + tags.put(TAGK, "ogg-01.ops.ankh.MORPORK.com"); + TagVFilter filter = new TagVWildcardFilter(TAGK, + "*.MorPork.com", true); + assertTrue(filter.match(tags).join()); + } + @Test public void matchPrefixCaseInsensitive() throws Exception { TagVFilter filter = new TagVWildcardFilter(TAGK, From ba2039e8a84c5705e27cf5ddd78c7edeb1eb12b6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 16 Dec 2015 13:26:04 -0800 Subject: [PATCH 371/826] Link to the new GWT Theme jar. Add the header image for the built in UI. Update the favicon. Signed-off-by: Chris Larsen --- Makefile.am | 12 ++++++++---- pom.xml.in | 6 ++++++ src/tsd/HttpQuery.java | 11 ++++------- src/tsd/QueryUi.gwt.xml | 2 +- src/tsd/RpcManager.java | 2 +- src/tsd/static/favicon.ico | Bin 1150 -> 1150 bytes src/tsd/static/opentsdb_header.jpg | Bin 0 -> 7519 bytes third_party/gwt/include.mk | 9 ++++++++- .../gwt/opentsdb-gwt-theme-1.0.0.jar.md5 | 1 + 9 files changed, 29 insertions(+), 14 deletions(-) mode change 100644 => 100755 src/tsd/static/favicon.ico create mode 100755 src/tsd/static/opentsdb_header.jpg create mode 100644 third_party/gwt/opentsdb-gwt-theme-1.0.0.jar.md5 diff --git a/Makefile.am b/Makefile.am index d5e8334f10..a2bc819fb6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -305,7 +305,9 @@ httpui_SRC := \ httpui_DEPS = src/tsd/QueryUi.gwt.xml #dist_pkgdata_DATA = src/logback.xml -dist_static_DATA = src/tsd/static/favicon.ico +dist_static_DATA = \ + src/tsd/static/favicon.ico \ + src/tsd/static/openTSDB_header.jpg EXTRA_DIST = tsdb.in $(tsdb_SRC) $(test_SRC) \ $(test_plugin_SRC) $(test_plugin_MF) $(test_plugin_SVCS:%=test/%) \ @@ -400,14 +402,14 @@ get_dep_classpath = `for jar in $(tsdb_DEPS); do $(find_jar); done | tr '\n' ':' @touch "$@" VALIDATION_API_CLASSPATH = `jar=$(VALIDATION_API); $(find_jar)`:`jar=$(VALIDATION_API_SOURCES); $(find_jar)` -GWT_CLASSPATH = $(VALIDATION_API_CLASSPATH):`jar=$(GWT_DEV); $(find_jar)`:`jar=$(GWT_USER); $(find_jar)`:$(srcdir)/src +GWT_CLASSPATH = $(VALIDATION_API_CLASSPATH):`jar=$(GWT_DEV); $(find_jar)`:`jar=$(GWT_USER); $(find_jar)`:`jar=$(GWT_THEME); $(find_jar)`:$(srcdir)/src # The GWT compiler is way too slow, that's not very Googley. So we save the # MD5 of the files we compile in the stamp file and everytime `make' things it # needs to recompile the GWT code, we verify whether the code really changed # or whether it's just a file that was touched (which happens frequently when # using Git while rebasing and whatnot). gwtc: .gwtc-stamp -.gwtc-stamp: $(httpui_SRC) $(httpui_DEPS) $(VALIDATION_API) $(VALIDATION_API_SOURCES) $(GWT_DEV) $(GWT_USER) +.gwtc-stamp: $(httpui_SRC) $(httpui_DEPS) $(VALIDATION_API) $(VALIDATION_API_SOURCES) $(GWT_DEV) $(GWT_USER) $(GWT_THEME) @$(mkdir_p) gwt { cd $(srcdir) && cat $(httpui_SRC); } | $(MD5) >"$@-t" cmp -s "$@" "$@-t" && exit 0; \ @@ -666,6 +668,7 @@ pom.xml: pom.xml.in Makefile -e 's/@ASYNCHBASE_VERSION@/$(ASYNCHBASE_VERSION)/' \ -e 's/@GUAVA_VERSION@/$(GUAVA_VERSION)/' \ -e 's/@GWT_VERSION@/$(GWT_VERSION)/' \ + -e 's/@GWT_THEME_VERSION@/$(GWT_THEME_VERSION)/' \ -e 's/@HAMCREST_VERSION@/$(HAMCREST_VERSION)/' \ -e 's/@JACKSON_VERSION@/$(JACKSON_VERSION)/' \ -e 's/@JAVASSIST_VERSION@/$(JAVASSIST_VERSION)/' \ @@ -733,7 +736,8 @@ debian: dist staticroot chmod 755 $(distdir)/debian/DEBIAN/* cp $(top_srcdir)/build-aux/deb/init.d/opentsdb $(distdir)/debian/etc/init.d cp $(jar) $(distdir)/debian/usr/share/opentsdb/lib - cp -r staticroot/favicon.ico $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/icon.ico $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/openTSDB_header.jpg $(distdir)/debian/usr/share/opentsdb/static cp -r gwt/queryui/* $(distdir)/debian/usr/share/opentsdb/static `for dep_jar in $(tsdb_DEPS); do cp $$dep_jar \ $(distdir)/debian/usr/share/opentsdb/lib; done;` diff --git a/pom.xml.in b/pom.xml.in index 9539eaa2a1..e18845d6cd 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -475,6 +475,12 @@ gwt-user @GWT_VERSION@ + + + net.opentsdb + opentsdb_gwt_theme + @GWT_THEME_VERSION@ + diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index bea01cb6f2..c2848ee7aa 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -1017,7 +1017,6 @@ protected Logger logger() { + "body{font-family:arial,sans-serif;margin-left:2em}" + "A.l:link{color:#6f6f6f}" + "A.u:link{color:green}" - + ".subg{background-color:#e2f4f7}" + ".fwf{font-family:monospace;white-space:pre-wrap}" + "//-->"; @@ -1025,12 +1024,10 @@ protected Logger logger() { "\n" + "" + "" - + "" - + "" + + "" diff --git a/src/tsd/QueryUi.gwt.xml b/src/tsd/QueryUi.gwt.xml index 0bf3faf297..781cb128d5 100644 --- a/src/tsd/QueryUi.gwt.xml +++ b/src/tsd/QueryUi.gwt.xml @@ -1,7 +1,7 @@ - + diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 96b98d6f46..61ff738dfe 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -568,7 +568,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) query.sendReply(HttpQuery.makePage( "", - "TSD", "Time Series Database", buf.toString())); + "OpenTSDB", "", buf.toString())); } } diff --git a/src/tsd/static/favicon.ico b/src/tsd/static/favicon.ico old mode 100644 new mode 100755 index 954d3c335e1a84af8551518b391caee26dab0913..b2d9ef22452baeca1a5bdd1676e7865921cb8713 GIT binary patch literal 1150 zcmbu8OHUI~6vqcO7)`V_zPhPa5*{v0V~jB|(rH5=7%ZfjU_XExh#1q9c4jKYG^QOd zvNbs%I{fbK~$4|bh`{RveL%A5N`&HNBLN*|R;5=B;?mOVS zWvEw!>&oTeYQpW#kBM&b)$r=cmU9Mth$8}`HMs~3-`*SJiLTx?6sKw^my8Yhf>9i8 z3wQ0Xu+N${{>0=`^Wz;}=%F5oMgQmQPr!ZF6d(K;>)()_=DOnI8;XNtt!!{Bt%=qI zrx?`OhSm`EgIwYld`IN>?dAC3N=({}omfK+j=p(mq$%A~(KM=Ejj^V-3ru`y4mWV< zq&1EnZDk_8<#ejM@;cYCJN|WUvhyYKQl%H3RPOELEY|$qvz19x4C+{krKdB+WOEAI zh)8ktW(*_Klg%ty7@VOP#4^;S+;7wG!mKpn0ZnXfqAT6GleMtcOm8{E(3?scNXzE1 zz?ZZKeaJCuS~;{c(m}nL%6W>Z3@t~4)GNh+BiaMn3yMK+VZ1Z_Uudr;I5a?4i$;5i zZ_3N^>D_0`(Gcwi#Q@5k1v>Ke{FwhXd@^!%jC%Jc7A_`RL&(?hO>|Rsr~@l;FV@#7 z2E0dZ2as^CLVB{*v49!{)-FK{Xuq+VxON?9iO!{j8h1Wq?m65U_Xqj*`%Ashe*C@9QOuv)Y`VXq&E6LT&X2=y`IRd@ literal 1150 zcmb`DT}V@59L8VC=q{v-E=%YlKe8mP3ojJIw3}|q{78bL6zxk&9at8XnQ6lik|0FU zY_>A3nMuv9rpwxrbodboxm9NS@FUOK+~(%f`@Z8G^CAf99G?B3J8y~>ngbBkRhp^!D&+0ZIRoPU05D~1Q{VuriM2S~k@jywHL zaQWO)-eyuEtubBtP7UYPr?)|?3BYDsu1w!Yk*&!~vOm_{Xx^ft&-b)MRm;KChUVA1MUnlHXw>E&L#sHk! zdRQ_EEZM7J$qI$rq>(t$Y>D(r{HwFuc`UE?V^g>n+Uz};e^Cbn{FrVjfw^D<1pR#x z#l^Z$rH!)6WO&_Mw?8FO%ssU?;@<8?_qy!KC+sNN;zwKrYJIv_w^?Z&xMC$chr zKvwL8-5!v37|YZzi^t3dJ!ao3R&;8$2N%u@sH${gXwbK!6IqdWPmcVt|9$uWdqOUS V{H0bAB40yD%|=2RLFjKMR&<%6~d0H9D201e<@ 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 Date: Wed, 16 Dec 2015 13:26:04 -0800 Subject: [PATCH 372/826] Link to the new GWT Theme jar. Add the header image for the built in UI. Update the favicon. Signed-off-by: Chris Larsen --- Makefile.am | 12 ++++++++---- pom.xml.in | 6 ++++++ src/tsd/HttpQuery.java | 11 ++++------- src/tsd/QueryUi.gwt.xml | 2 +- src/tsd/RpcManager.java | 2 +- src/tsd/static/favicon.ico | Bin 1150 -> 1150 bytes src/tsd/static/opentsdb_header.jpg | Bin 0 -> 7519 bytes third_party/gwt/include.mk | 9 ++++++++- .../gwt/opentsdb-gwt-theme-1.0.0.jar.md5 | 1 + 9 files changed, 29 insertions(+), 14 deletions(-) mode change 100644 => 100755 src/tsd/static/favicon.ico create mode 100755 src/tsd/static/opentsdb_header.jpg create mode 100644 third_party/gwt/opentsdb-gwt-theme-1.0.0.jar.md5 diff --git a/Makefile.am b/Makefile.am index 7ce950db64..35d7b5b055 100644 --- a/Makefile.am +++ b/Makefile.am @@ -404,7 +404,9 @@ expr_src_dir = $(builddir)/src/$(expr_package) get_expr_classes = `classes=''; for f in $(packagedir)$(expr_package)/*.class; do classes="$$classes $$f"; done; echo $$classes;` #dist_pkgdata_DATA = src/logback.xml -dist_static_DATA = src/tsd/static/favicon.ico +dist_static_DATA = \ + src/tsd/static/favicon.ico \ + src/tsd/static/openTSDB_header.jpg EXTRA_DIST = tsdb.in $(tsdb_SRC) $(test_SRC) $(expr_grammar) \ $(test_plugin_SRC) $(test_plugin_MF) $(test_plugin_SVCS:%=test/%) \ @@ -502,14 +504,14 @@ get_dep_classpath = `for jar in $(tsdb_DEPS); do $(find_jar); done | tr '\n' ':' @touch "$@" VALIDATION_API_CLASSPATH = `jar=$(VALIDATION_API); $(find_jar)`:`jar=$(VALIDATION_API_SOURCES); $(find_jar)` -GWT_CLASSPATH = $(VALIDATION_API_CLASSPATH):`jar=$(GWT_DEV); $(find_jar)`:`jar=$(GWT_USER); $(find_jar)`:$(srcdir)/src +GWT_CLASSPATH = $(VALIDATION_API_CLASSPATH):`jar=$(GWT_DEV); $(find_jar)`:`jar=$(GWT_USER); $(find_jar)`:`jar=$(GWT_THEME); $(find_jar)`:$(srcdir)/src # The GWT compiler is way too slow, that's not very Googley. So we save the # MD5 of the files we compile in the stamp file and everytime `make' things it # needs to recompile the GWT code, we verify whether the code really changed # or whether it's just a file that was touched (which happens frequently when # using Git while rebasing and whatnot). gwtc: .gwtc-stamp -.gwtc-stamp: $(httpui_SRC) $(httpui_DEPS) $(VALIDATION_API) $(VALIDATION_API_SOURCES) $(GWT_DEV) $(GWT_USER) +.gwtc-stamp: $(httpui_SRC) $(httpui_DEPS) $(VALIDATION_API) $(VALIDATION_API_SOURCES) $(GWT_DEV) $(GWT_USER) $(GWT_THEME) @$(mkdir_p) gwt { cd $(srcdir) && cat $(httpui_SRC); } | $(MD5) >"$@-t" cmp -s "$@" "$@-t" && exit 0; \ @@ -773,6 +775,7 @@ pom.xml: pom.xml.in Makefile -e 's/@ASYNCCASSANDRA_VERSION@/$(ASYNCCASSANDRA_VERSION)/' \ -e 's/@GUAVA_VERSION@/$(GUAVA_VERSION)/' \ -e 's/@GWT_VERSION@/$(GWT_VERSION)/' \ + -e 's/@GWT_THEME_VERSION@/$(GWT_THEME_VERSION)/' \ -e 's/@HAMCREST_VERSION@/$(HAMCREST_VERSION)/' \ -e 's/@JACKSON_VERSION@/$(JACKSON_VERSION)/' \ -e 's/@JAVASSIST_VERSION@/$(JAVASSIST_VERSION)/' \ @@ -845,7 +848,8 @@ debian: dist staticroot chmod 755 $(distdir)/debian/DEBIAN/* cp $(top_srcdir)/build-aux/deb/init.d/opentsdb $(distdir)/debian/etc/init.d cp $(jar) $(distdir)/debian/usr/share/opentsdb/lib - cp -r staticroot/favicon.ico $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/icon.ico $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/openTSDB_header.jpg $(distdir)/debian/usr/share/opentsdb/static cp -r gwt/queryui/* $(distdir)/debian/usr/share/opentsdb/static `for dep_jar in $(tsdb_DEPS); do cp $$dep_jar \ $(distdir)/debian/usr/share/opentsdb/lib; done;` diff --git a/pom.xml.in b/pom.xml.in index e5914cd031..c44b692ff9 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -479,6 +479,12 @@ gwt-user @GWT_VERSION@ + + + net.opentsdb + opentsdb_gwt_theme + @GWT_THEME_VERSION@ + diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index bea01cb6f2..c2848ee7aa 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -1017,7 +1017,6 @@ protected Logger logger() { + "body{font-family:arial,sans-serif;margin-left:2em}" + "A.l:link{color:#6f6f6f}" + "A.u:link{color:green}" - + ".subg{background-color:#e2f4f7}" + ".fwf{font-family:monospace;white-space:pre-wrap}" + "//-->"; @@ -1025,12 +1024,10 @@ protected Logger logger() { "\n" + "" + "
    " - + "T" - + "S" - + "D" - + "   
    "; + + "
    " + + "" + + " 
    "; private static final String PAGE_BODY_MID = "
    " - + "" - + "" + + "" diff --git a/src/tsd/QueryUi.gwt.xml b/src/tsd/QueryUi.gwt.xml index 0bf3faf297..781cb128d5 100644 --- a/src/tsd/QueryUi.gwt.xml +++ b/src/tsd/QueryUi.gwt.xml @@ -1,7 +1,7 @@ - + diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 99ecd57eb0..1814326c34 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -568,7 +568,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) query.sendReply(HttpQuery.makePage( "", - "TSD", "Time Series Database", buf.toString())); + "OpenTSDB", "", buf.toString())); } } diff --git a/src/tsd/static/favicon.ico b/src/tsd/static/favicon.ico old mode 100644 new mode 100755 index 954d3c335e1a84af8551518b391caee26dab0913..b2d9ef22452baeca1a5bdd1676e7865921cb8713 GIT binary patch literal 1150 zcmbu8OHUI~6vqcO7)`V_zPhPa5*{v0V~jB|(rH5=7%ZfjU_XExh#1q9c4jKYG^QOd zvNbs%I{fbK~$4|bh`{RveL%A5N`&HNBLN*|R;5=B;?mOVS zWvEw!>&oTeYQpW#kBM&b)$r=cmU9Mth$8}`HMs~3-`*SJiLTx?6sKw^my8Yhf>9i8 z3wQ0Xu+N${{>0=`^Wz;}=%F5oMgQmQPr!ZF6d(K;>)()_=DOnI8;XNtt!!{Bt%=qI zrx?`OhSm`EgIwYld`IN>?dAC3N=({}omfK+j=p(mq$%A~(KM=Ejj^V-3ru`y4mWV< zq&1EnZDk_8<#ejM@;cYCJN|WUvhyYKQl%H3RPOELEY|$qvz19x4C+{krKdB+WOEAI zh)8ktW(*_Klg%ty7@VOP#4^;S+;7wG!mKpn0ZnXfqAT6GleMtcOm8{E(3?scNXzE1 zz?ZZKeaJCuS~;{c(m}nL%6W>Z3@t~4)GNh+BiaMn3yMK+VZ1Z_Uudr;I5a?4i$;5i zZ_3N^>D_0`(Gcwi#Q@5k1v>Ke{FwhXd@^!%jC%Jc7A_`RL&(?hO>|Rsr~@l;FV@#7 z2E0dZ2as^CLVB{*v49!{)-FK{Xuq+VxON?9iO!{j8h1Wq?m65U_Xqj*`%Ashe*C@9QOuv)Y`VXq&E6LT&X2=y`IRd@ literal 1150 zcmb`DT}V@59L8VC=q{v-E=%YlKe8mP3ojJIw3}|q{78bL6zxk&9at8XnQ6lik|0FU zY_>A3nMuv9rpwxrbodboxm9NS@FUOK+~(%f`@Z8G^CAf99G?B3J8y~>ngbBkRhp^!D&+0ZIRoPU05D~1Q{VuriM2S~k@jywHL zaQWO)-eyuEtubBtP7UYPr?)|?3BYDsu1w!Yk*&!~vOm_{Xx^ft&-b)MRm;KChUVA1MUnlHXw>E&L#sHk! zdRQ_EEZM7J$qI$rq>(t$Y>D(r{HwFuc`UE?V^g>n+Uz};e^Cbn{FrVjfw^D<1pR#x z#l^Z$rH!)6WO&_Mw?8FO%ssU?;@<8?_qy!KC+sNN;zwKrYJIv_w^?Z&xMC$chr zKvwL8-5!v37|YZzi^t3dJ!ao3R&;8$2N%u@sH${gXwbK!6IqdWPmcVt|9$uWdqOUS V{H0bAB40yD%|=2RLFjKMR&<%6~d0H9D201e<@ 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 Date: Wed, 16 Dec 2015 18:11:07 -0800 Subject: [PATCH 373/826] Fix case on the new UI header filename. Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index a2bc819fb6..8ccf3c2f46 100644 --- a/Makefile.am +++ b/Makefile.am @@ -307,7 +307,7 @@ httpui_DEPS = src/tsd/QueryUi.gwt.xml #dist_pkgdata_DATA = src/logback.xml dist_static_DATA = \ src/tsd/static/favicon.ico \ - src/tsd/static/openTSDB_header.jpg + src/tsd/static/opentsdb_header.jpg EXTRA_DIST = tsdb.in $(tsdb_SRC) $(test_SRC) \ $(test_plugin_SRC) $(test_plugin_MF) $(test_plugin_SVCS:%=test/%) \ From f70b26d59c4942ce5532a0ee5a17ec61c6e9b35a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 16 Dec 2015 18:11:07 -0800 Subject: [PATCH 374/826] Fix case on the new UI header filename. Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 35d7b5b055..ad2457734c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -406,7 +406,7 @@ get_expr_classes = `classes=''; for f in $(packagedir)$(expr_package)/*.class; d #dist_pkgdata_DATA = src/logback.xml dist_static_DATA = \ src/tsd/static/favicon.ico \ - src/tsd/static/openTSDB_header.jpg + src/tsd/static/opentsdb_header.jpg EXTRA_DIST = tsdb.in $(tsdb_SRC) $(test_SRC) $(expr_grammar) \ $(test_plugin_SRC) $(test_plugin_MF) $(test_plugin_SVCS:%=test/%) \ From 53ed6572f8ed4a51c1a08641fc188bbc8e812c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=AC=E5=B9=B3?= Date: Wed, 9 Dec 2015 17:41:19 +0800 Subject: [PATCH 375/826] build shell scan all salted metrics Signed-off-by: Chris Larsen --- src/tools/DumpSeries.java | 81 ++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/src/tools/DumpSeries.java b/src/tools/DumpSeries.java index a090f1e851..a7e9222fcc 100644 --- a/src/tools/DumpSeries.java +++ b/src/tools/DumpSeries.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Map; import org.hbase.async.DeleteRequest; @@ -96,49 +97,51 @@ private static void doDump(final TSDB tsdb, final StringBuilder buf = new StringBuilder(); for (final Query query : queries) { - final Scanner scanner = Internal.getScanner(query); - ArrayList> rows; - while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { - for (final ArrayList row : rows) { - buf.setLength(0); - final byte[] key = row.get(0).key(); - final long base_time = Internal.baseTime(tsdb, key); - final String metric = Internal.metricName(tsdb, key); - // Print the row key. - if (!importformat) { - buf.append(Arrays.toString(key)) - .append(' ') - .append(metric) - .append(' ') - .append(base_time) - .append(" (").append(date(base_time)).append(") "); - try { - buf.append(Internal.getTags(tsdb, key)); - } catch (RuntimeException e) { - buf.append(e.getClass().getName() + ": " + e.getMessage()); - } - buf.append('\n'); - System.out.print(buf); - } - - // Print individual cells. - buf.setLength(0); - if (!importformat) { - buf.append(" "); - } - for (final KeyValue kv : row) { - // Discard everything or keep initial spaces. - buf.setLength(importformat ? 0 : 2); - formatKeyValue(buf, tsdb, importformat, kv, base_time, metric); - if (buf.length() > 0) { + final List scanners = Internal.getScanners(query); + for (Scanner scanner : scanners) { + ArrayList> rows; + while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { + for (final ArrayList row : rows) { + buf.setLength(0); + final byte[] key = row.get(0).key(); + final long base_time = Internal.baseTime(tsdb, key); + final String metric = Internal.metricName(tsdb, key); + // Print the row key. + if (!importformat) { + buf.append(Arrays.toString(key)) + .append(' ') + .append(metric) + .append(' ') + .append(base_time) + .append(" (").append(date(base_time)).append(") "); + try { + buf.append(Internal.getTags(tsdb, key)); + } catch (RuntimeException e) { + buf.append(e.getClass().getName() + ": " + e.getMessage()); + } buf.append('\n'); System.out.print(buf); } - } - if (delete) { - final DeleteRequest del = new DeleteRequest(table, key); - client.delete(del); + // Print individual cells. + buf.setLength(0); + if (!importformat) { + buf.append(" "); + } + for (final KeyValue kv : row) { + // Discard everything or keep initial spaces. + buf.setLength(importformat ? 0 : 2); + formatKeyValue(buf, tsdb, importformat, kv, base_time, metric); + if (buf.length() > 0) { + buf.append('\n'); + System.out.print(buf); + } + } + + if (delete) { + final DeleteRequest del = new DeleteRequest(table, key); + client.delete(del); + } } } } From 7438eefe12d16b9485bed6cede33954d2aa4e05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=AC=E5=B9=B3?= Date: Wed, 9 Dec 2015 17:41:19 +0800 Subject: [PATCH 376/826] build shell scan all salted metrics Signed-off-by: Chris Larsen --- src/tools/DumpSeries.java | 81 ++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/src/tools/DumpSeries.java b/src/tools/DumpSeries.java index a090f1e851..a7e9222fcc 100644 --- a/src/tools/DumpSeries.java +++ b/src/tools/DumpSeries.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Map; import org.hbase.async.DeleteRequest; @@ -96,49 +97,51 @@ private static void doDump(final TSDB tsdb, final StringBuilder buf = new StringBuilder(); for (final Query query : queries) { - final Scanner scanner = Internal.getScanner(query); - ArrayList> rows; - while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { - for (final ArrayList row : rows) { - buf.setLength(0); - final byte[] key = row.get(0).key(); - final long base_time = Internal.baseTime(tsdb, key); - final String metric = Internal.metricName(tsdb, key); - // Print the row key. - if (!importformat) { - buf.append(Arrays.toString(key)) - .append(' ') - .append(metric) - .append(' ') - .append(base_time) - .append(" (").append(date(base_time)).append(") "); - try { - buf.append(Internal.getTags(tsdb, key)); - } catch (RuntimeException e) { - buf.append(e.getClass().getName() + ": " + e.getMessage()); - } - buf.append('\n'); - System.out.print(buf); - } - - // Print individual cells. - buf.setLength(0); - if (!importformat) { - buf.append(" "); - } - for (final KeyValue kv : row) { - // Discard everything or keep initial spaces. - buf.setLength(importformat ? 0 : 2); - formatKeyValue(buf, tsdb, importformat, kv, base_time, metric); - if (buf.length() > 0) { + final List scanners = Internal.getScanners(query); + for (Scanner scanner : scanners) { + ArrayList> rows; + while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { + for (final ArrayList row : rows) { + buf.setLength(0); + final byte[] key = row.get(0).key(); + final long base_time = Internal.baseTime(tsdb, key); + final String metric = Internal.metricName(tsdb, key); + // Print the row key. + if (!importformat) { + buf.append(Arrays.toString(key)) + .append(' ') + .append(metric) + .append(' ') + .append(base_time) + .append(" (").append(date(base_time)).append(") "); + try { + buf.append(Internal.getTags(tsdb, key)); + } catch (RuntimeException e) { + buf.append(e.getClass().getName() + ": " + e.getMessage()); + } buf.append('\n'); System.out.print(buf); } - } - if (delete) { - final DeleteRequest del = new DeleteRequest(table, key); - client.delete(del); + // Print individual cells. + buf.setLength(0); + if (!importformat) { + buf.append(" "); + } + for (final KeyValue kv : row) { + // Discard everything or keep initial spaces. + buf.setLength(importformat ? 0 : 2); + formatKeyValue(buf, tsdb, importformat, kv, base_time, metric); + if (buf.length() > 0) { + buf.append('\n'); + System.out.print(buf); + } + } + + if (delete) { + final DeleteRequest del = new DeleteRequest(table, key); + client.delete(del); + } } } } From 8add257fce0af41002bf77db80b4ec6af221f0fc Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Thu, 7 Jan 2016 17:42:13 -0800 Subject: [PATCH 377/826] Added script to install HBase, OpenTSDB and TCollector on OSX for development and demonstration --- tools/osx_full_stack_install.sh | 93 +++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tools/osx_full_stack_install.sh diff --git a/tools/osx_full_stack_install.sh b/tools/osx_full_stack_install.sh new file mode 100644 index 0000000000..e409f03c7a --- /dev/null +++ b/tools/osx_full_stack_install.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# +# Script which installs HBase, OpenTSDB and TCollector on OSX +# +# 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 . +# +# +if [ $# -eq 0 ] + then + echo "No arguments supplied, please suggest an installation path, ex. $HOME" + exit 1; +fi +BASE_DIR=$1 +SUBBASE_DIR=opentsdb_stack; +if [ ! -d "${BASE_DIR}" ] ; then + echo "$BASE_DIR is not a directory"; + exit 1; +fi +export INSTALL_DIR=$BASE_DIR/$SUBBASE_DIR; +/bin/echo "Installing into $INSTALL_DIR"; +/bin/mkdir -p $INSTALL_DIR; +cd $INSTALL_DIR; +/usr/bin/curl -q http://mirror.cogentco.com/pub/apache/hbase/1.1.2/hbase-1.1.2-bin.tar.gz -o $INSTALL_DIR/hbase-1.1.2-bin.tar.gz 2>/dev/null; +/usr/bin/tar -xzvf hbase-1.1.2-bin.tar.gz -C $INSTALL_DIR/; +cd $INSTALL_DIR/hbase-1.1.2; +/bin/mkdir -p $INSTALL_DIR/data/hbase; +/bin/mkdir -p $INSTALL_DIR/data/zookeeper; +/bin/cat < conf/hbase-site.xml + + + + + + hbase.rootdir + file://$INSTALL_DIR/data/hbase + + + hbase.zookeeper.property.dataDir + $INSTALL_DIR/data/zookeeper + + +EOF +$INSTALL_DIR/hbase-1.1.2/bin/start-hbase.sh; +cd $INSTALL_DIR +/usr/bin/git clone https://github.com/OpenTSDB/opentsdb.git; +cd opentsdb +$INSTALL_DIR/opentsdb/build.sh clean; $INSTALL_DIR/opentsdb/build.sh; +/bin/mkdir $INSTALL_DIR/opentsdb/build/cache; +export HBASE_HOME=$INSTALL_DIR/hbase-1.1.2; +export COMPRESSION=NONE; +$INSTALL_DIR/opentsdb/src/create_table.sh; +$INSTALL_DIR/opentsdb/build/tsdb tsd --config=$INSTALL_DIR/opentsdb/src/opentsdb.conf --staticroot=$INSTALL_DIR/opentsdb/build/staticroot --cachedir=$INSTALL_DIR/opentsdb/build/cache --port=4242 --zkquorum=localhost:2181 --zkbasedir=/hbase --auto-metric & +cd $INSTALL_DIR +/usr/bin/git clone https://github.com/OpenTSDB/tcollector.git; +cd $INSTALL_DIR/tcollector/tcollector +/bin/rm -rf $INSTALL_DIR/tcollector/collectors/0/*; +/usr/bin/curl -q https://raw.githubusercontent.com/aalpern/tcollector-osx/master/dfstat.py -o $INSTALL_DIR/tcollector/collectors/0/dfstat.py 2>/dev/null; +/usr/bin/curl -q https://raw.githubusercontent.com/aalpern/tcollector-osx/master/iostat.py -o $INSTALL_DIR/tcollector/collectors/0/iostat.py 2>/dev/null; +/usr/bin/curl -q https://raw.githubusercontent.com/aalpern/tcollector-osx/master/vmstat.py -o $INSTALL_DIR/tcollector/collectors/0/vmstat.py 2>/dev/null; +/bin/chmod a+x collectors/0/*.py; +$INSTALL_DIR/tcollector/tcollector.py -L localhost:4242 -t host=`hostname` -t domain=dev -P $INSTALL_DIR/tcollector/tcollector.pid --logfile $INSTALL_DIR/tcollector/tcollector.log & +/bin/sleep 30; +/usr/bin/open http://localhost:4242/#start=10m-ago\&m=sum:df.inodes.free\&autoreload=15; From 106a98efcad0e2fc86ebc700600ddb14c100472c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 25 Jan 2016 19:40:21 -0800 Subject: [PATCH 378/826] Fix up the Makefile for debian creation with the updated UI files Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 8ccf3c2f46..9963574c64 100644 --- a/Makefile.am +++ b/Makefile.am @@ -736,8 +736,8 @@ debian: dist staticroot chmod 755 $(distdir)/debian/DEBIAN/* cp $(top_srcdir)/build-aux/deb/init.d/opentsdb $(distdir)/debian/etc/init.d cp $(jar) $(distdir)/debian/usr/share/opentsdb/lib - cp -r staticroot/icon.ico $(distdir)/debian/usr/share/opentsdb/static - cp -r staticroot/openTSDB_header.jpg $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/favicon.ico $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/opentsdb_header.jpg $(distdir)/debian/usr/share/opentsdb/static cp -r gwt/queryui/* $(distdir)/debian/usr/share/opentsdb/static `for dep_jar in $(tsdb_DEPS); do cp $$dep_jar \ $(distdir)/debian/usr/share/opentsdb/lib; done;` From 6d2102af7d7391b759698979cab9ae74bea15d80 Mon Sep 17 00:00:00 2001 From: Davide D'Amico Date: Mon, 4 Jan 2016 12:50:59 +0100 Subject: [PATCH 379/826] configure includes a sysconfdir option that is not present in Makefile.am. A sysconfigdir option is present, instead. This leads to tsdb using always a /etc/opentsdb config path that in FreeBSD is not the right one Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 9963574c64..b43e01c093 100644 --- a/Makefile.am +++ b/Makefile.am @@ -356,7 +356,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(prefix)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp From 95568bf0bd0fe74a6b5a97600cc946c7e757fd47 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 25 Jan 2016 19:40:21 -0800 Subject: [PATCH 380/826] Fix up the Makefile for debian creation with the updated UI files Signed-off-by: Chris Larsen --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index ad2457734c..78abae4d5d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -848,8 +848,8 @@ debian: dist staticroot chmod 755 $(distdir)/debian/DEBIAN/* cp $(top_srcdir)/build-aux/deb/init.d/opentsdb $(distdir)/debian/etc/init.d cp $(jar) $(distdir)/debian/usr/share/opentsdb/lib - cp -r staticroot/icon.ico $(distdir)/debian/usr/share/opentsdb/static - cp -r staticroot/openTSDB_header.jpg $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/favicon.ico $(distdir)/debian/usr/share/opentsdb/static + cp -r staticroot/opentsdb_header.jpg $(distdir)/debian/usr/share/opentsdb/static cp -r gwt/queryui/* $(distdir)/debian/usr/share/opentsdb/static `for dep_jar in $(tsdb_DEPS); do cp $$dep_jar \ $(distdir)/debian/usr/share/opentsdb/lib; done;` From 09ba49e1517d78fa1c10e96d01b55dc6820eedc7 Mon Sep 17 00:00:00 2001 From: Davide D'Amico Date: Mon, 4 Jan 2016 12:50:59 +0100 Subject: [PATCH 381/826] configure includes a sysconfdir option that is not present in Makefile.am. A sysconfigdir option is present, instead. This leads to tsdb using always a /etc/opentsdb config path that in FreeBSD is not the right one Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 78abae4d5d..15013eb61a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -455,7 +455,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(prefix)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp From 663302459e4509a9d3aed12c67cf48add4111df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=AC=E5=B9=B3?= Date: Sun, 17 Jan 2016 13:41:12 +0800 Subject: [PATCH 382/826] compare call in CompactionQueue.class Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 0f23b361d4..390167fb0c 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -792,16 +792,16 @@ public void run() { */ private static final class Cmp implements Comparator { - /** On how many bytes do we encode metrics IDs. */ - private final short metric_width; + /** The position with which the timestamp of metric starts. */ + private final short timestamp_pos; public Cmp(final TSDB tsdb) { - metric_width = tsdb.metrics.width(); + timestamp_pos = Const.SALT_WIDTH() + tsdb.metrics.width(); } @Override public int compare(final byte[] a, final byte[] b) { - final int c = Bytes.memcmp(a, b, metric_width, Const.TIMESTAMP_BYTES); + final int c = Bytes.memcmp(a, b, timestamp_pos, Const.TIMESTAMP_BYTES); // If the timestamps are equal, sort according to the entire row key. return c != 0 ? c : Bytes.memcmp(a, b); } From b181a530555df57c6a90f0d084c3834e9cb53037 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 25 Jan 2016 19:02:18 -0800 Subject: [PATCH 383/826] Cast to short Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 390167fb0c..523f4a0d6e 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -796,7 +796,7 @@ private static final class Cmp implements Comparator { private final short timestamp_pos; public Cmp(final TSDB tsdb) { - timestamp_pos = Const.SALT_WIDTH() + tsdb.metrics.width(); + timestamp_pos = (short) (Const.SALT_WIDTH() + tsdb.metrics.width()); } @Override From 14157685c31abe112f14beaafbb322af9989d357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=AC=E5=B9=B3?= Date: Sun, 17 Jan 2016 13:41:12 +0800 Subject: [PATCH 384/826] compare call in CompactionQueue.class Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 0f23b361d4..390167fb0c 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -792,16 +792,16 @@ public void run() { */ private static final class Cmp implements Comparator { - /** On how many bytes do we encode metrics IDs. */ - private final short metric_width; + /** The position with which the timestamp of metric starts. */ + private final short timestamp_pos; public Cmp(final TSDB tsdb) { - metric_width = tsdb.metrics.width(); + timestamp_pos = Const.SALT_WIDTH() + tsdb.metrics.width(); } @Override public int compare(final byte[] a, final byte[] b) { - final int c = Bytes.memcmp(a, b, metric_width, Const.TIMESTAMP_BYTES); + final int c = Bytes.memcmp(a, b, timestamp_pos, Const.TIMESTAMP_BYTES); // If the timestamps are equal, sort according to the entire row key. return c != 0 ? c : Bytes.memcmp(a, b); } From 42e6f9a82726eba54444aaea2874ad4300e7ff7c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 25 Jan 2016 19:02:18 -0800 Subject: [PATCH 385/826] Cast to short Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 390167fb0c..523f4a0d6e 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -796,7 +796,7 @@ private static final class Cmp implements Comparator { private final short timestamp_pos; public Cmp(final TSDB tsdb) { - timestamp_pos = Const.SALT_WIDTH() + tsdb.metrics.width(); + timestamp_pos = (short) (Const.SALT_WIDTH() + tsdb.metrics.width()); } @Override From a58d0505ded1a4bcadc383e6025006592c0895ae Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 17:21:55 -0800 Subject: [PATCH 386/826] Fix some error handling in the MetaSync utility where it could hang forever. This still needs some rework and UTs. Ugly. Signed-off-by: Chris Larsen --- src/tools/MetaSync.java | 251 +++++++++++++++++++++----------------- src/tools/UidManager.java | 3 +- 2 files changed, 138 insertions(+), 116 deletions(-) diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index b3c7510d23..5076ea2c4b 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -102,11 +102,28 @@ public MetaSync(final TSDB tsdb, final Scanner scanner, * Loops through the entire TSDB data set and exits when complete. */ public void run() { - // list of deferred calls used to act as a buffer final ArrayList> storage_calls = new ArrayList>(); final Deferred result = new Deferred(); + + final class ErrBack implements Callback { + @Override + public Object call(Exception e) throws Exception { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + LOG.warn("Unable to get to the root cause of the DGE"); + break; + } + ex = ex.getCause(); + } + LOG.error("Sync thread failed with exception", ex); + result.callback(null); + return null; + } + } + final ErrBack err_back = new ErrBack(); /** * Called when we have encountered a previously un-processed UIDMeta object. @@ -349,7 +366,7 @@ final class MetaScanner implements Callback> rows) } for (final ArrayList row : rows) { - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row.get(0).key(), - TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - - // if the current tsuid is the same as the last, just continue - // so we save time - if (last_tsuid != null && Arrays.equals(last_tsuid, tsuid)) { - continue; - } - last_tsuid = tsuid; - - // see if we've already processed this tsuid and if so, continue - if (processed_tsuids.contains(Arrays.hashCode(tsuid))) { - continue; - } - tsuid_string = UniqueId.uidToString(tsuid); - - // add tsuid to the processed list - processed_tsuids.add(Arrays.hashCode(tsuid)); - - // we may have a new TSUID or UIDs, so fetch the timestamp of the - // row for use as the "created" time. Depending on speed we could - // parse datapoints, but for now the hourly row time is enough - final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), - Const.SALT_WIDTH() + TSDB.metrics_width()); - - LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + - " row timestamp: " + timestamp); - - // now process the UID metric meta data - final byte[] metric_uid_bytes = - Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); - final String metric_uid = UniqueId.uidToString(metric_uid_bytes); - Long last_get = metric_uids.get(metric_uid); - - if (last_get == null || last_get == 0 || timestamp < last_get) { - // fetch and update. Returns default object if the meta doesn't - // exist, so we can just call sync on this to create a missing - // entry - final UidCB cb = new UidCB(UniqueIdType.METRIC, - metric_uid_bytes, timestamp); - final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, - UniqueIdType.METRIC, metric_uid_bytes).addCallbackDeferring(cb); - storage_calls.add(process_uid); - metric_uids.put(metric_uid, timestamp); - } - - // loop through the tags and process their meta - final List tags = UniqueId.getTagsFromTSUID(tsuid_string); - int idx = 0; - for (byte[] tag : tags) { - final UniqueIdType type = (idx % 2 == 0) ? UniqueIdType.TAGK : - UniqueIdType.TAGV; - idx++; - final String uid = UniqueId.uidToString(tag); + try { + final byte[] tsuid = UniqueId.getTSUIDFromKey(row.get(0).key(), + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - // check the maps to see if we need to bother updating - if (type == UniqueIdType.TAGK) { - last_get = tagk_uids.get(uid); - } else { - last_get = tagv_uids.get(uid); + // if the current tsuid is the same as the last, just continue + // so we save time + if (last_tsuid != null && Arrays.equals(last_tsuid, tsuid)) { + continue; } - if (last_get != null && last_get != 0 && last_get <= timestamp) { + last_tsuid = tsuid; + + // see if we've already processed this tsuid and if so, continue + if (processed_tsuids.contains(Arrays.hashCode(tsuid))) { continue; } - - // fetch and update. Returns default object if the meta doesn't - // exist, so we can just call sync on this to create a missing - // entry - final UidCB cb = new UidCB(type, tag, timestamp); - final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, type, tag) - .addCallbackDeferring(cb); - storage_calls.add(process_uid); - if (type == UniqueIdType.TAGK) { - tagk_uids.put(uid, timestamp); - } else { - tagv_uids.put(uid, timestamp); + tsuid_string = UniqueId.uidToString(tsuid); + + /** + * An error callback used to catch issues with a particular timeseries + * or UIDMeta such as a missing UID name. We want to continue + * processing when this happens so we'll just log the error and + * the user can issue a command later to clean up orphaned meta + * entries. + */ + final class RowErrBack implements Callback { + @Override + public Object call(Exception e) throws Exception { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + LOG.warn("Unable to get to the root cause of the DGE"); + break; + } + ex = ex.getCause(); + } + if (ex.getClass().equals(IllegalStateException.class)) { + LOG.error("Invalid data when processing TSUID [" + + tsuid_string + "]: " + ex.getMessage()); + } else if (ex.getClass().equals(IllegalArgumentException.class)) { + LOG.error("Invalid data when processing TSUID [" + + tsuid_string + "]: " + ex.getMessage()); + } else if (ex.getClass().equals(NoSuchUniqueId.class)) { + LOG.warn("Timeseries [" + tsuid_string + + "] includes a non-existant UID: " + ex.getMessage()); + } else { + LOG.error("Unknown exception processing row: " + row, ex); + } + return null; + } } - } - - /** - * An error callback used to cache issues with a particular timeseries - * or UIDMeta such as a missing UID name. We want to continue - * processing when this happens so we'll just log the error and - * the user can issue a command later to clean up orphaned meta - * entries. - */ - final class ErrBack implements Callback, Exception> { - @Override - public Deferred call(Exception e) throws Exception { + // add tsuid to the processed list + processed_tsuids.add(Arrays.hashCode(tsuid)); + + // we may have a new TSUID or UIDs, so fetch the timestamp of the + // row for use as the "created" time. Depending on speed we could + // parse datapoints, but for now the hourly row time is enough + final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), + Const.SALT_WIDTH() + TSDB.metrics_width()); + + LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + + " row timestamp: " + timestamp); + + // now process the UID metric meta data + final byte[] metric_uid_bytes = + Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + final String metric_uid = UniqueId.uidToString(metric_uid_bytes); + Long last_get = metric_uids.get(metric_uid); + + if (last_get == null || last_get == 0 || timestamp < last_get) { + // fetch and update. Returns default object if the meta doesn't + // exist, so we can just call sync on this to create a missing + // entry + final UidCB cb = new UidCB(UniqueIdType.METRIC, + metric_uid_bytes, timestamp); + final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, + UniqueIdType.METRIC, metric_uid_bytes) + .addCallbackDeferring(cb) + .addErrback(new RowErrBack()); + storage_calls.add(process_uid); + metric_uids.put(metric_uid, timestamp); + } + + // loop through the tags and process their meta + final List tags = UniqueId.getTagsFromTSUID(tsuid_string); + int idx = 0; + for (byte[] tag : tags) { + final UniqueIdType type = (idx % 2 == 0) ? UniqueIdType.TAGK : + UniqueIdType.TAGV; + idx++; + final String uid = UniqueId.uidToString(tag); - Throwable ex = e; - while (ex.getClass().equals(DeferredGroupException.class)) { - if (ex.getCause() == null) { - LOG.warn("Unable to get to the root cause of the DGE"); - break; - } - ex = ex.getCause(); + // check the maps to see if we need to bother updating + if (type == UniqueIdType.TAGK) { + last_get = tagk_uids.get(uid); + } else { + last_get = tagv_uids.get(uid); } - if (ex.getClass().equals(IllegalStateException.class)) { - LOG.error("Invalid data when processing TSUID [" + - tsuid_string + "]", ex); - } else if (ex.getClass().equals(IllegalArgumentException.class)) { - LOG.error("Invalid data when processing TSUID [" + - tsuid_string + "]", ex); - } else if (ex.getClass().equals(NoSuchUniqueId.class)) { - LOG.warn("Timeseries [" + tsuid_string + - "] includes a non-existant UID: " + ex.getMessage()); + if (last_get != null && last_get != 0 && last_get <= timestamp) { + continue; + } + + // fetch and update. Returns default object if the meta doesn't + // exist, so we can just call sync on this to create a missing + // entry + final UidCB cb = new UidCB(type, tag, timestamp); + final Deferred process_uid = + UIDMeta.getUIDMeta(tsdb, type, tag) + .addCallbackDeferring(cb) + .addErrback(new RowErrBack()); + storage_calls.add(process_uid); + if (type == UniqueIdType.TAGK) { + tagk_uids.put(uid, timestamp); } else { - LOG.error("Unmatched Exception: " + ex.getClass()); - throw e; + tagv_uids.put(uid, timestamp); } - - return Deferred.fromResult(false); } + // handle the timeseries meta last so we don't record it if one + // or more of the UIDs had an issue + final Deferred process_tsmeta = + TSMeta.getTSMeta(tsdb, tsuid_string) + .addCallbackDeferring(new TSMetaCB(tsuid, timestamp)) + .addErrback(new RowErrBack()); + storage_calls.add(process_tsmeta); + } catch (RuntimeException e) { + LOG.error("Processing row " + row + " failed with exception: " + + e.getMessage()); + LOG.debug("Row: " + row + " stack trace: ", e); } - - // handle the timeseries meta last so we don't record it if one - // or more of the UIDs had an issue - final Deferred process_tsmeta = - TSMeta.getTSMeta(tsdb, tsuid_string) - .addCallbackDeferring(new TSMetaCB(tsuid, timestamp)); - process_tsmeta.addErrback(new ErrBack()); - storage_calls.add(process_tsmeta); } /** diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 5dccc5837e..5e4185d4f4 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -127,6 +127,7 @@ public static void main(String[] args) throws Exception { rc = runCommand(tsdb, table, idwidth, ignorecase, args); } finally { try { + LOG.info("Shutting down TSD...."); tsdb.getClient().shutdown().joinUninterruptibly(); LOG.info("Gracefully shutdown the TSD"); } catch (Exception e) { @@ -1026,7 +1027,7 @@ private static int metaSync(final TSDB tsdb) throws Exception { thread.join(); LOG.info("Thread [" + thread + "] Finished"); } - + LOG.info("All metasync threads have completed"); // make sure buffered data is flushed to storage before exiting tsdb.flush().joinUninterruptibly(); From 5083a43cf9027bdb504f687d4bfe1c2313f2358d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 18:02:11 -0800 Subject: [PATCH 387/826] Fix #686 where the first batch of scanned data was returned but the rest wasn't if the limit was greater than 128. Thanks @mgoralczyk-viasat Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 12 ++++++------ test/search/TestTimeSeriesLookup.java | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index ff66760b0f..38b66efb9f 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -187,7 +187,8 @@ public Deferred> lookupAsync() { limit = 0; } - class ScannerCB implements Callback, ArrayList>> { + class ScannerCB implements Callback>, + ArrayList>> { private final Scanner scanner; // used to avoid dupes when scanning the data table private byte[] last_tsuid = null; @@ -198,11 +199,11 @@ class ScannerCB implements Callback, ArrayList> } Deferred> scan() { - return scanner.nextRows().addCallback(this); + return scanner.nextRows().addCallbackDeferring(this); } @Override - public List call(final ArrayList> rows) + public Deferred> call(final ArrayList> rows) throws Exception { if (rows == null) { scanner.close(); @@ -210,7 +211,7 @@ public List call(final ArrayList> rows) LOG.debug("Lookup query matched " + tsuids.size() + " time series in " + (System.currentTimeMillis() - start) + " ms"); } - return tsuids; + return Deferred.fromResult(tsuids); } for (final ArrayList row : rows) { @@ -260,8 +261,7 @@ public List call(final ArrayList> rows) ++rows_read; } - scan(); - return tsuids; + return scan(); } @Override diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index d07c14501b..60ededd380 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -519,6 +519,25 @@ public void limitVerification() throws Exception { assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); } + @Test (expected = RuntimeException.class) + public void scannerException() throws Exception { + generateData(tsdb, storage); + final byte[] row = Const.SALT_WIDTH() > 0 ? + MockBase.stringToBytes( + "0300000400000000000001000001000003000005") : + MockBase.stringToBytes( + "00000400000000000001000001000003000005"); + storage.throwException(row, new RuntimeException("Boo!")); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + query.setLimit(1); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + lookup.lookup(); + } + // TODO test the dump to stdout /** From 4ef411d9648e9349bac9fe7b2470fe954d30ef2e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 18:31:46 -0800 Subject: [PATCH 388/826] Fix #679 where an NPE is thrown if the logback config doesn't have a cyclic buffered appender to keep track of the last x log lines. Now it will throw a BadRequest exception with a useful message. Signed-off-by: Chris Larsen --- src/tsd/LogsRpc.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsd/LogsRpc.java b/src/tsd/LogsRpc.java index fab9581415..7aa91259a7 100644 --- a/src/tsd/LogsRpc.java +++ b/src/tsd/LogsRpc.java @@ -93,6 +93,11 @@ public LogIterator() { final Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); logbuf = (CyclicBufferAppender) root.getAppender("CYCLIC"); + if (logbuf == null) { + throw new BadRequestException( + "No CyclicBufferAppender found. Please configure logback " + + "to store the latest log entries."); + } } public Iterator iterator() { From 62cc3b7530fef7f1b876600b8496f6e0963b76ab Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 17:21:55 -0800 Subject: [PATCH 389/826] Fix some error handling in the MetaSync utility where it could hang forever. This still needs some rework and UTs. Ugly. Signed-off-by: Chris Larsen --- src/tools/MetaSync.java | 251 +++++++++++++++++++++----------------- src/tools/UidManager.java | 3 +- 2 files changed, 138 insertions(+), 116 deletions(-) diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index 75bf1b2957..c61578aa87 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -105,11 +105,28 @@ public MetaSync(final TSDB tsdb, final long start_id, final double quotient, * Loops through the entire TSDB data set and exits when complete. */ public void run() { - // list of deferred calls used to act as a buffer final ArrayList> storage_calls = new ArrayList>(); final Deferred result = new Deferred(); + + final class ErrBack implements Callback { + @Override + public Object call(Exception e) throws Exception { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + LOG.warn("Unable to get to the root cause of the DGE"); + break; + } + ex = ex.getCause(); + } + LOG.error("Sync thread failed with exception", ex); + result.callback(null); + return null; + } + } + final ErrBack err_back = new ErrBack(); /** * Called when we have encountered a previously un-processed UIDMeta object. @@ -360,7 +377,7 @@ public MetaScanner() { * been processed. */ public Object scan() { - return scanner.nextRows().addCallback(this); + return scanner.nextRows().addCallback(this).addErrback(err_back); } @Override @@ -372,132 +389,136 @@ public Object call(ArrayList> rows) } for (final ArrayList row : rows) { - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row.get(0).key(), - TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - - // if the current tsuid is the same as the last, just continue - // so we save time - if (last_tsuid != null && Arrays.equals(last_tsuid, tsuid)) { - continue; - } - last_tsuid = tsuid; - - // see if we've already processed this tsuid and if so, continue - if (processed_tsuids.contains(Arrays.hashCode(tsuid))) { - continue; - } - tsuid_string = UniqueId.uidToString(tsuid); - - // add tsuid to the processed list - processed_tsuids.add(Arrays.hashCode(tsuid)); - - // we may have a new TSUID or UIDs, so fetch the timestamp of the - // row for use as the "created" time. Depending on speed we could - // parse datapoints, but for now the hourly row time is enough - final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), - TSDB.metrics_width()); - - LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + - " row timestamp: " + timestamp); - - // now process the UID metric meta data - final byte[] metric_uid_bytes = - Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); - final String metric_uid = UniqueId.uidToString(metric_uid_bytes); - Long last_get = metric_uids.get(metric_uid); - - if (last_get == null || last_get == 0 || timestamp < last_get) { - // fetch and update. Returns default object if the meta doesn't - // exist, so we can just call sync on this to create a missing - // entry - final UidCB cb = new UidCB(UniqueIdType.METRIC, - metric_uid_bytes, timestamp); - final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, - UniqueIdType.METRIC, metric_uid_bytes).addCallbackDeferring(cb); - storage_calls.add(process_uid); - metric_uids.put(metric_uid, timestamp); - } - - // loop through the tags and process their meta - final List tags = UniqueId.getTagsFromTSUID(tsuid_string); - int idx = 0; - for (byte[] tag : tags) { - final UniqueIdType type = (idx % 2 == 0) ? UniqueIdType.TAGK : - UniqueIdType.TAGV; - idx++; - final String uid = UniqueId.uidToString(tag); + try { + final byte[] tsuid = UniqueId.getTSUIDFromKey(row.get(0).key(), + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - // check the maps to see if we need to bother updating - if (type == UniqueIdType.TAGK) { - last_get = tagk_uids.get(uid); - } else { - last_get = tagv_uids.get(uid); + // if the current tsuid is the same as the last, just continue + // so we save time + if (last_tsuid != null && Arrays.equals(last_tsuid, tsuid)) { + continue; } - if (last_get != null && last_get != 0 && last_get <= timestamp) { + last_tsuid = tsuid; + + // see if we've already processed this tsuid and if so, continue + if (processed_tsuids.contains(Arrays.hashCode(tsuid))) { continue; } - - // fetch and update. Returns default object if the meta doesn't - // exist, so we can just call sync on this to create a missing - // entry - final UidCB cb = new UidCB(type, tag, timestamp); - final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, type, tag) - .addCallbackDeferring(cb); - storage_calls.add(process_uid); - if (type == UniqueIdType.TAGK) { - tagk_uids.put(uid, timestamp); - } else { - tagv_uids.put(uid, timestamp); + tsuid_string = UniqueId.uidToString(tsuid); + + /** + * An error callback used to catch issues with a particular timeseries + * or UIDMeta such as a missing UID name. We want to continue + * processing when this happens so we'll just log the error and + * the user can issue a command later to clean up orphaned meta + * entries. + */ + final class RowErrBack implements Callback { + @Override + public Object call(Exception e) throws Exception { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + LOG.warn("Unable to get to the root cause of the DGE"); + break; + } + ex = ex.getCause(); + } + if (ex.getClass().equals(IllegalStateException.class)) { + LOG.error("Invalid data when processing TSUID [" + + tsuid_string + "]: " + ex.getMessage()); + } else if (ex.getClass().equals(IllegalArgumentException.class)) { + LOG.error("Invalid data when processing TSUID [" + + tsuid_string + "]: " + ex.getMessage()); + } else if (ex.getClass().equals(NoSuchUniqueId.class)) { + LOG.warn("Timeseries [" + tsuid_string + + "] includes a non-existant UID: " + ex.getMessage()); + } else { + LOG.error("Unknown exception processing row: " + row, ex); + } + return null; + } } - } - - /** - * An error callback used to cache issues with a particular timeseries - * or UIDMeta such as a missing UID name. We want to continue - * processing when this happens so we'll just log the error and - * the user can issue a command later to clean up orphaned meta - * entries. - */ - final class ErrBack implements Callback, Exception> { - @Override - public Deferred call(Exception e) throws Exception { + // add tsuid to the processed list + processed_tsuids.add(Arrays.hashCode(tsuid)); + + // we may have a new TSUID or UIDs, so fetch the timestamp of the + // row for use as the "created" time. Depending on speed we could + // parse datapoints, but for now the hourly row time is enough + final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), + TSDB.metrics_width()); + + LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + + " row timestamp: " + timestamp); + + // now process the UID metric meta data + final byte[] metric_uid_bytes = + Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + final String metric_uid = UniqueId.uidToString(metric_uid_bytes); + Long last_get = metric_uids.get(metric_uid); + + if (last_get == null || last_get == 0 || timestamp < last_get) { + // fetch and update. Returns default object if the meta doesn't + // exist, so we can just call sync on this to create a missing + // entry + final UidCB cb = new UidCB(UniqueIdType.METRIC, + metric_uid_bytes, timestamp); + final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, + UniqueIdType.METRIC, metric_uid_bytes) + .addCallbackDeferring(cb) + .addErrback(new RowErrBack()); + storage_calls.add(process_uid); + metric_uids.put(metric_uid, timestamp); + } + + // loop through the tags and process their meta + final List tags = UniqueId.getTagsFromTSUID(tsuid_string); + int idx = 0; + for (byte[] tag : tags) { + final UniqueIdType type = (idx % 2 == 0) ? UniqueIdType.TAGK : + UniqueIdType.TAGV; + idx++; + final String uid = UniqueId.uidToString(tag); - Throwable ex = e; - while (ex.getClass().equals(DeferredGroupException.class)) { - if (ex.getCause() == null) { - LOG.warn("Unable to get to the root cause of the DGE"); - break; - } - ex = ex.getCause(); + // check the maps to see if we need to bother updating + if (type == UniqueIdType.TAGK) { + last_get = tagk_uids.get(uid); + } else { + last_get = tagv_uids.get(uid); + } + if (last_get != null && last_get != 0 && last_get <= timestamp) { + continue; } - if (ex.getClass().equals(IllegalStateException.class)) { - LOG.error("Invalid data when processing TSUID [" + - tsuid_string + "]", ex); - } else if (ex.getClass().equals(IllegalArgumentException.class)) { - LOG.error("Invalid data when processing TSUID [" + - tsuid_string + "]", ex); - } else if (ex.getClass().equals(NoSuchUniqueId.class)) { - LOG.warn("Timeseries [" + tsuid_string + - "] includes a non-existant UID: " + ex.getMessage()); + + // fetch and update. Returns default object if the meta doesn't + // exist, so we can just call sync on this to create a missing + // entry + final UidCB cb = new UidCB(type, tag, timestamp); + final Deferred process_uid = + UIDMeta.getUIDMeta(tsdb, type, tag) + .addCallbackDeferring(cb) + .addErrback(new RowErrBack()); + storage_calls.add(process_uid); + if (type == UniqueIdType.TAGK) { + tagk_uids.put(uid, timestamp); } else { - LOG.error("Unmatched Exception: " + ex.getClass()); - throw e; + tagv_uids.put(uid, timestamp); } - - return Deferred.fromResult(false); } + // handle the timeseries meta last so we don't record it if one + // or more of the UIDs had an issue + final Deferred process_tsmeta = + TSMeta.getTSMeta(tsdb, tsuid_string) + .addCallbackDeferring(new TSMetaCB(tsuid, timestamp)) + .addErrback(new RowErrBack()); + storage_calls.add(process_tsmeta); + } catch (RuntimeException e) { + LOG.error("Processing row " + row + " failed with exception: " + + e.getMessage()); + LOG.debug("Row: " + row + " stack trace: ", e); } - - // handle the timeseries meta last so we don't record it if one - // or more of the UIDs had an issue - final Deferred process_tsmeta = - TSMeta.getTSMeta(tsdb, tsuid_string) - .addCallbackDeferring(new TSMetaCB(tsuid, timestamp)); - process_tsmeta.addErrback(new ErrBack()); - storage_calls.add(process_tsmeta); } /** diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 3e8a001ef6..9f086a9cca 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -125,6 +125,7 @@ public static void main(String[] args) throws Exception { rc = runCommand(tsdb, table, idwidth, ignorecase, args); } finally { try { + LOG.info("Shutting down TSD...."); tsdb.getClient().shutdown().joinUninterruptibly(); LOG.info("Gracefully shutdown the TSD"); } catch (Exception e) { @@ -990,7 +991,7 @@ private static int metaSync(final TSDB tsdb) throws Exception { threads[i].join(); LOG.info("[" + i + "] Finished"); } - + LOG.info("All metasync threads have completed"); // make sure buffered data is flushed to storage before exiting tsdb.flush().joinUninterruptibly(); From d7fb09ee21a76937dd44e92e92e29c362fcd9ea2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 18:31:46 -0800 Subject: [PATCH 390/826] Fix #679 where an NPE is thrown if the logback config doesn't have a cyclic buffered appender to keep track of the last x log lines. Now it will throw a BadRequest exception with a useful message. Signed-off-by: Chris Larsen --- src/tsd/LogsRpc.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsd/LogsRpc.java b/src/tsd/LogsRpc.java index fab9581415..7aa91259a7 100644 --- a/src/tsd/LogsRpc.java +++ b/src/tsd/LogsRpc.java @@ -93,6 +93,11 @@ public LogIterator() { final Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); logbuf = (CyclicBufferAppender) root.getAppender("CYCLIC"); + if (logbuf == null) { + throw new BadRequestException( + "No CyclicBufferAppender found. Please configure logback " + + "to store the latest log entries."); + } } public Iterator iterator() { From df72269f41d6ce528d6778beadb9993fae4c9e87 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 17:21:55 -0800 Subject: [PATCH 391/826] Fix some error handling in the MetaSync utility where it could hang forever. This still needs some rework and UTs. Ugly. Signed-off-by: Chris Larsen --- src/tools/MetaSync.java | 251 +++++++++++++++++++++----------------- src/tools/UidManager.java | 3 +- 2 files changed, 138 insertions(+), 116 deletions(-) diff --git a/src/tools/MetaSync.java b/src/tools/MetaSync.java index b3c7510d23..5076ea2c4b 100644 --- a/src/tools/MetaSync.java +++ b/src/tools/MetaSync.java @@ -102,11 +102,28 @@ public MetaSync(final TSDB tsdb, final Scanner scanner, * Loops through the entire TSDB data set and exits when complete. */ public void run() { - // list of deferred calls used to act as a buffer final ArrayList> storage_calls = new ArrayList>(); final Deferred result = new Deferred(); + + final class ErrBack implements Callback { + @Override + public Object call(Exception e) throws Exception { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + LOG.warn("Unable to get to the root cause of the DGE"); + break; + } + ex = ex.getCause(); + } + LOG.error("Sync thread failed with exception", ex); + result.callback(null); + return null; + } + } + final ErrBack err_back = new ErrBack(); /** * Called when we have encountered a previously un-processed UIDMeta object. @@ -349,7 +366,7 @@ final class MetaScanner implements Callback> rows) } for (final ArrayList row : rows) { - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row.get(0).key(), - TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - - // if the current tsuid is the same as the last, just continue - // so we save time - if (last_tsuid != null && Arrays.equals(last_tsuid, tsuid)) { - continue; - } - last_tsuid = tsuid; - - // see if we've already processed this tsuid and if so, continue - if (processed_tsuids.contains(Arrays.hashCode(tsuid))) { - continue; - } - tsuid_string = UniqueId.uidToString(tsuid); - - // add tsuid to the processed list - processed_tsuids.add(Arrays.hashCode(tsuid)); - - // we may have a new TSUID or UIDs, so fetch the timestamp of the - // row for use as the "created" time. Depending on speed we could - // parse datapoints, but for now the hourly row time is enough - final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), - Const.SALT_WIDTH() + TSDB.metrics_width()); - - LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + - " row timestamp: " + timestamp); - - // now process the UID metric meta data - final byte[] metric_uid_bytes = - Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); - final String metric_uid = UniqueId.uidToString(metric_uid_bytes); - Long last_get = metric_uids.get(metric_uid); - - if (last_get == null || last_get == 0 || timestamp < last_get) { - // fetch and update. Returns default object if the meta doesn't - // exist, so we can just call sync on this to create a missing - // entry - final UidCB cb = new UidCB(UniqueIdType.METRIC, - metric_uid_bytes, timestamp); - final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, - UniqueIdType.METRIC, metric_uid_bytes).addCallbackDeferring(cb); - storage_calls.add(process_uid); - metric_uids.put(metric_uid, timestamp); - } - - // loop through the tags and process their meta - final List tags = UniqueId.getTagsFromTSUID(tsuid_string); - int idx = 0; - for (byte[] tag : tags) { - final UniqueIdType type = (idx % 2 == 0) ? UniqueIdType.TAGK : - UniqueIdType.TAGV; - idx++; - final String uid = UniqueId.uidToString(tag); + try { + final byte[] tsuid = UniqueId.getTSUIDFromKey(row.get(0).key(), + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - // check the maps to see if we need to bother updating - if (type == UniqueIdType.TAGK) { - last_get = tagk_uids.get(uid); - } else { - last_get = tagv_uids.get(uid); + // if the current tsuid is the same as the last, just continue + // so we save time + if (last_tsuid != null && Arrays.equals(last_tsuid, tsuid)) { + continue; } - if (last_get != null && last_get != 0 && last_get <= timestamp) { + last_tsuid = tsuid; + + // see if we've already processed this tsuid and if so, continue + if (processed_tsuids.contains(Arrays.hashCode(tsuid))) { continue; } - - // fetch and update. Returns default object if the meta doesn't - // exist, so we can just call sync on this to create a missing - // entry - final UidCB cb = new UidCB(type, tag, timestamp); - final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, type, tag) - .addCallbackDeferring(cb); - storage_calls.add(process_uid); - if (type == UniqueIdType.TAGK) { - tagk_uids.put(uid, timestamp); - } else { - tagv_uids.put(uid, timestamp); + tsuid_string = UniqueId.uidToString(tsuid); + + /** + * An error callback used to catch issues with a particular timeseries + * or UIDMeta such as a missing UID name. We want to continue + * processing when this happens so we'll just log the error and + * the user can issue a command later to clean up orphaned meta + * entries. + */ + final class RowErrBack implements Callback { + @Override + public Object call(Exception e) throws Exception { + Throwable ex = e; + while (ex.getClass().equals(DeferredGroupException.class)) { + if (ex.getCause() == null) { + LOG.warn("Unable to get to the root cause of the DGE"); + break; + } + ex = ex.getCause(); + } + if (ex.getClass().equals(IllegalStateException.class)) { + LOG.error("Invalid data when processing TSUID [" + + tsuid_string + "]: " + ex.getMessage()); + } else if (ex.getClass().equals(IllegalArgumentException.class)) { + LOG.error("Invalid data when processing TSUID [" + + tsuid_string + "]: " + ex.getMessage()); + } else if (ex.getClass().equals(NoSuchUniqueId.class)) { + LOG.warn("Timeseries [" + tsuid_string + + "] includes a non-existant UID: " + ex.getMessage()); + } else { + LOG.error("Unknown exception processing row: " + row, ex); + } + return null; + } } - } - - /** - * An error callback used to cache issues with a particular timeseries - * or UIDMeta such as a missing UID name. We want to continue - * processing when this happens so we'll just log the error and - * the user can issue a command later to clean up orphaned meta - * entries. - */ - final class ErrBack implements Callback, Exception> { - @Override - public Deferred call(Exception e) throws Exception { + // add tsuid to the processed list + processed_tsuids.add(Arrays.hashCode(tsuid)); + + // we may have a new TSUID or UIDs, so fetch the timestamp of the + // row for use as the "created" time. Depending on speed we could + // parse datapoints, but for now the hourly row time is enough + final long timestamp = Bytes.getUnsignedInt(row.get(0).key(), + Const.SALT_WIDTH() + TSDB.metrics_width()); + + LOG.debug("[" + thread_id + "] Processing TSUID: " + tsuid_string + + " row timestamp: " + timestamp); + + // now process the UID metric meta data + final byte[] metric_uid_bytes = + Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width()); + final String metric_uid = UniqueId.uidToString(metric_uid_bytes); + Long last_get = metric_uids.get(metric_uid); + + if (last_get == null || last_get == 0 || timestamp < last_get) { + // fetch and update. Returns default object if the meta doesn't + // exist, so we can just call sync on this to create a missing + // entry + final UidCB cb = new UidCB(UniqueIdType.METRIC, + metric_uid_bytes, timestamp); + final Deferred process_uid = UIDMeta.getUIDMeta(tsdb, + UniqueIdType.METRIC, metric_uid_bytes) + .addCallbackDeferring(cb) + .addErrback(new RowErrBack()); + storage_calls.add(process_uid); + metric_uids.put(metric_uid, timestamp); + } + + // loop through the tags and process their meta + final List tags = UniqueId.getTagsFromTSUID(tsuid_string); + int idx = 0; + for (byte[] tag : tags) { + final UniqueIdType type = (idx % 2 == 0) ? UniqueIdType.TAGK : + UniqueIdType.TAGV; + idx++; + final String uid = UniqueId.uidToString(tag); - Throwable ex = e; - while (ex.getClass().equals(DeferredGroupException.class)) { - if (ex.getCause() == null) { - LOG.warn("Unable to get to the root cause of the DGE"); - break; - } - ex = ex.getCause(); + // check the maps to see if we need to bother updating + if (type == UniqueIdType.TAGK) { + last_get = tagk_uids.get(uid); + } else { + last_get = tagv_uids.get(uid); } - if (ex.getClass().equals(IllegalStateException.class)) { - LOG.error("Invalid data when processing TSUID [" + - tsuid_string + "]", ex); - } else if (ex.getClass().equals(IllegalArgumentException.class)) { - LOG.error("Invalid data when processing TSUID [" + - tsuid_string + "]", ex); - } else if (ex.getClass().equals(NoSuchUniqueId.class)) { - LOG.warn("Timeseries [" + tsuid_string + - "] includes a non-existant UID: " + ex.getMessage()); + if (last_get != null && last_get != 0 && last_get <= timestamp) { + continue; + } + + // fetch and update. Returns default object if the meta doesn't + // exist, so we can just call sync on this to create a missing + // entry + final UidCB cb = new UidCB(type, tag, timestamp); + final Deferred process_uid = + UIDMeta.getUIDMeta(tsdb, type, tag) + .addCallbackDeferring(cb) + .addErrback(new RowErrBack()); + storage_calls.add(process_uid); + if (type == UniqueIdType.TAGK) { + tagk_uids.put(uid, timestamp); } else { - LOG.error("Unmatched Exception: " + ex.getClass()); - throw e; + tagv_uids.put(uid, timestamp); } - - return Deferred.fromResult(false); } + // handle the timeseries meta last so we don't record it if one + // or more of the UIDs had an issue + final Deferred process_tsmeta = + TSMeta.getTSMeta(tsdb, tsuid_string) + .addCallbackDeferring(new TSMetaCB(tsuid, timestamp)) + .addErrback(new RowErrBack()); + storage_calls.add(process_tsmeta); + } catch (RuntimeException e) { + LOG.error("Processing row " + row + " failed with exception: " + + e.getMessage()); + LOG.debug("Row: " + row + " stack trace: ", e); } - - // handle the timeseries meta last so we don't record it if one - // or more of the UIDs had an issue - final Deferred process_tsmeta = - TSMeta.getTSMeta(tsdb, tsuid_string) - .addCallbackDeferring(new TSMetaCB(tsuid, timestamp)); - process_tsmeta.addErrback(new ErrBack()); - storage_calls.add(process_tsmeta); } /** diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 5dccc5837e..5e4185d4f4 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -127,6 +127,7 @@ public static void main(String[] args) throws Exception { rc = runCommand(tsdb, table, idwidth, ignorecase, args); } finally { try { + LOG.info("Shutting down TSD...."); tsdb.getClient().shutdown().joinUninterruptibly(); LOG.info("Gracefully shutdown the TSD"); } catch (Exception e) { @@ -1026,7 +1027,7 @@ private static int metaSync(final TSDB tsdb) throws Exception { thread.join(); LOG.info("Thread [" + thread + "] Finished"); } - + LOG.info("All metasync threads have completed"); // make sure buffered data is flushed to storage before exiting tsdb.flush().joinUninterruptibly(); From e0996c4215e77877d7f4808f20fe05b0d1e02b4b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 18:02:11 -0800 Subject: [PATCH 392/826] Fix #686 where the first batch of scanned data was returned but the rest wasn't if the limit was greater than 128. Thanks @mgoralczyk-viasat Signed-off-by: Chris Larsen --- src/search/TimeSeriesLookup.java | 12 ++++++------ test/search/TestTimeSeriesLookup.java | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index ff66760b0f..38b66efb9f 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -187,7 +187,8 @@ public Deferred> lookupAsync() { limit = 0; } - class ScannerCB implements Callback, ArrayList>> { + class ScannerCB implements Callback>, + ArrayList>> { private final Scanner scanner; // used to avoid dupes when scanning the data table private byte[] last_tsuid = null; @@ -198,11 +199,11 @@ class ScannerCB implements Callback, ArrayList> } Deferred> scan() { - return scanner.nextRows().addCallback(this); + return scanner.nextRows().addCallbackDeferring(this); } @Override - public List call(final ArrayList> rows) + public Deferred> call(final ArrayList> rows) throws Exception { if (rows == null) { scanner.close(); @@ -210,7 +211,7 @@ public List call(final ArrayList> rows) LOG.debug("Lookup query matched " + tsuids.size() + " time series in " + (System.currentTimeMillis() - start) + " ms"); } - return tsuids; + return Deferred.fromResult(tsuids); } for (final ArrayList row : rows) { @@ -260,8 +261,7 @@ public List call(final ArrayList> rows) ++rows_read; } - scan(); - return tsuids; + return scan(); } @Override diff --git a/test/search/TestTimeSeriesLookup.java b/test/search/TestTimeSeriesLookup.java index d07c14501b..60ededd380 100644 --- a/test/search/TestTimeSeriesLookup.java +++ b/test/search/TestTimeSeriesLookup.java @@ -519,6 +519,25 @@ public void limitVerification() throws Exception { assertArrayEquals(test_tsuids.get(0), tsuids.get(0)); } + @Test (expected = RuntimeException.class) + public void scannerException() throws Exception { + generateData(tsdb, storage); + final byte[] row = Const.SALT_WIDTH() > 0 ? + MockBase.stringToBytes( + "0300000400000000000001000001000003000005") : + MockBase.stringToBytes( + "00000400000000000001000001000003000005"); + storage.throwException(row, new RuntimeException("Boo!")); + final List> tags = + new ArrayList>(1); + tags.add(new Pair(TAGK_STRING, TAGV_STRING)); + final SearchQuery query = new SearchQuery(tags); + query.setUseMeta(false); + query.setLimit(1); + final TimeSeriesLookup lookup = new TimeSeriesLookup(tsdb, query); + lookup.lookup(); + } + // TODO test the dump to stdout /** From f82ebe1eeafa37311af8033c11af6382e9234d44 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 30 Jan 2016 18:31:46 -0800 Subject: [PATCH 393/826] Fix #679 where an NPE is thrown if the logback config doesn't have a cyclic buffered appender to keep track of the last x log lines. Now it will throw a BadRequest exception with a useful message. Signed-off-by: Chris Larsen --- src/tsd/LogsRpc.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsd/LogsRpc.java b/src/tsd/LogsRpc.java index fab9581415..7aa91259a7 100644 --- a/src/tsd/LogsRpc.java +++ b/src/tsd/LogsRpc.java @@ -93,6 +93,11 @@ public LogIterator() { final Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); logbuf = (CyclicBufferAppender) root.getAppender("CYCLIC"); + if (logbuf == null) { + throw new BadRequestException( + "No CyclicBufferAppender found. Please configure logback " + + "to store the latest log entries."); + } } public Iterator iterator() { From e093ea4332918bde982a7f3252522eeb77e7065d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 1 Feb 2016 10:42:06 -0800 Subject: [PATCH 394/826] Bump the ZK version to fix #671. Signed-off-by: Chris Larsen --- third_party/zookeeper/include.mk | 2 +- third_party/zookeeper/zookeeper-3.4.5.jar | Bin 0 -> 779974 bytes third_party/zookeeper/zookeeper-3.4.5.jar.md5 | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar create mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar.md5 diff --git a/third_party/zookeeper/include.mk b/third_party/zookeeper/include.mk index 69368ea853..514b9dc5ed 100644 --- a/third_party/zookeeper/include.mk +++ b/third_party/zookeeper/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ZOOKEEPER_VERSION := 3.3.6 +ZOOKEEPER_VERSION := 3.4.5 ZOOKEEPER := third_party/zookeeper/zookeeper-$(ZOOKEEPER_VERSION).jar ZOOKEEPER_BASE_URL := http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/$(ZOOKEEPER_VERSION) diff --git a/third_party/zookeeper/zookeeper-3.4.5.jar b/third_party/zookeeper/zookeeper-3.4.5.jar new file mode 100644 index 0000000000000000000000000000000000000000..a7966bbbce49344a67438bee8bb0cd1fd4952eee GIT binary patch literal 779974 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

    N}hR{0dLySoh!!h~(WZKRed%U$2{V7gCf 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 z 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

    r6T9k`3Qu3|?nk%Zsi+St;89-&5yV4X1>E zoDJQnyX@P`9Nut1-WX7A4*`AmaX!e)rTf?CzW}g)o?-QHiWO<>pzXaomcfFS@r&t{ubJP4f}1tTO>03`wv{?9;(f^B-g z9gN#)6Xq3_4-{H!@W0qfD~dFOCC8>_Ztl}-2M236{M~+_vxByvnv*A#w3wp>HU`Mj znzR!(SmpVZMWx1~BdTFCO$RaK@L^8eP`X)&ZH*+CFM_vP;RR4vSA-sgl?X4N+>8Dz z4_&AD0}Ux)3Ts9Ze}lRWM*s5a^K5x9pze|1z+059BH;3^?iWz3985K#4aQ$|UfhS5 z3qw5A-$VgecZ(A8i@buOCd3`)s}To!kx7E+zeHU&H}rQf{EAuRp){FkNl#Z4&4Gj| z5=bNm*;62{Y=y(f$K)W47}Y=Zo^-osWV45{P|FO)$Cu|AEe&cDwzfX9 zYQ!%XmX~F8Mk-@*$nEyg2gZLqrWE4;u7X~7JU4FFI*j(O`Ey8wJ0;2+OK8p~z9nT@ z&gvlZBJ%P_-e6MQvc=3P*tuCCwMswBXkHoD;~?j>jX$@c{%{Dh!5D!J6sx}FK$3a+ za%MtGz*H?75@3f9n{^)A~eYczgYw^+R>kWAm*;*b_h8)0I5-lQ5IU_M`6A-SErv z%xOrkB)KJJLdnk3yOTvi^YymJ)>K+Ptfo`RY<--M*Q9&B-sw9Hipvu_fP(vF&$*DEXtO_~EqqrtE zDVIk(1_V~6y9TDys)-scoQYYiU3=bxzuOi4>TtPl+NZ=A85m z&ATFuzj4uIGTsTM9H?qhrH^q+jbg*9y*EX|JFG^^mUTh8N+qMlY|W~rm$ofnQ7mx#u*7=N>_xraf1V4LpoqXKsyC0$_r!qcyXrB= zy6S<}wY!EMuoSA-u4>z|q^7M)YK*aQj-vUcq^+A;K(DTDQckm^8gytSt!rFmZFchRgJ%XN#3yh+`Pq0&CB1eN?+nFyTtFTM1Jw1N$ct*3eE#^%XeyUVVMeNbU1+0%aQ9I?eq zyYgoBTvxlxyy}JSDKO2u0ug*qyyd+pnPB5wKJ~pZ%~=G4c}d>}yugq6{fC_aQ6UeC z%0D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 z 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≥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%0D>1+d;giZJ7s_0K3j_X9S~I1 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 HIGX Date: Mon, 1 Feb 2016 10:42:06 -0800 Subject: [PATCH 395/826] Bump the ZK version to fix #671. Signed-off-by: Chris Larsen --- third_party/zookeeper/include.mk | 2 +- third_party/zookeeper/zookeeper-3.4.5.jar | Bin 0 -> 779974 bytes third_party/zookeeper/zookeeper-3.4.5.jar.md5 | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar create mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar.md5 diff --git a/third_party/zookeeper/include.mk b/third_party/zookeeper/include.mk index 69368ea853..514b9dc5ed 100644 --- a/third_party/zookeeper/include.mk +++ b/third_party/zookeeper/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ZOOKEEPER_VERSION := 3.3.6 +ZOOKEEPER_VERSION := 3.4.5 ZOOKEEPER := third_party/zookeeper/zookeeper-$(ZOOKEEPER_VERSION).jar ZOOKEEPER_BASE_URL := http://central.maven.org/maven2/org/apache/zookeeper/zookeeper/$(ZOOKEEPER_VERSION) diff --git a/third_party/zookeeper/zookeeper-3.4.5.jar b/third_party/zookeeper/zookeeper-3.4.5.jar new file mode 100644 index 0000000000000000000000000000000000000000..a7966bbbce49344a67438bee8bb0cd1fd4952eee GIT binary patch literal 779974 zcma&N18`;SvOgRrJGO0SV%xTD+nm^*m=jwQ+qONi?PQ{F?m72=&-+f@bMCjRR@JUm z&8Pd(uX`y-gMvW=0YL!)>F}Eh0{!I&00IM&6;%k0 z_J_bgKvZBrK&Zdj{)bdnP)<@zR9S^yRxJ6Oto@n*V(3-M8-ZFDHlT*zKLznYfU-(~ zP!i@Mk5>i9yOw>0+r@oS{HyO08)gY%BX1SY-9*RLM76ohH5uiF4`>-jB=Aj{xwi0Z zIP5a5jDK310q}=&Pd4`hUy|zZw!9ZOnXncYI_xFfDiiQ=c{%hJ8rJYSVI@3Zc`~meXAeED&FdG6gHUaj z$!eD_OxRn~!*T+=R zq^}NCG^;BX`GTTjb{8{oOuvS~+ycD=GWk+pPQU(Y{#mZU!1?ZXMBMZM@~^x=WYOer zXxI(4MF0ZQqXhzj`JERslER{L%A)ix9xj_&+Rp1kXns#MD^A7AmJr3+ES5M&5q0~a zRl=4ct#;(!!pLd~n;~jP=m{g=A9zzc0}4&tf2qk9vr3+?)*szwUu6x#+_ZL!GK^;< zU%8fXmFV%@?eizOuDR@+}kMxvIJH(~25AmFno!)TO&hWv^)TaCfHQtE_=>bocdkcYfHR^V6MCRv&+O-@FY& zc!mQ!ThUjmRB`kAJK25oSu@{JWB{7K$&zT1aWnQvLYit*n5ak0xuDe`H`W=slq0G= zEU6er=ws-i@@ZSLYjk4WcoSupI!LQVObS-2jtX^wyevhpXFtoOVe?sdOeGmtwC!jx z7|Wy5$bPN$YR3s)bsvLP0HB}l5bkrFM=yRBzjtIg(}ZT@g4wh+BBQ%$dyT9UDGS(| zl2l9HM<5%AL6Tl1$CF6{wEVHQH5rC_K=lNxaP8qB@G32#iK?Ipu$9_+=C_-T)q8); zHB_B+kbZt&IWrt~^5}2%FsD74jcZ~@WE?G@-RTu)W-t{Kcia`yzg4PNZvuMKyhf(Z z+xx2hiVY%IqdOwp)?zst?u^u9LKkgm3xK4KRD3Uw@I zf;aZu+QcOrt9vs#e~?9XJwHBNF<5eg9vb#T3hr$~)N=g-+Av9mK^EK>b2SK73ZkKy zW9I!f{*`S{MyE&XEj3&BP%rFrh8&>{TQLRA)5in&6schm9u2rt`vMo7XAp}5xb=W^ zVX0OdKm~QOh%c^XuUb_Za&T`gi~bp`-B3K|z#^%MhFjVG18WpDJ~*E_$ik(<(fzuB z`Js0B+NptvqX^_IY=cBl50+mc_^sx_t>K=#rAQIWXT#$z@Y6uto-0HQd{>7x|tpKp)-S%nWkb$ooCj(M3igQ_olkOKFP0872Y9+`onb za%vVv?pM$8g#!GMT?IAWpu_Q<@mH3|c>v}4j`Uob0~85ek^f~MqY+5$$y=S4CU88D z#O8=B$}N9?5y&hmXl)W-lF2}bznuYu2S*={HZ=Z{i5e%YhW3b%pff?^)AjzT zTEy>oeOVa0-=ezIYwt)%m5m=$N1@fnP9bY|gODu{*|}qZHugH;-cCA6n*Yqw6EQUicFto^Zp(k&N*$kmNdgDib=puo}D*nB{ceolI_ zc^YaQBn3$jok_1uuq2_@%GkR?wrBl%LyZHKTl{v@$e<1^Gc$l23xIBY4}a0 zvjLUL0)tDeqS6XJ9&(d{X#*l3e6R3ZRU53_GLT493K?1iaYe#xDTP#?Yf{;_(viT5TRls%{7g&bLXFo-u*Ul@0bybshs4-W#y+AiyNd7)O=;^Jpzn*Rk1AM{^cI;Ay|(YJDxucV zY=M4Aa{&{dLSV29Pzh?O{UAnjf@CjUIzWVHbyv|4k}6O50J!n1@tM&iSRaTnFkz;P z2%6b8;5ckJxu_gi4yRnt&5y(ddz7)14I2cXXbSW6DeBaW&ql*1HYY!#2<6J2Q!3CO z+&7V&hbf2iLDOY=kI*BtHENK`niH{U!5XhuAY3@#3zu9}fc z*W)ImsYoVZNe~A6PYvvbV*qgmD}(f(Edr$gb|7b#RlH+#3Ya&gZ##le_J&h?yw1^a zD4gRb9XvF(!E;#<+aZ;S<8xJ&~+ zr*y3FeD@+me>NYm@Eb8E9DKu@PIB%;cbg$#hTN0_+x!H*(&=GZL`XB3ae@sX4hW3J z$(NqJ(`3E^)`c|We)%4L03K2r$L1QtgAnO&1?2bg6<9GEZ-o&Fl1K5eGTwkZ3s$y= z54wG<%f5D#1&<@UGJ54el?JD*_vf398%HfMV6+Oyu|DYBRZdHGRa09acV?Cn&q_b% z8RHUp&0wBN9|`!wKS4xIemoW<7}PQBtb(p|n$*5LYs)CE8miHbK85uc|DzCmJhU+v zm+6eQnxGxohYaBn&Qp|1EpcbBEFK0?k|(H zQf%JisPiITCssuf6cbJDI=kuO#~b(x{FXH0y~eCA03#jCe|sn63qnp$1b|SW%pR_L*Nu^+GUVli@Wfza;%+kX(fx zfMWASSx6EbHaRoNlT>LncYWrAypxPS@Z`l40O(R=^(MB;qFt+W)eV*KxpKqtvGPTJQ#*@Q@sv8eX|6U0&@;PD z;|vacb2Oqll#nI1#AVoOj_HGak=@E<#JH-PP&RF$T-Z=nrpd!I8@WHy<8G4wi3sDRHTS8mfSdfr3}{<1SQNzdNQ^fu&u{hC%}$Dz*U+bn^lZ&SfFJ5 zDvewZlgP2ffm=Ufla?G)mxHTIRvgETkKmMcYI}Qf_jNwQGegwM47i^-txav`F=4r8 z!ar*F0&X!iW~Uf--c9-fF9k_yGDZPDs~lAy%n;jz{sxD!eH)lcgRx0O`}l17!?9!7ZIHqUl&VgDuBw}jU+hq3ByW8zmGN4 z#?cNP3r?;x$+suH>l2FW1@kaWcm^UyxUi6Iv3yJH!alC{JNGpp#l?vV)o*_HfyD|` z4dSO_A8;p)(R{$SwQnyrBgNb7`fyW8Kurs|N4rwoR@v8cP3QyxUjudF83Fm_nV{^o z3!cVN$p-!%5|h#yfoP+Kd{4Sc4JO;}Ombj@97t{Mwa+PP{1mv`gcz zUoM9a-s9K_E(eDPH#d!3yF2uvGTYy@J`S@rD*e#E>Q`6$Us`kl`wg6wN>j`R)2OnM z%VLQ*D`7dD0?E>+M`9CuL)RHhydJ#2o`F<$d9~_vwrf{^c)ia*oOU7d>uzZ{GU3b{;N6qU#M^?84(d?!52HZTPjccWUZ8Hy*6_LD-`UeB!e$x||8H zGjE{D^@H=6%i6QfIdeI{&4$D&ypat|9v1P~y1;WF;j{NNx&@z4C2=*4!k&KQBp27c zh@M$8kJmk=a{9oN4~kwjZXAcwPp4{lg3sjI5%Ow4_$Ei)z1js)4+9?QQ8tZyfhgPZ*U=iZq(Q-$Oc1ladcNyO%KmYLj<9hY1 zsYhr$n~?{+Nb+d^N48mY&$^YU2Z(|=_E@!wPk7Gocl>vFRQK|W>%~MCMB{okL3cy8 zm|P(wd(V*Cpo!EPt!e8zpwUEgd^Ei$DRXR5EE4TcueXImTS zh!hEzXCtYy#jWRU({ph_$%WNpC#ew>qvl(04!1|exMqOq;%%@;-}s`2qS?fBIs9wX z)iuA*D&nYT<+Kxj`95@>)m?Cpr?GZMxD15`o0M9 zV@L>xFStxBf$K%9qSy%p8jO@X=mWx@3QgqjJXPNu>Dhv5r(%-`3?;Ap>emlBFM9fF znWwPlE`gK+!RIS_aM!biVy^EPhya0`0{kxSkuf(n_Ndlfc>klg8{(u4FEt8}Hxrw$IDD2eY$pulvx-ky18JT_n0lD;#FsT6V$yS|$V&ogmit%eAs2tz$h zcRKyTZ+W%yeMeR)Ce!bm$=${O$Yw_U7PBC?< z0XG1PTeTjG=pX?2PU|vk|M+4lxqEhbuXBg-3H<+V7Lv3!36c76jkK=w}N4F96Y z{)6Jb?(P3YVd!9JY+?Er!aqeJ{+-au)#b0$e~QKWU#N}kZ5=FaOr8Fs+X=wfQ}7Xf*HxP$h00bcg@)~2Qof7AIV3+}&TF}ATZwR8Co%Kkm4y{)ai-CsDr z|6wNfztd@A=wkR6zCUgKV_5&?o|Ub~U)cUshx6~$IXKz7*#Cv`PksN^LuXSbxBob* zME_3Ze{&kTx>)=N#gzXC`|prCTROX#+8O^f3;#6#k97LiaQ(Ltj;{7juD1WdjNic4 zUuVa^6JTaz>S1YQ^Pfy1{XZnRI+!~dn*1jT|JR7Rx>(x$#g#vE;NPZ>i^qS;RJ?y@ z_wVpKTmCJ{|D4Rk{}=1uEQb2W;>F`grW5iTYR&-x0%G|)|36k7Axk?$Cr?Q`2UizC zCu0jsH&fF8_sYc7gWlN2(AhaTK~}o|Hx#`kYpgtPVQu@aCIYq&9g`!1DhQsZaQ_|3 zYrR;yoy?9@R`hGMIfEXU==CYG9Sk!V*^*@Or=qas9xG-nqgG8pcI1<)=@oH*sowy+u`N9VQOfB9WG}J3V;?$0DR)+tgg=8j=k=e&oxr*A4>PFgaU(EZ) z;b+c^ZU`B=JEE{fxzPh{*X%fV_n)#~U}qS2%PiQbxUgWAhKiT*58xu*9*`=z-huXF zlLBZr``6k>7@r*IOebKe0Y%|9?BqKg0UC za~gsJ0R%+*8!i50YX6(C{xg&@YODGv>Zl)fATms_fr=naDzjQRfdUncT8?nxF#|>P zlor}yCQx~X;xKL!gHuf_hiTrn zE0FcRU2_Th(iT)4%IL^j&cjOSEVI@#R2<%E<@Bb`VyTglFHkj77~kJlq+l;QgIA;y zag@moLmL}sjP~rpI%(BEW{%!WBn{2rI8H;oqokCTee1*n^vgVzaCT%MLKQM5%r=sQj4Dp@l*O7^k|ljfv~rVRWqw=NtQ^%oBo(eN5e;qA#o{%- zFoY84c{|1=2D0W6?y{?l)$w;wuEUC=W#a9&BO0;fpXE;Xp8ur9Qjf4F0|Ujd=eY1zUR zINb?YeFw(Te|KtP6m+|dPR>?rw8a7FVB zM8L#C=a$2DyD%DnhkHU2ik2nAPWDyBM5}KYny*(7kW2jx>ut&7xd6l7tS*R}J?P7a z5!?1EDnC{^1f6apr7}mAtuuzhMZj>O(r#(p;p^TWIDG?-p^oMgQNz*BjNGMm4Oh+R z!a&d&V;Yo_`=QoLhz#?_&&4H}Zfa2>;S~H* zQ4CHc|4}VQD*G}9yHaFRoAs3M++ET4pp>aUE=u9GTr4J`nqt{2G#5P&zY@EV0J5W% z8kV0y2(J>`)gN=ERNbN~G)IxWx_#YeSz+9+;T?;k52M|#pf}9367v$8bt(9D=N z6r7m7;o^fx=?fR#a)J@B8aInj-vX8vUfyE;g2>T^043O0O!0wqLd8w0wNuQ5#?8;4 zj@=AvoKdxBzf?zTyT2bmR%VOSG+t!vaBiTrd86Cf*0-3WTOZY4UWEXkQV*KaktR~7 z$||o<&slMKzsbryk6+|y z|BR4U<-Tb-8}QTG`vsTP>|g@yj`Q5gv<-pL(hK?=aiFPJooEUquM~d4r$ZqJGDo-Z zN~2{L2}(9X1fm?0uDHQq6&G>lh_e6rmlE}V-Bbt!ziP5W0s(O#{Vr4g+jE1wtINL* z4*%R?vem7faTc-s)Do-K*&;5UktO`&Ge(pGEhLC!h3J1UHZlT8hsY(h+tikB$$yxr z(Hw!u>Ow7}L!naML)#Gv*cXz)6XO+U`yI>h-wT%zyym@-*ycV}Hl!46QqA9-d&f?B zPTzGb*PnHNyv|Pp`2f@l(qi(H1fUMUSSH9EXz-NYI8^W21ry@Oc z22k?fsz|4H)|s>sZ}5=hBR!g{c7V{gL$SLJAV>3^NeT}YA;g_o& zF?iuHREyIjyAyDA8*3PjDI-?r8Xog3;4oZCd8=j+qmJdR$Q#a87Jjv>FyZuOxi5aR zAxI+(W;Rx3x&JEnx?5rs6=3KfkvY~{Dq5g2lDI&PhFUzNV8t0a$p~%a8HF6=c6HLh zd8BcmL>9Tsv*FZA*&+_=5N8!mHJFe{;za`Ng`S%Lajem*f_N|sWXfb!&>%sDfH58# zhz~!LbI4ptRMrqSrkWgOWsZ5bk0iggyJ$>BI# z=g$nQGD>$Ef5rO2Y=0848jjElmYzMc6A$CdOPMs!5}ELiwkXwMYO~?WNE8wF8pFwl za(V|)>SBL_FNZ9J8H&TU^ZQmn4ykeEI0-G#RE{k=jdzE&WTx{+^Ty5Nj&PZL;@rsJ z8_PC-E+-705nm$Sh@tY&+>rmAyAi!VYk|uwc+QLEz9GTmACh-DPtb(tUu2BVJ;5AY z=la=egv!5s1JhBl$BTp^+7Ty;N3bo7*F8{U@@UQ3r9KRdyKsZ^rZy~|?=CrTb?uEr zAnvF+j9e{p6A_F3GiFzL=qsl`WQp9DQ*1V!t4CLOcUD|{&SPCd>?LsBwh3{Lq942S z@)Na+w6|)>?Uf9y$O|snRf1+a%DF9V?zZV-l4^0~)eDO#w{{|zGT@Wv6`fY2Rw@e`t$K9EH5CCC5l_?TQp83_a^twK_VY!Nqp4c6T;s;; z+V(NY_Jp6s+KoRF4hBoNxwO2Ha9$i8XmVxW2mzAZ*ED3Lc)fKfj)h-(Hd(%2h~Sv_ zWK_(=>WCgo{DSUNM|_dU9#Lqjcwes#QF3>!Lu+cjNjRw#6s~2zM6{6 zr;ms0-qvy<<;f`94NzCRAgMZdZ`8jfu0fQhuZ+P&Z?!BWmvwD3NGmT?AQAkSY50jb~M*A47|G zb=*-OXjGjM`FND_^6AdR8LmQ$7i*sbRe(th=&L-}0h{}ox_6GX?MACu;xV$CyqT3o zKf~sA4%`{W4TXDSfiDv1WdpNQ23i;M?#F3{j(ToYwoFY_?KLxw)iL}|wS;>iC`{CG z{Ke%6jr4JPNBB3L7(*mW&5Is_b`tBgY5G!GchdkG?`gV)Mfdio&)xi=`AdFF^ESNR ze3iOy(_NtnO!VxJL`~@;>_cBF%?Q1&3D<9l+-8YFPrp>yWJTm`pOWJ5Y_Y_C4=&K? zVeE*lxf6YXdjpQ!wdZ+7klD!{sX#s7Nlp+Yn0{a`MY!cp4GYH{hY$6_^n7BB9Qcp`>Hk*F*y@hdkr_Q)jWw!>pLe6QYBQ_Gc_7bpPw9t7?5Ln5Um`ht)isLoTra6Uu4s26i%LO^0IaN(Xdafy9yom zg}l0`D&cn&?K@ZYKVq9-otv zcBtDH+EIo9D}vgc0V{#V-q(KmH-cK_N=b~RO31?07s*0l?G2&w9dtjbkqweF#it(S zV2)`8-pNGI6zgf$geR~4P7XN(*-5R@65d;ZTs^4kGT^f#m80AN3aY(q8S&Onfq_h1<(3_zuxIctD2Ym-gWnXKNSDV z?knu<_O~lL<3IP_kZ4}$J^{p#EejTG(BfM4-Ze7u`0w#Tv^>3S@GnF*LT=ysH9de3 z$n0wC9r5q*5zM-lUvIz=N$z)Ep`dyjFSQ0s@{Rha8GXw$r|`9sF?g?}SEc)IOI1}f zh0M*HJbj<)2yM>TP<|v(n=87d3{GFsllvnUYdL4C%we(xTM$M{oyzFgkf!op$}G}0U=?;;E_P4y;F|W*s0qpa!RiKk^I{*D|LoJ zV01>kkk^Xg&Unp2MM`bgJeOJNoXk(Zo^o@$fhvtQMIfR`P^HPm6j%)V85R3}ATPsJ zVmiPj!Bs*RdRKkdri9tI&bAnTaO3GF+r03TZ%e+kW8FA;!WZ+SSxG(vvj;Pr@g`KD zgcn_R1@4@)lnSWRLL57KcZb93e8jfRiC*UFTCVn1{QkDDpsoVOF_M>eJL18^da` zG=yu0-Dq-vbf??s_YH_ZF+-=!aZ3PMZ4p?9Y2NtV>UQs~vww3AmO;Rk*0V^2DR=qO zb$%V@5SlGG*a3?KhXN?tgHNf!1?++C8IO(y0r>VkE~P)K1P zWWMr(n^pe{#0BKNZ`O;4#q=e~JJzrI)$>>RFD@Bn3^EH2fK)@$7_${<${B6aI#vV*NNBE1gx@QDlo(|PggPdLvOM9wE$lTgk@5)ohG4Hw{4qBbP$|zrfA@Sw|L)u* z`@i$`pEI*l{ndSb3GZ{nk$7FP2QyI!3ejSTT#is-4NXD{BQqHM)&c+szAWO9GCI|$ zCM`i7TC%fVeyE#0`W_cjy?Gpk-o&PvTJL7g@4~G+{ZVL}OEdbz(WGxX3y(lock{J# zYx?Ll>20z5^NrJAw*^}a&&<@s^cYEn2&0c=*Dky_nh=O4kn+|j4aGVx)In;<7zv*n zPY}k5X=>!z5XziHBZvUdN)Rqu5Leifpp=D}#&1 z@V!UURqjIOEE{2eG9AGBaE&CJy%0?gcvy#16G{=v?IdrdU6p zO_Gu@CFkB4BMC+RASv5-8sMUaGd3+}F{J29+@HMSpQFTVT%LeFeQ7k4A^B~733xN@ zQpMTIO6-g$y}3ay(}dps0kbUqK1+;PHHC|=fmS#f@#a`jJ&j!Uv8ZvPFqr#I;+JK- zQb!_K`iQ*bO05iOg1kBISFqr{Q{Jx+2;M7!BMoW>w1)E5ic)DzM%%+xOK8Gz^Q)t)q`63U^^zPCi{#nOR4W!rebl ziITa+BK3iHE#J3UJLn6WXgomesh{Bv2Zuxf6;X*onDQMBCR%ujlA1Ob?pH*}X zYrO6)wjOzf`n8Ros{#dKsh-I%O{sB{Z<+$#ueq_h)g1uB`#*%xC^;e(v972+4WMbbHrQ`lJP~JBf+3Ojvjv6U#{QEIr-Ort6c` zg$$IMQGM7lSnpc1>@?O)?AeN<;*$ot%T*U-Vlu`V1uu-+1IYExXNaU3k{;XCw(JN{ zEe-Z+jTiUpr;KYG^|#p?$!kh@FSqR9&-`o^SE>-}u<26Kik>C(CeutO#~nL2{ngRb zvoEoy%0kOH?~p@6$Th6(z3J+BI11FXzAwsR{jzZ~sT3%IIj%j# z`rfz238`+!LIY?x&nIWOt0)a2;M&?oLyQT@H9O-t>v4qpR02n6a3vV-vTT4bdS0;acfaQ+^;kPdO63bQJZqil z$6ijATDY%w3O^h6IwPt(d=hN8Va`2tk84ctEEC>#<^JBCi5;xmFI|cg$6&OW%w$i{ zM1&?DZg#(-f~N}5J0GkrXB=U3Az7@9WgwCYtHiR88$_>jLptLNaao-29CI53UAQG= z+m?_N!QtJFVQDT2ZV@=^7sRMqcOyWIoL_v9@4hy~x|&7KCXzR|NM|PX$&7V#iHwKR zd$@TNWq}>CGmc-@_s8uBHc&+(G@`+ym;vAvu5#`NEVL7{YQzXEsW1lZ6+dS*%Gv;gA6uJJJTzf4Db+j;aDHeXQ+sW3&9|8bf_9RTHxXy}uwdFhg} zIZoJh)k*r(t^KL@kzp3c+U#wqJM>ZrrhEGAKvwaNxGyu`{IKqYOOOHCrqFde_mPXI ze}q4Z&Lj4!^k~&=YBhK-*mnCI^1K1~@+V;NvbAs*%W;N+jDqJ>-&uA<-qXf1Xm`Z{ zI7W7MglCCO^z2ZP0&}=&4eYnv8vDSKDylWc>F3+J2!^73Q*KOtMMT3685q4@wvMsX zRnJ?mAfGLh{xoxReG{OcMls#W$T?KgKd6?OkcxSmAf;?&>i9X>x-LU0PB0Q$aUw>L zn_`BGL^t$gW7$scQO-t!yU8hm-9D4ie@VFZP?qeat_zc<3oj25< z@$XLT+p-%Sod1Nx_$rD1wIu3OFhxIMnSXTGy6sv2vm&a1Xo7GueS3Yh-9mH`r0I>u z!g*9(fmLp;9Moe4pV{JocW4!Fw>?3s`Q~S6&)5HHD)?ssM=5fHx$}1cr~7vs0QvuZ ziuhl^wHUP*XB1ORzb45{TaER^L z(kUw+kqv|$E`T>mv$s8Fdi}(%3v-taKOXekq}4($3_{Ll&T!35&tAk}0k{{xW$h%Y zQhG{qhr&7+OVgFEYF1fSn^@}d4uVtkO_JHQ45tx`-NolDKPDeiusW+`PdJjYnV2kY zv{;?)uHwNelZ%pDp(v?u)m5t1x{&BcZ}V(w)E7H05a3Qdr577IZ6s$Y-!bb>EZs4; zyGNsJWh_P^v+2j&#urUNrK<`AGlZ`_VBX!GYVehybVe{)$0y_> z^#%d=f+a(60=qeUol>wp{_sDx)O$x(Tb=C`8_n4eDQ742iBzw_xa^3p?Hs~|*vIJH zxSYFh7<%bubEW*AmN$w#xQKW9s044J*@+R!3Nv`%Yb zQ*s%gOxLW^$6&^+*&t^R*8z-vuFFAR_H9BpkrZw7cF3#Q8^glBwx`akmDRQv9u>1F z7b>ev{M;PPA7Umc$=!Y}3`B4c>=pH)G_q5!1+_%4Q0^t$P!m&jTahd!z`2+{M+Fd2 zVGaaAU!q>ml?G~q50Uv)ZUc#lTz!M$Q@zIGQ@;l9ir%IH*WddACUPAUXn%u;p?pI{ zF7BDV-w#4?dbrv^n~9Z^wTCOYqM%uVCYo3~_>DV-Aq5(|zoDV5%V#MqfAI&CXnL;i z9Pg>e;dPIl0t~oZdS`S+NvqXJ6^-n8%8qRdYuTxQ*ur{PcJP${Jnc3ibO}VZc1_D5 zO?I4PXN0Xa7B9(OPN1zinO}42;l(4EyZ2B|?OqiT`uoypO!uv_!n(fnLFo#M zxKz1AbH~5xOS#X=g7v+7Z0HJOdPY z41#6{#Hd7sZj;0rb9uRWw9JEPFkay^qA-iYWqzx=eWu?AkaoQ?4A6oT*;0x*@yNA) zL2Hq7_0=s(hrdTFkUh&aWonk{YDfo+tF8Ia*schBl~sl#U-ov=?+^})aPHs#V$P1T z&>f0f<8W7g!^dGi8F*=7x)g4<6={l6Rac`0@E*Hjp?hFRty@}7{(^}&z8No#BYTV} zHx-%XN_DtkvJZSAbW1y?8_<+4ggdThEyN?$7v|oZYYKH!7ltWmj_aI(W_@q@q<-fU z=mrJ$BiPO)?B=o24HZwPP#Fr=-*Ma{UMgm$;o)*-A8=#9=;x~(a=B2bhIEa^~Rj^fc-sBWN@A=_mUdMi({5+xdMVd1-*3{GQwFFy7ho}#$nO-pZMlHrwfpzuKC93CB!^0SwWnV9-$ zV(F_DhgQ;-%q6(4Brl#W@BKRU;`j|rUfiKxgXB&pB{cmitkS`oXS1}1o5RX=nIN-; zcRKTF)jM?jn)0qbHj?4{!z{P56=IbZLm|GOP7r>&&Nmx=Yx^!U0R9LBkMHE0UQxgM z@wM*bG$dCRj7wU5-Qg$r-(er#5t5|f-y2`@@8^$ps(*AaNZS1m#>4$!zsv^#0U-=w z=LR9?20I>qAnrY?RC<8a5hM29~gA0E8tq zAS3`-qMoT1SQHa$MQ|4u_mApWQh}tbn8=bE$5xb}N%8Ph%7Hb9eUR6A(aP11YiyQWcMq@Aob&fb zPiHtiB-N}s7A>UV+|h2-xq(Rg(htDd3ceB-IX_WjO`pb^(E|7H7EDMC9la}etgv=` zFp)heW_Si`MZ*ngBNtjXVHEYCkaLu9oUql3$<6PwsNL85Ye7{o=6vKGP)J{9iB%oj zbq^6y1CT3baYPcp) z=**z$OWI&~qPPML-B%iTYU*W%%w6?P!U@T5Y!YmomKilCZ@WyFYQ?KPoF=+h4H5J( z0`t3X*N8?c%`F=se;^6kh0<2z#4O6Ak`RXXw9hF9!igOCDPMM&%OJx!hLFaqNI+{t zuMWiu&5cl!M(Kv^*OtWdKtrz%2MEFY?n4bjQ~k<9$#wpop?q z_#Lj1-{JbBh3y~V`d?03zKPoMeP3_~<Q6%Q`xA_<*am8)$!0Wl=Qw}Z&c{tRy8}_Ij2mzV;RSBgJCIg%N{J~jT20bl zD9;z=*#4B*@pwGh!4v%09W?54X;~gOaJSgqfRlaNlUtG*DL;{LoeFBg(LH$M*MxU`;tcliW(u#KRQ=(pdm2w-m^D*Qo_s$904mQW)soK_ zaI@}5gQ?6yL{x=LEdLP-*=OWR;qz08Cf<*5I{0D#e%0}qRAag3m$-<+8`X@J`(hc? z7?M-Vss&uPMuxuuh#^fO6nTQu8C4_na04-x32UOT9?5*#>EQ(YVI!$nQV>ZDMR6$cyZYJ|NnEU~RTgi*LWbo(AIk9lSqUC;t&RB~xR2 zCzJmMzhft@Km-wkf7h6fGY}DdLOmSg_YX&d0tsnFuKA|e&S=M{n5H71EjagqeJDq5 zz|eSCW>{2peHL!+?_dmqn}H1d>GL{GD@=%O3lnj-jn z^6uGDQKkLH##w%UYW}~iHUAs~CDY%{j!yp^iDWfvbrf+-Kk^7W9Z(6xs9m%wAt2?3 zB0@D$#JmmYJW3;melrPl_EbAI+zW!;iY9b#&Wjsm{(H!K{dz8PB9o(dI67i4jTxyw zP_l@j@z4l2E_RPG1(s)43>}sh#{+v<9il6C4+n+frs`+BV8hNUj@s?~4;hW11L~8q9ou#UZtE}YD$Hp&E&7<{bX-5uq@4%p4@2AFVF=lj#^wy> zlXO*>7yBZeF!m$+llN6d6Q}ZBYZ)!By|^d74#L>!n4t zF5!A~8c!+53+`OC7X86b4ps2FNCqlR%cRJlxU^ELX;nt%oq+h-e~6uJ#IA@2EGgKP zwre@T%*~fw>_muaZ_@Xs$M=*Zr!trVn72fg84c!-ZQCup-NBTubQ48?TpnCVX?Rc2 zRP37_B~MVRtB0}u0J+G11@blSL$g>%AVlo8<r<7WNP`9f~0s=MxuwEwJR@U=Lp zOzTDKQBa0S;xn9t25nNdSjCl&)$B~o6290;HwdLA(i%{?gSEFAXB2?aewN`M6xg#G z#Fc*G?4eAqFit3TyakGTyR4g)`Pdi zbz6$mO==_HcBAmy+A5XbGFj062BxEee63(QST)wx6S9deMt$~hI(r080ru3gy|A~2 zV1ai^Fvme83hO2rAF=AMl++5)x!CaDZSSKGC}692iSC6DwFuV1$rH;M~# znagif-pPZ(oi2~uWj$HhLUD7x(yVHC>SqWxE9NMt6>p<%_eH#Py&Sycb<>61R8!9p96a!ZdRMcfJNlpS3NOEZo}*3LPjn)=vzgBot<7jUsBzD1us$>E>Hc*UzL zvY=+3C-Gu;+^37}>OkYhTNm*GrLazH1+FJ28<6f=v2|JNB`F-n=UetVKXS$sWXHQN z2EHAgL11oqg*_x<6?4EVXpw9sK=|U%NKDZJA3cAH7P=}e60J{HL z?<841H@0(jE4$#3r+(y_QMc0a%Mf>0jo4PjHQ7bu&t+XR?YjqmFQizw&9Y?+shnr< zNyYOPruRt-34>L4=W+zub>nHB8`!6(zECd zX5^Z?`(@}p@ix-7yZ7W%JaaS`bR62W$j-ZabZXNkyYBENlpE>s} zBTUOi-Myu~`19|JV;)MGHiaMrW7!XP*VLah3G<73l z&q7h8MMXHFO3G5LXxYUVSjjJ=h2H>yn@_&2FduVpHlAz0D)_x1JHvkp<#w5q2PeQ9 z3@Fw;`V2LmK(yuaz{%t6blGeH{DQH2*ARs8THsiPz@lcXJP?g zG;);_gmb~VW7_p2eUqRr9Sqdyyb>RaYlw~x1pQ)Sf`%Kc7((Ac^eBtPiIoyqFfy)@ zbR*E~*xOvVU5J^A#sezc%XkbOf)72){_atMZNG@WSH+n9Didm^(nDkts}vho$IZ5! ziPA%-tK!QjTX^$1O3E0m;_70C<}1tkj_I_5z_lF{F@nhwb601bY*|M#4jT(AD<#%% zMtU0eSf0{C1 zsN|et%THX&OUvc7O3#kN0+$5`k|M<1*8 zr8(b?+-0g!*O!+SM^Jh!lP#wzIdksRi7nH0LOEN6iL?-DlH+}iTxe(u4ppM!t!Bc# z@zI*ruq)N2XH(9VKgmGD8lz?M4m8wZ#gb(I1RykK6dyU+700iy)~$k5A6Ol97#6Ea zql!oHBg1!3v0$sJ2VV&(FTNh=^Ydi=k0b%QI z+YOD9#lgkdn554&7G)q(`kCA6`C>m8?gI)--@Teg=RvB1zpcA+`o4&eTRP3};FK4& zWy2ewUPVnqB5O5O7?Ie{&;MeH8Q8G>0T!4Z|Z)*MK^Yl-q^kg6=9xKX?s?;l*!dwd@=HeickbU>>$r8gOK|;X{?gA8!)=$R1;q}_QNBsLf_6VB^e0%3wkDvh_AcoOW$nF8 zuQ-(Xl0fj3Vl!9UcRw19dboXf?U87zq})6w{APbSAb6~hw~!n0Bg%)T{qlhwg+_Ca zAE!F#1$x-Sgks=U>54#oHTn|VQya9sYA99bQLqrihbaVTMkwrD-$Ko$+Ou78Y;=}+)xZ#j1r3||5&1Rt09E+)Pd;S|n zw+nYKVU*8Y7l=v*NHFkScmgU)-LK1_hxZ0UT%7B(57X0&<^Wn;oV%csm4#ny)KdPr z`-U|`LoiUdzEZ%USb2uZXuj`;tGg3}8&W3%{b|kxr?sny@d_$?vGF-B%F!)gE#ABz zWra~V)S{3_%*<@aD}=CVv~Qy}6Wchn4#_n7e8(aki)7wLSvWtmmXSnYPygt+-D=J_VN)ga}Vht$BaXwSghj>MM9 z3uro$8tK<7yUryYkGtUonpSZZ@dNU_`-=YP%3w6X&h(*8`(fHPfN(M@@ zG?1}XRJ>2kSe&!eF%JBh2lZo3O&fN8p|^>5UHLeYIju8&$@62XndEeP{2|3eIhN%$ z;k9|2_2~0;O}0M|*HeBwwI7H?UHZ~v1_QBO&s_4*6&dd=7=rTkdw&B`0Pb*5HS$0_ zbm5-y{XZ$N77~^i3)T9<>U8%mk60AUiw4Iefpom! zj-K3MJSD#Eq0Hl65GWnYGL*c!x`u*|z#(YmiNhaoo@FHF1)kuLW7 z8krE#4{A99oYOO8J|C+4zyU(A*i5?^{_&fLbsho2P-)nf4#e-vhRllP{ciDL;pIuY zh=IC6IIhB!egKv~F4vQ%;owB&!x)G>$wzBz_sf4XIo+?Z{M)I|V=lunjm;)%pxhUt zhAwNYG`Sv&BP$v??hvbNo62p{BMaa0n+WU|zz0g4j_164@hSq`h(m0)Kh(Jbd~$~q zENW+Ht9q-NLPwM+j7K9>b!Yt+4`&b({zQQu&RwurMP;Z)F)Dqp(3Ek(zJnrM!>YAE zH6S2Kg({Wmj9CmONrcEZNDehhVsj*DIiAjYJd)&l{QE>x$O< zRXO5O>TDF{>7bkq?1e?*y!|!8+%t(H-CeGSKPGB8fLv2<8U|~YXjEHTlphEKk1gy2 zwcuOHm0pR;U$)1rXLW465ZD&iT$>J84f1@zm3$LCJF}2e$4D>}BiYi=kx+k{=idyY zJSwDzLu_UAQJ3pJtk&<1$2j(^gF3G@q8k-h(xhMyL^?O*r{9w&G4B|nn{YHhmXwxc zR8RC~^4o^c^S{9&XDdHAIzZNdOI(%axJ{9w5RI9{392SD+5$g&rpi|Bo|4~IAP-Q^ zDY>;TLI1OI`fj#;CBKCIqr1dm4-6zRM_`|m;|$rzb-yQ5`y|Na7h`0=fG$g>_^xzK z5?=D`A(YgDgQ}eVqO@56&r~#NLgKYO-nbj;?~IDwh&RRqhI^Z-rlJvNRn0MrKY@-cKhegg=cdOOq!DcQ2)|a9<)I+Wrq8uj~&#D=;qVQDm02s6) zTDhbWy#B8>{*z#BUY$TK!(FsUGT}qP6Nu8XoBJ?)1K*C0?ZGLI%k-f|?1EplC@fHd zB2X^{1T*Xg*@d@!Kfm+zv>hL8M-%$oUq{ z?2jbVCiuc40u>kJi2-lG-@?$7B4z@rZQN2xLdhD`8YcPr%?$3ORl*21qfR9~r%N{O zFUr}l!k_6hNq+ztwd||;Xm{hHX?_V%@4Wd(J8gay8Y6@tFZ3vX6nfzI(BJYtqG;WM z9~lgvmL{};l*CZ@ci&~m8R)8qx+H#+KZOI@pvSE%p*`9UXmD=mn9N8C?Xoyq>G$qa zYk8?O8mzqrP0jOyd#<45lnhA{`!v9y{Q4LEr6@ zWbI_$;m9#6>hFA7w2ggUUN1ZoPHd4s!TADe-4kCoe_bNdl#^@iug%kFYV_l#GevG| z{WQaWOy#KRatjqrwT4L#ZIUJO4Sf~q@WH7=mh`#j+|wJ}vAv9=*6eiRu5H;9+d69 z8&}wB53(CUmq>x>ZH621H7GavZ)2<^r7dX{nmWSL&D`4a+RKF|QQPq{QLy^rd|q0D z=|g3;Q|^_YxhdR8_aE%_=dQ8JT7lNcO>4_u&1~Ato3*_#iQT)$CLxUs1H1yJmIQ;aQ3C+>QgBbVdD&GACSeWOmd^ zUrIb-=jkKogn_@l_2|&$$B=^a(TETVm7a=&UfI*s5PiGNh!VAg!?}w|2g2&HZ|d8X z`PZ#9?8{x%4V4Nwe^X%uu{#8p#x$s5h8E#=WFmIe)*lzohnp%pwc~0z_Y^g2EUh{{ zGd(;%b3IAST&MH#F>5STcX2(HYfsCQuG%g>esmg}X18}fjl?-Tmc`-qWFFM1Ea^H^~NMF~PQwuZyzy^vcZI(~SwZICC0ozTok?lTyW*3DZNzi&Xqmn$F8pe>aK zwQIgbnUqXBE=JW=>++GCsKwwj5`z|Isbm-184z|&k9$T!8tHT>P07R>iWR_@5}N8u z2~9Roy>6+wsDqpfuV`+e8$-i(oQfbw8RsezbR3{@PI4LgaNBehL*$gCh zA232el6(n7o;p}jpOr7SV62sRlx}F}kI)7r3GZ+C!6~}hl$PNFNdvXDP7|rMt^9sI zuiyWK@^b4m$6Ut6XGOq?OfEVa=s)QqdRS(n z-d+!-GoiHf$eRzZ3KHZc(HAO9?w`wHz~V9@zQ;NbNYZ5HA$Q`+#67vp;d>N0D({-p zpfM-({hGoG1^za@Z<@C)b8u|$BUK-y#o~2dFUc2MC~Tnz34k%lkW??NPoL<`j{@=0 zS@2B}@p)cQc2ci|nNd)P;&z`f*hcG}87Cj=c1X|r9%6)!O_m0u{b>?s@DX?8V@LD4 zN20EZ7jDQD+x^>sw*LKbmAckTkWYC!`bDKPn*C8OfWr`Yx2&I?%!CV>E@@!zd|g#l%NK9NB-2ll87Y-neYse zL_rixt$lF1oRD%X=}o0tT7e;3Of^kqY;lB-pFwp)Rs^FK*sD(p-w1^?K(j)dLA7zdh=;TdrJYV zu<|V4SiNQrEZesn9DlJKv2GZF99oP;IRuX`&DKMkvdz{bSM-L}hD(OPL$;<GQk+@F8@fNc!dL6sC6y9jGKz0l@m7e?1dJoiZ7 z5GcdxLA9R2Xmd@lGy>%oYesv6fk{4CL3)vP+P!fP1Oxr zVlb#w=lsU~TDv0{97>#%)n>IkYcY9P$$!2VKx?2}^TKx=IeB&f|;m<>AfFx*g9fZO#i;Ht|4G5i)^5oi*N= zkI*p6T@;V9cCUY2j4h-u$o;}UO{JCZ>M?=-_gA@{jiswv%QBqsC8Q=fpjOZyODCOZ znlm_n90Dbsw3GA<1Lr401TJllkrhnv-nm}#C_--{a>kzqw2ZFt(AGi}Rc+ZmR!9-? zA9kZA5`!K(Qx1r{eF6&(G~JLcii2ztTP_C-tcFrqd09J=5pBp6&1}H+I!=`haSWCf z=%n6v4B$jx=uaN+@#!#;;6yT23<;z2!h{VILq%Jg7fr#g^x26!;E=1M=gT_L z-N@C^iwpQb-joK6&U39IsKw63q@jV~X?*b79@1jYsp8f#)aW$J!z4%HLJCXUV9%8< z!8M7B)T={38MrF%9UY5IPKOWfRd8yOo-PpAi5cyAcnXPo)JQCb7+e+ihUl2INUEe= z;t1HCa^pxKB2IeK%^{1I`sr>2K6EU9^sXy1^)QicVjtx^DzmpYpBzOe6w@FPucu0n zbcA74&Ol7Qz>;C;fJ&6aYLhz8R^t_w4BoI2Ks9>K0vb2{DOvz_OU17wz zgDWijp>m3}&U~11s#oF=U2yTt0hoY)L+#Zo#W%?*c3e{$?gcH+c+-6mIa>9)!{p=Z zKQ~fZ>w4Ws@r$kUf8d>Onr6@tx-*l(s+q!It($7HWdVp)A;L#O50p%qv)@fkMb6H+ z`#TK-jjz>3^ztlB?D5A*>v%rEAa^SQC5R)eD_}imhrzIr8HA56cKGmq;lUGL0d0?X zeEV`w>^b`MQoHc@x_~F1(Fd%#2esnJ*g29?Rxs=(o0ep@25n<~@(I!toVZMi;Vcfn z?^w0eAEr&~lBWduT@t1|AiRoGP)C}=O;!5`#Zna+Lx4{uV1{VGb+*Ap4+OR4!!-lYG0BLC0# z|E*hzR9RJ;RY1`#qRj7uNK6kyFGho`h*48iL?|K9hZBN=~E3N3%48-q>f>X%{UW7V$;;R#+{%v%2Bg zMrLW!217wI+6Q=nIxaY~x_@0JnU4!&5?DUz6Y2^Z)8JyEM)tseHjGZG&Yu6~K@O!M zj8WEz+6JJoBHqIM(vYR&iS597Agfp3kxt!q1xJLL(PFlTb?V=#z~Tu`u{~;SiSUIP z8rnf>bR^0e(dc5v3@-+LMbJQw!tCOz%G*_(RF++iy8ELdmNDt&QthJZjZP^TC~)~e z18CHXR$7$RH*MHtdd)DnaB+p5LMwh@$aX1IIrlQ~((BVHMw@Y`sM4xnJSA1AxzDHV zKRb!@kWS1FkEvt+oXE+=EKP5-EWj$_T7VfLo;iQ<4d*X+#Hl`LO;XP0p6(tJzcliN zabDfcH=}$WgT|BR%u$(a=9BMJtf|Gw`%KWodgQCNPLfA?-^`EQ0TJ5}8%^dq1|Q@>*D`o)r80F9e}_@gVsTGk9%x>YMJ+nE>gj- zY@JsGd(1o__d^34osCF)LjoTH{PQ+S!L*Ca3!IZ9zb4e9?ugi4n6_83Lt_*ntjS+d zskeATr@R!SH2*7$IKXJX8b}0fDZvM)Lsr){9lW!>d{m5Sgh+(eRpDSs3thpT0-;?R z6dT`!g+?+ZEVB*eA+lnpL~J)5Wqt79nMeOpDl4~C;%|KUoyO7s+3)lp_rSk6p+dvP z9c3QXmt3DY73K#b0s_kqA&K9z-zbstktA5sexpW!zIIoqlEmAZE-tTQ6su}mYQGW| z&#RT&^slIeW1t|BpjBwC=GIqeY1gY-l`W`M*kl(?&hq=bY@{wPn=lck^SswQZlpR* zcx^cGPc+vKetO+u{MLG_MWL!hA3qem1w%!?62)W(en>NQly6Z}8GEY^L{jays(M3s z8LyG-o!O}|A~qI3VcDXN+fa2x*+DFy3wc!qaI2u~rc+(?H5z+s_rshiUg1t}7W~RS zxB`8HMdpK=NXwTGTe829 zB-mP5bci&@188%aRHF-EAswsmtk~iyA(hix@AY`&Vr< zUa~0@CkIXInA6Cfb)!J%B4x2Yhg6B8g_UW#+QpQ^r&4X<>f8;}-Z%o*Py=OdNmNZS z{C4_sofL8y*@ga9NmIjG3ry@8-c?wy+78|U_WkkeVmzy_i2;uneSsb%^v$50OKL`a ztr+v4(v>N6RwPPh+i8Jw7A7J{YU*_lrd`BAQ{@r1yQ~&PVw8_~%GN>sh;P3mij3b> zgoZ6=nUt+aWbnHc%YuHy-_p1pt9i+kKk^K^>C4l^&Ak%oiqRM>4G($d3Cj;Ep|@x` z)4Lr_tE_Dh!hkwv8^zhP$uj-5I7f5)WNFc2p^$XPwtN`AD=CiA^m>m%DKaY^lU*N?7T=Y zMCw@P)beGyx~dn?c*+m^Br)*IP}0Z@mh!Dn6Ph|}rOcz=XDt+nvDh|BYv^!Vt2gJi zAXr^MhYrn(GsP+-$sD^B z>T`prMvFxnJL<{G!n*BUNdz8>mpcmT%Pv%_VN{Ow+6V0=lUB#Ur|(#BTe74XZWn

    N}hR{0dLySoh!!h~(WZKRed%U$2{V7gCf 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 z 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

    r6T9k`3Qu3|?nk%Zsi+St;89-&5yV4X1>E zoDJQnyX@P`9Nut1-WX7A4*`AmaX!e)rTf?CzW}g)o?-QHiWO<>pzXaomcfFS@r&t{ubJP4f}1tTO>03`wv{?9;(f^B-g z9gN#)6Xq3_4-{H!@W0qfD~dFOCC8>_Ztl}-2M236{M~+_vxByvnv*A#w3wp>HU`Mj znzR!(SmpVZMWx1~BdTFCO$RaK@L^8eP`X)&ZH*+CFM_vP;RR4vSA-sgl?X4N+>8Dz z4_&AD0}Ux)3Ts9Ze}lRWM*s5a^K5x9pze|1z+059BH;3^?iWz3985K#4aQ$|UfhS5 z3qw5A-$VgecZ(A8i@buOCd3`)s}To!kx7E+zeHU&H}rQf{EAuRp){FkNl#Z4&4Gj| z5=bNm*;62{Y=y(f$K)W47}Y=Zo^-osWV45{P|FO)$Cu|AEe&cDwzfX9 zYQ!%XmX~F8Mk-@*$nEyg2gZLqrWE4;u7X~7JU4FFI*j(O`Ey8wJ0;2+OK8p~z9nT@ z&gvlZBJ%P_-e6MQvc=3P*tuCCwMswBXkHoD;~?j>jX$@c{%{Dh!5D!J6sx}FK$3a+ za%MtGz*H?75@3f9n{^)A~eYczgYw^+R>kWAm*;*b_h8)0I5-lQ5IU_M`6A-SErv z%xOrkB)KJJLdnk3yOTvi^YymJ)>K+Ptfo`RY<--M*Q9&B-sw9Hipvu_fP(vF&$*DEXtO_~EqqrtE zDVIk(1_V~6y9TDys)-scoQYYiU3=bxzuOi4>TtPl+NZ=A85m z&ATFuzj4uIGTsTM9H?qhrH^q+jbg*9y*EX|JFG^^mUTh8N+qMlY|W~rm$ofnQ7mx#u*7=N>_xraf1V4LpoqXKsyC0$_r!qcyXrB= zy6S<}wY!EMuoSA-u4>z|q^7M)YK*aQj-vUcq^+A;K(DTDQckm^8gytSt!rFmZFchRgJ%XN#3yh+`Pq0&CB1eN?+nFyTtFTM1Jw1N$ct*3eE#^%XeyUVVMeNbU1+0%aQ9I?eq zyYgoBTvxlxyy}JSDKO2u0ug*qyyd+pnPB5wKJ~pZ%~=G4c}d>}yugq6{fC_aQ6UeC z%0D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 z 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≥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%0D>1+d;giZJ7s_0K3j_X9S~I1 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 HIGX Date: Mon, 1 Feb 2016 19:51:32 -0800 Subject: [PATCH 396/826] Fix #691 by making sure the order of async calls completes properly so that the result set is filled BEFORE we try to serialize it. Signed-off-by: Chris Larsen --- src/core/RowKey.java | 1 + src/tsd/QueryRpc.java | 18 +++++++++--------- test/core/TestRowKey.java | 9 +++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/core/RowKey.java b/src/core/RowKey.java index 0635b21a97..694ac119c5 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -98,6 +98,7 @@ public static byte[] rowKeyFromTSUID(final TSDB tsdb, final byte[] tsuid, System.arraycopy(tsuid, tsdb.metrics.width(), row, Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES, tsuid.length - tsdb.metrics.width()); + RowKey.prefixKeyWithSalt(row); return row; } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 8baa487ea3..3aa42c3d32 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -314,9 +314,9 @@ public String toString() { } } - final class FetchCB implements Callback> { + final class FetchCB implements Callback, ArrayList> { @Override - public Object call(final ArrayList dps) throws Exception { + public Deferred call(final ArrayList dps) throws Exception { synchronized(results) { for (final IncomingDataPoint dp : dps) { if (dp != null) { @@ -324,7 +324,7 @@ public Object call(final ArrayList dps) throws Exception { } } } - return null; + return Deferred.fromResult(null); } @Override public String toString() { @@ -337,8 +337,8 @@ public String toString() { * metric and/or tags. If matches were found, it fires off a number of * getLastPoint requests, adding the deferreds to the calls list */ - final class TSUIDQueryCB implements Callback> { - public Object call(final ByteMap tsuids) throws Exception { + final class TSUIDQueryCB implements Callback, ByteMap> { + public Deferred call(final ByteMap tsuids) throws Exception { if (tsuids == null || tsuids.isEmpty()) { return null; } @@ -349,8 +349,7 @@ public Object call(final ByteMap tsuids) throws Exception { data_query.getResolveNames(), data_query.getBackScan(), entry.getValue())); } - calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); - return null; + return Deferred.group(deferreds).addCallbackDeferring(new FetchCB()); } @Override public String toString() { @@ -397,12 +396,13 @@ public String toString() { deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(), data_query.getBackScan())); } else { - calls.add(tsuid_query.getLastWriteTimes().addCallback(new TSUIDQueryCB())); + calls.add(tsuid_query.getLastWriteTimes() + .addCallbackDeferring(new TSUIDQueryCB())); } } if (deferreds.size() > 0) { - calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); + calls.add(Deferred.group(deferreds).addCallbackDeferring(new FetchCB())); } } diff --git a/test/core/TestRowKey.java b/test/core/TestRowKey.java index aea4ce34d2..59032a9f0c 100644 --- a/test/core/TestRowKey.java +++ b/test/core/TestRowKey.java @@ -148,21 +148,22 @@ public void rowKeyFromTSUIDMillis() throws Exception { public void rowKeyFromTSUIDSalted() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 2 }; - byte[] key = { 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + byte[] key = { 1, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); // zero timestamp - key = new byte[] { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2 }; + key = new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 0)); // negative timestamp; honey badger don't care - key = new byte[] { 0, 0, 0, 1, -1, -21, 88, -128, 0, 0, 1, 0, 0, 2 }; + key = new byte[] { 1, 0, 0, 1, -1, -21, 88, -128, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, -1356998400)); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); - key = new byte[] { 0, 0, 0, 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + key = new byte[] { 0, 0, 0, 1, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); } From 081f8ed2f3e626b0ead45519d42cee53898b2a64 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 1 Feb 2016 19:51:32 -0800 Subject: [PATCH 397/826] Fix #691 by making sure the order of async calls completes properly so that the result set is filled BEFORE we try to serialize it. Signed-off-by: Chris Larsen --- src/core/RowKey.java | 1 + src/tsd/QueryRpc.java | 18 +++++++++--------- test/core/TestRowKey.java | 9 +++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/core/RowKey.java b/src/core/RowKey.java index 0635b21a97..694ac119c5 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -98,6 +98,7 @@ public static byte[] rowKeyFromTSUID(final TSDB tsdb, final byte[] tsuid, System.arraycopy(tsuid, tsdb.metrics.width(), row, Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES, tsuid.length - tsdb.metrics.width()); + RowKey.prefixKeyWithSalt(row); return row; } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 2c93eeef1e..bef1a64cf7 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -354,9 +354,9 @@ public String toString() { } } - final class FetchCB implements Callback> { + final class FetchCB implements Callback, ArrayList> { @Override - public Object call(final ArrayList dps) throws Exception { + public Deferred call(final ArrayList dps) throws Exception { synchronized(results) { for (final IncomingDataPoint dp : dps) { if (dp != null) { @@ -364,7 +364,7 @@ public Object call(final ArrayList dps) throws Exception { } } } - return null; + return Deferred.fromResult(null); } @Override public String toString() { @@ -377,8 +377,8 @@ public String toString() { * metric and/or tags. If matches were found, it fires off a number of * getLastPoint requests, adding the deferreds to the calls list */ - final class TSUIDQueryCB implements Callback> { - public Object call(final ByteMap tsuids) throws Exception { + final class TSUIDQueryCB implements Callback, ByteMap> { + public Deferred call(final ByteMap tsuids) throws Exception { if (tsuids == null || tsuids.isEmpty()) { return null; } @@ -389,8 +389,7 @@ public Object call(final ByteMap tsuids) throws Exception { data_query.getResolveNames(), data_query.getBackScan(), entry.getValue())); } - calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); - return null; + return Deferred.group(deferreds).addCallbackDeferring(new FetchCB()); } @Override public String toString() { @@ -437,12 +436,13 @@ public String toString() { deferreds.add(tsuid_query.getLastPoint(data_query.getResolveNames(), data_query.getBackScan())); } else { - calls.add(tsuid_query.getLastWriteTimes().addCallback(new TSUIDQueryCB())); + calls.add(tsuid_query.getLastWriteTimes() + .addCallbackDeferring(new TSUIDQueryCB())); } } if (deferreds.size() > 0) { - calls.add(Deferred.group(deferreds).addCallback(new FetchCB())); + calls.add(Deferred.group(deferreds).addCallbackDeferring(new FetchCB())); } } diff --git a/test/core/TestRowKey.java b/test/core/TestRowKey.java index aea4ce34d2..59032a9f0c 100644 --- a/test/core/TestRowKey.java +++ b/test/core/TestRowKey.java @@ -148,21 +148,22 @@ public void rowKeyFromTSUIDMillis() throws Exception { public void rowKeyFromTSUIDSalted() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); final byte[] tsuid = { 0, 0, 1, 0, 0, 1, 0, 0, 2 }; - byte[] key = { 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + byte[] key = { 1, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); // zero timestamp - key = new byte[] { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2 }; + key = new byte[] { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 0)); // negative timestamp; honey badger don't care - key = new byte[] { 0, 0, 0, 1, -1, -21, 88, -128, 0, 0, 1, 0, 0, 2 }; + key = new byte[] { 1, 0, 0, 1, -1, -21, 88, -128, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, -1356998400)); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(4); - key = new byte[] { 0, 0, 0, 0, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + key = new byte[] { 0, 0, 0, 1, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; assertArrayEquals(key, RowKey.rowKeyFromTSUID(tsdb, tsuid, 1356998400)); } From 5df84897b34e49518bf5486033e43453f9a56439 Mon Sep 17 00:00:00 2001 From: Can ZHANG Date: Wed, 3 Feb 2016 10:53:02 +0800 Subject: [PATCH 398/826] Remove extra getFromStorage --- src/meta/TSMeta.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index a9d6dd2a69..a1863803b9 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -338,15 +338,14 @@ public Deferred call(ArrayList validated) } /** - * Attempts to store a new, blank timeseries meta object via a CompareAndSet + * Attempts to store a new, blank timeseries meta object * Note: This should not be called by user accessible methods as it will * overwrite any data already in the column. * Note: This call does not guarantee that the UIDs exist before * storing as it should only be called *after* a data point has been recorded * or during a meta sync. * @param tsdb The TSDB to use for storage access - * @return True if the CAS completed successfully (and no TSMeta existed - * previously), false if something was already stored in the TSMeta column. + * @return True if the TSMeta created(or updated) successfully * @throws HBaseException if there was an issue fetching * @throws IllegalArgumentException if parsing failed * @throws JSONException if the object could not be serialized @@ -588,10 +587,10 @@ public Deferred call(Boolean success) throws Exception { } LOG.info("Successfullly created new TSUID entry for: " + meta); - final Deferred meta = getFromStorage(tsdb, tsuid) - .addCallbackDeferring( - new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))); - return meta.addCallbackDeferring(new FetchNewCB()); + return Deferred.fromResult(meta) + .addCallbackDeferring( + new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + .addCallbackDeferring(new FetchNewCB()); } } From f99147fd03704654649103085f05d1a698ed4a5e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 2 Feb 2016 19:15:01 -0800 Subject: [PATCH 399/826] Set tsd.query.allow_simultaneous_duplicates = true by default. This was causing some conflicts and confusion so we'll let folks enable it if they have problems with abuse. Signed-off-by: Chris Larsen --- src/utils/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index 554891e0b1..f1ff0e03f1 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -500,7 +500,7 @@ protected void setDefaults() { default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); - default_map.put("tsd.query.allow_simultaneous_duplicates", "false"); + default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); From 03232783cfdc2c237e274de0de431348dd98b167 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 2 Feb 2016 19:15:01 -0800 Subject: [PATCH 400/826] Set tsd.query.allow_simultaneous_duplicates = true by default. This was causing some conflicts and confusion so we'll let folks enable it if they have problems with abuse. Signed-off-by: Chris Larsen --- src/utils/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index 3344ff2e3c..2227800017 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -492,7 +492,7 @@ protected void setDefaults() { default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); - default_map.put("tsd.query.allow_simultaneous_duplicates", "false"); + default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); From adb091b4f7d73bf68dd3e622b6710cdf87c2de99 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 4 Feb 2016 14:37:30 -0800 Subject: [PATCH 401/826] Fix #684 by allowing the max unsigned value to return the max SIGNED value (With a note) so that if someone does bump their UIDs to the maximum allowed it will properly display the stats (at least until Long.MAX_VALUE uids are assigned, then it will be funky.) Signed-off-by: Chris Larsen --- src/core/Internal.java | 16 +++++++++++----- test/core/TestInternal.java | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index e8d7867874..733dc04859 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -888,17 +888,23 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, } /** - * Simple helper to calculate the max value for any width of long from 0 to 7 + * Simple helper to calculate the max value for any width of long from 0 to 8 * bytes. * @param width The width of the byte array we're comparing - * @return The maximum unsigned integer value on {@link width} bytes. + * @return The maximum unsigned integer value on {@link width} bytes. Note: + * If you ask for 8 bytes, it will return the max signed value. This is due + * to Java lacking unsigned integers... *sigh*. * @since 2.2 */ public static long getMaxUnsignedValueOnBytes(final int width) { - if (width < 0 || width > 7) { - throw new IllegalArgumentException("Width must be from 1 to 7 bytes: " + if (width < 0 || width > 8) { + throw new IllegalArgumentException("Width must be from 1 to 8 bytes: " + width); } - return ((long) 1 << width * Byte.SIZE) - 1; + if (width < 8) { + return ((long) 1 << width * Byte.SIZE) - 1; + } else { + return Long.MAX_VALUE; + } } } diff --git a/test/core/TestInternal.java b/test/core/TestInternal.java index 40a08f8192..c654074078 100644 --- a/test/core/TestInternal.java +++ b/test/core/TestInternal.java @@ -822,9 +822,10 @@ public void getMaxUnsignedValueOnBytes() throws Exception { assertEquals(1099511627775L, Internal.getMaxUnsignedValueOnBytes(5)); assertEquals(281474976710655L, Internal.getMaxUnsignedValueOnBytes(6)); assertEquals(72057594037927935L, Internal.getMaxUnsignedValueOnBytes(7)); + assertEquals(Long.MAX_VALUE, Internal.getMaxUnsignedValueOnBytes(8)); try { - Internal.getMaxUnsignedValueOnBytes(8); + Internal.getMaxUnsignedValueOnBytes(9); fail("Expected an IllegalArgumentException"); } catch (IllegalArgumentException e) { assertNotNull(e); From 60c2234c0bc8436aceee576c6ea4d3513db50676 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 4 Feb 2016 14:37:30 -0800 Subject: [PATCH 402/826] Fix #684 by allowing the max unsigned value to return the max SIGNED value (With a note) so that if someone does bump their UIDs to the maximum allowed it will properly display the stats (at least until Long.MAX_VALUE uids are assigned, then it will be funky.) Signed-off-by: Chris Larsen --- src/core/Internal.java | 16 +++++++++++----- test/core/TestInternal.java | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/core/Internal.java b/src/core/Internal.java index e8d7867874..733dc04859 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -888,17 +888,23 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, } /** - * Simple helper to calculate the max value for any width of long from 0 to 7 + * Simple helper to calculate the max value for any width of long from 0 to 8 * bytes. * @param width The width of the byte array we're comparing - * @return The maximum unsigned integer value on {@link width} bytes. + * @return The maximum unsigned integer value on {@link width} bytes. Note: + * If you ask for 8 bytes, it will return the max signed value. This is due + * to Java lacking unsigned integers... *sigh*. * @since 2.2 */ public static long getMaxUnsignedValueOnBytes(final int width) { - if (width < 0 || width > 7) { - throw new IllegalArgumentException("Width must be from 1 to 7 bytes: " + if (width < 0 || width > 8) { + throw new IllegalArgumentException("Width must be from 1 to 8 bytes: " + width); } - return ((long) 1 << width * Byte.SIZE) - 1; + if (width < 8) { + return ((long) 1 << width * Byte.SIZE) - 1; + } else { + return Long.MAX_VALUE; + } } } diff --git a/test/core/TestInternal.java b/test/core/TestInternal.java index 40a08f8192..c654074078 100644 --- a/test/core/TestInternal.java +++ b/test/core/TestInternal.java @@ -822,9 +822,10 @@ public void getMaxUnsignedValueOnBytes() throws Exception { assertEquals(1099511627775L, Internal.getMaxUnsignedValueOnBytes(5)); assertEquals(281474976710655L, Internal.getMaxUnsignedValueOnBytes(6)); assertEquals(72057594037927935L, Internal.getMaxUnsignedValueOnBytes(7)); + assertEquals(Long.MAX_VALUE, Internal.getMaxUnsignedValueOnBytes(8)); try { - Internal.getMaxUnsignedValueOnBytes(8); + Internal.getMaxUnsignedValueOnBytes(9); fail("Expected an IllegalArgumentException"); } catch (IllegalArgumentException e) { assertNotNull(e); From b549eed5415ed411d9f69a5fd16b67e674dca320 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sun, 7 Feb 2016 23:17:44 -0800 Subject: [PATCH 403/826] Fixed typos in api/config/filters --- src/query/filter/TagVLiteralOrFilter.java | 4 ++-- src/query/filter/TagVNotLiteralOrFilter.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/query/filter/TagVLiteralOrFilter.java b/src/query/filter/TagVLiteralOrFilter.java index d80618e3be..f2ba5446ce 100644 --- a/src/query/filter/TagVLiteralOrFilter.java +++ b/src/query/filter/TagVLiteralOrFilter.java @@ -147,7 +147,7 @@ public String getType() { /** @return a string describing the filter */ public static String description() { return "Accepts one or more exact values and matches if the series contains " - + "any of them. Multiple values can be included and must be seperated " + + "any of them. Multiple values can be included and must be separated " + "by the | (pipe) character. The filter is case sensitive and will not " + "allow characters that TSDB does not allow at write time."; } @@ -196,7 +196,7 @@ public boolean equals(final Object obj) { /** @return a string describing the filter */ public static String description() { return "Accepts one or more exact values and matches if the series contains " - + "any of them. Multiple values can be included and must be seperated " + + "any of them. Multiple values can be included and must be separated " + "by the | (pipe) character. The filter is case insensitive and will not " + "allow characters that TSDB does not allow at write time."; } diff --git a/src/query/filter/TagVNotLiteralOrFilter.java b/src/query/filter/TagVNotLiteralOrFilter.java index c384697d67..78fae685ea 100644 --- a/src/query/filter/TagVNotLiteralOrFilter.java +++ b/src/query/filter/TagVNotLiteralOrFilter.java @@ -124,7 +124,7 @@ public int hashCode() { public static String description() { return "Accepts one or more exact values and matches if the series does NOT " + "contain any of them. Multiple values can be included and must be " - + "seperated by the | (pipe) character. The filter is case sensitive " + + "separated by the | (pipe) character. The filter is case sensitive " + "and will not allow characters that TSDB does not allow at write time."; } @@ -173,7 +173,7 @@ public boolean equals(final Object obj) { public static String description() { return "Accepts one or more exact values and matches if the series does NOT " + "contain any of them. Multiple values can be included and must be " - + "seperated by the | (pipe) character. The filter is case insensitive " + + "separated by the | (pipe) character. The filter is case insensitive " + "and will not allow characters that TSDB does not allow at write time."; } From d0e877c06b22942a5ef642a61c4744f9e675e575 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sun, 7 Feb 2016 23:17:44 -0800 Subject: [PATCH 404/826] Fixed typos in api/config/filters --- src/query/filter/TagVLiteralOrFilter.java | 4 ++-- src/query/filter/TagVNotLiteralOrFilter.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/query/filter/TagVLiteralOrFilter.java b/src/query/filter/TagVLiteralOrFilter.java index d80618e3be..f2ba5446ce 100644 --- a/src/query/filter/TagVLiteralOrFilter.java +++ b/src/query/filter/TagVLiteralOrFilter.java @@ -147,7 +147,7 @@ public String getType() { /** @return a string describing the filter */ public static String description() { return "Accepts one or more exact values and matches if the series contains " - + "any of them. Multiple values can be included and must be seperated " + + "any of them. Multiple values can be included and must be separated " + "by the | (pipe) character. The filter is case sensitive and will not " + "allow characters that TSDB does not allow at write time."; } @@ -196,7 +196,7 @@ public boolean equals(final Object obj) { /** @return a string describing the filter */ public static String description() { return "Accepts one or more exact values and matches if the series contains " - + "any of them. Multiple values can be included and must be seperated " + + "any of them. Multiple values can be included and must be separated " + "by the | (pipe) character. The filter is case insensitive and will not " + "allow characters that TSDB does not allow at write time."; } diff --git a/src/query/filter/TagVNotLiteralOrFilter.java b/src/query/filter/TagVNotLiteralOrFilter.java index c384697d67..78fae685ea 100644 --- a/src/query/filter/TagVNotLiteralOrFilter.java +++ b/src/query/filter/TagVNotLiteralOrFilter.java @@ -124,7 +124,7 @@ public int hashCode() { public static String description() { return "Accepts one or more exact values and matches if the series does NOT " + "contain any of them. Multiple values can be included and must be " - + "seperated by the | (pipe) character. The filter is case sensitive " + + "separated by the | (pipe) character. The filter is case sensitive " + "and will not allow characters that TSDB does not allow at write time."; } @@ -173,7 +173,7 @@ public boolean equals(final Object obj) { public static String description() { return "Accepts one or more exact values and matches if the series does NOT " + "contain any of them. Multiple values can be included and must be " - + "seperated by the | (pipe) character. The filter is case insensitive " + + "separated by the | (pipe) character. The filter is case insensitive " + "and will not allow characters that TSDB does not allow at write time."; } From c3956a428067651c1dacf070d7941571c8ae2eae Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 12:54:27 -0800 Subject: [PATCH 405/826] Bump to AsyncHbase 1.7.1 release Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.1.jar.md5 | 1 + third_party/hbase/include.mk | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.7.1.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 deleted file mode 100644 index 9d3a066783..0000000000 --- a/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -84b8410ba9003ecadbeececb02943ee1 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.1.jar.md5 b/third_party/hbase/asynchbase-1.7.1.jar.md5 new file mode 100644 index 0000000000..45ad0e9669 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.1.jar.md5 @@ -0,0 +1 @@ +f236854721eac6d40b6710ec7d59f4a8 diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index 0e25495aba..bb25220311 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.1-20151004.015637-1 +ASYNCHBASE_VERSION := 1.7.1 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar -ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.1-SNAPSHOT/ +ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From 53dd48645c63ea256f99a15de937c03be8b24ea8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 20 Nov 2015 17:06:22 -0800 Subject: [PATCH 406/826] Add a try/catch to the FSCK utility to log problems found when printing row information. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 62 ++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 45dde4539f..6295ff32f4 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -643,36 +643,40 @@ private void fsckDataPoints(final Map> datapoints) dp_index++) { duplicates.getAndIncrement(); DP dp = time_map.getValue().get(dp_index); - final byte flags = (byte)Internal.getFlagsFromQualifier(dp.kv.qualifier()); - buf.append(" ") - .append("write time: (") - .append(dp.kv.timestamp()) - .append(" - ") - .append(new Date(dp.kv.timestamp())) - .append(") ") - .append(" compacted: (") - .append(dp.compacted) - .append(") qualifier: ") - .append(Arrays.toString(dp.kv.qualifier())) - .append(" value: ") - .append(Internal.isFloat(dp.kv.qualifier()) ? - Internal.extractFloatingPointValue(dp.value(), 0, flags) : - Internal.extractIntegerValue(dp.value(), 0, flags)) - .append("\n"); - unique_columns.put(dp.kv.qualifier(), dp.kv.value()); - if (options.fix() && options.resolveDupes()) { - if (compact_row) { - // Scheduled for deletion by compaction. - duplicates_fixed_comp.getAndIncrement(); - } else if (!dp.compacted) { - LOG.debug("Removing duplicate data point: " + dp.kv); - tsdb.getClient().delete( - new DeleteRequest( - tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() - ) - ); - duplicates_fixed.getAndIncrement(); + try { + final byte flags = (byte)Internal.getFlagsFromQualifier(dp.kv.qualifier()); + buf.append(" ") + .append("write time: (") + .append(dp.kv.timestamp()) + .append(" - ") + .append(new Date(dp.kv.timestamp())) + .append(") ") + .append(" compacted: (") + .append(dp.compacted) + .append(") qualifier: ") + .append(Arrays.toString(dp.kv.qualifier())) + .append(" value: ") + .append(Internal.isFloat(dp.kv.qualifier()) ? + Internal.extractFloatingPointValue(dp.value(), 0, flags) : + Internal.extractIntegerValue(dp.value(), 0, flags)) + .append("\n"); + unique_columns.put(dp.kv.qualifier(), dp.kv.value()); + if (options.fix() && options.resolveDupes()) { + if (compact_row) { + // Scheduled for deletion by compaction. + duplicates_fixed_comp.getAndIncrement(); + } else if (!dp.compacted) { + LOG.debug("Removing duplicate data point: " + dp.kv); + tsdb.getClient().delete( + new DeleteRequest( + tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() + ) + ); + duplicates_fixed.getAndIncrement(); + } } + } catch (Exception e) { + LOG.error("Unexpected exception processing DP: " + dp); } } if (options.lastWriteWins()) { From 000e80c808ae448f2be68f14db36d7146a811fe3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 12:54:27 -0800 Subject: [PATCH 407/826] Bump to AsyncHbase 1.7.1 release Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.1.jar.md5 | 1 + third_party/hbase/include.mk | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.7.1.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 b/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 deleted file mode 100644 index 9d3a066783..0000000000 --- a/third_party/hbase/asynchbase-1.7.0-20150910.030815-3.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -84b8410ba9003ecadbeececb02943ee1 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.1.jar.md5 b/third_party/hbase/asynchbase-1.7.1.jar.md5 new file mode 100644 index 0000000000..45ad0e9669 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.1.jar.md5 @@ -0,0 +1 @@ +f236854721eac6d40b6710ec7d59f4a8 diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index 0e25495aba..bb25220311 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.1-20151004.015637-1 +ASYNCHBASE_VERSION := 1.7.1 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar -ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.7.1-SNAPSHOT/ +ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From cae191d6eabd4b81cd34b9fdc6572866756cba50 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 20 Nov 2015 17:06:22 -0800 Subject: [PATCH 408/826] Add a try/catch to the FSCK utility to log problems found when printing row information. Signed-off-by: Chris Larsen --- src/tools/Fsck.java | 62 ++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 45dde4539f..6295ff32f4 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -643,36 +643,40 @@ private void fsckDataPoints(final Map> datapoints) dp_index++) { duplicates.getAndIncrement(); DP dp = time_map.getValue().get(dp_index); - final byte flags = (byte)Internal.getFlagsFromQualifier(dp.kv.qualifier()); - buf.append(" ") - .append("write time: (") - .append(dp.kv.timestamp()) - .append(" - ") - .append(new Date(dp.kv.timestamp())) - .append(") ") - .append(" compacted: (") - .append(dp.compacted) - .append(") qualifier: ") - .append(Arrays.toString(dp.kv.qualifier())) - .append(" value: ") - .append(Internal.isFloat(dp.kv.qualifier()) ? - Internal.extractFloatingPointValue(dp.value(), 0, flags) : - Internal.extractIntegerValue(dp.value(), 0, flags)) - .append("\n"); - unique_columns.put(dp.kv.qualifier(), dp.kv.value()); - if (options.fix() && options.resolveDupes()) { - if (compact_row) { - // Scheduled for deletion by compaction. - duplicates_fixed_comp.getAndIncrement(); - } else if (!dp.compacted) { - LOG.debug("Removing duplicate data point: " + dp.kv); - tsdb.getClient().delete( - new DeleteRequest( - tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() - ) - ); - duplicates_fixed.getAndIncrement(); + try { + final byte flags = (byte)Internal.getFlagsFromQualifier(dp.kv.qualifier()); + buf.append(" ") + .append("write time: (") + .append(dp.kv.timestamp()) + .append(" - ") + .append(new Date(dp.kv.timestamp())) + .append(") ") + .append(" compacted: (") + .append(dp.compacted) + .append(") qualifier: ") + .append(Arrays.toString(dp.kv.qualifier())) + .append(" value: ") + .append(Internal.isFloat(dp.kv.qualifier()) ? + Internal.extractFloatingPointValue(dp.value(), 0, flags) : + Internal.extractIntegerValue(dp.value(), 0, flags)) + .append("\n"); + unique_columns.put(dp.kv.qualifier(), dp.kv.value()); + if (options.fix() && options.resolveDupes()) { + if (compact_row) { + // Scheduled for deletion by compaction. + duplicates_fixed_comp.getAndIncrement(); + } else if (!dp.compacted) { + LOG.debug("Removing duplicate data point: " + dp.kv); + tsdb.getClient().delete( + new DeleteRequest( + tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() + ) + ); + duplicates_fixed.getAndIncrement(); + } } + } catch (Exception e) { + LOG.error("Unexpected exception processing DP: " + dp); } } if (options.lastWriteWins()) { From e56f18f1e99a03b1bc1d43dfd121e8a0b784279d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 16:39:06 -0800 Subject: [PATCH 409/826] Rework the QueryStats before release by adding a bunch of timing around the scanner, HBase, serialization, etc. The class was reworked and the API is different but I think more useful and expandable. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 143 ++++- src/core/TsdbQuery.java | 115 +++- src/stats/QueryStats.java | 797 ++++++++++++++++++++++----- src/tsd/AbstractHttpQuery.java | 73 ++- src/tsd/HttpJsonSerializer.java | 70 ++- src/tsd/HttpSerializer.java | 3 +- src/tsd/QueryRpc.java | 95 ++-- src/tsd/RpcHandler.java | 1 + src/tsd/StatsRpc.java | 2 +- src/utils/DateTime.java | 37 ++ test/stats/TestQueryStats.java | 203 ++++--- test/tsd/NettyMocks.java | 11 + test/tsd/TestHttpJsonSerializer.java | 38 +- test/tsd/TestQueryRpc.java | 3 + test/utils/TestDateTime.java | 29 + 15 files changed, 1288 insertions(+), 332 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 4acfc39ac9..9498284e00 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -25,7 +25,10 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.DateTime; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.DeleteRequest; @@ -84,6 +87,13 @@ public class SaltScanner { /** The TSDB to which we belong */ private final TSDB tsdb; + /** A stats object associated with the sub query used for storing stats + * about scanner operations. */ + private final QueryStats query_stats; + + /** Index of the sub query in the main query list */ + private final int query_index; + /** A counter used to determine how many scanners are still running */ private AtomicInteger completed_tasks = new AtomicInteger(); @@ -117,7 +127,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters) { - this(tsdb, metric, scanners, spans, filters, false); + this(tsdb, metric, scanners, spans, filters, false, null, 0); } /** @@ -129,6 +139,8 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, * @param spans The span map to store results in * @param delete Whether or not to delete the queried data * @param filters A list of filters for processing + * @param query_stats A stats object for tracking timing + * @param query_index The index of the sub query in the main query list * @throws IllegalArgumentException if any required data was missing or * we had invalid parameters. */ @@ -136,7 +148,9 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters, - final boolean delete) { + final boolean delete, + final QueryStats query_stats, + final int query_index) { if (Const.SALT_WIDTH() < 1) { throw new IllegalArgumentException( "Salting is disabled. Use the regular scanner"); @@ -173,6 +187,8 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.tsdb = tsdb; this.filters = filters; this.delete = delete; + this.query_stats = query_stats; + this.query_index = query_index; } /** @@ -184,8 +200,9 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, */ public Deferred> scan() { start_time = System.currentTimeMillis(); + int i = 0; for (final Scanner scanner: scanners) { - new ScannerCB(scanner).scan(); + new ScannerCB(scanner, i++).scan(); } return results; } @@ -208,6 +225,7 @@ private void mergeAndReturnResults() { } // Merge sorted spans together + final long merge_start = DateTime.nanoTime(); for (final List kvs : kv_map.values()) { if (kvs == null || kvs.isEmpty()) { LOG.warn("Found a key value list that was null or empty"); @@ -260,6 +278,10 @@ private void mergeAndReturnResults() { } } + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, + (DateTime.nanoTime() - merge_start)); + } if (LOG.isDebugEnabled()) { LOG.debug("Scanning completed in " + (hbase_time - start_time) + " ms, " + rows + " rows, and stored in " + spans.size() + " spans"); @@ -280,14 +302,29 @@ private void mergeAndReturnResults() { final class ScannerCB implements Callback>> { private final Scanner scanner; + private final int index; private final List kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); private final Set skips = new HashSet(); private final Set keepers = new HashSet(); - public ScannerCB(final Scanner scanner) { + private long scanner_start = -1; + /** nanosecond timestamps */ + private long fetch_start = 0; // reset each time we send an RPC to HBase + private long fetch_time = 0; // cumulation of time waiting on HBase + private long uid_resolve_time = 0; // cumulation of time resolving UIDs + private long uids_resolved = 0; + private long compaction_time = 0; // cumulation of time compacting + private long dps_post_filter = 0; + private long rows_post_filter = 0; + + public ScannerCB(final Scanner scanner, final int index) { this.scanner = scanner; + this.index = index; + if (query_stats != null) { + query_stats.addScannerId(query_index, index, scanner.toString()); + } } /** Error callback that will capture an exception from AsyncHBase and store @@ -297,7 +334,7 @@ class ErrorCb implements Callback { @Override public Object call(final Exception e) throws Exception { LOG.error("Scanner " + scanner + " threw an exception", e); - scanner.close(); + close(false); handleException(e); return null; } @@ -310,6 +347,10 @@ public Object call(final Exception e) throws Exception { * found */ public Object scan() { + if (scanner_start < 0) { + scanner_start = DateTime.nanoTime(); + } + fetch_start = DateTime.nanoTime(); return scanner.nextRows().addCallback(this).addErrback(new ErrorCb()); } @@ -322,9 +363,17 @@ public Object scan() { public Object call(final ArrayList> rows) throws Exception { try { + fetch_time += DateTime.nanoTime() - fetch_start; if (rows == null) { - scanner.close(); - validateAndTriggerCallback(kvs, annotations); + close(true); + return null; + } else if (exception != null) { + close(false); + // don't need to handleException here as it's already taken care of + // due to the fact that exception was set. + if (LOG.isDebugEnabled()) { + LOG.debug("Closing scanner as there was an exception: " + scanner); + } return null; } @@ -336,7 +385,7 @@ public Object call(final ArrayList> rows) for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (RowKey.rowKeyContainsMetric(metric, key) != 0) { - scanner.close(); + close(false); handleException(new IllegalDataException( "HBase returned a row that doesn't match" + " our scanner (" + scanner + ")! " + row + " does not start" @@ -359,6 +408,8 @@ public Object call(final ArrayList> rows) continue; } if (!keepers.contains(tsuid)) { + final long uid_start = DateTime.nanoTime(); + /** CB to called after all of the UIDs have been resolved */ class MatchCB implements Callback> { @Override @@ -383,6 +434,8 @@ class GetTagsCB implements @Override public Deferred> call( final Map tags) throws Exception { + uid_resolve_time += (DateTime.nanoTime() - uid_start); + uids_resolved += tags.size(); final List> matches = new ArrayList>(filters.size()); @@ -420,7 +473,7 @@ public Object call(final ArrayList group) throws Exception { } } catch (final RuntimeException e) { LOG.error("Unexpected exception on scanner " + this, e); - scanner.close(); + close(false); handleException(e); return null; } @@ -447,11 +500,67 @@ void processRow(final byte[] key, final ArrayList row) { final KeyValue compacted; // let IllegalDataExceptions bubble up so the handler above can close // the scanner - compacted = tsdb.compact(row, notes); + final long compaction_start = DateTime.nanoTime(); + try { + compacted = tsdb.compact(row, notes); + } catch (IllegalDataException idex) { + compaction_time += (DateTime.nanoTime() - compaction_start); + close(false); + handleException(idex); + return; + } + compaction_time += (DateTime.nanoTime() - compaction_start); if (compacted != null) { // Can be null if we ignored all KVs. kvs.add(compacted); } } + + /** + * Closes the scanner and sets the various stats after filtering + * @param ok Whether or not the scanner closed with an exception or + * closed due to natural causes (e.g. ran out of data or we wanted to stop + * it early) + */ + void close(final boolean ok) { + scanner.close(); + + if (query_stats != null) { + query_stats.addScannerStat(query_index, index, QueryStat.SCANNER_TIME, + DateTime.nanoTime() - scanner_start); + + // Scanner Stats + /* Uncomment when AsyncHBase has this feature: + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_FROM_STORAGE, scanner.getRowsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.COLUMNS_FROM_STORAGE, scanner.getColumnsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.BYTES_FROM_STORAGE, scanner.getBytesFetched()); */ + query_stats.addScannerStat(query_index, index, + QueryStat.HBASE_TIME, fetch_time); + query_stats.addScannerStat(query_index, index, + QueryStat.SUCCESSFUL_SCAN, ok ? 1 : 0); + + // Post Scan stats + /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_POST_FILTER, rows_post_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_POST_FILTER, dps_post_filter); */ + query_stats.addScannerStat(query_index, index, + QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); + query_stats.addScannerStat(query_index, index, + QueryStat.UID_PAIRS_RESOLVED, uids_resolved); + query_stats.addScannerStat(query_index, index, + QueryStat.COMPACTION_TIME, compaction_time); + } + + if (ok && exception == null) { + validateAndTriggerCallback(kvs, annotations); + } else { + completed_tasks.incrementAndGet(); + } + } } /** @@ -493,10 +602,19 @@ private void validateAndTriggerCallback(final List kvs, */ private void handleException(final Exception e) { // make sure only one scanner can set the exception + completed_tasks.incrementAndGet(); if (exception == null) { synchronized (this) { if (exception == null) { exception = e; + // fail once and fast on the first scanner to throw an exception + try { + mergeAndReturnResults(); + } catch (Exception ex) { + LOG.error("Failed merging and returning results, " + + "calling back with exception", ex); + results.callback(ex); + } } else { // TODO - it would be nice to close and cancel the other scanners but // for now we have to wait for them to finish and/or throw exceptions. @@ -504,10 +622,5 @@ private void handleException(final Exception e) { } } } - - final int tasks = completed_tasks.incrementAndGet(); - if (tasks >= Const.SALT_BUCKETS()) { - results.callback(exception); - } } } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 7b060a44ed..5d96c4e1c3 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -41,6 +41,8 @@ import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.Histogram; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; @@ -71,6 +73,9 @@ final class TsdbQuery implements Query { /** The TSDB we belong to. */ private final TSDB tsdb; + + /** The time, in ns, when we start scanning for data **/ + private long scan_start_time; /** Value used for timestamps that are uninitialized. */ private static final int UNSET = -1; @@ -132,6 +137,9 @@ final class TsdbQuery implements Query { /** 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; + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -313,6 +321,7 @@ public Deferred configureFromQuery(final TSQuery query, setEndTime(query.endTime()); setDelete(query.getDelete()); query_index = index; + query_stats = query.getQueryStats(); // set common options aggregator = sub_query.aggregator(); @@ -541,10 +550,12 @@ private Deferred> findSpans() throws HBaseException { for (int i = 0; i < Const.SALT_BUCKETS(); i++) { scanners.add(getScanner(i)); } - return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters) - .scan(); + scan_start_time = DateTime.nanoTime(); + return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, + delete, query_stats, query_index).scan(); } + scan_start_time = DateTime.nanoTime(); final Scanner scanner = getScanner(); final Deferred> results = new Deferred>(); @@ -561,11 +572,17 @@ final class ScannerCB implements Callback skips = new HashSet(); private final Set keepers = new HashSet(); + private final int index = 0; // only used for salted scanners + /** nanosecond timestamps */ + private long fetch_start = 0; // reset each time we send an RPC to HBase + private long fetch_time = 0; // cumulation of time waiting on HBase + private long uid_resolve_time = 0; // cumulation of time resolving UIDs + private long uids_resolved = 0; + private long compaction_time = 0; // cumulation of time compacting /** Error callback that will capture an exception from AsyncHBase and store * it so we can bubble it up to the caller. @@ -574,8 +591,7 @@ class ErrorCB implements Callback { @Override public Object call(final Exception e) throws Exception { LOG.error("Scanner " + scanner + " threw an exception", e); - scanner.close(); - results.callback(e); + close(e); return null; } } @@ -587,7 +603,7 @@ public Object call(final Exception e) throws Exception { * found */ public Object scan() { - starttime = System.nanoTime(); + fetch_start = DateTime.nanoTime(); return scanner.nextRows().addCallback(this).addErrback(new ErrorCB()); } @@ -599,23 +615,18 @@ public Object scan() { @Override public Object call(final ArrayList> rows) throws Exception { - hbase_time += (System.nanoTime() - starttime) / 1000000; + fetch_time += DateTime.nanoTime() - fetch_start; try { if (rows == null) { - hbase_time += (System.nanoTime() - starttime) / 1000000; - scanlatency.add(hbase_time); + scanlatency.add((int)DateTime.msFromNano(fetch_time)); LOG.info(TsdbQuery.this + " matched " + nrows + " rows in " + - spans.size() + " spans in " + hbase_time + "ms"); - if (nrows < 1 && !seenAnnotation) { - results.callback(null); - } else { - results.callback(spans); - } - scanner.close(); + spans.size() + " spans in " + DateTime.msFromNano(fetch_time) + "ms"); + close(null); return null; } - if (timeout > 0 && hbase_time > timeout) { + if (timeout > 0 && DateTime.msFromNanoDiff( + DateTime.nanoTime(), scanner_start) > timeout) { throw new InterruptedException("Query timeout exceeded!"); } @@ -649,6 +660,8 @@ public Object call(final ArrayList> rows) continue; } if (!keepers.contains(tsuid)) { + final long uid_start = DateTime.nanoTime(); + /** CB to called after all of the UIDs have been resolved */ class MatchCB implements Callback> { @Override @@ -673,6 +686,8 @@ class GetTagsCB implements @Override public Deferred> call( final Map tags) throws Exception { + uid_resolve_time += (DateTime.nanoTime() - uid_start); + uids_resolved += tags.size(); final List> matches = new ArrayList>(scanner_filters.size()); @@ -709,8 +724,7 @@ public Object call(final ArrayList group) throws Exception { return scan(); } } catch (Exception e) { - scanner.close(); - results.callback(e); + close(e); return null; } } @@ -731,15 +745,60 @@ void processRow(final byte[] key, final ArrayList row) { datapoints = new Span(tsdb); spans.put(key, datapoints); } + final long compaction_start = DateTime.nanoTime(); final KeyValue compacted = tsdb.compact(row, datapoints.getAnnotations()); + compaction_time += (DateTime.nanoTime() - compaction_start); seenAnnotation |= !datapoints.getAnnotations().isEmpty(); if (compacted != null) { // Can be null if we ignored all KVs. datapoints.addRow(compacted); ++nrows; } } - } + + void close(final Exception e) { + scanner.close(); + + if (query_stats != null) { + query_stats.addScannerStat(query_index, index, + QueryStat.SCANNER_TIME, DateTime.nanoTime() - scan_start_time); + + // Scanner Stats + /* Uncomment when AsyncHBase has this feature: + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_FROM_STORAGE, scanner.getRowsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.COLUMNS_FROM_STORAGE, scanner.getColumnsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.BYTES_FROM_STORAGE, scanner.getBytesFetched()); */ + query_stats.addScannerStat(query_index, index, + QueryStat.HBASE_TIME, fetch_time); + query_stats.addScannerStat(query_index, index, + QueryStat.SUCCESSFUL_SCAN, e == null ? 1 : 0); + + // Post Scan stats + /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_POST_FILTER, rows_post_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_POST_FILTER, dps_post_filter); */ + query_stats.addScannerStat(query_index, index, + QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); + query_stats.addScannerStat(query_index, index, + QueryStat.UID_PAIRS_RESOLVED, uids_resolved); + query_stats.addScannerStat(query_index, index, + QueryStat.COMPACTION_TIME, compaction_time); + } + + if (e != null) { + results.callback(e); + } else if (nrows < 1 && !seenAnnotation) { + results.callback(null); + } else { + results.callback(spans); + } + } + } new ScannerCB().scan(); return results; @@ -761,7 +820,15 @@ private class GroupByAndAggregateCB implements */ @Override public DataPoints[] call(final TreeMap spans) throws Exception { + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, + (System.nanoTime() - TsdbQuery.this.scan_start_time)); + } + if (spans == null || spans.size() <= 0) { + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } return NO_RESULT; } if (group_bys == null) { @@ -775,6 +842,9 @@ public DataPoints[] call(final TreeMap spans) throws Exception { aggregator, sample_interval_ms, downsampler, query_index, fill_policy); + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } return new SpanGroup[] { group }; } @@ -831,6 +901,9 @@ public DataPoints[] call(final TreeMap spans) throws Exception { //for (final Map.Entry entry : groups) { // LOG.info("group for " + Arrays.toString(entry.getKey()) + ": " + entry.getValue()); //} + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } return groups.values().toArray(new SpanGroup[groups.size()]); } } diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 7f1892f86f..757a9bdca9 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -15,8 +15,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -27,10 +31,12 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import net.opentsdb.core.Const; import net.opentsdb.core.QueryException; import net.opentsdb.core.TSQuery; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; +import net.opentsdb.utils.Pair; /** * This class stores information about OpenTSDB queries executed through the @@ -44,17 +50,21 @@ * The cache will store each query based on the combination of the client, query * and the result code. If the same query was executed multiple times then it * will increment the "executed" counter for the query in the cache. + * + * NOTE: Record everything in nano seconds, then convert to floating millis for + * serialization. * @since 2.2 */ public class QueryStats { private static final Logger LOG = LoggerFactory.getLogger(QueryStats.class); + private static final Logger QUERY_LOG = LoggerFactory.getLogger("QueryLog"); /** Determines how many query stats to keep in the cache */ private static int COMPLETED_QUERY_CACHE_SIZE = 256; /** Whether or not to allow duplicate queries from the same endpoint to * run simultaneously. */ - private static boolean ENABLE_DUPLICATES = false; + private static boolean ENABLE_DUPLICATES = true; /** Stores queries currently executing. If a thread doesn't call into * markComplete then it's possible for this map to fill up. @@ -67,51 +77,164 @@ public class QueryStats { private static Cache completed_queries = CacheBuilder.newBuilder().maximumSize(COMPLETED_QUERY_CACHE_SIZE).build(); - /** Start time for the query. Can be set post construction if necessary */ - private final long query_start; + /** Start time for the query in nano seconds. Can be set post construction + * if necessary */ + private final long query_start_ns; + + /** Start timestamp for the query in millis for printing */ + private final long query_start_ms; + + /** When the query was marked completed in nanoseconds */ + private long query_completed_ts; /** The remote address as :, may be ipv6 */ private final String remote_address; /** The TSQuery object that contains the query specification */ private final TSQuery query; - - /** Amount of time taken for the query to complete, set on {@link markComplete} */ - private long time_total; - /** Time it took to retrieve data from storage in ms*/ - private long time_storage; + /** HTTP response when the query was completed, either successfully or failed */ + private HttpResponseStatus response; - /** Time it took to aggregate over the data */ - private long time_aggregation; + /** Set if the query terminated with an exception */ + private Throwable exception; - /** Time it took to serialize the data. Includes aggregation time and tag - * lookups */ - private long time_serialization; + /** How many times this exact query was executed. Only updated on completion */ + private long executed; - /** Number of data points emitted, NOT the number of data points fetched */ - private long size; + /** The users (if known) who executed this query (could be pulled from a header) */ + private String user; - /** Total number of data points fetched from storage */ - private long aggregated_size; + /** Stats for the entire query */ + private final Map overall_stats; - /** HTTP response when the query was completed, either successfully or failed */ - private HttpResponseStatus response; + /** Hold a list of stats for the sub queries */ + private final Map> query_stats; - /** How many times this exact query was executed. Only updated on completion */ - private long executed; + /** Holds a list of stats for each scanner */ + private final Map>> scanner_stats; - /** A possible exception if thrown when this query completes */ - private Throwable exception; + /** Hold a list of the region servers encountered for each scanner */ + private final Map>> scanner_servers; + + /** Holds a lis tof the scanner IDs for each scanner */ + private final Map> scanner_ids; + + /** Holds a copy of the headers from the request */ + private final Map headers; + + /** Whether or not the data was successfully sent to the client */ + private boolean sent_to_client; + + /** + * A list of statistics surrounding individual queries + */ + public enum QueryStat { + // Query Setup stats + STRING_TO_UID_TIME ("stringToUidTime", true), + + // Storage stats + COLUMNS_FROM_STORAGE ("columnsFromStorage", false), + ROWS_FROM_STORAGE ("rowsFromStorage", false), + BYTES_FROM_STORAGE ("bytesFromStorage", false), + SUCCESSFUL_SCAN ("successfulScan", false), + + // Single Scanner stats + DPS_POST_FILTER ("dpsPostFilter", false), + ROWS_POST_FILTER ("rowsPostFilter", false), + SCANNER_UID_TO_STRING_TIME ("scannerUidToStringTime", true), + COMPACTION_TIME ("compactionTime", true), + HBASE_TIME ("hbaseTime", true), + UID_PAIRS_RESOLVED ("uidPairsResolved", false), + SCANNER_TIME ("scannerTime", true), + + // Overall Salt Scanner stats + SCANNER_MERGE_TIME ("saltScannerMergeTime", true), + + // Post Scan stats + QUERY_SCAN_TIME ("queryScanTime", true), + GROUP_BY_TIME ("groupByTime", true), + + // Serialization time stats + UID_TO_STRING_TIME ("uidToStringTime", true), + AGGREGATED_SIZE ("emittedDPs", false), + NAN_DPS ("nanDPs", false), + AGGREGATION_TIME ("aggregationTime", true), + SERIALIZATION_TIME ("serializationTime", true), + + // Final stats + PROCESSING_PRE_WRITE_TIME ("processingPreWriteTime", true), + TOTAL_TIME ("totalTime", true), + + // MAX and Agg Times + MAX_HBASE_TIME ("maxHBaseTime", true), + AVG_HBASE_TIME ("avgHBaseTime", true), + MAX_SALT_SCANNER_TIME ("maxScannerTime", true), + AVG_SALT_SCANNER_TIME ("avgScannerTime", true), + MAX_UID_TO_STRING ("maxUidToStringTime", true), + AVG_UID_TO_STRING ("avgUidToStringTime", true), + MAX_COMPACTION_TIME ("maxCompactionTime", true), + AVG_COMPACTION_TIME ("avgCompactionTime", 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), + MAX_SCAN_TIME ("maxQueryScanTime", true), + AVG_SCAN_TIME ("avgQueryScanTime", true), + MAX_AGGREGATION_TIME ("maxAggregationTime", true), + AVG_AGGREGATION_TIME ("avgAggregationTime", true), + MAX_SERIALIZATION_TIME ("maxSerializationTime", true), + AVG_SERIALIZATION_TIME ("avgSerializationTime", true) + ; + + /** The serializable name for this enum */ + private final String stat_name; + /** Whether or not the stat is time based */ + private final boolean is_time; + + private QueryStat(final String stat_name, final boolean is_time) { + this.stat_name = stat_name; + this.is_time = is_time; + } + + @Override + public String toString() { + return stat_name; + } + } + + // always AVG, MAX in the pair order + static final Map> AGG_MAP = + new HashMap>(); + static { + AGG_MAP.put(QueryStat.HBASE_TIME, new Pair( + QueryStat.AVG_HBASE_TIME, QueryStat.MAX_HBASE_TIME)); + AGG_MAP.put(QueryStat.SCANNER_TIME, new Pair( + QueryStat.AVG_SALT_SCANNER_TIME, QueryStat.MAX_HBASE_TIME)); + AGG_MAP.put(QueryStat.UID_TO_STRING_TIME, new Pair( + QueryStat.MAX_UID_TO_STRING, QueryStat.MAX_UID_TO_STRING)); + AGG_MAP.put(QueryStat.SCANNER_UID_TO_STRING_TIME, new Pair( + QueryStat.MAX_SCANNER_UID_TO_STRING_TIME, + QueryStat.AVG_SCANNER_UID_TO_STRING_TIME)); + AGG_MAP.put(QueryStat.QUERY_SCAN_TIME, new Pair( + QueryStat.MAX_SCAN_TIME, QueryStat.AVG_SCAN_TIME)); + AGG_MAP.put(QueryStat.AGGREGATION_TIME, new Pair( + QueryStat.MAX_AGGREGATION_TIME, QueryStat.AVG_AGGREGATION_TIME)); + AGG_MAP.put(QueryStat.SERIALIZATION_TIME, new Pair( + QueryStat.MAX_SERIALIZATION_TIME, + QueryStat.AVG_SERIALIZATION_TIME)); + } /** * Default CTor * @param remote_address Remote address of the client * @param query Query being executed + * @param headers The HTTP headers passed with the query * @throws QueryException if the exact query is already running, e.g if the * client submitted the same query twice */ - public QueryStats(final String remote_address, final TSQuery query) { + public QueryStats(final String remote_address, final TSQuery query, + final Map headers) { if (remote_address == null || remote_address.isEmpty()) { throw new IllegalArgumentException("Remote address was null or empty"); } @@ -120,8 +243,16 @@ public QueryStats(final String remote_address, final TSQuery query) { } this.remote_address = remote_address; this.query = query; + this.headers = headers; // can be null executed = 1; - query_start = DateTime.currentTimeMillis(); + query_start_ns = DateTime.nanoTime(); + query_start_ms = DateTime.currentTimeMillis(); + overall_stats = new HashMap(); + query_stats = new ConcurrentHashMap>(1); + scanner_stats = new ConcurrentHashMap>>(1); + scanner_servers = new ConcurrentHashMap>>(1); + scanner_ids = new ConcurrentHashMap>(1); if (LOG.isDebugEnabled()) { LOG.debug("New query for remote " + remote_address + " with hash " + hashCode() + " on thread " + Thread.currentThread().getId()); @@ -134,22 +265,27 @@ public QueryStats(final String remote_address, final TSQuery query) { throw new QueryException("Query is already executing for endpoint: " + remote_address); } - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Successfully put new query for remote " + remote_address + - " with hash " + hashCode() + " on thread " + - Thread.currentThread().getId() + " w q " + query.toString()); - } } + if (LOG.isDebugEnabled()) { + LOG.debug("Successfully put new query for remote " + remote_address + + " with hash " + hashCode() + " on thread " + + Thread.currentThread().getId() + " w q " + query.toString()); + } + LOG.info("Executing new query=" + JSON.serializeToString(this)); } /** * Returns the hash based on the remote address and the query */ + @Override public int hashCode() { - return Objects.hashCode(remote_address, query.hashCode()); + return remote_address.hashCode() ^ query.hashCode(); } + /** + * Equals is based solely on the endpoint and the original query + */ + @Override public boolean equals(final Object obj) { if (obj == null) { return false; @@ -167,53 +303,61 @@ public boolean equals(final Object obj) { @Override public String toString() { - final StringBuilder buf = new StringBuilder(256); - buf.append("remote=") - .append(remote_address) - .append(", query=") - .append(query) - .append(", start=") - .append(query_start) - .append(", exception=") - .append(exception == null ? "null" : exception.getMessage()); - return buf.toString(); + // have to hack it to get the details. By default we dump just the highest + // level of stats. + final Map details = new HashMap(); + details.put("queryStartTimestamp", getQueryStartTimestamp()); + details.put("queryCompletedTimestamp", getQueryCompletedTimestamp()); + details.put("exception", getException()); + details.put("httpResponse", getHttpResponse()); + details.put("numRunningQueries", getNumRunningQueries()); + details.put("query", getQuery()); + details.put("user", getUser()); + details.put("requestHeaders", getRequestHeaders()); + details.put("executed", getExecuted()); + details.put("stats", getStats(true, true)); + return JSON.serializeToString(details); } /** - * Marks a query as completed successfully with the 200 HTTP response code. + * Marks a query as completed successfully with the 200 HTTP response code + * without an exception. * Moves it from the running map to the cache, updating the cache if it already * existed. */ - public void markComplete() { - markComplete(HttpResponseStatus.OK, null); + public void markSerializationSuccessful() { + markSerialized(HttpResponseStatus.OK, null); } /** - * Marks a query as completed with the given HTTP code and moves it from the - * running map to the cache, updating the cache if it already existed. - * @param response The HttpStatus code to store - * @param exception An optional exception + * Marks a query as completed with the given HTTP code with exception and + * moves it from the running map to the cache, updating the cache if it + * already existed. + * @param response The HTTP response to log + * @param exception The exception thrown */ - public void markComplete(final HttpResponseStatus response, + public void markSerialized(final HttpResponseStatus response, final Throwable exception) { this.exception = exception; - LOG.debug("Marking query as complete for " + remote_address + " with hash " + - hashCode() + " on thread " + Thread.currentThread().getId() + " And q: " + - query.toString()); this.response = response; - time_total = DateTime.currentTimeMillis() - query_start; + + query_completed_ts = DateTime.currentTimeMillis(); + overall_stats.put(QueryStat.PROCESSING_PRE_WRITE_TIME, DateTime.nanoTime() - query_start_ns); synchronized (running_queries) { if (!running_queries.containsKey(this.hashCode())) { if (!ENABLE_DUPLICATES) { LOG.warn("Query was already marked as complete: " + this); } - return; } - running_queries.remove(this.hashCode()); - LOG.debug("Removed completed query " + remote_address + " with hash " + - hashCode() + " on thread " + Thread.currentThread().getId()); + running_queries.remove(hashCode()); + if (LOG.isDebugEnabled()) { + LOG.debug("Removed completed query " + remote_address + " with hash " + + hashCode() + " on thread " + Thread.currentThread().getId()); + } } + aggQueryStats(); + final int cache_hash = this.hashCode() ^ response.toString().hashCode(); synchronized (completed_queries) { final QueryStats old_query = completed_queries.getIfPresent(cache_hash); @@ -223,7 +367,25 @@ public void markComplete(final HttpResponseStatus response, old_query.executed++; } } - LOG.info("completed_query=" + JSON.serializeToString(this)); + } + + /** + * Marks the query as complete and logs it to the proper logs. This is called + * after the data has been sent to the client. + */ + public void markSent() { + sent_to_client = true; + overall_stats.put(QueryStat.TOTAL_TIME, DateTime.nanoTime() - query_start_ns); + LOG.info("Completing query=" + JSON.serializeToString(this)); + QUERY_LOG.info(this.toString()); + } + + /** Leaves the sent_to_client field as false when we were unable to write to + * the client end point. */ + public void markSendFailed() { + overall_stats.put(QueryStat.TOTAL_TIME, DateTime.nanoTime() - query_start_ns); + LOG.info("Completing query=" + JSON.serializeToString(this)); + QUERY_LOG.info(this.toString()); } /** @@ -231,15 +393,13 @@ public void markComplete(final HttpResponseStatus response, * returned to a caller. * @return A map for serialization */ - public static Map>> buildStats() { - Map>> root = - new HashMap>>(); + public static Map getRunningAndCompleteStats() { + Map root = new TreeMap(); if (running_queries.isEmpty()) { - root.put("running", Collections.> emptyList()); + root.put("running", Collections.emptyList()); } else { - final List> running = - new ArrayList>(running_queries.size()); + final List running = new ArrayList(running_queries.size()); root.put("running", running); // don't need to lock the map beyond what the iterator will do implicitly @@ -247,38 +407,20 @@ public static Map>> buildStats() { final Map obj = new HashMap(10); obj.put("query", stats.query); obj.put("remote", stats.remote_address); - obj.put("queryStart", stats.query_start); - obj.put("timeTotal", stats.time_total); - obj.put("elapsed", DateTime.currentTimeMillis() - stats.query_start); + obj.put("user", stats.user); + obj.put("headers", stats.headers);; + obj.put("queryStart", DateTime.msFromNano(stats.query_start_ns)); + obj.put("elapsed", DateTime.msFromNanoDiff(DateTime.nanoTime(), + stats.query_start_ns)); running.add(obj); } } final Map completed = completed_queries.asMap(); if (completed.isEmpty()) { - root.put("completed", Collections.> emptyList()); + root.put("completed", Collections.emptyList()); } else { - final List> running = - new ArrayList>(completed.size()); - root.put("completed", running); - - // don't need to lock the map beyond what the iterator will do implicitly - for (final QueryStats stats : completed.values()) { - final Map obj = new HashMap(10); - obj.put("query", stats.query); - obj.put("remote", stats.remote_address); - obj.put("queryStart", stats.query_start); - obj.put("timeTotal", stats.time_total); - obj.put("executed", stats.executed); - obj.put("datapoints", stats.size); - obj.put("rawDatapoints", stats.aggregated_size); - obj.put("status", stats.response.getCode()); - obj.put("timeStorage", stats.time_storage); - obj.put("timeAggregation", stats.time_aggregation); - obj.put("timeSerialization", stats.time_serialization); - obj.put("exception", stats.exception); - running.add(obj); - } + root.put("completed", completed.values()); } return root; @@ -290,92 +432,461 @@ public static Map>> buildStats() { */ public static void collectStats(final StatsCollector collector) { collector.record("query.count", running_queries.size(), "type=running"); + } + + /** + * Add an overall statistic for the query (i.e. not associated with a sub + * query or scanner) + * @param name The name of the stat + * @param value The value to store + */ + public void addStat(final QueryStat name, final long value) { + overall_stats.put(name, value); + } + + /** + * Adds a stat for a sub query, replacing it if it exists. Times must be + * in nanoseconds. + * @param query_index The index of the sub query to update + * @param name The name of the stat to update + * @param value The value to set + */ + public void addStat(final int query_index, final QueryStat name, + final long value) { + Map qs = query_stats.get(query_index); + if (qs == null) { + qs = new HashMap(); + query_stats.put(query_index, qs); + } + qs.put(name, value); + } + + /** + * Aggregates the various stats from the lower to upper levels. This includes + * calculating max and average time values for stats marked as time based. + */ + public void aggQueryStats() { + // These are overall aggregations + final Map> overall_cumulations = + new HashMap>(); - final Map completed = completed_queries.asMap(); - int completed_success = 0; - int completed_error = 0; - for (final QueryStats stats : completed.values()) { - if (stats.response == HttpResponseStatus.OK) { - completed_success += stats.executed; - } else { - completed_error += stats.executed; + // scanner aggs + for (final Entry>> entry : + scanner_stats.entrySet()) { + final int query_index = entry.getKey(); + + final Map> cumulations = + new HashMap>(); + + for (final Entry> scanner : + entry.getValue().entrySet()) { + + for (final Entry stat : scanner.getValue().entrySet()) { + if (stat.getKey().is_time) { + if (!AGG_MAP.containsKey(stat.getKey())) { + // we're not aggregating this value + continue; + } + + // per query aggs + Pair pair = cumulations.get(stat.getKey()); + if (pair == null) { + pair = new Pair(0L, Long.MIN_VALUE); + cumulations.put(stat.getKey(), pair); + } + pair.setKey(pair.getKey() + stat.getValue()); + if (stat.getValue() > pair.getValue()) { + pair.setValue(stat.getValue()); + } + + // overall aggs required here for proper time averaging + pair = overall_cumulations.get(stat.getKey()); + if (pair == null) { + pair = new Pair(0L, Long.MIN_VALUE); + overall_cumulations.put(stat.getKey(), pair); + } + pair.setKey(pair.getKey() + stat.getValue()); + if (stat.getValue() > pair.getValue()) { + pair.setValue(stat.getValue()); + } + } else { + // only add counters for the per query maps as they'll be rolled + // up below into the overall + updateStat(query_index, stat.getKey(), stat.getValue()); + } + } + } + + // per query aggs + for (final Entry> cumulation : + cumulations.entrySet()) { + // names can't be null as we validate above that it exists + final Pair names = AGG_MAP.get(cumulation.getKey()); + addStat(query_index, names.getKey(), + (cumulation.getValue().getKey() / entry.getValue().size())); + addStat(query_index, names.getValue(), cumulation.getValue().getValue()); + } + } + + // handle the per scanner aggs + for (final Entry> cumulation : + overall_cumulations.entrySet()) { + // names can't be null as we validate above that it exists + final Pair names = AGG_MAP.get(cumulation.getKey()); + addStat(names.getKey(), + (cumulation.getValue().getKey() / + (scanner_stats.size() * Const.SALT_BUCKETS()))); + addStat(names.getValue(), cumulation.getValue().getValue()); + } + overall_cumulations.clear(); + + // aggregate counters from the sub queries + for (final Map sub_query : query_stats.values()) { + for (final Entry stat : sub_query.entrySet()) { + if (stat.getKey().is_time) { + if (!AGG_MAP.containsKey(stat.getKey())) { + // we're not aggregating this value + continue; + } + Pair pair = overall_cumulations.get(stat.getKey()); + if (pair == null) { + pair = new Pair(0L, Long.MIN_VALUE); + overall_cumulations.put(stat.getKey(), pair); + } + pair.setKey(pair.getKey() + stat.getValue()); + if (stat.getValue() > pair.getValue()) { + pair.setValue(stat.getValue()); + } + } else if (overall_stats.containsKey(stat.getKey())) { + overall_stats.put(stat.getKey(), + overall_stats.get(stat.getKey()) + stat.getValue()); + } else { + overall_stats.put(stat.getKey(), stat.getValue()); + } } } - collector.record("query.count", completed_success, "type=successful"); - collector.record("query.count", completed_error, "type=failed"); + for (final Entry> cumulation : + overall_cumulations.entrySet()) { + // names can't be null as we validate above that it exists + final Pair names = AGG_MAP.get(cumulation.getKey()); + overall_stats.put(names.getKey(), + (cumulation.getValue().getKey() / query_stats.size())); + overall_stats.put(names.getValue(), cumulation.getValue().getValue()); + } } - /** @return the start time of the query in ms */ - public long getQueryStart() { - return query_start; + /** + * Increments the cumulative value for a cumulative stat. If it's a time then + * it must be in nanoseconds + * @param query_index The index of the sub query + * @param name The name of the stat + * @param value The value to add to the existing value + */ + public void updateStat(final int query_index, final QueryStat name, + final long value) { + Map qs = query_stats.get(query_index); + long cum_time = value; + if (qs == null) { + qs = new HashMap(); + query_stats.put(query_index, qs); + } + + if (qs.containsKey(name)) { + cum_time += qs.get(name); + } + qs.put(name, cum_time); + } + + /** + * Adds a value for a specific scanner for a specific sub query. If it's a time + * then it must be in nanoseconds. + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param name The name of the stat + * @param value The value to add to the map + */ + public void addScannerStat(final int query_index, final int id, + final QueryStat name, final long value) { + Map> qs = scanner_stats.get(query_index); + if (qs == null) { + qs = new ConcurrentHashMap>(Const.SALT_BUCKETS()); + scanner_stats.put(query_index, qs); + } + Map scanner_stat_map = qs.get(id); + if (scanner_stat_map == null) { + scanner_stat_map = new HashMap(); + qs.put(id, scanner_stat_map); + } + scanner_stat_map.put(name, value); } - /** @return the total number of data points emitted for the query */ - public long getSize() { - return size; + /** + * Adds or overwrites the list of servers scanned by a scanner + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param servers The list of servers encountered + */ + public void addScannerServers(final int query_index, final int id, + final Set servers) { + Map> query_servers = scanner_servers.get(query_index); + if (query_servers == null) { + query_servers = new ConcurrentHashMap>( + Const.SALT_BUCKETS()); + scanner_servers.put(query_index, query_servers); + } + query_servers.put(id, servers); } - /** @param size increments the number of data points emitted */ - public void addSize(int size) { - this.size += size; + /** + * Updates or adds a stat for a specific scanner. IF it's a time it must + * be in nanoseconds + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param name The name of the stat + * @param value The value to update to the map + */ + public void updateScannerStat(final int query_index, final int id, + final QueryStat name, final long value) { + Map> qs = scanner_stats.get(query_index); + long cum_time = value; + if (qs == null) { + qs = new ConcurrentHashMap>(); + scanner_stats.put(query_index, qs); + } + Map scanner_stat_map = qs.get(id); + if (scanner_stat_map == null) { + scanner_stat_map = new HashMap(); + qs.put(id, scanner_stat_map); + } + + if (scanner_stat_map.containsKey(name)) { + cum_time += scanner_stat_map.get(name); + } + + scanner_stat_map.put(name, cum_time); + } + + /** + * Adds a scanner for a sub query to the stats along with the description of + * the scanner. + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param string_id The description of the scanner + */ + public void addScannerId(final int query_index, final int id, + final String string_id) { + Map scanners = scanner_ids.get(query_index); + if (scanners == null) { + scanners = new ConcurrentHashMap(); + scanner_ids.put(query_index, scanners); + } + scanners.put(id, string_id); } - /** @return the total number of data points retrieved from storage */ - public long getAggregatedSize() { - return aggregated_size; + /** @return the start time of the query in nano seconds */ + public long queryStart() { + return query_start_ns; + } + + /** @param user The user who executed the query */ + public void setUser(final String user) { + this.user = user; } - /** @param size increments the number of data points retrieved from storage */ - public void addAggregatedSize(int size) { - aggregated_size += size; + /** @return The user who executed the query if known */ + public String getUser() { + return user; } - /** @param time_storage the amount of time it took to fetch data from storage in ms */ - public void setTimeStorage(final long time_storage) { - this.time_storage = time_storage; + /** @return The multi-mapped set of request headers associated with the query */ + public Map getRequestHeaders() { + return headers; } - /** @return the amount of time it took to fetch data from storage in ms */ - public long getTimeStorage() { - return time_storage; + /** @return The number of currently running queries */ + public int getNumRunningQueries() { + return running_queries.size(); } - /** @param time_aggregation increments the amount of time spent aggregating in ms */ - public void addTimeAggregation(final long time_aggregation) { - this.time_aggregation += time_aggregation; + /** @return An exception associated with the query or null if the query + * returned successfully. */ + public String getException() { + if (exception == null) { + return "null"; + } + return exception.getMessage() + + (exception.getStackTrace() != null && exception.getStackTrace().length > 0 + ? "\n" + exception.getStackTrace()[0].toString() : ""); } - /** @return the mount of time spent aggregating in ms */ - public long getTimeAggregation() { - return time_aggregation; + /** @return The HTTP status response for the query */ + public HttpResponseStatus getHttpResponse() { + return response; } - /** @param time_serialization the amount of time spent serializing in ms */ - public void setTimeSerialization(final long time_serialization) { - this.time_serialization = time_serialization; + /** @return The number of times this query has been executed from the same + * endpoint. */ + public long getExecuted() { + return executed; } - /** @return the mount of time spent serializing in ms */ - public long getTimeSerialization() { - return time_serialization; + /** @return The full query */ + public TSQuery getQuery() { + return query; } - /** @return the amount of time working on the query in ms */ - public long getTimeTotal() { - return time_total; + /** @return When the query was received and started executing, in ms */ + public long getQueryStartTimestamp() { + return query_start_ms; } - - /** @return the Http status code */ - public HttpResponseStatus getStatus() { - return response; + + /** @return When the query was marked as completed, in ms */ + public long getQueryCompletedTimestamp() { + return query_completed_ts; + } + + /** @return Whether or not the data was successfully sent to the client */ + public boolean getSentToClient() { + return sent_to_client; + } + + /** @return A map with the subset of query measurements, not including scanners + * or sub queries */ + public Map getStats() { + return getStats(false, false); } - /** @return an exception if it was associated with this query */ - public Throwable getException() { - return exception; + /** + * Returns measurements of the given query + * @param with_scanners Whether or not to dump individual scanner stats + * @return A map with stats for the query + */ + public Map getStats(final boolean with_sub_queries, + final boolean with_scanners) { + final Map map = new TreeMap(); + + for (final Entry entry : overall_stats.entrySet()) { + if (entry.getKey().is_time) { + map.put(entry.getKey().toString(), DateTime.msFromNano(entry.getValue())); + } else { + map.put(entry.getKey().toString(), entry.getValue()); + } + } + + if (with_sub_queries) { + final Iterator>> it = + query_stats.entrySet().iterator(); + while (it.hasNext()) { + final Entry> entry = it.next(); + final Map qs = new HashMap(1); + qs.put(String.format("queryIdx_%02d", entry.getKey()), + getQueryStats(entry.getKey(), with_scanners)); + map.putAll(qs); + } + } + return map; } + /** + * Returns a map of stats for a single sub query + * @param index The sub query to fetch + * @param with_scanners Whether or not to print detailed stats for each scanner + * @return A map with stats to print or null if the sub query didn't have any + * data. + */ + public Map getQueryStats(final int index, + final boolean with_scanners) { + + final Map qs = query_stats.get(index); + if (qs == null) { + return null; + } + + final Map query_map = new TreeMap(); + query_map.put("queryIndex", index); + + final Iterator> stats_it = + qs.entrySet().iterator(); + while (stats_it.hasNext()) { + final Entry stat = stats_it.next(); + if (stat.getKey().is_time) { + query_map.put(stat.getKey().toString(), + DateTime.msFromNano(stat.getValue())); + } else { + query_map.put(stat.getKey().toString(), stat.getValue()); + } + } + + if (with_scanners) { + final Map> scanner_stats_map = + scanner_stats.get(index); + + final Map scanner_maps = new TreeMap(); + + query_map.put("scannerStats", scanner_maps); + if (scanner_stats_map != null) { + final Map scanners = scanner_ids.get(index); + final Iterator>> scanner_it = + scanner_stats_map.entrySet().iterator(); + while (scanner_it.hasNext()) { + final Entry> scanner = scanner_it.next(); + final Map scanner_map = new TreeMap(); + scanner_maps.put(String.format("scannerIdx_%02d", scanner.getKey()), + scanner_map); + final String id; + if (scanners != null) { + id = scanners.get(scanner.getKey()); + } else { + id = null; + } + scanner_map.put("scannerId", id); + /* Uncomment when AsyncHBase supports this + final Map> servers = scanner_servers.get(index); + if (servers != null) { + scanner_map.put("regionServers", servers.get(scanner.getKey())); + } else { + scanner_map.put("regionServers", null); + } + */ + final Iterator> scanner_stats_it = + scanner.getValue().entrySet().iterator(); + while (scanner_stats_it.hasNext()) { + final Entry scanner_stats = scanner_stats_it.next(); + if (!scanner_stats.getKey().is_time) { + scanner_map.put(scanner_stats.getKey().toString(), + scanner_stats.getValue()); + } else { + scanner_map.put(scanner_stats.getKey().toString(), + DateTime.msFromNano(scanner_stats.getValue())); + } + } + } + } + } + return query_map; + } + + /** @return A stat for the overall query or -1 if the stat didn't exist. */ + public long getStat(final QueryStat stat) { + if (!overall_stats.containsKey(stat)) { + return -1; + } + return overall_stats.get(stat); + } + + /** @return a timed stat for the overall query or NaN if the stat didn't exist */ + public double getTimeStat(final QueryStat stat) { + if (!stat.is_time) { + throw new IllegalArgumentException("The stat is not a time stat"); + } + if (!overall_stats.containsKey(stat)) { + return Double.NaN; + } + return DateTime.msFromNano(overall_stats.get(stat)); + } + /** @param whether or not to allow duplicate queries to run */ public static void setEnableDuplicates(final boolean enable_dupes) { ENABLE_DUPLICATES = enable_dupes; diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index ba6b736124..967eaab09f 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -14,8 +14,10 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import com.google.common.base.Objects; import com.stumbleupon.async.Deferred; @@ -35,6 +37,7 @@ import org.slf4j.LoggerFactory; import net.opentsdb.core.TSDB; +import net.opentsdb.stats.QueryStats; /** * Abstract base class for HTTP queries. @@ -70,6 +73,9 @@ public abstract class AbstractHttpQuery { /** The {@code TSDB} instance we belong to */ protected final TSDB tsdb; + /** Used for recording query statistics */ + protected QueryStats stats; + /** * Set up required internal state. For subclasses. * @@ -112,6 +118,58 @@ public String getRemoteAddress() { return chan.getRemoteAddress().toString(); } + /** + * Copies the header list and obfuscates the "cookie" header in case it + * contains auth tokens, etc. Note that it flattens duplicate headers keys + * as comma separated lists per the RFC + * @return The full set of headers for this query with the cookie obfuscated + */ + public Map getPrintableHeaders() { + final Map headers = new HashMap( + request.getHeaders().size()); + for (final Entry header : request.getHeaders()) { + if (header.getKey().toLowerCase().equals("cookie")) { + // null out the cookies + headers.put(header.getKey(), "*******"); + } else { + // http://tools.ietf.org/html/rfc2616#section-4.2 + if (headers.containsKey(header.getKey())) { + headers.put(header.getKey(), + headers.get(header.getKey()) + "," + header.getValue()); + } else { + headers.put(header.getKey(), header.getValue()); + } + } + } + return headers; + } + + /** + * Copies the header list so modifications won't affect the original set. + * Note that it flattens duplicate headers keys as comma separated lists + * per the RFC + * @return The full set of headers for this query + */ + public Map getHeaders() { + final Map headers = new HashMap( + request.getHeaders().size()); + for (final Entry header : request.getHeaders()) { + // http://tools.ietf.org/html/rfc2616#section-4.2 + if (headers.containsKey(header.getKey())) { + headers.put(header.getKey(), + headers.get(header.getKey()) + "," + header.getValue()); + } else { + headers.put(header.getKey(), header.getValue()); + } + } + return headers; + } + + /** @param stats The stats object to mark after writing is complete */ + public void setStats(final QueryStats stats) { + this.stats = stats; + } + /** Return the time in nanoseconds that this query object was * created. */ @@ -192,7 +250,6 @@ public boolean hasQueryStringParam(final String paramname) { public List getQueryStringParams(final String paramname) { return getQueryString().get(paramname); } - /** * Returns only the path component of the URI as a string @@ -339,6 +396,9 @@ public void sendStatusOnly(final HttpResponseStatus status) { HttpHeaders.setContentLength(response, 0); } final ChannelFuture future = chan.write(response); + if (stats != null) { + future.addListener(new SendSuccess()); + } if (!keepalive) { future.addListener(ChannelFutureListener.CLOSE); } @@ -369,12 +429,23 @@ public void sendBuffer(final HttpResponseStatus status, HttpHeaders.setContentLength(response, buf.readableBytes()); } final ChannelFuture future = chan.write(response); + if (stats != null) { + future.addListener(new SendSuccess()); + } if (!keepalive) { future.addListener(ChannelFutureListener.CLOSE); } done(); } + /** A simple class that marks a query as complete when the stats are set */ + private class SendSuccess implements ChannelFutureListener { + @Override + public void operationComplete(final ChannelFuture future) throws Exception { + stats.markSent(); + } + } + /** @return Information about the query */ public String toString() { return Objects.toStringHelper(this) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 57fcf1b567..e57acf208a 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -45,6 +45,7 @@ import net.opentsdb.meta.UIDMeta; import net.opentsdb.search.SearchQuery; import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.tree.Branch; import net.opentsdb.tree.Tree; import net.opentsdb.tree.TreeRule; @@ -604,6 +605,8 @@ class DPsResolver implements Callback, Object> { new ArrayList>(); /** The data points to serialize */ final DataPoints dps; + /** Starting time in nanos when we sent the UID resolution queries off */ + long uid_start; public DPsResolver(final DataPoints dps) { this.dps = dps; @@ -653,6 +656,9 @@ public WriteToBuffer(final DataPoints dps) { * variables. */ public Object call(final ArrayList deferreds) throws Exception { + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.UID_TO_STRING_TIME, (DateTime.nanoTime() - uid_start)); + final long local_serialization_start = DateTime.nanoTime(); final TSSubQuery orig_query = data_query.getQueries() .get(dps.getQueryIndex()); @@ -715,8 +721,9 @@ public Object call(final ArrayList deferreds) throws Exception { // now the fun stuff, dump the data and time just the iteration over // the data points - final long dps_start = DateTime.currentTimeMillis(); + final long dps_start = DateTime.nanoTime(); json.writeFieldName("dps"); + long counter = 0; // default is to write a map, otherwise write arrays if (!timeout_flag.get(0) && as_arrays) { @@ -743,6 +750,7 @@ public Object call(final ArrayList deferreds) throws Exception { } } json.writeEndArray(); + ++counter; } json.writeEndArray(); } else if (!timeout_flag.get(0)) { @@ -766,28 +774,39 @@ public Object call(final ArrayList deferreds) throws Exception { json.writeNumberField(Long.toString(timestamp), dp.doubleValue()); } } + ++counter; } json.writeEndObject(); + } else { // skipping data points all together due to timeout json.writeStartObject(); json.writeEndObject(); } - - final long agg_time = DateTime.currentTimeMillis() - dps_start; - data_query.getQueryStats().addTimeAggregation(agg_time); - data_query.getQueryStats().addAggregatedSize(dps.aggregatedSize()); - data_query.getQueryStats().addSize(dps.size()); + final long agg_time = DateTime.nanoTime() - dps_start; + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.AGGREGATION_TIME, agg_time); + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.AGGREGATED_SIZE, counter); + + // yeah, it's a little early but we need to dump it out with the results. + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.SERIALIZATION_TIME, + DateTime.nanoTime() - local_serialization_start); if (!timeout_flag.get(0) && data_query.getShowStats()) { - json.writeFieldName("stats"); - json.writeStartObject(); - json.writeNumberField("datapoints", dps.size()); - json.writeNumberField("rawDatapoints", dps.aggregatedSize()); - json.writeNumberField("aggregationTime", agg_time); - json.writeNumberField("timeSeries", dps.getTSUIDs().size()); - // todo - timing for just this query - json.writeEndObject(); + int query_index = (dps == null) ? -1 : dps.getQueryIndex(); + QueryStats stats = data_query.getQueryStats(); + + if (query_index >= 0) { + json.writeFieldName("stats"); + final Map s = stats.getQueryStats(query_index, false); + if (s != null) { + json.writeObject(s); + } else { + json.writeStringField("ERROR", "NO STATS FOUND"); + } + } } // close the results for this particular query @@ -801,6 +820,8 @@ public Object call(final ArrayList deferreds) throws Exception { * then prints to the output buffer once they are completed. */ public Deferred call(final Object obj) throws Exception { + this.uid_start = DateTime.nanoTime(); + resolve_deferreds.add(dps.metricNameAsync() .addCallback(new MetricResolver())); resolve_deferreds.add(dps.getTagsAsync() @@ -832,23 +853,19 @@ public Deferred call(final Object obj) throws Exception { class FinalCB implements Callback { public ChannelBuffer call(final Object obj) throws Exception { - data_query.getQueryStats().setTimeSerialization( - DateTime.currentTimeMillis() - start); - data_query.getQueryStats().markComplete(); + + // Call this here so we rollup sub metrics into a summary. It's not + // completely accurate, of course, because we still have to write the + // summary and close the writer. But it's close. + data_query.getQueryStats().markSerializationSuccessful(); // dump overall stats as an extra object in the array + // TODO - yeah, I've heard this sucks, we need to figure out a better way. if (data_query.getShowSummary()) { final QueryStats stats = data_query.getQueryStats(); json.writeStartObject(); json.writeFieldName("statsSummary"); - json.writeStartObject(); - json.writeNumberField("datapoints", stats.getSize()); - json.writeNumberField("rawDatapoints", stats.getAggregatedSize()); - json.writeNumberField("aggregationTime", stats.getTimeAggregation()); - json.writeNumberField("serializationTime", stats.getTimeSerialization()); - json.writeNumberField("storageTime", stats.getTimeStorage()); - json.writeNumberField("timeTotal", stats.getTimeTotal()); - json.writeEndObject(); + json.writeObject(stats.getStats(true, true)); json.writeEndObject(); } @@ -1060,8 +1077,7 @@ public ChannelBuffer formatJVMStatsV1(final Map> sta * @throws BadRequestException if the plugin has not implemented this method * @since 2.2 */ - public ChannelBuffer formatQueryStatsV1( - final Map>> query_stats) { + public ChannelBuffer formatQueryStatsV1(final Map query_stats) { return serializeJSON(query_stats); } diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 8f16ea9590..34adc3e01d 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -720,8 +720,7 @@ public ChannelBuffer formatJVMStatsV1(final Map> map * @throws BadRequestException if the plugin has not implemented this method * @since 2.2 */ - public ChannelBuffer formatQueryStatsV1( - final Map>> query_stats) { + public ChannelBuffer formatQueryStatsV1(final Map query_stats) { throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "The requested API endpoint has not been implemented", this.getClass().getCanonicalName() + diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 3aa42c3d32..40c80a7e05 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -19,7 +19,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import org.hbase.async.HBaseException; +import org.hbase.async.RpcTimedOutException; import org.hbase.async.Bytes.ByteMap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.handler.codec.http.HttpMethod; @@ -44,6 +47,7 @@ import net.opentsdb.meta.TSUIDQuery; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.StatsCollector; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; @@ -62,6 +66,11 @@ final class QueryRpc implements HttpRpc { private static final Logger LOG = LoggerFactory.getLogger(QueryRpc.class); + /** Various counters and metrics for reporting query stats */ + static final AtomicLong query_invalid = new AtomicLong(); + static final AtomicLong query_exceptions = new AtomicLong(); + static final AtomicLong query_success = new AtomicLong(); + /** * Implements the /api/query endpoint to fetch data from OpenTSDB. * @param tsdb The TSDB to use for fetching data @@ -109,7 +118,8 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { case 1: data_query = query.serializer().parseQueryV1(); break; - default: + default: + query_invalid.incrementAndGet(); throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); @@ -135,8 +145,10 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { // if the user tried this query multiple times from the same IP and src port // they'll be rejected on subsequent calls final QueryStats query_stats = - new QueryStats(query.getRemoteAddress(), data_query); + new QueryStats(query.getRemoteAddress(), data_query, + query.getPrintableHeaders()); data_query.setQueryStats(query_stats); + query.setStats(query_stats); final int nqueries = data_query.getQueries().size(); final ArrayList results = new ArrayList(nqueries); @@ -145,43 +157,56 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query) { /** This has to be attached to callbacks or we may never respond to clients */ class ErrorCB implements Callback { public Object call(final Exception e) throws Exception { + Throwable ex = e; try { - if (e instanceof DeferredGroupException) { - Throwable ex = e.getCause(); + LOG.error("Query exception: ", e); + if (ex instanceof DeferredGroupException) { + ex = e.getCause(); while (ex != null && ex instanceof DeferredGroupException) { ex = ex.getCause(); } - if (ex != null) { - if (ex instanceof NoSuchUniqueName) { - query_stats.markComplete(HttpResponseStatus.BAD_REQUEST, ex); - query.badRequest(new BadRequestException( - HttpResponseStatus.NOT_FOUND, ex.getMessage())); - return null; - } - LOG.error("Query failed", ex); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); - query.badRequest(new BadRequestException(ex)); - } else { - LOG.error("Unable to find the cause of the DGE", e); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); - query.badRequest(new BadRequestException(e)); + if (ex == null) { + LOG.error("The deferred group exception didn't have a cause???"); } - } else if (e.getClass() == QueryException.class) { - query_stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); - query.badRequest(new BadRequestException((QueryException)e)); + } + + if (ex instanceof RpcTimedOutException) { + query_stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, ex); + query.badRequest(new BadRequestException( + HttpResponseStatus.REQUEST_TIMEOUT, ex.getMessage())); + query_exceptions.incrementAndGet(); + } else if (ex instanceof HBaseException) { + query_stats.markSerialized(HttpResponseStatus.FAILED_DEPENDENCY, ex); + query.badRequest(new BadRequestException( + HttpResponseStatus.FAILED_DEPENDENCY, ex.getMessage())); + query_exceptions.incrementAndGet(); + } else if (ex instanceof QueryException) { + query_stats.markSerialized(((QueryException)ex).getStatus(), ex); + query.badRequest(new BadRequestException( + ((QueryException)ex).getStatus(), ex.getMessage())); + query_exceptions.incrementAndGet(); + } else if (ex instanceof BadRequestException) { + query_stats.markSerialized(((BadRequestException)ex).getStatus(), ex); + query.badRequest((BadRequestException)ex); + query_invalid.incrementAndGet(); + } else if (ex instanceof NoSuchUniqueName) { + query_stats.markSerialized(HttpResponseStatus.BAD_REQUEST, ex); + query.badRequest(new BadRequestException(ex)); + query_invalid.incrementAndGet(); } else { - LOG.error("Query failed", e); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); - query.badRequest(new BadRequestException(e)); + query_stats.markSerialized(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); + query.badRequest(new BadRequestException(ex)); + query_exceptions.incrementAndGet(); } - return null; - } catch (RuntimeException ex) { - LOG.error("Exception thrown during exception handling", ex); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); + + } catch (RuntimeException ex2) { + LOG.error("Exception thrown during exception handling", ex2); + query_stats.markSerialized(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex2); query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, - ex.getMessage().getBytes()); - return null; + ex2.getMessage().getBytes()); + query_exceptions.incrementAndGet(); } + return null; } } @@ -198,11 +223,11 @@ public Object call(final ArrayList query_results) class SendIt implements Callback { public Object call(final ChannelBuffer buffer) throws Exception { query.sendReply(buffer); + query_success.incrementAndGet(); return null; } } - query_stats.setTimeStorage(System.currentTimeMillis() - start); switch (query.apiVersion()) { case 0: case 1: @@ -210,6 +235,7 @@ public Object call(final ChannelBuffer buffer) throws Exception { globals).addCallback(new SendIt()).addErrback(new ErrorCB()); break; default: + query_invalid.incrementAndGet(); throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); @@ -701,6 +727,13 @@ private LastPointQuery parseLastPointQuery(final TSDB tsdb, return query; } + /** @param collector Populates the collector with statistics */ + public static void collectStats(final StatsCollector collector) { + collector.record("http.query.invalid_requests", query_invalid); + collector.record("http.query.exceptions", query_exceptions); + collector.record("http.query.success", query_success); + } + public static class LastPointQuery { private boolean resolve_names; diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index e6bf4b588c..69224ca8b5 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -331,6 +331,7 @@ public static void collectStats(final StatsCollector collector) { HttpQuery.collectStats(collector); GraphHandler.collectStats(collector); PutDataPointRpc.collectStats(collector); + QueryRpc.collectStats(collector); } /** diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index 0e65d17a47..bd0ee910d1 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -331,7 +331,7 @@ private void printQueryStats(final HttpQuery query) { case 0: case 1: query.sendReply(query.serializer().formatQueryStatsV1( - QueryStats.buildStats())); + QueryStats.getRunningAndCompleteStats())); break; default: throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 97b052c0bb..d3d9a08c44 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -268,4 +268,41 @@ public static long currentTimeMillis() { return System.currentTimeMillis(); } + /** + * Pass through to {@link System.nanoTime} for use in classes to + * make unit testing easier. Mocking System.class is a bad idea in general + * so placing this here and mocking DateTime.class is MUCH cleaner. + * @return The current epoch time in milliseconds + * @since 2.2 + */ + public static long nanoTime() { + return System.nanoTime(); + } + + /** + * Converts the long nanosecond value to a double in milliseconds + * @param ts The timestamp or value in nanoseconds + * @return The timestamp in milliseconds + * @since 2.2 + */ + public static double msFromNano(final long ts) { + return (double)ts / 1000000; + } + + /** + * Calculates the difference between two values and returns the time in + * milliseconds as a double. + * @param end The end timestamp + * @param start The start timestamp + * @return The value in milliseconds + * @throws IllegalArgumentException if end is less than start + * @since 2.2 + */ + public static double msFromNanoDiff(final long end, final long start) { + if (end < start) { + throw new IllegalArgumentException("End (" + end + ") cannot be less " + + "than start (" + start + ")"); + } + return ((double) end - (double) start) / 1000000; + } } diff --git a/test/stats/TestQueryStats.java b/test/stats/TestQueryStats.java index 5ee4dcf163..e81477d15d 100644 --- a/test/stats/TestQueryStats.java +++ b/test/stats/TestQueryStats.java @@ -15,9 +15,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyLong; import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -26,14 +31,21 @@ import net.opentsdb.core.QueryException; import net.opentsdb.core.TSQuery; +import net.opentsdb.stats.QueryStats.QueryStat; +import net.opentsdb.utils.DateTime; import org.jboss.netty.handler.codec.http.HttpResponseStatus; 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; @RunWith(PowerMockRunner.class) +@PrepareForTest({ DateTime.class, QueryStats.class }) public final class TestQueryStats { private static String remote = "192.168.1.1:4242"; @@ -56,65 +68,100 @@ public final class TestQueryStats { } } + private Map headers; + @Before public void before() throws Exception { running_queries.set(null, new ConcurrentHashMap()); completed_queries.set(null, CacheBuilder.newBuilder().maximumSize(2).build()); + headers = new HashMap(1); + headers.put("Cookie", "Hide me!"); + PowerMockito.mockStatic(DateTime.class); + PowerMockito.doAnswer(new Answer() { + long ts = 1000L; + @Override + public Long answer(InvocationOnMock invocation) throws Throwable { + return ts += 1000000000L; + } + + }).when(DateTime.class, "nanoTime"); + PowerMockito.doCallRealMethod().when(DateTime.class, + "msFromNano", anyLong()); + PowerMockito.doCallRealMethod().when(DateTime.class, + "msFromNanoDiff", anyLong(), anyLong()); } @Test public void ctor() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); - final Map>> map = QueryStats.buildStats(); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(1, map.get("running").size()); - assertEquals(0, map.get("completed").size()); + assertEquals(1, ((List)map.get("running")).size()); + assertEquals(0, ((Collection)map.get("completed")).size()); + assertSame(headers, stats.getRequestHeaders()); } - @Test (expected = QueryException.class) + @Test public void ctorDuplicate() throws Exception { + QueryStats.setEnableDuplicates(false); final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); - final Map>> map = QueryStats.buildStats(); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(1, map.get("running").size()); - assertEquals(0, map.get("completed").size()); - new QueryStats(remote, query); + assertEquals(1, ((List)map.get("running")).size()); + assertEquals(0, ((Collection)map.get("completed")).size()); + try { + new QueryStats(remote, query, headers); + fail("Expected a QueryException"); + } catch (QueryException e) { } + QueryStats.setEnableDuplicates(true); } @Test (expected = IllegalArgumentException.class) public void ctorNullRemote() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - new QueryStats(null, query); + new QueryStats(null, query, headers); } @Test (expected = IllegalArgumentException.class) public void ctorNullQuery() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - new QueryStats(remote, null); + new QueryStats(remote, null, headers); + } + + @Test + public void ctorNullHeaders() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query, null); + assertNotNull(stats); + final Map map = QueryStats.getRunningAndCompleteStats(); + assertNotNull(map); + assertEquals(1, ((List)map.get("running")).size()); + assertEquals(0, ((Collection)map.get("completed")).size()); } @Test public void testHashCodeandEquals() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); final int hash_a = stats.hashCode(); // have to mark the old one as complete before we can test equality - stats.markComplete(); + stats.markSerializationSuccessful(); final TSQuery query2 = new TSQuery(); query2.setStart("1h-ago"); - final QueryStats stats2 = new QueryStats(remote, query2); + final QueryStats stats2 = new QueryStats(remote, query2, headers); assertNotNull(stats); assertEquals(hash_a, stats2.hashCode()); assertEquals(stats, stats2); @@ -125,13 +172,13 @@ public void testHashCodeandEquals() throws Exception { public void testHashCodeandNotEquals() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); final int hash_a = stats.hashCode(); final TSQuery query2 = new TSQuery(); query2.setStart("2h-ago"); - final QueryStats stats2 = new QueryStats(remote, query2); + final QueryStats stats2 = new QueryStats(remote, query2, headers); assertNotNull(stats); assertTrue(hash_a != stats2.hashCode()); assertFalse(stats.equals(stats2)); @@ -142,7 +189,7 @@ public void testHashCodeandNotEquals() throws Exception { public void testEqualsNull() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertFalse(stats.equals(null)); } @@ -150,7 +197,7 @@ public void testEqualsNull() throws Exception { public void testEqualsWrongType() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertFalse(stats.equals(new String("foo"))); } @@ -158,7 +205,7 @@ public void testEqualsWrongType() throws Exception { public void testEqualsSame() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertTrue(stats.equals(stats)); } @@ -166,84 +213,92 @@ public void testEqualsSame() throws Exception { public void markComplete() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(); - final Map>> map = QueryStats.buildStats(); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerializationSuccessful(); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - final Map completed = map.get("completed").get(0); - assertEquals(200, completed.get("status")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + final QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(200, completed.getHttpResponse().getCode()); } @Test public void markCompleteTimeout() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - final Map>> map = QueryStats.buildStats(); + final QueryStats stats = new QueryStats(remote, query, headers); + final RuntimeException timeout = new RuntimeException("Timeout!"); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, timeout); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - final Map completed = map.get("completed").get(0); - assertEquals(408, completed.get("status")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + final QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(408, completed.getHttpResponse().getCode()); + assertTrue(completed.getException().startsWith("Timeout!\n")); } @Test - public void markCompleteDoubleMark() throws Exception { + public void executed() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(); - final Map>> map = QueryStats.buildStats(); - assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - Map completed = map.get("completed").get(0); - assertEquals(200, completed.get("status")); - stats.markComplete(); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, null); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - completed = map.get("completed").get(0); - assertEquals(200, completed.get("status")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + final QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(1, completed.getExecuted()); } @Test - public void executed() throws Exception { + public void executedTwice() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - final Map>> map = QueryStats.buildStats(); + QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, null); + Map map = QueryStats.getRunningAndCompleteStats(); + assertNotNull(map); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(1, completed.getExecuted()); + + stats = new QueryStats(remote, query, headers); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, null); + map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - final Map completed = map.get("completed").get(0); - assertEquals(1L, completed.get("executed")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(2, completed.getExecuted()); + } + + @Test + public void getStat() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.addStat(QueryStat.AGGREGATED_SIZE, 42); + stats.markSerializationSuccessful(); + assertEquals(42, stats.getStat(QueryStat.AGGREGATED_SIZE)); + assertEquals(-1, stats.getStat(QueryStat.BYTES_FROM_STORAGE)); } @Test - public void executedTwice() throws Exception { + public void getStatTime() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - Map>> map = QueryStats.buildStats(); - assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - Map completed = map.get("completed").get(0); - assertEquals(1L, completed.get("executed")); - - stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - map = QueryStats.buildStats(); - assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - completed = map.get("completed").get(0); - assertEquals(2L, completed.get("executed")); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerializationSuccessful(); + assertEquals(1000.0, stats.getTimeStat(QueryStat.PROCESSING_PRE_WRITE_TIME), 0.001); + assertEquals(Double.NaN, stats.getTimeStat(QueryStat.AVG_AGGREGATION_TIME), 0.001); } } diff --git a/test/tsd/NettyMocks.java b/test/tsd/NettyMocks.java index 7937f97bbf..f641ea9550 100644 --- a/test/tsd/NettyMocks.java +++ b/test/tsd/NettyMocks.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.tsd; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -22,8 +23,11 @@ import net.opentsdb.core.TSDB; import net.opentsdb.utils.Config; +import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.DefaultChannelFuture; import org.jboss.netty.channel.DefaultChannelPipeline; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpMethod; @@ -198,6 +202,13 @@ public static HttpQuery contentQuery(final TSDB tsdb, final String uri, req.headers().set("Content-Type", type); return new HttpQuery(tsdb, req, channelMock); } + + /** @param the query to mock a future callback for */ + public static void mockChannelFuture(final HttpQuery query) { + final ChannelFuture future = new DefaultChannelFuture(query.channel(), false); + when(query.channel().write(any(ChannelBuffer.class))).thenReturn(future); + future.setSuccess(); + } /** * Returns a simple pipeline with an HttpRequestDecoder and an diff --git a/test/tsd/TestHttpJsonSerializer.java b/test/tsd/TestHttpJsonSerializer.java index a4669a87b1..1c746f55a2 100644 --- a/test/tsd/TestHttpJsonSerializer.java +++ b/test/tsd/TestHttpJsonSerializer.java @@ -266,18 +266,15 @@ public void formatQueryAsyncV1wStatsSummary() throws Exception { assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); assertTrue(json.contains("\"1356998700\":1,")); assertTrue(json.contains("\"1357058700\":201")); - //assert stats assertTrue(json.contains("\"stats\":{")); - assertTrue(json.contains("\"datapoints\":400")); - assertTrue(json.contains("\"rawDatapoints\":800")); - assertTrue(json.contains("\"timeSeries\":2")); - + assertTrue(json.contains("\"emittedDPs\":401")); + System.out.println(json); //assert stats summary assertTrue(json.contains("{\"statsSummary\":{")); - assertTrue(json.contains("\"serializationTime\":1500")); - assertTrue(json.contains("\"storageTime\":0")); - assertTrue(json.contains("\"timeTotal\":2500")); + assertTrue(json.contains("\"serializationTime\":")); + assertTrue(json.contains("\"processingPreWriteTime\":")); + assertTrue(json.contains("\"queryIdx_00\":")); } @Test @@ -299,12 +296,9 @@ public void formatQueryAsyncV1wStatsWoSummary() throws Exception { assertTrue(json.contains("\"1356998700\":1,")); assertTrue(json.contains("\"1357058700\":201")); - //assert stats assertTrue(json.contains("\"stats\":{")); - assertTrue(json.contains("\"datapoints\":400")); - assertTrue(json.contains("\"rawDatapoints\":800")); - assertTrue(json.contains("\"timeSeries\":2")); + assertTrue(json.contains("\"emittedDPs\":401")); //assert stats summary assertFalse(json.contains("{\"statsSummary\":{")); @@ -327,15 +321,16 @@ public void formatQueryAsyncV1woStatsWSummary() throws Exception { assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); assertTrue(json.contains("\"1356998700\":1,")); assertTrue(json.contains("\"1357058700\":201")); - + //assert stats assertFalse(json.contains("\"stats\":{")); //assert stats summary assertTrue(json.contains("{\"statsSummary\":{")); - assertTrue(json.contains("\"serializationTime\":1500")); - assertTrue(json.contains("\"storageTime\":0")); - assertTrue(json.contains("\"timeTotal\":2500")); + assertTrue(json.contains("\"serializationTime\":")); + assertTrue(json.contains("\"processingPreWriteTime\":")); + assertTrue(json.contains("\"emittedDPs\":401")); + assertTrue(json.contains("\"queryIdx_00\":")); } @Test @@ -570,7 +565,7 @@ private TSQuery getTestQuery(final boolean show_stats, final boolean show_summar */ private void validateTestQuery(final TSQuery data_query) { data_query.validateAndSetQuery(); - data_query.setQueryStats(new QueryStats(remote, data_query)); + data_query.setQueryStats(new QueryStats(remote, data_query, null)); } /** @@ -590,6 +585,15 @@ public Long answer(InvocationOnMock invocation) throws Throwable { return ts; } }); + + PowerMockito.when(DateTime.nanoTime()) + .thenAnswer(new Answer () { + public Long answer(InvocationOnMock invocation) throws Throwable { + long ts = timestamp.get(0); + timestamp.set(0, ts + 500); + return ts * 1000000; + } + }); } } diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index d498b963ff..a51b5ac5db 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -438,6 +438,7 @@ public void postQuerySimplePass() throws Exception { "{\"start\":1425440315306,\"queries\":" + "[{\"metric\":\"somemetric\",\"aggregator\":\"sum\",\"rate\":true," + "\"rateOptions\":{\"counter\":false}}]}"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); } @@ -465,6 +466,7 @@ public void postQueryNoMetricBadRequest() throws Exception { public void executeEmpty() throws Exception { final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String json = query.response().getContent().toString(Charset.forName("UTF-8")); @@ -480,6 +482,7 @@ public void execute() throws Exception { final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String json = query.response().getContent().toString(Charset.forName("UTF-8")); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 23867039c4..63ae2ffab8 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import java.text.SimpleDateFormat; @@ -371,5 +372,33 @@ public void currentTimeMillis() { when(System.currentTimeMillis()).thenReturn(1388534400000L); assertEquals(1388534400000L, DateTime.currentTimeMillis()); } + + @Test + public void nanoTime() { + PowerMockito.mockStatic(System.class); + when(System.nanoTime()).thenReturn(1388534400000000000L); + assertEquals(1388534400000000000L, DateTime.nanoTime()); + } + + @Test + public void msFromNano() { + assertEquals(0, DateTime.msFromNano(0), 0.0001); + assertEquals(1, DateTime.msFromNano(1000000), 0.0001); + assertEquals(-1, DateTime.msFromNano(-1000000), 0.0001); + assertEquals(1.5, DateTime.msFromNano(1500000), 0.0001); + assertEquals(1.123, DateTime.msFromNano(1123000), 0.0001); + } + + @Test + public void msFromNanoDiff() { + assertEquals(0, DateTime.msFromNanoDiff(1000000, 1000000), 0.0001); + assertEquals(0.5, DateTime.msFromNanoDiff(1500000, 1000000), 0.0001); + assertEquals(1.5, DateTime.msFromNanoDiff(1500000, 0), 0.0001); + assertEquals(0.5, DateTime.msFromNanoDiff(-1000000, -1500000), 0.0001); + try { + assertEquals(0.5, DateTime.msFromNanoDiff(1000000, 1500000), 0.0001); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) {} + } } From d9d60d1991daeb294ebff0260eb266724fbb0b0f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 16:39:06 -0800 Subject: [PATCH 410/826] Rework the QueryStats before release by adding a bunch of timing around the scanner, HBase, serialization, etc. The class was reworked and the API is different but I think more useful and expandable. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 143 ++++- src/core/TsdbQuery.java | 115 +++- src/stats/QueryStats.java | 797 ++++++++++++++++++++++----- src/tsd/AbstractHttpQuery.java | 73 ++- src/tsd/HttpJsonSerializer.java | 70 ++- src/tsd/HttpSerializer.java | 3 +- src/tsd/QueryRpc.java | 95 ++-- src/tsd/RpcHandler.java | 1 + src/tsd/StatsRpc.java | 2 +- src/utils/DateTime.java | 37 ++ test/stats/TestQueryStats.java | 203 ++++--- test/tsd/NettyMocks.java | 11 + test/tsd/TestHttpJsonSerializer.java | 38 +- test/tsd/TestQueryRpc.java | 3 + test/utils/TestDateTime.java | 29 + 15 files changed, 1288 insertions(+), 332 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 4acfc39ac9..9498284e00 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -25,7 +25,10 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.DateTime; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.DeleteRequest; @@ -84,6 +87,13 @@ public class SaltScanner { /** The TSDB to which we belong */ private final TSDB tsdb; + /** A stats object associated with the sub query used for storing stats + * about scanner operations. */ + private final QueryStats query_stats; + + /** Index of the sub query in the main query list */ + private final int query_index; + /** A counter used to determine how many scanners are still running */ private AtomicInteger completed_tasks = new AtomicInteger(); @@ -117,7 +127,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters) { - this(tsdb, metric, scanners, spans, filters, false); + this(tsdb, metric, scanners, spans, filters, false, null, 0); } /** @@ -129,6 +139,8 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, * @param spans The span map to store results in * @param delete Whether or not to delete the queried data * @param filters A list of filters for processing + * @param query_stats A stats object for tracking timing + * @param query_index The index of the sub query in the main query list * @throws IllegalArgumentException if any required data was missing or * we had invalid parameters. */ @@ -136,7 +148,9 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters, - final boolean delete) { + final boolean delete, + final QueryStats query_stats, + final int query_index) { if (Const.SALT_WIDTH() < 1) { throw new IllegalArgumentException( "Salting is disabled. Use the regular scanner"); @@ -173,6 +187,8 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.tsdb = tsdb; this.filters = filters; this.delete = delete; + this.query_stats = query_stats; + this.query_index = query_index; } /** @@ -184,8 +200,9 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, */ public Deferred> scan() { start_time = System.currentTimeMillis(); + int i = 0; for (final Scanner scanner: scanners) { - new ScannerCB(scanner).scan(); + new ScannerCB(scanner, i++).scan(); } return results; } @@ -208,6 +225,7 @@ private void mergeAndReturnResults() { } // Merge sorted spans together + final long merge_start = DateTime.nanoTime(); for (final List kvs : kv_map.values()) { if (kvs == null || kvs.isEmpty()) { LOG.warn("Found a key value list that was null or empty"); @@ -260,6 +278,10 @@ private void mergeAndReturnResults() { } } + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, + (DateTime.nanoTime() - merge_start)); + } if (LOG.isDebugEnabled()) { LOG.debug("Scanning completed in " + (hbase_time - start_time) + " ms, " + rows + " rows, and stored in " + spans.size() + " spans"); @@ -280,14 +302,29 @@ private void mergeAndReturnResults() { final class ScannerCB implements Callback>> { private final Scanner scanner; + private final int index; private final List kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); private final Set skips = new HashSet(); private final Set keepers = new HashSet(); - public ScannerCB(final Scanner scanner) { + private long scanner_start = -1; + /** nanosecond timestamps */ + private long fetch_start = 0; // reset each time we send an RPC to HBase + private long fetch_time = 0; // cumulation of time waiting on HBase + private long uid_resolve_time = 0; // cumulation of time resolving UIDs + private long uids_resolved = 0; + private long compaction_time = 0; // cumulation of time compacting + private long dps_post_filter = 0; + private long rows_post_filter = 0; + + public ScannerCB(final Scanner scanner, final int index) { this.scanner = scanner; + this.index = index; + if (query_stats != null) { + query_stats.addScannerId(query_index, index, scanner.toString()); + } } /** Error callback that will capture an exception from AsyncHBase and store @@ -297,7 +334,7 @@ class ErrorCb implements Callback { @Override public Object call(final Exception e) throws Exception { LOG.error("Scanner " + scanner + " threw an exception", e); - scanner.close(); + close(false); handleException(e); return null; } @@ -310,6 +347,10 @@ public Object call(final Exception e) throws Exception { * found */ public Object scan() { + if (scanner_start < 0) { + scanner_start = DateTime.nanoTime(); + } + fetch_start = DateTime.nanoTime(); return scanner.nextRows().addCallback(this).addErrback(new ErrorCb()); } @@ -322,9 +363,17 @@ public Object scan() { public Object call(final ArrayList> rows) throws Exception { try { + fetch_time += DateTime.nanoTime() - fetch_start; if (rows == null) { - scanner.close(); - validateAndTriggerCallback(kvs, annotations); + close(true); + return null; + } else if (exception != null) { + close(false); + // don't need to handleException here as it's already taken care of + // due to the fact that exception was set. + if (LOG.isDebugEnabled()) { + LOG.debug("Closing scanner as there was an exception: " + scanner); + } return null; } @@ -336,7 +385,7 @@ public Object call(final ArrayList> rows) for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (RowKey.rowKeyContainsMetric(metric, key) != 0) { - scanner.close(); + close(false); handleException(new IllegalDataException( "HBase returned a row that doesn't match" + " our scanner (" + scanner + ")! " + row + " does not start" @@ -359,6 +408,8 @@ public Object call(final ArrayList> rows) continue; } if (!keepers.contains(tsuid)) { + final long uid_start = DateTime.nanoTime(); + /** CB to called after all of the UIDs have been resolved */ class MatchCB implements Callback> { @Override @@ -383,6 +434,8 @@ class GetTagsCB implements @Override public Deferred> call( final Map tags) throws Exception { + uid_resolve_time += (DateTime.nanoTime() - uid_start); + uids_resolved += tags.size(); final List> matches = new ArrayList>(filters.size()); @@ -420,7 +473,7 @@ public Object call(final ArrayList group) throws Exception { } } catch (final RuntimeException e) { LOG.error("Unexpected exception on scanner " + this, e); - scanner.close(); + close(false); handleException(e); return null; } @@ -447,11 +500,67 @@ void processRow(final byte[] key, final ArrayList row) { final KeyValue compacted; // let IllegalDataExceptions bubble up so the handler above can close // the scanner - compacted = tsdb.compact(row, notes); + final long compaction_start = DateTime.nanoTime(); + try { + compacted = tsdb.compact(row, notes); + } catch (IllegalDataException idex) { + compaction_time += (DateTime.nanoTime() - compaction_start); + close(false); + handleException(idex); + return; + } + compaction_time += (DateTime.nanoTime() - compaction_start); if (compacted != null) { // Can be null if we ignored all KVs. kvs.add(compacted); } } + + /** + * Closes the scanner and sets the various stats after filtering + * @param ok Whether or not the scanner closed with an exception or + * closed due to natural causes (e.g. ran out of data or we wanted to stop + * it early) + */ + void close(final boolean ok) { + scanner.close(); + + if (query_stats != null) { + query_stats.addScannerStat(query_index, index, QueryStat.SCANNER_TIME, + DateTime.nanoTime() - scanner_start); + + // Scanner Stats + /* Uncomment when AsyncHBase has this feature: + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_FROM_STORAGE, scanner.getRowsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.COLUMNS_FROM_STORAGE, scanner.getColumnsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.BYTES_FROM_STORAGE, scanner.getBytesFetched()); */ + query_stats.addScannerStat(query_index, index, + QueryStat.HBASE_TIME, fetch_time); + query_stats.addScannerStat(query_index, index, + QueryStat.SUCCESSFUL_SCAN, ok ? 1 : 0); + + // Post Scan stats + /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_POST_FILTER, rows_post_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_POST_FILTER, dps_post_filter); */ + query_stats.addScannerStat(query_index, index, + QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); + query_stats.addScannerStat(query_index, index, + QueryStat.UID_PAIRS_RESOLVED, uids_resolved); + query_stats.addScannerStat(query_index, index, + QueryStat.COMPACTION_TIME, compaction_time); + } + + if (ok && exception == null) { + validateAndTriggerCallback(kvs, annotations); + } else { + completed_tasks.incrementAndGet(); + } + } } /** @@ -493,10 +602,19 @@ private void validateAndTriggerCallback(final List kvs, */ private void handleException(final Exception e) { // make sure only one scanner can set the exception + completed_tasks.incrementAndGet(); if (exception == null) { synchronized (this) { if (exception == null) { exception = e; + // fail once and fast on the first scanner to throw an exception + try { + mergeAndReturnResults(); + } catch (Exception ex) { + LOG.error("Failed merging and returning results, " + + "calling back with exception", ex); + results.callback(ex); + } } else { // TODO - it would be nice to close and cancel the other scanners but // for now we have to wait for them to finish and/or throw exceptions. @@ -504,10 +622,5 @@ private void handleException(final Exception e) { } } } - - final int tasks = completed_tasks.incrementAndGet(); - if (tasks >= Const.SALT_BUCKETS()) { - results.callback(exception); - } } } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 7b060a44ed..5d96c4e1c3 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -41,6 +41,8 @@ import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.Histogram; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; @@ -71,6 +73,9 @@ final class TsdbQuery implements Query { /** The TSDB we belong to. */ private final TSDB tsdb; + + /** The time, in ns, when we start scanning for data **/ + private long scan_start_time; /** Value used for timestamps that are uninitialized. */ private static final int UNSET = -1; @@ -132,6 +137,9 @@ final class TsdbQuery implements Query { /** 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; + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -313,6 +321,7 @@ public Deferred configureFromQuery(final TSQuery query, setEndTime(query.endTime()); setDelete(query.getDelete()); query_index = index; + query_stats = query.getQueryStats(); // set common options aggregator = sub_query.aggregator(); @@ -541,10 +550,12 @@ private Deferred> findSpans() throws HBaseException { for (int i = 0; i < Const.SALT_BUCKETS(); i++) { scanners.add(getScanner(i)); } - return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters) - .scan(); + scan_start_time = DateTime.nanoTime(); + return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, + delete, query_stats, query_index).scan(); } + scan_start_time = DateTime.nanoTime(); final Scanner scanner = getScanner(); final Deferred> results = new Deferred>(); @@ -561,11 +572,17 @@ final class ScannerCB implements Callback skips = new HashSet(); private final Set keepers = new HashSet(); + private final int index = 0; // only used for salted scanners + /** nanosecond timestamps */ + private long fetch_start = 0; // reset each time we send an RPC to HBase + private long fetch_time = 0; // cumulation of time waiting on HBase + private long uid_resolve_time = 0; // cumulation of time resolving UIDs + private long uids_resolved = 0; + private long compaction_time = 0; // cumulation of time compacting /** Error callback that will capture an exception from AsyncHBase and store * it so we can bubble it up to the caller. @@ -574,8 +591,7 @@ class ErrorCB implements Callback { @Override public Object call(final Exception e) throws Exception { LOG.error("Scanner " + scanner + " threw an exception", e); - scanner.close(); - results.callback(e); + close(e); return null; } } @@ -587,7 +603,7 @@ public Object call(final Exception e) throws Exception { * found */ public Object scan() { - starttime = System.nanoTime(); + fetch_start = DateTime.nanoTime(); return scanner.nextRows().addCallback(this).addErrback(new ErrorCB()); } @@ -599,23 +615,18 @@ public Object scan() { @Override public Object call(final ArrayList> rows) throws Exception { - hbase_time += (System.nanoTime() - starttime) / 1000000; + fetch_time += DateTime.nanoTime() - fetch_start; try { if (rows == null) { - hbase_time += (System.nanoTime() - starttime) / 1000000; - scanlatency.add(hbase_time); + scanlatency.add((int)DateTime.msFromNano(fetch_time)); LOG.info(TsdbQuery.this + " matched " + nrows + " rows in " + - spans.size() + " spans in " + hbase_time + "ms"); - if (nrows < 1 && !seenAnnotation) { - results.callback(null); - } else { - results.callback(spans); - } - scanner.close(); + spans.size() + " spans in " + DateTime.msFromNano(fetch_time) + "ms"); + close(null); return null; } - if (timeout > 0 && hbase_time > timeout) { + if (timeout > 0 && DateTime.msFromNanoDiff( + DateTime.nanoTime(), scanner_start) > timeout) { throw new InterruptedException("Query timeout exceeded!"); } @@ -649,6 +660,8 @@ public Object call(final ArrayList> rows) continue; } if (!keepers.contains(tsuid)) { + final long uid_start = DateTime.nanoTime(); + /** CB to called after all of the UIDs have been resolved */ class MatchCB implements Callback> { @Override @@ -673,6 +686,8 @@ class GetTagsCB implements @Override public Deferred> call( final Map tags) throws Exception { + uid_resolve_time += (DateTime.nanoTime() - uid_start); + uids_resolved += tags.size(); final List> matches = new ArrayList>(scanner_filters.size()); @@ -709,8 +724,7 @@ public Object call(final ArrayList group) throws Exception { return scan(); } } catch (Exception e) { - scanner.close(); - results.callback(e); + close(e); return null; } } @@ -731,15 +745,60 @@ void processRow(final byte[] key, final ArrayList row) { datapoints = new Span(tsdb); spans.put(key, datapoints); } + final long compaction_start = DateTime.nanoTime(); final KeyValue compacted = tsdb.compact(row, datapoints.getAnnotations()); + compaction_time += (DateTime.nanoTime() - compaction_start); seenAnnotation |= !datapoints.getAnnotations().isEmpty(); if (compacted != null) { // Can be null if we ignored all KVs. datapoints.addRow(compacted); ++nrows; } } - } + + void close(final Exception e) { + scanner.close(); + + if (query_stats != null) { + query_stats.addScannerStat(query_index, index, + QueryStat.SCANNER_TIME, DateTime.nanoTime() - scan_start_time); + + // Scanner Stats + /* Uncomment when AsyncHBase has this feature: + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_FROM_STORAGE, scanner.getRowsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.COLUMNS_FROM_STORAGE, scanner.getColumnsFetched()); + query_stats.addScannerStat(query_index, index, + QueryStat.BYTES_FROM_STORAGE, scanner.getBytesFetched()); */ + query_stats.addScannerStat(query_index, index, + QueryStat.HBASE_TIME, fetch_time); + query_stats.addScannerStat(query_index, index, + QueryStat.SUCCESSFUL_SCAN, e == null ? 1 : 0); + + // Post Scan stats + /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_POST_FILTER, rows_post_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_POST_FILTER, dps_post_filter); */ + query_stats.addScannerStat(query_index, index, + QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); + query_stats.addScannerStat(query_index, index, + QueryStat.UID_PAIRS_RESOLVED, uids_resolved); + query_stats.addScannerStat(query_index, index, + QueryStat.COMPACTION_TIME, compaction_time); + } + + if (e != null) { + results.callback(e); + } else if (nrows < 1 && !seenAnnotation) { + results.callback(null); + } else { + results.callback(spans); + } + } + } new ScannerCB().scan(); return results; @@ -761,7 +820,15 @@ private class GroupByAndAggregateCB implements */ @Override public DataPoints[] call(final TreeMap spans) throws Exception { + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, + (System.nanoTime() - TsdbQuery.this.scan_start_time)); + } + if (spans == null || spans.size() <= 0) { + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } return NO_RESULT; } if (group_bys == null) { @@ -775,6 +842,9 @@ public DataPoints[] call(final TreeMap spans) throws Exception { aggregator, sample_interval_ms, downsampler, query_index, fill_policy); + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } return new SpanGroup[] { group }; } @@ -831,6 +901,9 @@ public DataPoints[] call(final TreeMap spans) throws Exception { //for (final Map.Entry entry : groups) { // LOG.info("group for " + Arrays.toString(entry.getKey()) + ": " + entry.getValue()); //} + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } return groups.values().toArray(new SpanGroup[groups.size()]); } } diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 7f1892f86f..757a9bdca9 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -15,8 +15,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -27,10 +31,12 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import net.opentsdb.core.Const; import net.opentsdb.core.QueryException; import net.opentsdb.core.TSQuery; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; +import net.opentsdb.utils.Pair; /** * This class stores information about OpenTSDB queries executed through the @@ -44,17 +50,21 @@ * The cache will store each query based on the combination of the client, query * and the result code. If the same query was executed multiple times then it * will increment the "executed" counter for the query in the cache. + * + * NOTE: Record everything in nano seconds, then convert to floating millis for + * serialization. * @since 2.2 */ public class QueryStats { private static final Logger LOG = LoggerFactory.getLogger(QueryStats.class); + private static final Logger QUERY_LOG = LoggerFactory.getLogger("QueryLog"); /** Determines how many query stats to keep in the cache */ private static int COMPLETED_QUERY_CACHE_SIZE = 256; /** Whether or not to allow duplicate queries from the same endpoint to * run simultaneously. */ - private static boolean ENABLE_DUPLICATES = false; + private static boolean ENABLE_DUPLICATES = true; /** Stores queries currently executing. If a thread doesn't call into * markComplete then it's possible for this map to fill up. @@ -67,51 +77,164 @@ public class QueryStats { private static Cache completed_queries = CacheBuilder.newBuilder().maximumSize(COMPLETED_QUERY_CACHE_SIZE).build(); - /** Start time for the query. Can be set post construction if necessary */ - private final long query_start; + /** Start time for the query in nano seconds. Can be set post construction + * if necessary */ + private final long query_start_ns; + + /** Start timestamp for the query in millis for printing */ + private final long query_start_ms; + + /** When the query was marked completed in nanoseconds */ + private long query_completed_ts; /** The remote address as :, may be ipv6 */ private final String remote_address; /** The TSQuery object that contains the query specification */ private final TSQuery query; - - /** Amount of time taken for the query to complete, set on {@link markComplete} */ - private long time_total; - /** Time it took to retrieve data from storage in ms*/ - private long time_storage; + /** HTTP response when the query was completed, either successfully or failed */ + private HttpResponseStatus response; - /** Time it took to aggregate over the data */ - private long time_aggregation; + /** Set if the query terminated with an exception */ + private Throwable exception; - /** Time it took to serialize the data. Includes aggregation time and tag - * lookups */ - private long time_serialization; + /** How many times this exact query was executed. Only updated on completion */ + private long executed; - /** Number of data points emitted, NOT the number of data points fetched */ - private long size; + /** The users (if known) who executed this query (could be pulled from a header) */ + private String user; - /** Total number of data points fetched from storage */ - private long aggregated_size; + /** Stats for the entire query */ + private final Map overall_stats; - /** HTTP response when the query was completed, either successfully or failed */ - private HttpResponseStatus response; + /** Hold a list of stats for the sub queries */ + private final Map> query_stats; - /** How many times this exact query was executed. Only updated on completion */ - private long executed; + /** Holds a list of stats for each scanner */ + private final Map>> scanner_stats; - /** A possible exception if thrown when this query completes */ - private Throwable exception; + /** Hold a list of the region servers encountered for each scanner */ + private final Map>> scanner_servers; + + /** Holds a lis tof the scanner IDs for each scanner */ + private final Map> scanner_ids; + + /** Holds a copy of the headers from the request */ + private final Map headers; + + /** Whether or not the data was successfully sent to the client */ + private boolean sent_to_client; + + /** + * A list of statistics surrounding individual queries + */ + public enum QueryStat { + // Query Setup stats + STRING_TO_UID_TIME ("stringToUidTime", true), + + // Storage stats + COLUMNS_FROM_STORAGE ("columnsFromStorage", false), + ROWS_FROM_STORAGE ("rowsFromStorage", false), + BYTES_FROM_STORAGE ("bytesFromStorage", false), + SUCCESSFUL_SCAN ("successfulScan", false), + + // Single Scanner stats + DPS_POST_FILTER ("dpsPostFilter", false), + ROWS_POST_FILTER ("rowsPostFilter", false), + SCANNER_UID_TO_STRING_TIME ("scannerUidToStringTime", true), + COMPACTION_TIME ("compactionTime", true), + HBASE_TIME ("hbaseTime", true), + UID_PAIRS_RESOLVED ("uidPairsResolved", false), + SCANNER_TIME ("scannerTime", true), + + // Overall Salt Scanner stats + SCANNER_MERGE_TIME ("saltScannerMergeTime", true), + + // Post Scan stats + QUERY_SCAN_TIME ("queryScanTime", true), + GROUP_BY_TIME ("groupByTime", true), + + // Serialization time stats + UID_TO_STRING_TIME ("uidToStringTime", true), + AGGREGATED_SIZE ("emittedDPs", false), + NAN_DPS ("nanDPs", false), + AGGREGATION_TIME ("aggregationTime", true), + SERIALIZATION_TIME ("serializationTime", true), + + // Final stats + PROCESSING_PRE_WRITE_TIME ("processingPreWriteTime", true), + TOTAL_TIME ("totalTime", true), + + // MAX and Agg Times + MAX_HBASE_TIME ("maxHBaseTime", true), + AVG_HBASE_TIME ("avgHBaseTime", true), + MAX_SALT_SCANNER_TIME ("maxScannerTime", true), + AVG_SALT_SCANNER_TIME ("avgScannerTime", true), + MAX_UID_TO_STRING ("maxUidToStringTime", true), + AVG_UID_TO_STRING ("avgUidToStringTime", true), + MAX_COMPACTION_TIME ("maxCompactionTime", true), + AVG_COMPACTION_TIME ("avgCompactionTime", 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), + MAX_SCAN_TIME ("maxQueryScanTime", true), + AVG_SCAN_TIME ("avgQueryScanTime", true), + MAX_AGGREGATION_TIME ("maxAggregationTime", true), + AVG_AGGREGATION_TIME ("avgAggregationTime", true), + MAX_SERIALIZATION_TIME ("maxSerializationTime", true), + AVG_SERIALIZATION_TIME ("avgSerializationTime", true) + ; + + /** The serializable name for this enum */ + private final String stat_name; + /** Whether or not the stat is time based */ + private final boolean is_time; + + private QueryStat(final String stat_name, final boolean is_time) { + this.stat_name = stat_name; + this.is_time = is_time; + } + + @Override + public String toString() { + return stat_name; + } + } + + // always AVG, MAX in the pair order + static final Map> AGG_MAP = + new HashMap>(); + static { + AGG_MAP.put(QueryStat.HBASE_TIME, new Pair( + QueryStat.AVG_HBASE_TIME, QueryStat.MAX_HBASE_TIME)); + AGG_MAP.put(QueryStat.SCANNER_TIME, new Pair( + QueryStat.AVG_SALT_SCANNER_TIME, QueryStat.MAX_HBASE_TIME)); + AGG_MAP.put(QueryStat.UID_TO_STRING_TIME, new Pair( + QueryStat.MAX_UID_TO_STRING, QueryStat.MAX_UID_TO_STRING)); + AGG_MAP.put(QueryStat.SCANNER_UID_TO_STRING_TIME, new Pair( + QueryStat.MAX_SCANNER_UID_TO_STRING_TIME, + QueryStat.AVG_SCANNER_UID_TO_STRING_TIME)); + AGG_MAP.put(QueryStat.QUERY_SCAN_TIME, new Pair( + QueryStat.MAX_SCAN_TIME, QueryStat.AVG_SCAN_TIME)); + AGG_MAP.put(QueryStat.AGGREGATION_TIME, new Pair( + QueryStat.MAX_AGGREGATION_TIME, QueryStat.AVG_AGGREGATION_TIME)); + AGG_MAP.put(QueryStat.SERIALIZATION_TIME, new Pair( + QueryStat.MAX_SERIALIZATION_TIME, + QueryStat.AVG_SERIALIZATION_TIME)); + } /** * Default CTor * @param remote_address Remote address of the client * @param query Query being executed + * @param headers The HTTP headers passed with the query * @throws QueryException if the exact query is already running, e.g if the * client submitted the same query twice */ - public QueryStats(final String remote_address, final TSQuery query) { + public QueryStats(final String remote_address, final TSQuery query, + final Map headers) { if (remote_address == null || remote_address.isEmpty()) { throw new IllegalArgumentException("Remote address was null or empty"); } @@ -120,8 +243,16 @@ public QueryStats(final String remote_address, final TSQuery query) { } this.remote_address = remote_address; this.query = query; + this.headers = headers; // can be null executed = 1; - query_start = DateTime.currentTimeMillis(); + query_start_ns = DateTime.nanoTime(); + query_start_ms = DateTime.currentTimeMillis(); + overall_stats = new HashMap(); + query_stats = new ConcurrentHashMap>(1); + scanner_stats = new ConcurrentHashMap>>(1); + scanner_servers = new ConcurrentHashMap>>(1); + scanner_ids = new ConcurrentHashMap>(1); if (LOG.isDebugEnabled()) { LOG.debug("New query for remote " + remote_address + " with hash " + hashCode() + " on thread " + Thread.currentThread().getId()); @@ -134,22 +265,27 @@ public QueryStats(final String remote_address, final TSQuery query) { throw new QueryException("Query is already executing for endpoint: " + remote_address); } - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Successfully put new query for remote " + remote_address + - " with hash " + hashCode() + " on thread " + - Thread.currentThread().getId() + " w q " + query.toString()); - } } + if (LOG.isDebugEnabled()) { + LOG.debug("Successfully put new query for remote " + remote_address + + " with hash " + hashCode() + " on thread " + + Thread.currentThread().getId() + " w q " + query.toString()); + } + LOG.info("Executing new query=" + JSON.serializeToString(this)); } /** * Returns the hash based on the remote address and the query */ + @Override public int hashCode() { - return Objects.hashCode(remote_address, query.hashCode()); + return remote_address.hashCode() ^ query.hashCode(); } + /** + * Equals is based solely on the endpoint and the original query + */ + @Override public boolean equals(final Object obj) { if (obj == null) { return false; @@ -167,53 +303,61 @@ public boolean equals(final Object obj) { @Override public String toString() { - final StringBuilder buf = new StringBuilder(256); - buf.append("remote=") - .append(remote_address) - .append(", query=") - .append(query) - .append(", start=") - .append(query_start) - .append(", exception=") - .append(exception == null ? "null" : exception.getMessage()); - return buf.toString(); + // have to hack it to get the details. By default we dump just the highest + // level of stats. + final Map details = new HashMap(); + details.put("queryStartTimestamp", getQueryStartTimestamp()); + details.put("queryCompletedTimestamp", getQueryCompletedTimestamp()); + details.put("exception", getException()); + details.put("httpResponse", getHttpResponse()); + details.put("numRunningQueries", getNumRunningQueries()); + details.put("query", getQuery()); + details.put("user", getUser()); + details.put("requestHeaders", getRequestHeaders()); + details.put("executed", getExecuted()); + details.put("stats", getStats(true, true)); + return JSON.serializeToString(details); } /** - * Marks a query as completed successfully with the 200 HTTP response code. + * Marks a query as completed successfully with the 200 HTTP response code + * without an exception. * Moves it from the running map to the cache, updating the cache if it already * existed. */ - public void markComplete() { - markComplete(HttpResponseStatus.OK, null); + public void markSerializationSuccessful() { + markSerialized(HttpResponseStatus.OK, null); } /** - * Marks a query as completed with the given HTTP code and moves it from the - * running map to the cache, updating the cache if it already existed. - * @param response The HttpStatus code to store - * @param exception An optional exception + * Marks a query as completed with the given HTTP code with exception and + * moves it from the running map to the cache, updating the cache if it + * already existed. + * @param response The HTTP response to log + * @param exception The exception thrown */ - public void markComplete(final HttpResponseStatus response, + public void markSerialized(final HttpResponseStatus response, final Throwable exception) { this.exception = exception; - LOG.debug("Marking query as complete for " + remote_address + " with hash " + - hashCode() + " on thread " + Thread.currentThread().getId() + " And q: " + - query.toString()); this.response = response; - time_total = DateTime.currentTimeMillis() - query_start; + + query_completed_ts = DateTime.currentTimeMillis(); + overall_stats.put(QueryStat.PROCESSING_PRE_WRITE_TIME, DateTime.nanoTime() - query_start_ns); synchronized (running_queries) { if (!running_queries.containsKey(this.hashCode())) { if (!ENABLE_DUPLICATES) { LOG.warn("Query was already marked as complete: " + this); } - return; } - running_queries.remove(this.hashCode()); - LOG.debug("Removed completed query " + remote_address + " with hash " + - hashCode() + " on thread " + Thread.currentThread().getId()); + running_queries.remove(hashCode()); + if (LOG.isDebugEnabled()) { + LOG.debug("Removed completed query " + remote_address + " with hash " + + hashCode() + " on thread " + Thread.currentThread().getId()); + } } + aggQueryStats(); + final int cache_hash = this.hashCode() ^ response.toString().hashCode(); synchronized (completed_queries) { final QueryStats old_query = completed_queries.getIfPresent(cache_hash); @@ -223,7 +367,25 @@ public void markComplete(final HttpResponseStatus response, old_query.executed++; } } - LOG.info("completed_query=" + JSON.serializeToString(this)); + } + + /** + * Marks the query as complete and logs it to the proper logs. This is called + * after the data has been sent to the client. + */ + public void markSent() { + sent_to_client = true; + overall_stats.put(QueryStat.TOTAL_TIME, DateTime.nanoTime() - query_start_ns); + LOG.info("Completing query=" + JSON.serializeToString(this)); + QUERY_LOG.info(this.toString()); + } + + /** Leaves the sent_to_client field as false when we were unable to write to + * the client end point. */ + public void markSendFailed() { + overall_stats.put(QueryStat.TOTAL_TIME, DateTime.nanoTime() - query_start_ns); + LOG.info("Completing query=" + JSON.serializeToString(this)); + QUERY_LOG.info(this.toString()); } /** @@ -231,15 +393,13 @@ public void markComplete(final HttpResponseStatus response, * returned to a caller. * @return A map for serialization */ - public static Map>> buildStats() { - Map>> root = - new HashMap>>(); + public static Map getRunningAndCompleteStats() { + Map root = new TreeMap(); if (running_queries.isEmpty()) { - root.put("running", Collections.> emptyList()); + root.put("running", Collections.emptyList()); } else { - final List> running = - new ArrayList>(running_queries.size()); + final List running = new ArrayList(running_queries.size()); root.put("running", running); // don't need to lock the map beyond what the iterator will do implicitly @@ -247,38 +407,20 @@ public static Map>> buildStats() { final Map obj = new HashMap(10); obj.put("query", stats.query); obj.put("remote", stats.remote_address); - obj.put("queryStart", stats.query_start); - obj.put("timeTotal", stats.time_total); - obj.put("elapsed", DateTime.currentTimeMillis() - stats.query_start); + obj.put("user", stats.user); + obj.put("headers", stats.headers);; + obj.put("queryStart", DateTime.msFromNano(stats.query_start_ns)); + obj.put("elapsed", DateTime.msFromNanoDiff(DateTime.nanoTime(), + stats.query_start_ns)); running.add(obj); } } final Map completed = completed_queries.asMap(); if (completed.isEmpty()) { - root.put("completed", Collections.> emptyList()); + root.put("completed", Collections.emptyList()); } else { - final List> running = - new ArrayList>(completed.size()); - root.put("completed", running); - - // don't need to lock the map beyond what the iterator will do implicitly - for (final QueryStats stats : completed.values()) { - final Map obj = new HashMap(10); - obj.put("query", stats.query); - obj.put("remote", stats.remote_address); - obj.put("queryStart", stats.query_start); - obj.put("timeTotal", stats.time_total); - obj.put("executed", stats.executed); - obj.put("datapoints", stats.size); - obj.put("rawDatapoints", stats.aggregated_size); - obj.put("status", stats.response.getCode()); - obj.put("timeStorage", stats.time_storage); - obj.put("timeAggregation", stats.time_aggregation); - obj.put("timeSerialization", stats.time_serialization); - obj.put("exception", stats.exception); - running.add(obj); - } + root.put("completed", completed.values()); } return root; @@ -290,92 +432,461 @@ public static Map>> buildStats() { */ public static void collectStats(final StatsCollector collector) { collector.record("query.count", running_queries.size(), "type=running"); + } + + /** + * Add an overall statistic for the query (i.e. not associated with a sub + * query or scanner) + * @param name The name of the stat + * @param value The value to store + */ + public void addStat(final QueryStat name, final long value) { + overall_stats.put(name, value); + } + + /** + * Adds a stat for a sub query, replacing it if it exists. Times must be + * in nanoseconds. + * @param query_index The index of the sub query to update + * @param name The name of the stat to update + * @param value The value to set + */ + public void addStat(final int query_index, final QueryStat name, + final long value) { + Map qs = query_stats.get(query_index); + if (qs == null) { + qs = new HashMap(); + query_stats.put(query_index, qs); + } + qs.put(name, value); + } + + /** + * Aggregates the various stats from the lower to upper levels. This includes + * calculating max and average time values for stats marked as time based. + */ + public void aggQueryStats() { + // These are overall aggregations + final Map> overall_cumulations = + new HashMap>(); - final Map completed = completed_queries.asMap(); - int completed_success = 0; - int completed_error = 0; - for (final QueryStats stats : completed.values()) { - if (stats.response == HttpResponseStatus.OK) { - completed_success += stats.executed; - } else { - completed_error += stats.executed; + // scanner aggs + for (final Entry>> entry : + scanner_stats.entrySet()) { + final int query_index = entry.getKey(); + + final Map> cumulations = + new HashMap>(); + + for (final Entry> scanner : + entry.getValue().entrySet()) { + + for (final Entry stat : scanner.getValue().entrySet()) { + if (stat.getKey().is_time) { + if (!AGG_MAP.containsKey(stat.getKey())) { + // we're not aggregating this value + continue; + } + + // per query aggs + Pair pair = cumulations.get(stat.getKey()); + if (pair == null) { + pair = new Pair(0L, Long.MIN_VALUE); + cumulations.put(stat.getKey(), pair); + } + pair.setKey(pair.getKey() + stat.getValue()); + if (stat.getValue() > pair.getValue()) { + pair.setValue(stat.getValue()); + } + + // overall aggs required here for proper time averaging + pair = overall_cumulations.get(stat.getKey()); + if (pair == null) { + pair = new Pair(0L, Long.MIN_VALUE); + overall_cumulations.put(stat.getKey(), pair); + } + pair.setKey(pair.getKey() + stat.getValue()); + if (stat.getValue() > pair.getValue()) { + pair.setValue(stat.getValue()); + } + } else { + // only add counters for the per query maps as they'll be rolled + // up below into the overall + updateStat(query_index, stat.getKey(), stat.getValue()); + } + } + } + + // per query aggs + for (final Entry> cumulation : + cumulations.entrySet()) { + // names can't be null as we validate above that it exists + final Pair names = AGG_MAP.get(cumulation.getKey()); + addStat(query_index, names.getKey(), + (cumulation.getValue().getKey() / entry.getValue().size())); + addStat(query_index, names.getValue(), cumulation.getValue().getValue()); + } + } + + // handle the per scanner aggs + for (final Entry> cumulation : + overall_cumulations.entrySet()) { + // names can't be null as we validate above that it exists + final Pair names = AGG_MAP.get(cumulation.getKey()); + addStat(names.getKey(), + (cumulation.getValue().getKey() / + (scanner_stats.size() * Const.SALT_BUCKETS()))); + addStat(names.getValue(), cumulation.getValue().getValue()); + } + overall_cumulations.clear(); + + // aggregate counters from the sub queries + for (final Map sub_query : query_stats.values()) { + for (final Entry stat : sub_query.entrySet()) { + if (stat.getKey().is_time) { + if (!AGG_MAP.containsKey(stat.getKey())) { + // we're not aggregating this value + continue; + } + Pair pair = overall_cumulations.get(stat.getKey()); + if (pair == null) { + pair = new Pair(0L, Long.MIN_VALUE); + overall_cumulations.put(stat.getKey(), pair); + } + pair.setKey(pair.getKey() + stat.getValue()); + if (stat.getValue() > pair.getValue()) { + pair.setValue(stat.getValue()); + } + } else if (overall_stats.containsKey(stat.getKey())) { + overall_stats.put(stat.getKey(), + overall_stats.get(stat.getKey()) + stat.getValue()); + } else { + overall_stats.put(stat.getKey(), stat.getValue()); + } } } - collector.record("query.count", completed_success, "type=successful"); - collector.record("query.count", completed_error, "type=failed"); + for (final Entry> cumulation : + overall_cumulations.entrySet()) { + // names can't be null as we validate above that it exists + final Pair names = AGG_MAP.get(cumulation.getKey()); + overall_stats.put(names.getKey(), + (cumulation.getValue().getKey() / query_stats.size())); + overall_stats.put(names.getValue(), cumulation.getValue().getValue()); + } } - /** @return the start time of the query in ms */ - public long getQueryStart() { - return query_start; + /** + * Increments the cumulative value for a cumulative stat. If it's a time then + * it must be in nanoseconds + * @param query_index The index of the sub query + * @param name The name of the stat + * @param value The value to add to the existing value + */ + public void updateStat(final int query_index, final QueryStat name, + final long value) { + Map qs = query_stats.get(query_index); + long cum_time = value; + if (qs == null) { + qs = new HashMap(); + query_stats.put(query_index, qs); + } + + if (qs.containsKey(name)) { + cum_time += qs.get(name); + } + qs.put(name, cum_time); + } + + /** + * Adds a value for a specific scanner for a specific sub query. If it's a time + * then it must be in nanoseconds. + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param name The name of the stat + * @param value The value to add to the map + */ + public void addScannerStat(final int query_index, final int id, + final QueryStat name, final long value) { + Map> qs = scanner_stats.get(query_index); + if (qs == null) { + qs = new ConcurrentHashMap>(Const.SALT_BUCKETS()); + scanner_stats.put(query_index, qs); + } + Map scanner_stat_map = qs.get(id); + if (scanner_stat_map == null) { + scanner_stat_map = new HashMap(); + qs.put(id, scanner_stat_map); + } + scanner_stat_map.put(name, value); } - /** @return the total number of data points emitted for the query */ - public long getSize() { - return size; + /** + * Adds or overwrites the list of servers scanned by a scanner + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param servers The list of servers encountered + */ + public void addScannerServers(final int query_index, final int id, + final Set servers) { + Map> query_servers = scanner_servers.get(query_index); + if (query_servers == null) { + query_servers = new ConcurrentHashMap>( + Const.SALT_BUCKETS()); + scanner_servers.put(query_index, query_servers); + } + query_servers.put(id, servers); } - /** @param size increments the number of data points emitted */ - public void addSize(int size) { - this.size += size; + /** + * Updates or adds a stat for a specific scanner. IF it's a time it must + * be in nanoseconds + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param name The name of the stat + * @param value The value to update to the map + */ + public void updateScannerStat(final int query_index, final int id, + final QueryStat name, final long value) { + Map> qs = scanner_stats.get(query_index); + long cum_time = value; + if (qs == null) { + qs = new ConcurrentHashMap>(); + scanner_stats.put(query_index, qs); + } + Map scanner_stat_map = qs.get(id); + if (scanner_stat_map == null) { + scanner_stat_map = new HashMap(); + qs.put(id, scanner_stat_map); + } + + if (scanner_stat_map.containsKey(name)) { + cum_time += scanner_stat_map.get(name); + } + + scanner_stat_map.put(name, cum_time); + } + + /** + * Adds a scanner for a sub query to the stats along with the description of + * the scanner. + * @param query_index The index of the sub query + * @param id The numeric ID of the scanner + * @param string_id The description of the scanner + */ + public void addScannerId(final int query_index, final int id, + final String string_id) { + Map scanners = scanner_ids.get(query_index); + if (scanners == null) { + scanners = new ConcurrentHashMap(); + scanner_ids.put(query_index, scanners); + } + scanners.put(id, string_id); } - /** @return the total number of data points retrieved from storage */ - public long getAggregatedSize() { - return aggregated_size; + /** @return the start time of the query in nano seconds */ + public long queryStart() { + return query_start_ns; + } + + /** @param user The user who executed the query */ + public void setUser(final String user) { + this.user = user; } - /** @param size increments the number of data points retrieved from storage */ - public void addAggregatedSize(int size) { - aggregated_size += size; + /** @return The user who executed the query if known */ + public String getUser() { + return user; } - /** @param time_storage the amount of time it took to fetch data from storage in ms */ - public void setTimeStorage(final long time_storage) { - this.time_storage = time_storage; + /** @return The multi-mapped set of request headers associated with the query */ + public Map getRequestHeaders() { + return headers; } - /** @return the amount of time it took to fetch data from storage in ms */ - public long getTimeStorage() { - return time_storage; + /** @return The number of currently running queries */ + public int getNumRunningQueries() { + return running_queries.size(); } - /** @param time_aggregation increments the amount of time spent aggregating in ms */ - public void addTimeAggregation(final long time_aggregation) { - this.time_aggregation += time_aggregation; + /** @return An exception associated with the query or null if the query + * returned successfully. */ + public String getException() { + if (exception == null) { + return "null"; + } + return exception.getMessage() + + (exception.getStackTrace() != null && exception.getStackTrace().length > 0 + ? "\n" + exception.getStackTrace()[0].toString() : ""); } - /** @return the mount of time spent aggregating in ms */ - public long getTimeAggregation() { - return time_aggregation; + /** @return The HTTP status response for the query */ + public HttpResponseStatus getHttpResponse() { + return response; } - /** @param time_serialization the amount of time spent serializing in ms */ - public void setTimeSerialization(final long time_serialization) { - this.time_serialization = time_serialization; + /** @return The number of times this query has been executed from the same + * endpoint. */ + public long getExecuted() { + return executed; } - /** @return the mount of time spent serializing in ms */ - public long getTimeSerialization() { - return time_serialization; + /** @return The full query */ + public TSQuery getQuery() { + return query; } - /** @return the amount of time working on the query in ms */ - public long getTimeTotal() { - return time_total; + /** @return When the query was received and started executing, in ms */ + public long getQueryStartTimestamp() { + return query_start_ms; } - - /** @return the Http status code */ - public HttpResponseStatus getStatus() { - return response; + + /** @return When the query was marked as completed, in ms */ + public long getQueryCompletedTimestamp() { + return query_completed_ts; + } + + /** @return Whether or not the data was successfully sent to the client */ + public boolean getSentToClient() { + return sent_to_client; + } + + /** @return A map with the subset of query measurements, not including scanners + * or sub queries */ + public Map getStats() { + return getStats(false, false); } - /** @return an exception if it was associated with this query */ - public Throwable getException() { - return exception; + /** + * Returns measurements of the given query + * @param with_scanners Whether or not to dump individual scanner stats + * @return A map with stats for the query + */ + public Map getStats(final boolean with_sub_queries, + final boolean with_scanners) { + final Map map = new TreeMap(); + + for (final Entry entry : overall_stats.entrySet()) { + if (entry.getKey().is_time) { + map.put(entry.getKey().toString(), DateTime.msFromNano(entry.getValue())); + } else { + map.put(entry.getKey().toString(), entry.getValue()); + } + } + + if (with_sub_queries) { + final Iterator>> it = + query_stats.entrySet().iterator(); + while (it.hasNext()) { + final Entry> entry = it.next(); + final Map qs = new HashMap(1); + qs.put(String.format("queryIdx_%02d", entry.getKey()), + getQueryStats(entry.getKey(), with_scanners)); + map.putAll(qs); + } + } + return map; } + /** + * Returns a map of stats for a single sub query + * @param index The sub query to fetch + * @param with_scanners Whether or not to print detailed stats for each scanner + * @return A map with stats to print or null if the sub query didn't have any + * data. + */ + public Map getQueryStats(final int index, + final boolean with_scanners) { + + final Map qs = query_stats.get(index); + if (qs == null) { + return null; + } + + final Map query_map = new TreeMap(); + query_map.put("queryIndex", index); + + final Iterator> stats_it = + qs.entrySet().iterator(); + while (stats_it.hasNext()) { + final Entry stat = stats_it.next(); + if (stat.getKey().is_time) { + query_map.put(stat.getKey().toString(), + DateTime.msFromNano(stat.getValue())); + } else { + query_map.put(stat.getKey().toString(), stat.getValue()); + } + } + + if (with_scanners) { + final Map> scanner_stats_map = + scanner_stats.get(index); + + final Map scanner_maps = new TreeMap(); + + query_map.put("scannerStats", scanner_maps); + if (scanner_stats_map != null) { + final Map scanners = scanner_ids.get(index); + final Iterator>> scanner_it = + scanner_stats_map.entrySet().iterator(); + while (scanner_it.hasNext()) { + final Entry> scanner = scanner_it.next(); + final Map scanner_map = new TreeMap(); + scanner_maps.put(String.format("scannerIdx_%02d", scanner.getKey()), + scanner_map); + final String id; + if (scanners != null) { + id = scanners.get(scanner.getKey()); + } else { + id = null; + } + scanner_map.put("scannerId", id); + /* Uncomment when AsyncHBase supports this + final Map> servers = scanner_servers.get(index); + if (servers != null) { + scanner_map.put("regionServers", servers.get(scanner.getKey())); + } else { + scanner_map.put("regionServers", null); + } + */ + final Iterator> scanner_stats_it = + scanner.getValue().entrySet().iterator(); + while (scanner_stats_it.hasNext()) { + final Entry scanner_stats = scanner_stats_it.next(); + if (!scanner_stats.getKey().is_time) { + scanner_map.put(scanner_stats.getKey().toString(), + scanner_stats.getValue()); + } else { + scanner_map.put(scanner_stats.getKey().toString(), + DateTime.msFromNano(scanner_stats.getValue())); + } + } + } + } + } + return query_map; + } + + /** @return A stat for the overall query or -1 if the stat didn't exist. */ + public long getStat(final QueryStat stat) { + if (!overall_stats.containsKey(stat)) { + return -1; + } + return overall_stats.get(stat); + } + + /** @return a timed stat for the overall query or NaN if the stat didn't exist */ + public double getTimeStat(final QueryStat stat) { + if (!stat.is_time) { + throw new IllegalArgumentException("The stat is not a time stat"); + } + if (!overall_stats.containsKey(stat)) { + return Double.NaN; + } + return DateTime.msFromNano(overall_stats.get(stat)); + } + /** @param whether or not to allow duplicate queries to run */ public static void setEnableDuplicates(final boolean enable_dupes) { ENABLE_DUPLICATES = enable_dupes; diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index ba6b736124..967eaab09f 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -14,8 +14,10 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import com.google.common.base.Objects; import com.stumbleupon.async.Deferred; @@ -35,6 +37,7 @@ import org.slf4j.LoggerFactory; import net.opentsdb.core.TSDB; +import net.opentsdb.stats.QueryStats; /** * Abstract base class for HTTP queries. @@ -70,6 +73,9 @@ public abstract class AbstractHttpQuery { /** The {@code TSDB} instance we belong to */ protected final TSDB tsdb; + /** Used for recording query statistics */ + protected QueryStats stats; + /** * Set up required internal state. For subclasses. * @@ -112,6 +118,58 @@ public String getRemoteAddress() { return chan.getRemoteAddress().toString(); } + /** + * Copies the header list and obfuscates the "cookie" header in case it + * contains auth tokens, etc. Note that it flattens duplicate headers keys + * as comma separated lists per the RFC + * @return The full set of headers for this query with the cookie obfuscated + */ + public Map getPrintableHeaders() { + final Map headers = new HashMap( + request.getHeaders().size()); + for (final Entry header : request.getHeaders()) { + if (header.getKey().toLowerCase().equals("cookie")) { + // null out the cookies + headers.put(header.getKey(), "*******"); + } else { + // http://tools.ietf.org/html/rfc2616#section-4.2 + if (headers.containsKey(header.getKey())) { + headers.put(header.getKey(), + headers.get(header.getKey()) + "," + header.getValue()); + } else { + headers.put(header.getKey(), header.getValue()); + } + } + } + return headers; + } + + /** + * Copies the header list so modifications won't affect the original set. + * Note that it flattens duplicate headers keys as comma separated lists + * per the RFC + * @return The full set of headers for this query + */ + public Map getHeaders() { + final Map headers = new HashMap( + request.getHeaders().size()); + for (final Entry header : request.getHeaders()) { + // http://tools.ietf.org/html/rfc2616#section-4.2 + if (headers.containsKey(header.getKey())) { + headers.put(header.getKey(), + headers.get(header.getKey()) + "," + header.getValue()); + } else { + headers.put(header.getKey(), header.getValue()); + } + } + return headers; + } + + /** @param stats The stats object to mark after writing is complete */ + public void setStats(final QueryStats stats) { + this.stats = stats; + } + /** Return the time in nanoseconds that this query object was * created. */ @@ -192,7 +250,6 @@ public boolean hasQueryStringParam(final String paramname) { public List getQueryStringParams(final String paramname) { return getQueryString().get(paramname); } - /** * Returns only the path component of the URI as a string @@ -339,6 +396,9 @@ public void sendStatusOnly(final HttpResponseStatus status) { HttpHeaders.setContentLength(response, 0); } final ChannelFuture future = chan.write(response); + if (stats != null) { + future.addListener(new SendSuccess()); + } if (!keepalive) { future.addListener(ChannelFutureListener.CLOSE); } @@ -369,12 +429,23 @@ public void sendBuffer(final HttpResponseStatus status, HttpHeaders.setContentLength(response, buf.readableBytes()); } final ChannelFuture future = chan.write(response); + if (stats != null) { + future.addListener(new SendSuccess()); + } if (!keepalive) { future.addListener(ChannelFutureListener.CLOSE); } done(); } + /** A simple class that marks a query as complete when the stats are set */ + private class SendSuccess implements ChannelFutureListener { + @Override + public void operationComplete(final ChannelFuture future) throws Exception { + stats.markSent(); + } + } + /** @return Information about the query */ public String toString() { return Objects.toStringHelper(this) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 57fcf1b567..e57acf208a 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -45,6 +45,7 @@ import net.opentsdb.meta.UIDMeta; import net.opentsdb.search.SearchQuery; import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.tree.Branch; import net.opentsdb.tree.Tree; import net.opentsdb.tree.TreeRule; @@ -604,6 +605,8 @@ class DPsResolver implements Callback, Object> { new ArrayList>(); /** The data points to serialize */ final DataPoints dps; + /** Starting time in nanos when we sent the UID resolution queries off */ + long uid_start; public DPsResolver(final DataPoints dps) { this.dps = dps; @@ -653,6 +656,9 @@ public WriteToBuffer(final DataPoints dps) { * variables. */ public Object call(final ArrayList deferreds) throws Exception { + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.UID_TO_STRING_TIME, (DateTime.nanoTime() - uid_start)); + final long local_serialization_start = DateTime.nanoTime(); final TSSubQuery orig_query = data_query.getQueries() .get(dps.getQueryIndex()); @@ -715,8 +721,9 @@ public Object call(final ArrayList deferreds) throws Exception { // now the fun stuff, dump the data and time just the iteration over // the data points - final long dps_start = DateTime.currentTimeMillis(); + final long dps_start = DateTime.nanoTime(); json.writeFieldName("dps"); + long counter = 0; // default is to write a map, otherwise write arrays if (!timeout_flag.get(0) && as_arrays) { @@ -743,6 +750,7 @@ public Object call(final ArrayList deferreds) throws Exception { } } json.writeEndArray(); + ++counter; } json.writeEndArray(); } else if (!timeout_flag.get(0)) { @@ -766,28 +774,39 @@ public Object call(final ArrayList deferreds) throws Exception { json.writeNumberField(Long.toString(timestamp), dp.doubleValue()); } } + ++counter; } json.writeEndObject(); + } else { // skipping data points all together due to timeout json.writeStartObject(); json.writeEndObject(); } - - final long agg_time = DateTime.currentTimeMillis() - dps_start; - data_query.getQueryStats().addTimeAggregation(agg_time); - data_query.getQueryStats().addAggregatedSize(dps.aggregatedSize()); - data_query.getQueryStats().addSize(dps.size()); + final long agg_time = DateTime.nanoTime() - dps_start; + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.AGGREGATION_TIME, agg_time); + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.AGGREGATED_SIZE, counter); + + // yeah, it's a little early but we need to dump it out with the results. + data_query.getQueryStats().addStat(dps.getQueryIndex(), + QueryStat.SERIALIZATION_TIME, + DateTime.nanoTime() - local_serialization_start); if (!timeout_flag.get(0) && data_query.getShowStats()) { - json.writeFieldName("stats"); - json.writeStartObject(); - json.writeNumberField("datapoints", dps.size()); - json.writeNumberField("rawDatapoints", dps.aggregatedSize()); - json.writeNumberField("aggregationTime", agg_time); - json.writeNumberField("timeSeries", dps.getTSUIDs().size()); - // todo - timing for just this query - json.writeEndObject(); + int query_index = (dps == null) ? -1 : dps.getQueryIndex(); + QueryStats stats = data_query.getQueryStats(); + + if (query_index >= 0) { + json.writeFieldName("stats"); + final Map s = stats.getQueryStats(query_index, false); + if (s != null) { + json.writeObject(s); + } else { + json.writeStringField("ERROR", "NO STATS FOUND"); + } + } } // close the results for this particular query @@ -801,6 +820,8 @@ public Object call(final ArrayList deferreds) throws Exception { * then prints to the output buffer once they are completed. */ public Deferred call(final Object obj) throws Exception { + this.uid_start = DateTime.nanoTime(); + resolve_deferreds.add(dps.metricNameAsync() .addCallback(new MetricResolver())); resolve_deferreds.add(dps.getTagsAsync() @@ -832,23 +853,19 @@ public Deferred call(final Object obj) throws Exception { class FinalCB implements Callback { public ChannelBuffer call(final Object obj) throws Exception { - data_query.getQueryStats().setTimeSerialization( - DateTime.currentTimeMillis() - start); - data_query.getQueryStats().markComplete(); + + // Call this here so we rollup sub metrics into a summary. It's not + // completely accurate, of course, because we still have to write the + // summary and close the writer. But it's close. + data_query.getQueryStats().markSerializationSuccessful(); // dump overall stats as an extra object in the array + // TODO - yeah, I've heard this sucks, we need to figure out a better way. if (data_query.getShowSummary()) { final QueryStats stats = data_query.getQueryStats(); json.writeStartObject(); json.writeFieldName("statsSummary"); - json.writeStartObject(); - json.writeNumberField("datapoints", stats.getSize()); - json.writeNumberField("rawDatapoints", stats.getAggregatedSize()); - json.writeNumberField("aggregationTime", stats.getTimeAggregation()); - json.writeNumberField("serializationTime", stats.getTimeSerialization()); - json.writeNumberField("storageTime", stats.getTimeStorage()); - json.writeNumberField("timeTotal", stats.getTimeTotal()); - json.writeEndObject(); + json.writeObject(stats.getStats(true, true)); json.writeEndObject(); } @@ -1060,8 +1077,7 @@ public ChannelBuffer formatJVMStatsV1(final Map> sta * @throws BadRequestException if the plugin has not implemented this method * @since 2.2 */ - public ChannelBuffer formatQueryStatsV1( - final Map>> query_stats) { + public ChannelBuffer formatQueryStatsV1(final Map query_stats) { return serializeJSON(query_stats); } diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 8f16ea9590..34adc3e01d 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -720,8 +720,7 @@ public ChannelBuffer formatJVMStatsV1(final Map> map * @throws BadRequestException if the plugin has not implemented this method * @since 2.2 */ - public ChannelBuffer formatQueryStatsV1( - final Map>> query_stats) { + public ChannelBuffer formatQueryStatsV1(final Map query_stats) { throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "The requested API endpoint has not been implemented", this.getClass().getCanonicalName() + diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index bef1a64cf7..a9e87566c2 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -19,7 +19,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import org.hbase.async.HBaseException; +import org.hbase.async.RpcTimedOutException; import org.hbase.async.Bytes.ByteMap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.handler.codec.http.HttpMethod; @@ -46,6 +49,7 @@ import net.opentsdb.query.expression.Expressions; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.StatsCollector; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; @@ -65,6 +69,11 @@ final class QueryRpc implements HttpRpc { private static final Logger LOG = LoggerFactory.getLogger(QueryRpc.class); + /** Various counters and metrics for reporting query stats */ + static final AtomicLong query_invalid = new AtomicLong(); + static final AtomicLong query_exceptions = new AtomicLong(); + static final AtomicLong query_success = new AtomicLong(); + /** * Implements the /api/query endpoint to fetch data from OpenTSDB. * @param tsdb The TSDB to use for fetching data @@ -121,7 +130,8 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, case 1: data_query = query.serializer().parseQueryV1(); break; - default: + default: + query_invalid.incrementAndGet(); throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); @@ -149,8 +159,10 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, // if the user tried this query multiple times from the same IP and src port // they'll be rejected on subsequent calls final QueryStats query_stats = - new QueryStats(query.getRemoteAddress(), data_query); + new QueryStats(query.getRemoteAddress(), data_query, + query.getPrintableHeaders()); data_query.setQueryStats(query_stats); + query.setStats(query_stats); final int nqueries = data_query.getQueries().size(); final ArrayList results = new ArrayList(nqueries); @@ -159,43 +171,56 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, /** This has to be attached to callbacks or we may never respond to clients */ class ErrorCB implements Callback { public Object call(final Exception e) throws Exception { + Throwable ex = e; try { - if (e instanceof DeferredGroupException) { - Throwable ex = e.getCause(); + LOG.error("Query exception: ", e); + if (ex instanceof DeferredGroupException) { + ex = e.getCause(); while (ex != null && ex instanceof DeferredGroupException) { ex = ex.getCause(); } - if (ex != null) { - if (ex instanceof NoSuchUniqueName) { - query_stats.markComplete(HttpResponseStatus.BAD_REQUEST, ex); - query.badRequest(new BadRequestException( - HttpResponseStatus.NOT_FOUND, ex.getMessage())); - return null; - } - LOG.error("Query failed", ex); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); - query.badRequest(new BadRequestException(ex)); - } else { - LOG.error("Unable to find the cause of the DGE", e); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); - query.badRequest(new BadRequestException(e)); + if (ex == null) { + LOG.error("The deferred group exception didn't have a cause???"); } - } else if (e.getClass() == QueryException.class) { - query_stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); - query.badRequest(new BadRequestException((QueryException)e)); + } + + if (ex instanceof RpcTimedOutException) { + query_stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, ex); + query.badRequest(new BadRequestException( + HttpResponseStatus.REQUEST_TIMEOUT, ex.getMessage())); + query_exceptions.incrementAndGet(); + } else if (ex instanceof HBaseException) { + query_stats.markSerialized(HttpResponseStatus.FAILED_DEPENDENCY, ex); + query.badRequest(new BadRequestException( + HttpResponseStatus.FAILED_DEPENDENCY, ex.getMessage())); + query_exceptions.incrementAndGet(); + } else if (ex instanceof QueryException) { + query_stats.markSerialized(((QueryException)ex).getStatus(), ex); + query.badRequest(new BadRequestException( + ((QueryException)ex).getStatus(), ex.getMessage())); + query_exceptions.incrementAndGet(); + } else if (ex instanceof BadRequestException) { + query_stats.markSerialized(((BadRequestException)ex).getStatus(), ex); + query.badRequest((BadRequestException)ex); + query_invalid.incrementAndGet(); + } else if (ex instanceof NoSuchUniqueName) { + query_stats.markSerialized(HttpResponseStatus.BAD_REQUEST, ex); + query.badRequest(new BadRequestException(ex)); + query_invalid.incrementAndGet(); } else { - LOG.error("Query failed", e); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); - query.badRequest(new BadRequestException(e)); + query_stats.markSerialized(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); + query.badRequest(new BadRequestException(ex)); + query_exceptions.incrementAndGet(); } - return null; - } catch (RuntimeException ex) { - LOG.error("Exception thrown during exception handling", ex); - query_stats.markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); + + } catch (RuntimeException ex2) { + LOG.error("Exception thrown during exception handling", ex2); + query_stats.markSerialized(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex2); query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, - ex.getMessage().getBytes()); - return null; + ex2.getMessage().getBytes()); + query_exceptions.incrementAndGet(); } + return null; } } @@ -224,11 +249,11 @@ public Object call(final ArrayList query_results) class SendIt implements Callback { public Object call(final ChannelBuffer buffer) throws Exception { query.sendReply(buffer); + query_success.incrementAndGet(); return null; } } - query_stats.setTimeStorage(System.currentTimeMillis() - start); switch (query.apiVersion()) { case 0: case 1: @@ -236,6 +261,7 @@ public Object call(final ChannelBuffer buffer) throws Exception { globals).addCallback(new SendIt()).addErrback(new ErrorCB()); break; default: + query_invalid.incrementAndGet(); throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); @@ -768,6 +794,13 @@ private LastPointQuery parseLastPointQuery(final TSDB tsdb, return query; } + /** @param collector Populates the collector with statistics */ + public static void collectStats(final StatsCollector collector) { + collector.record("http.query.invalid_requests", query_invalid); + collector.record("http.query.exceptions", query_exceptions); + collector.record("http.query.success", query_success); + } + public static class LastPointQuery { private boolean resolve_names; diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index e6bf4b588c..69224ca8b5 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -331,6 +331,7 @@ public static void collectStats(final StatsCollector collector) { HttpQuery.collectStats(collector); GraphHandler.collectStats(collector); PutDataPointRpc.collectStats(collector); + QueryRpc.collectStats(collector); } /** diff --git a/src/tsd/StatsRpc.java b/src/tsd/StatsRpc.java index 0e65d17a47..bd0ee910d1 100644 --- a/src/tsd/StatsRpc.java +++ b/src/tsd/StatsRpc.java @@ -331,7 +331,7 @@ private void printQueryStats(final HttpQuery query) { case 0: case 1: query.sendReply(query.serializer().formatQueryStatsV1( - QueryStats.buildStats())); + QueryStats.getRunningAndCompleteStats())); break; default: throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 97b052c0bb..d3d9a08c44 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -268,4 +268,41 @@ public static long currentTimeMillis() { return System.currentTimeMillis(); } + /** + * Pass through to {@link System.nanoTime} for use in classes to + * make unit testing easier. Mocking System.class is a bad idea in general + * so placing this here and mocking DateTime.class is MUCH cleaner. + * @return The current epoch time in milliseconds + * @since 2.2 + */ + public static long nanoTime() { + return System.nanoTime(); + } + + /** + * Converts the long nanosecond value to a double in milliseconds + * @param ts The timestamp or value in nanoseconds + * @return The timestamp in milliseconds + * @since 2.2 + */ + public static double msFromNano(final long ts) { + return (double)ts / 1000000; + } + + /** + * Calculates the difference between two values and returns the time in + * milliseconds as a double. + * @param end The end timestamp + * @param start The start timestamp + * @return The value in milliseconds + * @throws IllegalArgumentException if end is less than start + * @since 2.2 + */ + public static double msFromNanoDiff(final long end, final long start) { + if (end < start) { + throw new IllegalArgumentException("End (" + end + ") cannot be less " + + "than start (" + start + ")"); + } + return ((double) end - (double) start) / 1000000; + } } diff --git a/test/stats/TestQueryStats.java b/test/stats/TestQueryStats.java index 5ee4dcf163..e81477d15d 100644 --- a/test/stats/TestQueryStats.java +++ b/test/stats/TestQueryStats.java @@ -15,9 +15,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyLong; import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -26,14 +31,21 @@ import net.opentsdb.core.QueryException; import net.opentsdb.core.TSQuery; +import net.opentsdb.stats.QueryStats.QueryStat; +import net.opentsdb.utils.DateTime; import org.jboss.netty.handler.codec.http.HttpResponseStatus; 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; @RunWith(PowerMockRunner.class) +@PrepareForTest({ DateTime.class, QueryStats.class }) public final class TestQueryStats { private static String remote = "192.168.1.1:4242"; @@ -56,65 +68,100 @@ public final class TestQueryStats { } } + private Map headers; + @Before public void before() throws Exception { running_queries.set(null, new ConcurrentHashMap()); completed_queries.set(null, CacheBuilder.newBuilder().maximumSize(2).build()); + headers = new HashMap(1); + headers.put("Cookie", "Hide me!"); + PowerMockito.mockStatic(DateTime.class); + PowerMockito.doAnswer(new Answer() { + long ts = 1000L; + @Override + public Long answer(InvocationOnMock invocation) throws Throwable { + return ts += 1000000000L; + } + + }).when(DateTime.class, "nanoTime"); + PowerMockito.doCallRealMethod().when(DateTime.class, + "msFromNano", anyLong()); + PowerMockito.doCallRealMethod().when(DateTime.class, + "msFromNanoDiff", anyLong(), anyLong()); } @Test public void ctor() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); - final Map>> map = QueryStats.buildStats(); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(1, map.get("running").size()); - assertEquals(0, map.get("completed").size()); + assertEquals(1, ((List)map.get("running")).size()); + assertEquals(0, ((Collection)map.get("completed")).size()); + assertSame(headers, stats.getRequestHeaders()); } - @Test (expected = QueryException.class) + @Test public void ctorDuplicate() throws Exception { + QueryStats.setEnableDuplicates(false); final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); - final Map>> map = QueryStats.buildStats(); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(1, map.get("running").size()); - assertEquals(0, map.get("completed").size()); - new QueryStats(remote, query); + assertEquals(1, ((List)map.get("running")).size()); + assertEquals(0, ((Collection)map.get("completed")).size()); + try { + new QueryStats(remote, query, headers); + fail("Expected a QueryException"); + } catch (QueryException e) { } + QueryStats.setEnableDuplicates(true); } @Test (expected = IllegalArgumentException.class) public void ctorNullRemote() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - new QueryStats(null, query); + new QueryStats(null, query, headers); } @Test (expected = IllegalArgumentException.class) public void ctorNullQuery() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - new QueryStats(remote, null); + new QueryStats(remote, null, headers); + } + + @Test + public void ctorNullHeaders() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query, null); + assertNotNull(stats); + final Map map = QueryStats.getRunningAndCompleteStats(); + assertNotNull(map); + assertEquals(1, ((List)map.get("running")).size()); + assertEquals(0, ((Collection)map.get("completed")).size()); } @Test public void testHashCodeandEquals() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); final int hash_a = stats.hashCode(); // have to mark the old one as complete before we can test equality - stats.markComplete(); + stats.markSerializationSuccessful(); final TSQuery query2 = new TSQuery(); query2.setStart("1h-ago"); - final QueryStats stats2 = new QueryStats(remote, query2); + final QueryStats stats2 = new QueryStats(remote, query2, headers); assertNotNull(stats); assertEquals(hash_a, stats2.hashCode()); assertEquals(stats, stats2); @@ -125,13 +172,13 @@ public void testHashCodeandEquals() throws Exception { public void testHashCodeandNotEquals() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertNotNull(stats); final int hash_a = stats.hashCode(); final TSQuery query2 = new TSQuery(); query2.setStart("2h-ago"); - final QueryStats stats2 = new QueryStats(remote, query2); + final QueryStats stats2 = new QueryStats(remote, query2, headers); assertNotNull(stats); assertTrue(hash_a != stats2.hashCode()); assertFalse(stats.equals(stats2)); @@ -142,7 +189,7 @@ public void testHashCodeandNotEquals() throws Exception { public void testEqualsNull() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertFalse(stats.equals(null)); } @@ -150,7 +197,7 @@ public void testEqualsNull() throws Exception { public void testEqualsWrongType() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertFalse(stats.equals(new String("foo"))); } @@ -158,7 +205,7 @@ public void testEqualsWrongType() throws Exception { public void testEqualsSame() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); + final QueryStats stats = new QueryStats(remote, query, headers); assertTrue(stats.equals(stats)); } @@ -166,84 +213,92 @@ public void testEqualsSame() throws Exception { public void markComplete() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(); - final Map>> map = QueryStats.buildStats(); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerializationSuccessful(); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - final Map completed = map.get("completed").get(0); - assertEquals(200, completed.get("status")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + final QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(200, completed.getHttpResponse().getCode()); } @Test public void markCompleteTimeout() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - final Map>> map = QueryStats.buildStats(); + final QueryStats stats = new QueryStats(remote, query, headers); + final RuntimeException timeout = new RuntimeException("Timeout!"); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, timeout); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - final Map completed = map.get("completed").get(0); - assertEquals(408, completed.get("status")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + final QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(408, completed.getHttpResponse().getCode()); + assertTrue(completed.getException().startsWith("Timeout!\n")); } @Test - public void markCompleteDoubleMark() throws Exception { + public void executed() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(); - final Map>> map = QueryStats.buildStats(); - assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - Map completed = map.get("completed").get(0); - assertEquals(200, completed.get("status")); - stats.markComplete(); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, null); + final Map map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - completed = map.get("completed").get(0); - assertEquals(200, completed.get("status")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + final QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(1, completed.getExecuted()); } @Test - public void executed() throws Exception { + public void executedTwice() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - final QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - final Map>> map = QueryStats.buildStats(); + QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, null); + Map map = QueryStats.getRunningAndCompleteStats(); + assertNotNull(map); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + QueryStats completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(1, completed.getExecuted()); + + stats = new QueryStats(remote, query, headers); + stats.markSerialized(HttpResponseStatus.REQUEST_TIMEOUT, null); + map = QueryStats.getRunningAndCompleteStats(); assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - final Map completed = map.get("completed").get(0); - assertEquals(1L, completed.get("executed")); + assertEquals(0, ((List)map.get("running")).size()); + assertEquals(1, ((Collection)map.get("completed")).size()); + completed = ((Collection)map.get("completed")) + .iterator().next(); + assertEquals(2, completed.getExecuted()); + } + + @Test + public void getStat() throws Exception { + final TSQuery query = new TSQuery(); + query.setStart("1h-ago"); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.addStat(QueryStat.AGGREGATED_SIZE, 42); + stats.markSerializationSuccessful(); + assertEquals(42, stats.getStat(QueryStat.AGGREGATED_SIZE)); + assertEquals(-1, stats.getStat(QueryStat.BYTES_FROM_STORAGE)); } @Test - public void executedTwice() throws Exception { + public void getStatTime() throws Exception { final TSQuery query = new TSQuery(); query.setStart("1h-ago"); - QueryStats stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - Map>> map = QueryStats.buildStats(); - assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - Map completed = map.get("completed").get(0); - assertEquals(1L, completed.get("executed")); - - stats = new QueryStats(remote, query); - stats.markComplete(HttpResponseStatus.REQUEST_TIMEOUT, null); - map = QueryStats.buildStats(); - assertNotNull(map); - assertEquals(0, map.get("running").size()); - assertEquals(1, map.get("completed").size()); - completed = map.get("completed").get(0); - assertEquals(2L, completed.get("executed")); + final QueryStats stats = new QueryStats(remote, query, headers); + stats.markSerializationSuccessful(); + assertEquals(1000.0, stats.getTimeStat(QueryStat.PROCESSING_PRE_WRITE_TIME), 0.001); + assertEquals(Double.NaN, stats.getTimeStat(QueryStat.AVG_AGGREGATION_TIME), 0.001); } } diff --git a/test/tsd/NettyMocks.java b/test/tsd/NettyMocks.java index 7937f97bbf..f641ea9550 100644 --- a/test/tsd/NettyMocks.java +++ b/test/tsd/NettyMocks.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.tsd; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -22,8 +23,11 @@ import net.opentsdb.core.TSDB; import net.opentsdb.utils.Config; +import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.DefaultChannelFuture; import org.jboss.netty.channel.DefaultChannelPipeline; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpMethod; @@ -198,6 +202,13 @@ public static HttpQuery contentQuery(final TSDB tsdb, final String uri, req.headers().set("Content-Type", type); return new HttpQuery(tsdb, req, channelMock); } + + /** @param the query to mock a future callback for */ + public static void mockChannelFuture(final HttpQuery query) { + final ChannelFuture future = new DefaultChannelFuture(query.channel(), false); + when(query.channel().write(any(ChannelBuffer.class))).thenReturn(future); + future.setSuccess(); + } /** * Returns a simple pipeline with an HttpRequestDecoder and an diff --git a/test/tsd/TestHttpJsonSerializer.java b/test/tsd/TestHttpJsonSerializer.java index a4669a87b1..1c746f55a2 100644 --- a/test/tsd/TestHttpJsonSerializer.java +++ b/test/tsd/TestHttpJsonSerializer.java @@ -266,18 +266,15 @@ public void formatQueryAsyncV1wStatsSummary() throws Exception { assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); assertTrue(json.contains("\"1356998700\":1,")); assertTrue(json.contains("\"1357058700\":201")); - //assert stats assertTrue(json.contains("\"stats\":{")); - assertTrue(json.contains("\"datapoints\":400")); - assertTrue(json.contains("\"rawDatapoints\":800")); - assertTrue(json.contains("\"timeSeries\":2")); - + assertTrue(json.contains("\"emittedDPs\":401")); + System.out.println(json); //assert stats summary assertTrue(json.contains("{\"statsSummary\":{")); - assertTrue(json.contains("\"serializationTime\":1500")); - assertTrue(json.contains("\"storageTime\":0")); - assertTrue(json.contains("\"timeTotal\":2500")); + assertTrue(json.contains("\"serializationTime\":")); + assertTrue(json.contains("\"processingPreWriteTime\":")); + assertTrue(json.contains("\"queryIdx_00\":")); } @Test @@ -299,12 +296,9 @@ public void formatQueryAsyncV1wStatsWoSummary() throws Exception { assertTrue(json.contains("\"1356998700\":1,")); assertTrue(json.contains("\"1357058700\":201")); - //assert stats assertTrue(json.contains("\"stats\":{")); - assertTrue(json.contains("\"datapoints\":400")); - assertTrue(json.contains("\"rawDatapoints\":800")); - assertTrue(json.contains("\"timeSeries\":2")); + assertTrue(json.contains("\"emittedDPs\":401")); //assert stats summary assertFalse(json.contains("{\"statsSummary\":{")); @@ -327,15 +321,16 @@ public void formatQueryAsyncV1woStatsWSummary() throws Exception { assertTrue(json.contains("\"metric\":\"system.cpu.user\",")); assertTrue(json.contains("\"1356998700\":1,")); assertTrue(json.contains("\"1357058700\":201")); - + //assert stats assertFalse(json.contains("\"stats\":{")); //assert stats summary assertTrue(json.contains("{\"statsSummary\":{")); - assertTrue(json.contains("\"serializationTime\":1500")); - assertTrue(json.contains("\"storageTime\":0")); - assertTrue(json.contains("\"timeTotal\":2500")); + assertTrue(json.contains("\"serializationTime\":")); + assertTrue(json.contains("\"processingPreWriteTime\":")); + assertTrue(json.contains("\"emittedDPs\":401")); + assertTrue(json.contains("\"queryIdx_00\":")); } @Test @@ -570,7 +565,7 @@ private TSQuery getTestQuery(final boolean show_stats, final boolean show_summar */ private void validateTestQuery(final TSQuery data_query) { data_query.validateAndSetQuery(); - data_query.setQueryStats(new QueryStats(remote, data_query)); + data_query.setQueryStats(new QueryStats(remote, data_query, null)); } /** @@ -590,6 +585,15 @@ public Long answer(InvocationOnMock invocation) throws Throwable { return ts; } }); + + PowerMockito.when(DateTime.nanoTime()) + .thenAnswer(new Answer () { + public Long answer(InvocationOnMock invocation) throws Throwable { + long ts = timestamp.get(0); + timestamp.set(0, ts + 500); + return ts * 1000000; + } + }); } } diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index b4f0f5caa1..dfd885fcb0 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -442,6 +442,7 @@ public void postQuerySimplePass() throws Exception { "{\"start\":1425440315306,\"queries\":" + "[{\"metric\":\"somemetric\",\"aggregator\":\"sum\",\"rate\":true," + "\"rateOptions\":{\"counter\":false}}]}"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); } @@ -469,6 +470,7 @@ public void postQueryNoMetricBadRequest() throws Exception { public void executeEmpty() throws Exception { final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String json = query.response().getContent().toString(Charset.forName("UTF-8")); @@ -484,6 +486,7 @@ public void execute() throws Exception { final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String json = query.response().getContent().toString(Charset.forName("UTF-8")); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 23867039c4..63ae2ffab8 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import java.text.SimpleDateFormat; @@ -371,5 +372,33 @@ public void currentTimeMillis() { when(System.currentTimeMillis()).thenReturn(1388534400000L); assertEquals(1388534400000L, DateTime.currentTimeMillis()); } + + @Test + public void nanoTime() { + PowerMockito.mockStatic(System.class); + when(System.nanoTime()).thenReturn(1388534400000000000L); + assertEquals(1388534400000000000L, DateTime.nanoTime()); + } + + @Test + public void msFromNano() { + assertEquals(0, DateTime.msFromNano(0), 0.0001); + assertEquals(1, DateTime.msFromNano(1000000), 0.0001); + assertEquals(-1, DateTime.msFromNano(-1000000), 0.0001); + assertEquals(1.5, DateTime.msFromNano(1500000), 0.0001); + assertEquals(1.123, DateTime.msFromNano(1123000), 0.0001); + } + + @Test + public void msFromNanoDiff() { + assertEquals(0, DateTime.msFromNanoDiff(1000000, 1000000), 0.0001); + assertEquals(0.5, DateTime.msFromNanoDiff(1500000, 1000000), 0.0001); + assertEquals(1.5, DateTime.msFromNanoDiff(1500000, 0), 0.0001); + assertEquals(0.5, DateTime.msFromNanoDiff(-1000000, -1500000), 0.0001); + try { + assertEquals(0.5, DateTime.msFromNanoDiff(1000000, 1500000), 0.0001); + fail("Expected an IllegalArgumentException"); + } catch (IllegalArgumentException e) {} + } } From ec39cb29948f198ab10fbc8a75fb2a02b8ea1096 Mon Sep 17 00:00:00 2001 From: Can ZHANG Date: Thu, 4 Feb 2016 15:48:01 +0800 Subject: [PATCH 411/826] Create TSMeta by get then put If enable_tsuid_incrementing is false and config.enable_realtime_ts is true, TSMeta will be created through get, check and put, instead of atomicIncrement and put. --- src/core/TSDB.java | 4 ++- src/meta/TSMeta.java | 62 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index cb3a190415..0d3127388a 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -718,8 +718,10 @@ private Deferred addPointInternal(final String metric, 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()) { + } else if (config.enable_tsuid_incrementing() && config.enable_realtime_ts()) { TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } else if (!config.enable_tsuid_incrementing() && config.enable_realtime_ts()) { + TSMeta.storeIfNecessary(TSDB.this, tsuid); } if (rt_publisher != null) { diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index a1863803b9..35a56f546e 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -612,6 +612,68 @@ public Deferred call(Boolean success) throws Exception { return tsdb.getClient().atomicIncrement(inc).addCallbackDeferring( new TSMetaCB()); } + + public static void storeIfNecessary(final TSDB tsdb, final byte[] tsuid) { + final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); + get.family(FAMILY); + get.qualifier(META_QUALIFIER); + + final class CreateNewCB implements Callback, Object> { + + @Override + public Deferred call(Object arg0) throws Exception { + final TSMeta meta = new TSMeta(tsuid, System.currentTimeMillis() / 1000); + + final class FetchNewCB implements Callback, TSMeta> { + + @Override + public Deferred call(TSMeta stored_meta) throws Exception { + + // pass to the search plugin + tsdb.indexTSMeta(stored_meta); + + // pass through the trees + tsdb.processTSMetaThroughTrees(stored_meta); + + return Deferred.fromResult(true); + } + } + + final class StoreNewCB implements Callback, Boolean> { + + @Override + public Deferred call(Boolean success) throws Exception { + if (!success) { + LOG.warn("Unable to save metadata: " + meta); + return Deferred.fromResult(false); + } + + LOG.info("Successfullly created new TSUID entry for: " + meta); + return Deferred.fromResult(meta) + .addCallbackDeferring( + new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + .addCallbackDeferring(new FetchNewCB()); + } + } + + return meta.storeNew(tsdb).addCallbackDeferring(new StoreNewCB()); + } + } + + final class ExistsCB implements Callback, ArrayList> { + + @Override + public Deferred call(ArrayList row) throws Exception { + if (row == null || row.isEmpty() || row.get(0).value() == null) { + return Deferred.fromResult(new Object()) + .addCallbackDeferring(new CreateNewCB()); + } + return Deferred.fromResult(true); + } + } + + tsdb.getClient().get(get).addCallbackDeferring(new ExistsCB()); + } /** * Attempts to fetch the timeseries meta data from storage. From 022ae9f2ecd89b2c1cb95a94c63509a135166ed8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 12:59:59 -0800 Subject: [PATCH 412/826] Rollback 6d2102af7d7391b759698979cab9ae74bea15d80 as I was mistaken in that on Linux hosts it did indeed point to the wrong config directory when installing locally and via package. We'll need to revisit this for FreeBSD. Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index b43e01c093..9963574c64 100644 --- a/Makefile.am +++ b/Makefile.am @@ -356,7 +356,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(prefix)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp From 48f079cd54e846ecd458a528406ad1c8be37795d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 12:59:59 -0800 Subject: [PATCH 413/826] Rollback 6d2102af7d7391b759698979cab9ae74bea15d80 as I was mistaken in that on Linux hosts it did indeed point to the wrong config directory when installing locally and via package. We'll need to revisit this for FreeBSD. Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 15013eb61a..78abae4d5d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -455,7 +455,7 @@ printdeps: # This is kind of a hack, but I couldn't find a better way to adjust the paths # in the script before it gets installed... install-exec-hook: - script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(prefix)/etc/opentsdb'; \ + script=tsdb; pkgdatadir='$(pkgdatadir)'; configdir='$(sysconfigdir)/etc/opentsdb'; \ abs_srcdir=''; abs_builddir=''; $(edit_tsdb_script) cat tsdb.tmp >"$(DESTDIR)$(bindir)/tsdb" rm -f tsdb.tmp From dda37221cfcb27fe99a033c6702eddbc3bf32ddc Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 12:35:58 -0800 Subject: [PATCH 414/826] Update the logback.xml config file with disabled configs for the query log. Also add the file appender to the /src/logback.xml file. Signed-off-by: Chris Larsen --- build-aux/deb/logback.xml | 33 +++++++++++++++++++-- build-aux/rpm/logback.xml | 33 +++++++++++++++++++-- src/logback.xml | 60 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/build-aux/deb/logback.xml b/build-aux/deb/logback.xml index 7f0fb57694..7ae2c3fcfc 100644 --- a/build-aux/deb/logback.xml +++ b/build-aux/deb/logback.xml @@ -9,10 +9,15 @@ + 1024 + /var/log/opentsdb/opentsdb.log true @@ -27,16 +32,40 @@ 128MB - %d{HH:mm:ss.SSS} %-5level [%logger{0}.%M] - %msg%n + + + + /var/log/opentsdb/queries.log + true + + + /var/log/opentsdb/queries.log.%i + 1 + 4 + + + + 128MB + + + %date{ISO8601} [%logger.%M] %msg%n + + + + + + + + diff --git a/build-aux/rpm/logback.xml b/build-aux/rpm/logback.xml index 7f0fb57694..7ae2c3fcfc 100644 --- a/build-aux/rpm/logback.xml +++ b/build-aux/rpm/logback.xml @@ -9,10 +9,15 @@ + 1024 + /var/log/opentsdb/opentsdb.log true @@ -27,16 +32,40 @@ 128MB - %d{HH:mm:ss.SSS} %-5level [%logger{0}.%M] - %msg%n + + + + /var/log/opentsdb/queries.log + true + + + /var/log/opentsdb/queries.log.%i + 1 + 4 + + + + 128MB + + + %date{ISO8601} [%logger.%M] %msg%n + + + + + + + + diff --git a/src/logback.xml b/src/logback.xml index b06776504a..ff97a50889 100644 --- a/src/logback.xml +++ b/src/logback.xml @@ -8,15 +8,67 @@ + + 1024 + + + + /var/log/opentsdb/opentsdb.log + true + + /home/y/logs/opentsdb2/opentsdb.log.%i + 1 + 4 + + + 512MB + + + + %date{ISO8601} [%thread] %-5level [%logger{0}.%M] - %msg%n + + + + + + /var/log/opentsdb/queries.log + true - - - - + + /var/log/opentsdb/queries.log.%i + 1 + 4 + + + + 128MB + + + %date{ISO8601} [%logger.%M] %msg%n + + + + + + + + + + + + + + + From ef4402c0678ac8e56dfd23c656f52832ce944387 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 12:35:58 -0800 Subject: [PATCH 415/826] Update the logback.xml config file with disabled configs for the query log. Also add the file appender to the /src/logback.xml file. Signed-off-by: Chris Larsen --- build-aux/deb/logback.xml | 33 +++++++++++++++++++-- build-aux/rpm/logback.xml | 33 +++++++++++++++++++-- src/logback.xml | 60 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/build-aux/deb/logback.xml b/build-aux/deb/logback.xml index 7f0fb57694..7ae2c3fcfc 100644 --- a/build-aux/deb/logback.xml +++ b/build-aux/deb/logback.xml @@ -9,10 +9,15 @@ + 1024 + /var/log/opentsdb/opentsdb.log true @@ -27,16 +32,40 @@ 128MB - %d{HH:mm:ss.SSS} %-5level [%logger{0}.%M] - %msg%n + + + + /var/log/opentsdb/queries.log + true + + + /var/log/opentsdb/queries.log.%i + 1 + 4 + + + + 128MB + + + %date{ISO8601} [%logger.%M] %msg%n + + + + + + + + diff --git a/build-aux/rpm/logback.xml b/build-aux/rpm/logback.xml index 7f0fb57694..7ae2c3fcfc 100644 --- a/build-aux/rpm/logback.xml +++ b/build-aux/rpm/logback.xml @@ -9,10 +9,15 @@ + 1024 + /var/log/opentsdb/opentsdb.log true @@ -27,16 +32,40 @@ 128MB - %d{HH:mm:ss.SSS} %-5level [%logger{0}.%M] - %msg%n + + + + /var/log/opentsdb/queries.log + true + + + /var/log/opentsdb/queries.log.%i + 1 + 4 + + + + 128MB + + + %date{ISO8601} [%logger.%M] %msg%n + + + + + + + + diff --git a/src/logback.xml b/src/logback.xml index b06776504a..ff97a50889 100644 --- a/src/logback.xml +++ b/src/logback.xml @@ -8,15 +8,67 @@ + + 1024 + + + + /var/log/opentsdb/opentsdb.log + true + + /home/y/logs/opentsdb2/opentsdb.log.%i + 1 + 4 + + + 512MB + + + + %date{ISO8601} [%thread] %-5level [%logger{0}.%M] - %msg%n + + + + + + /var/log/opentsdb/queries.log + true - - - - + + /var/log/opentsdb/queries.log.%i + 1 + 4 + + + + 128MB + + + %date{ISO8601} [%logger.%M] %msg%n + + + + + + + + + + + + + + + From 2b5acecd85b9aaae1b4fdbfa62c3137075773a3d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 13:36:29 -0800 Subject: [PATCH 416/826] Cut 2.1.4 Signed-off-by: Chris Larsen --- NEWS | 11 +++++++++++ configure.ac | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 029a4d509a..d94c1df182 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,16 @@ OpenTSDB - User visible changes. +* Version 2.1.4 (2016-02-14) + +Bug Fixes: + - Fix the meta table where the UID/TSMeta APIs were not sorting tags properly + prior to creating the row key, thus allowing for duplicates if the caller changed + the order of tags. + - Fix a situation where meta sync could hang forever if a routine threw an exception. + - Fix an NPE thrown when accessing the /logs endpoint if the Cyclic appender is not + enabled in the logback config. + - Remove an overly chatty log line in TSMeta on new time series creation. + * Version 2.1.3 (2015-11-11) Bug Fixes: diff --git a/configure.ac b/configure.ac index e953a43527..eccf64827d 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.1.3], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.1.4], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From e7aa6a83cbf0cd884cb11ff2a27197ee46f3d090 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 13:34:44 -0800 Subject: [PATCH 417/826] Cut 2.2.0 Update News and Thanks! Signed-off-by: Chris Larsen --- NEWS | 27 +++++++++++++++++++++++++++ THANKS | 6 ++++++ configure.ac | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 1a7515e1e9..5378eb836c 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,32 @@ OpenTSDB - User visible changes. +* Version 2.2.0 (2016-02-14) + +Noteworthy Changes + - Rework the QueryStats output to be a bit more useful and add timings from the + various scanners and query components. + - Modify the UI to allow for group by or aggregate per tag (use the new query feature) + - Rework the UI skin with the new TSDB logo and color scheme. + - Add the QueryLog config to logback.xml so users can optionally enable logging of + all queries along with their stats. + +Buf Fixes: + - Properly handle append data points in the FSCK utility. + - Fix FSCK to handle salting properly. + - Fix the IncomingDataPoints class for the CLI import tool to properly account for + salting. + - Fix the QueryStats maps by making sure the hash accounts for an unmodified list of + filters (return copies to callers so sorting won't break the hash code). + - Fix the case-insensitive wildcard filter to properly ignore the case. + - Fix the CLI dumper/scan utility to properly handle salted data. + - Fix a case where the compaction queue could grow unbounded when salting was enabled. + - Allow duplicate queries by default (as we did in the past) and users must now block + them explicitly. + - Fix the /api/stats endpoint to allow for returning a value if the max UID width is + set to 8 for any type. Previously it would throw an exception. + - Add a try catch to FSCK to debug issues where printing a problematic row would cause + a hangup or no output. + * Version 2.2.0 RC3 (2015-11-11) Bug Fixes: diff --git a/THANKS b/THANKS index 4761912619..05e9ae58ff 100644 --- a/THANKS +++ b/THANKS @@ -22,11 +22,14 @@ Chris McClymont Cristian Sechel Christophe Furmaniak Dave Barr +Davide D Amico Filippo Giunchedi Gabriel Nicolas Avellaneda Guenther Schmuelling +Hari Krishna Dara Hong Dai Thanh Hugo Trippaers +Ivan Babrou Jacek Masiulaniec Jari Takkala James Royalty @@ -42,8 +45,10 @@ Josh Thomas Kieren Hynd Kimoon Kim Kris Beevers +Kyle Brandt Lex Herbert Liangliang He +Liu Yubao Loïs Burg Lou Yunlong Matt Jibson @@ -74,6 +79,7 @@ Thomas Sanchez Tibor Vass Tristan Colgate-McFarlane Tony Landells +Utkarsh Bhatnagar Vasiliy Kiryanov Yulai Fu Zachary Kurey \ No newline at end of file diff --git a/configure.ac b/configure.ac index ad2b2b4893..9b8c7f5c75 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.2.0RC3], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.0], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From efc8fc099a38570fe63bad67b2ca6d9c8471a6c3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 14 Feb 2016 17:43:12 -0800 Subject: [PATCH 418/826] Fix merge issues --- NEWS | 95 ++++++++++++++++++++++++++++++++++++++++++++++ src/core/TSDB.java | 24 ------------ 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/NEWS b/NEWS index d94c1df182..277824cc13 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,100 @@ OpenTSDB - User visible changes. +* Version 2.2.0 (2016-02-14) + +Noteworthy Changes + - Rework the QueryStats output to be a bit more useful and add timings from the + various scanners and query components. + - Modify the UI to allow for group by or aggregate per tag (use the new query feature) + - Rework the UI skin with the new TSDB logo and color scheme. + - Add the QueryLog config to logback.xml so users can optionally enable logging of + all queries along with their stats. + +Buf Fixes: + - Properly handle append data points in the FSCK utility. + - Fix FSCK to handle salting properly. + - Fix the IncomingDataPoints class for the CLI import tool to properly account for + salting. + - Fix the QueryStats maps by making sure the hash accounts for an unmodified list of + filters (return copies to callers so sorting won't break the hash code). + - Fix the case-insensitive wildcard filter to properly ignore the case. + - Fix the CLI dumper/scan utility to properly handle salted data. + - Fix a case where the compaction queue could grow unbounded when salting was enabled. + - Allow duplicate queries by default (as we did in the past) and users must now block + them explicitly. + - Fix the /api/stats endpoint to allow for returning a value if the max UID width is + set to 8 for any type. Previously it would throw an exception. + - Add a try catch to FSCK to debug issues where printing a problematic row would cause + a hangup or no output. + +* Version 2.2.0 RC3 (2015-11-11) + +Bug Fixes: + - Fix build issues where the static files were not copied into the proper location. + +* Version 2.2.0 RC2 (2015-11-09) + +Noteworthy Changes: + - Allow overriding the metric and tag UID widths via config file instead of + having to modify the source code. + +Bug Fixes: + - OOM handling script now handles multiple TSDs installed on the same host. + - Fix a bug where queries never return if an exception is thrown from the + storage layer. + - Fix random metric UID assignment in the CLI tool. + - Fix for meta data sync when salting is enabled. + - + +* Version 2.2.0 RC1 (2015-09-12) + +Noteworthy Changes: + - Add the option to randomly assign UIDs to metrics to improve distribution across + HBase region servers. + - Introduce salting of data to improve distribution of high cardinality regions + across region servers. + - Introduce query stats for tracking various timings related to TSD queries. + - Add more stats endpoints including /threads, /jvm and /region_clients + - Allow for deleting UID mappings via CLI or the API + - Name the various threads for easier debugging, particularly for distinguishing + between TSD and AsyncHBase threads. + - Allow for pre-fetching all of the meta information for the tables to improve + performance. + - Update to the latest AsyncHBase with support for secure HBase clusters and RPC + timeouts. + - Allow for overriding metric and tag widths via the config file. (Be careful!) + - URLs from the API are now relative instead of absolute, allowing for easier reverse + proxy use. + - Allow for percent deviation in the Nagios check + - Let queries skip over unknown tag values that may not exist yet (via config) + - Add various query filters such as case (in)sensitive pipes, wildcards and pipes + over tag values. Filters do not work over metrics at this time. + - Add support for writing data points using Appends in HBase as a way of writing + compacted data without having to read and re-write at the top of each hour. + - Introduce an option to emit NaNs or Nulls in the JSON output when downsampling and + a bucket is missing values. + - Introduce query time flags to show the original query along with some timing stats + in the response. + - Introduce a storage exception handler plugin that will allow users to spool or + requeue data points that fail writes to HBase due to various issues. + - Rework the HTTP pipeline to support plugins with RPC implementations. + - Allow for some style options in the Gnuplot graphs. + - Allow for timing out long running HTTP queries. + - Text importer will now log and continue bad rows instead of failing. + - New percentile and count aggregators. + - Add the /api/annotations endpoint to fetch multiple annotations in one call. + - Add a class to support improved bulk imports by batching requests in memory for a + full hour before writing. + +Bug Fixes: + - Modify the .rpm build to allow dashes in the name. + - Allow the Nagios check script to handle 0 values properly in checks. + - Fix FSCK where floating point values were not processed correctly (#430) + - Fix missing information from the /appi/uid/tsmeta calls (#498) + - Fix more issues with the FSCK around deleting columns that were in the list (#436) + - Avoid OOM issues over Telnet when the sending client isn't reading errors off it's + socket fast enough by blocking writes. + * Version 2.1.4 (2016-02-14) Bug Fixes: diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 55b562ab5f..64cf75c779 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -428,30 +428,6 @@ public Deferred getUIDAsync(final UniqueIdType type, final String name) } } - /** - * Attempts to find the UID matching a given name - * @param type The type of UID - * @param name The name to search for - * @throws IllegalArgumentException if the type is not valid - * @throws NoSuchUniqueName if the name was not found - * @since 2.1 - */ - public Deferred getUIDAsync(final UniqueIdType type, final String name) { - if (name == null || name.isEmpty()) { - throw new IllegalArgumentException("Missing UID name"); - } - switch (type) { - case METRIC: - return metrics.getIdAsync(name); - case TAGK: - return tag_names.getIdAsync(name); - case TAGV: - return tag_values.getIdAsync(name); - default: - throw new IllegalArgumentException("Unrecognized UID type"); - } - } - /** * Verifies that the data and UID tables exist in HBase and optionally the * tree and meta data tables if the user has enabled meta tracking or tree From 07d0464d2bd26e51e25744ee61192c48349448ff Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 16 Feb 2016 11:51:33 -0800 Subject: [PATCH 419/826] Removing the Zookeeper jar. How'd that get in there !?!?!? --- third_party/zookeeper/zookeeper-3.4.5.jar | Bin 779974 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar diff --git a/third_party/zookeeper/zookeeper-3.4.5.jar b/third_party/zookeeper/zookeeper-3.4.5.jar deleted file mode 100644 index a7966bbbce49344a67438bee8bb0cd1fd4952eee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 779974 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

    N}hR{0dLySoh!!h~(WZKRed%U$2{V7gCf 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 z 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

    r6T9k`3Qu3|?nk%Zsi+St;89-&5yV4X1>E zoDJQnyX@P`9Nut1-WX7A4*`AmaX!e)rTf?CzW}g)o?-QHiWO<>pzXaomcfFS@r&t{ubJP4f}1tTO>03`wv{?9;(f^B-g z9gN#)6Xq3_4-{H!@W0qfD~dFOCC8>_Ztl}-2M236{M~+_vxByvnv*A#w3wp>HU`Mj znzR!(SmpVZMWx1~BdTFCO$RaK@L^8eP`X)&ZH*+CFM_vP;RR4vSA-sgl?X4N+>8Dz z4_&AD0}Ux)3Ts9Ze}lRWM*s5a^K5x9pze|1z+059BH;3^?iWz3985K#4aQ$|UfhS5 z3qw5A-$VgecZ(A8i@buOCd3`)s}To!kx7E+zeHU&H}rQf{EAuRp){FkNl#Z4&4Gj| z5=bNm*;62{Y=y(f$K)W47}Y=Zo^-osWV45{P|FO)$Cu|AEe&cDwzfX9 zYQ!%XmX~F8Mk-@*$nEyg2gZLqrWE4;u7X~7JU4FFI*j(O`Ey8wJ0;2+OK8p~z9nT@ z&gvlZBJ%P_-e6MQvc=3P*tuCCwMswBXkHoD;~?j>jX$@c{%{Dh!5D!J6sx}FK$3a+ za%MtGz*H?75@3f9n{^)A~eYczgYw^+R>kWAm*;*b_h8)0I5-lQ5IU_M`6A-SErv z%xOrkB)KJJLdnk3yOTvi^YymJ)>K+Ptfo`RY<--M*Q9&B-sw9Hipvu_fP(vF&$*DEXtO_~EqqrtE zDVIk(1_V~6y9TDys)-scoQYYiU3=bxzuOi4>TtPl+NZ=A85m z&ATFuzj4uIGTsTM9H?qhrH^q+jbg*9y*EX|JFG^^mUTh8N+qMlY|W~rm$ofnQ7mx#u*7=N>_xraf1V4LpoqXKsyC0$_r!qcyXrB= zy6S<}wY!EMuoSA-u4>z|q^7M)YK*aQj-vUcq^+A;K(DTDQckm^8gytSt!rFmZFchRgJ%XN#3yh+`Pq0&CB1eN?+nFyTtFTM1Jw1N$ct*3eE#^%XeyUVVMeNbU1+0%aQ9I?eq zyYgoBTvxlxyy}JSDKO2u0ug*qyyd+pnPB5wKJ~pZ%~=G4c}d>}yugq6{fC_aQ6UeC z%0D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 z z*6^eY7c0@iH?bc-zA~Dv>3hBOZb?q_Gqr!1P(7Miz9P-39cRUx2^99g9V-6245S%6`OOwpZJjH^J@S; z9;A55--^6fNQy1+f;m2g>6?|hM`q2G%kkvMa(t;)1_;hsU0tG%?+A=Hn^r#e3()>ihHJMGau&mLY~u z=GSa!-hUj!!=5h&x{V^WO1q@UhAT!oE&kv{K|94$j!x|$ZCD#ejgfw^*1WBTuvQ?q zlPgz?l7?Shv#YyGAiu(LrJi@U=}V&mbxkS%6o`rj*vJ zSdR2OB`?}H-|Z3%BGF=s^&4{+(US|6a3$1|Mybr>b%1eX@W@JtUFvjnMqD{JX=95; zM@rI5vcQmGAk@@8oCO-X7wxeetYT=4R&c2eXm1~ z*x)kgRf=UA{SPqCX3=q5V48HgaUvPDGuh^VxW;x#A0wa~k^nr^{n;MDulXf;c|LC^ zYP^3xul_e$xM?sgpX4`p1&eml20IB=*$`!CRUmVBfHraMe%MYV^R@fXxS(50^?stb z(rv|u9CBuD)rDb+inQYPBBM~M&Q<#*A@t5${T(5lTs;H4xG#4C{&)PCYI6@6ox{=M zEQt?EIB7Y9MO1S%oisT#4TKg{{6nFL4_MecJ&b}X^29_qQ4v-s`j`%69KxoSll8Fc z^Qa4hs%M)f(^Q!~X0C7QRQNFX&*!X>lJH>D-g`wZD>AdQzX;@y4Px7m0wT`v6;e)= z-McJxv7gS9fTd*Z{wX!92svx5q&GAmJ2)t)96BVEl*v#gvsN<;vuwRyU7t@sgm-%o zaY1n@Tn;f>@z4`KPK~AB-39nLrG1Q?qj$nQoqI6Ca?pXu>**pV?Z4A>lWSVyzYs@0MoRr9<2JKS& z^dI2Hw0`HKx-jZ8#f<@+mN)!c+M0b&O`ks4&#}^1=qviyOFkkG-0UIl$Vq6C|N>0p|r`eJqs?z<4xxKrlH zg;B}TT+oHvWh*sy@QLB4wLfTh=9IS0NK2b=A9GNVc!jI`f1m7br3CCe)kn%R&9Y~{ z{+r97}c4eVuKJw^gY&3=%-3(n$Q z!x1MgIjOH`LYKc1#tMVMUsyTm`wD+%3|2d|6B17PdT1=&MFz6eTI|!2Hh+$txTAs? zc2X}%8@l0JG#XrFFenNvgftcm0m;E6Cqv6L)wvve&U}J+*KWm;3iV*hc*@S=z{T_~ zEh@;`KY~rWE|-7(3d(aHpq2<*DNvmLS&PG{nUV#$u4GUkho5W*sPNgkw`|b|STGoW zKs~c=_U?D22A4t5D^Y9?Kr)SPSEQl~5_Cb=sPP!D!)?2~4^ZM+^+Cav-Y>48QY*D& zDVng{T9q!PYC%3#1)&UWS?q!5)S00C?vkz9m=s|tSM-~ODV0s<#ucmA@ToUX4%eYr zq)Sf9Xt~LkYV#9Rw&A5G7as#U&T(VX4-cQx^1}9~Iq- z+{w5JVIHnE2b6)dT)bkLiD&D(rwLnhiT-QgO;I!G@6^Jfp8L?a^I(ovKZc5!zs=&1xtgU*&Z2R;sRglK0Z8FL0>*3 z{7;?IU?~~jAT6lp%$usDQ6e>T++`{(uEg_XNJ4!&Fcu&C9ca(eZE|?+U39qar9$1y z4{U3=?Ok?2b96wAj7+1?<%S{`v9$F`ntC$S4N#!9J)o+br=fOhk;9L36}SzqVjbP` z{fd?LvC3ZB`b<&fUvEc|Cz=7ut-14xiNKizhzwg?)1UZ@;|+TD}3>gW7E7auK`Tf@=zM0zkas#Pxm z30M16LbWmCGj|Htb~E7!7OViRd0~qc(6|ws`?M~dq=E*A^iwPY$CtSlzvp`QvO5Xb zd6u6;tnq(Xk5~q*7|`m>DLXvyj_TtTo)&I2*peb1mZxR;%!1j?Lmg-c_*w!l7^@fm z>V5FA8_o;1tyuC`>g;JMc}18IbwrRCB)yO&Pf0!WpKth)9kQ2i@`&?)GNQKySr9*X zE$2*#^7(@3u6Vv1`%#!)0b4P@tv@mDJtE2Y*{9p^ z$Em_^jkY)iJwhHt!;PB5j4_rao(kmvGj1(i>B#(}a33s>=`YS-N1OliyG<~GKL zF>{RFPd_rH_kXgr8<#Ybz zGWyE-xv;fLUXAg`*30wB(ls*)q4v;nXcnjA%+BuHbL-1`V(<6Y7os2fZFdxxlQaiq zAp~!6*)Szz7MgG^zBk9AI6v5sovM&_NR%Vf%wYhAE^=*coCywE^sQIwP& zrm#f#us$Z17)cbA`gSNIKTMhdIqiM}40H>KxKKzxGPiqFK9h-Nc~~_(Ym|tA3Dt15 zNr*$H*5Q_gG#RbU=9WoWSajK;1<9eA2T92~9A@&f1*Nbv6PoE-r&Xu%{`Q_6iIi|{ zI>xaaT1~qO$3?}JNHj^V(O(5H()fBBo1+vdO%`ECO-!JCpd&b?Rri5;nZov--2P2{ zva`$kG?UxwsWpPu_a(vXPDWDYZoOufyBr;w(neYDEb>Mc3^qE_^g{8z3S(>2GR_|n z;B0Io`J(wcAIutgX+=jNu0br$8%M{&lI0+FMz!i!r8;6xYHOmlj5~QI@v7d#-jcti?I|R? z`>D_vkOghE%gmT13Qi0pc7hN#qpcD`3EAkHGmTOh%wiaF@#or+S61#1S)NFZ5T4YP z?hyJ#2iIC2PpF&&4pwYmp)sqha}Lb9qYnx8e51I6eEQ70#k_n62xC>lG{L!G1G+S5z0lZ4W_V9X>KFSZ7=w5}sc7a#Gq^rRi z3F{lS%&ELfYJ+!~Uk_<~eOk;x73RtMtT;7oCIAhWj|$8YHTjq&z+GYwvTC2yF>k*m zl0@SVjdB9uE>Z{2cdiC5(fx(uP0p)FVl1WI36>!lv>KFZwkmytYqTzb(?TF;3oa?H z2u+^|y0y-B&=&@rH|MB-j4-WfeqDUBZ$+k?&*?0QCFv?;P}AIn@Ff+tg_Mf;U9ti&9l0=gszFH%sQ3@@gWQvXkMz#fWqVI*e zzrsV;|3nkEMMx-;$AR*t8twSHGQii*uj9l~ElzH?Y$3Lsi?x$H$fFt2--dkutK?N* zbtB>H*XkYpw+UhYA8P0SqkL7;ceE1zzb7C6iL)wJu~tG6NAUTlyJol^^dKNeG;Sf` zjzbSUPci~bLjn?8~F4V(yYJBjW~-H zj-J7`kRjUUiMEb||Kz4Az?;;lnVh5@_1rd7D2W<#c*hDZz~#bRK(JHzS^60?9J(|5 zFSfQ2MqFz?B*HpwqfW&Rb1rsZ{Fz>^+7Sw+xk_k8p-eE?9!B}R;~o{__B&z9Ia{Ts zI;B&HRk5^C_qX>^xn+VZ9X{w@Xx@BbN{sw|X<8{b;%<&?FW!PlJc*~w7MS<_ou#+Yk}~j=i&rQM+EhSuEnk5wv35qdvA}LqLYNg{r1B4QLOxy-^f~Se zd=1xIc5Z`xy0vq3bQaocQ%xsHbxP8>Q+ioz_WVO?Z{r9zGpwnWPBEMnE_RkfjMaii z8O%Haz=KYif z=D^j9fpVs(W@Zk6;bjRmW1tt6yX%|Be*ltdqQ!!nU$xM2`yXDJ^AEmpVt?01m}=3r z8Yi&(L-a>PEt-@s_G1N(x5V*fLlz~B8y#=}ie`sA7*<%QPb)Kva$_2eykk^X;{-Au z1@w);s~9>0Mf9z}#|O@q;1q*(ihT37L3T*j1x@|*7)2DJSApH&-N$NypAa-8WBZV2t5BA0YBI?^_FpF zkxE=0zn~XSD0Th~{=a6FhgG3S$lr`&{!2#sKglTn8vvI43jjkFMB!D^Y@vyyr57Zn z&@|S29T3sYxCQ=H7~w9?FG4V2U+3;Ly*`}E zW_~!isdWS}w_+oVA=D2sZW2ml^h1O+$3!$!h>Eb6NxP#U8{$}xqU1rhS)6wB$4nP8 zC7qH?OTA6VH`Z%K%DL9aoI$r?4r*bBiT3@Bg4wT9+JmXjGJj~^yMS|Lf5MH7YsoS^ zT}iplIQWG#fpk(^*__dv4vDlF%@)98VWifA~vB>FU0;3-H*0)SUS)vS~Z+Ho3*4(?Qc< z49wV={tFMvas^u5lP*#J`vR*RD#~XuUtp~sW4bY{a(%-E3W*7-(WN>U-U+oapPvUE z36*jil3yuBebb4O1SwP5rxU_gn~|PK1hjp!uCv|HrgKEK&IxK;Pcy|0mZDr2kQJl? zYN@VP=l_DlM==&T5j!V7P~Q0jbGK2$`1oVGX$Cs*katdnRuul3ngesS45TJsEmyJR zJb1j6=k>J2Qcq-1f)crfIx0e++@|vvL(#GM zgDJ5KOr<3P$aCpmxs9ltl5k9T29VRVGx#|Nh~>Mc3&k_f5}lWVyov07(BI)a(CM zwuSYrtPJ!GEhT7#Sm%+9aqJ$6_ZKtjE*72%2Xv^{e1H zAM$8&Q#$)OGcl!L!m3W^8)+0twzz6 z&}R#d1Uf5$3UJ>|0(~$2w`8nhm#juB6FPEN5ek|hjx!563i|xd#spp$K?3Eg0qr!? zfycXKnZlg8P^!k{7t62(J;Cw0vm+D4N={%bx;n3`u3V#~=<;QLoUCIFQYAUNSaO>1 zCu2xX?yW>qUe(ZqD5|QGb{Ivnfs1V_t z%OuCXfnf81X1|RvROK2)8{I6~ST&%jvaqWFh&fV`o#j}&%gpK^Oh24e2q=oiK4uhuR8vlF=sIGW+llYRN)nj#d7NuTh{>X%r_#-PI@Jz%m};5Xsvm z!h9!{Nbxj|UY#?ulUJ->$yZAK6J#j0(+iq2u6`og^=K>YR^$f=SgQ^a^lrZ^ZC4mL zNQiAE6{Xi~fYA7_G!SY-2vjA$t?;yUtKsoezQbqvZp27^k?C6U45Xpx%uE7x$yr0y zi#S=E*T7xFA~^o>(t^JBz=}(x;qroDLHZhV!x`Op&VtPwS*jO>iYu(Dtx!%iYBy}D z8OU1f$XsO=gdyWuxow8CnRFZB^nN%zt6sBpDhfdhh;Mh@HUn~ zT6UQ8d&7aY`Qg%NFhJ0RmhJ~B!dG3Zc!V9yt6Yg{ejV#(o3$QBwKS#VC8b9r^c1sO zDSdZ8ldiuNWdrrV%3N+UU%*F8FEJ+M8pk))b)JT^gu3E5gy#bE`3S7Fqr1q=W*DfG z-4WC!$fv$!Du~e{o=>&CKT>pqWT<^6xs2Ske(V{l6_ek4A<0^a7!L;*H@e#p)?drr zsfOWLt#ZED=;a^py@4*Kt*{rf2VoR&csY0YFt3nm)~k~&Ug4ywXDWATWBO8R*-N0T zYb(cHD7Y!83z#K^&Oe+fho%(ds@>wGM5|pe4OL%~*6GYXRdn&o(#UIGgjc99*E{iS zx_%vz-B@t5X6@w->|k?(h%Y;nBD5@X%3@2Hsxr7o_Wn z=TEq8!*QCgYJ&QJ9Aj-`T?fd)Dd`x}&>gW==SNdw1JH8RqSX2awbQHFPb}_^x+p(H zRjthC3hNUn7urMje^0deV_iMgyyLy^fgO>qhK|Q@f2p zFy%!4-IJ%D=pVXczdn&qaM#~g@W=*l{IONVV&cUwAS%9vNO$MemaZ$=pYw(A`1Gv{5$Ky_c_5c(k?TX{6g#nNu5u640xw`QBFjN*#^ zyd>%idfLDr+3IAONp(`x#Z1zRW_@OqU6a*HuZX&>J(PuN3q)?CNb= z_6ja7zUr}ng8Q8OghrPvZ}FGxy!=i3BhYA}9_j@`>MHK<5@h$RN?{7nnVLyM-?(|7 zNZq~RNtkr#rnEQNikQ}JGT%;JV!H!Xk!&PRJoyNc^CGt|nQFu)mq;BegLLI`m$iuV z>x|LkSiLy@(KnW;2gA(ex}Z{~GMlqrb5@!BvVF1=ZW9DzEV1@+JLV9?5a|=w@wloF zeE}qKhc{;AGOMe$r?jKfABs@@yGt#E)l$28M9$j6I;KCX_I}q+4y2R8z(Iw$Q%-hK zhkYQqn8s2PhCr$P43ayrLf>W9b+f0OVh70XFlu8ZB2^-9N^KkCdLpsvSGXZ851`|Q z5?y&G+Wo8z8e&N@H`n(7YXXFppA^x%3wGD<;W*^64s^fKzCtmME$HxnQ zY%UN-Xflyh+Tvf-xPGsAlADveevFwz;omiwe0{puSkxbp_&*P2RJux7==W`9G<4># zGK0pjkudGhI_uGNj8SRH+%W4Mt7tIDE@}~(cY!Gos&12{lH<=f*E{=yYThrw>t@y1 zOj%YDd-)Iir=CnG&et8Z*ITA@*oqN(*Y^x4MV6bobM3 zo2X0;*B(1hB6(iR$m@@GC#)gDK6{)*FlSUQ#37a@gSsd1?Z*!5^%)9t$=Mrt{+{1y@i zUd4#Hp}#cPa^58RtB$eb)2PUb5{K$Q{lWKfC#1kCqc$ejLY}d=;)$bW%g5CRJ-+@W|2$d2}?8m||AlyotVO~6= z4sxA^>v?rIx*&+rw+SUKMLdkz=stUUa}OyrAx&SM`+EQEF!s!jfiNQ=f0;Bx#k&(= z!eNH5N!;kbdGRLPglP;!feU+26v~%Nk51J&A*M$yRtK-TH`8h|=*T&Rzlum@3*E2c zyc~AxA%=V{S9<}oJz(hGUjp-(V;(XG4(}1uL$eq2axww8;%?`YV@ZYR9fmCMsPJQw zLp0@xW>+*+9e}te+06I?PS;~poKAN1)&FESk}m8VGp^H_QEhg3{*y-H33OG7dn6r< z7h!y54sS@B9m9*WG3*Amt8a4j;-#`#$jPYQm_=<#WdNTzi#V$vb^1cF2Of1OF7%B| zxI@;&ZwS@9N99ph4;-#zbzq;c?hw(Fo6eQ(A8*ttN}5SDd;W-4Lixwp1KTuSR_>;+ zC<5gcmMSgZB720lOS%hQV|a(R!qAlYACJ?( z$C9q^@}%n`{=;xd)0Oq(eIggnwlwLZ0`n?M?el!|$C4Smt#%pH+O5*DJO_9>CBnec z@xC)GwNoD96KJV7p`7vuQLfp989w;wf9qU&RS!7X^z-gDp(Xb`*|6Cs7hM7;($2bN z55`>dNao4L+QZ9St7G2vBh6T=?q-~`AYG+*=gR8gAShe$Q^-(K#n((ZVa%zEQq9@f{u1WK%*C;3*M-8%vS6nx7zg{`TS|>VqJJYM<1v~i1QfH<-B&on$_$a|4$CHHZ6vey653pFfr~8?& zCegO~9d(pQEd}Fb%t&X9fgfyX^YTLx%q}eH%JVI&I84|?CQY^Z z%$V@vAF52U<^gp@n6ftP#FSBV17>GfkuR(yMBS-3)@G4UPa5`4v@t{P0}3Ua=Y^bR zU?0^X!u4x>lUPj$3-mam?0j|_!POTgb#rIgT%1ez>g zG)wz6rhZWzpv!w9I`wO0%WySu#f6@bdBT&lp=v9MxB`OlbwsFa?hdpgA3zw;o&7h zw!s{isZDSO_UHmnKwxwls4*dlt9{EAGhf=>xOoEc(hmHbB^G=bO<;-pX=_bp1oPmK zEB*Zv92jyNnt6p@A#{+(*;)il7j@YL@h(wq`1qinXw$NFC)vb41<9Yr>HlVRbZv84u+#K^yK|KoxVs=8O3;zMED&qvGeiRgW@mI%r*G^kxMR%S3i?vt} z#Qt&#+I9fJw#2#jacP5z%A7mbIdI)Q9x=#j81AQ7lwbF=uG)N+$PHjHacPU2f{E#8 zaE}J3`KCCTrTSrA@z6q;BC#=<%P3~wnVTX}o?PthP*vzuvH)G1-WRWIbOgjXoz4eN zW=lfvRWgEFAJSpKw#r0XvC-YazyxP54lkwxqQh0AbX2kT_a8$xci6nl!>$8Rz1@h$ zVqj%OFokqp-WNh^f^b8L0X1H%p!04^UrFCTTz}r|Y23|B?q?QPC#&l<;y1tYckLGp zA>~Jnd@oi+@`&np+#W4M?Z;ZMP9%hxJBP`n+Q;^^Mho z*EIVGt_zq-S0cdNi@;Yq7-vw=49&7RRim;+aqV^IU6(p=E%qSLSC*f61iL&zbXdLU zAv14a1K9M1mkfz!Vf4zt=>MYP+t6p=GUIOvC2rM$1IP78A0scDor~r1{U)&XAKYNU zQT1BHT89k1{gff*V8HI`?8XyFU2G`g<`=zFq;hBM(f9?ixZv5GaD&XeQG?brAe3Q1 zc(d!3dCZ!FNTFw_2s(5@JkNCBgHo?cV%^UGJ?5nGOFg?j`8*b+bMx7|obb1Aj$bB2 zLr)WB&`q6c(0R|Thp(JsG3r4ry)sj6PvFfy1?hdm_%DySe?^(u1V4;AEGc8FB?Iws zc0Zokk znas@tXYHAtd|9MmgR1bfL1P(!WLyU}s9rxrf0LO`EZz>-#CZiB9LE=;c_4q&kH){d zFMU;r=D#`~6&d6jHG~VJ9cEr>uCcwotgzH`4F&{Bx+>aWD%>9eNO@_a;Ip=>3$ST8 z?D_=ZKR+h^EK21cj5TPDpU0o_Hq*X zr)=A{ZQFL8s+xMEZ^!i9i0FHWNhOMtVgS$XhXfVqDI1C z&#RCq*|V$%9IF@;jf<5t8Q+QwTs5j;?e|#7OljX1BVs)-w4SrlcgyXHC_bkiV-RUU zGCwYq&R4d8%i&b^#ak#PD4rJ+8>OBO(U4wpb7QwQS@{f15+8IIJ`pTFvsqvyQ==uq z1t)zOjM0SI$POQr1Ja1Hh5L!ZeL82=txWxHCX~jyVC^-?Y8(>FA9$$wL<9|CH0sCVb4*6`g?k%i1HS4%449a7>|I8ctWtr^R7{cII;5!b%aSH!DLe2o}%=!0^2#d)RH@);&VK8h=a=S65cR44ijmjvW23`Sx$H?=pgR^74G zTqtnjM8S)ogs{dHnbKfCjfe^ywO2!VX=AEwr!S*}mtrv-vycUWPxz?8{fgrr+8Zf- z?-V_wZmw{7H>T%0Y%rikUC&tEE<{uzCDZ*wS_rd#DZcl4QPD<4i41DUv z+jr6t2cTopaAe^YioB#jhAR@dq!Anh)mZ)O)0`N=sHuGQ5=+NrPVI*K5#tI?1cke? z>N`t~=S|KIYDRGI{f)8|C`q2u@pX;yaRH`Ox?L%o&?MxHN0506i~AFrMkd*W3w611 z?W@Imp0Di1=7^_8Uy?E*nk74Q(zy;x=g-uZlNz&h^Q+LTa-M7r^XHNj-VbmoMQsPZ;sDn%iL6MS5O^s$9 zIXtPuQB_Chx$K(D^@R}3WOrKe0L5QSQU#4Q{7FP>u5ojAXn66wEJT8>Y|IcFO>%Cb zGMCM12QL?5aSFHUz0UcUKfS5l^_IqsVhysBVXwEkL6P8jd?dKptp+eRFdy&g z;C#!`D`RPzsl3cZSWa41Y_m*C`>(D%G;SZGUULL z&belj;%P-DCutC~1{fftxj}pyd9I&IlkxBU*d>7O)JADYJ^f}ND~g9jdj{kFDgjw+ z*^S;;X#2MCbNLIk3F@YLmE<_W?VYE}Gx#h<%nS4HO$2NhR&n?J@n!pf#_ zRHEgi0+?4$es89s*aeK8E2tEY{8jT|k4tEHl!Lk?JJMkr$Whx8%JnEbD--b*(+jGb zaOT-irXc@3Aybj6oYG5=Ur=k>aiF@aEqPVg=u0@ewiwLCz5UYry*n;0=_*|1qV7CDKoV5e;_9TFB`M1o>=P#sYLq%B9=ZHq^#qnU`(k4c zr+J^AjNJUgyOj_hU^9Lw&Ds24Ji6K8smRE&^P%adK1^#yKitJ20eNlYtbR)S z785s(-zI2h^~!*?e0IB*QqJifK7R1@G$?NPB(`)(XfOZ!rsO|Jt7J*QfVEIGcy>2>XST`2{rpD7_=ndqDeSZSe%=zF?o{bNEKAGYVop_9^dtQD~&?pg-6g!4t@ElZ84a^w>17yrvnP7e$z1-U>f1We=vW<5M32u zqP(hrVnmOLp7*v z3UH$?uU%JqCw3sl)z<@6LFZt|E!o~i(k4(V@xeB=uj`{ zxH&fVbKem9Iw#RYL?ga-U9fi?<{RH9Mk!xo7Xq#sZLrib{j})N*R!VChC;tXJ=M}u zc|whf^sBxx{p%)n&O-qU;EE{IhQ;ZGSS{3qko#BtRi{vGa1xyu&gxufXaz@42#mg1 zv@2VULF|!g?#OCmFb3s_Y-9E^o|}D6cuPv$gV-0Y-Zydc9OghfZ^wU38<64$5zK|y z+C7Mr#2qZ5w71mgLklZ({6|uBuo5$=w}MJerC|>(LQp+7Lr)xMedMQGcUO@i%i`pC zXxI;+`7H!7(wtcq2tP4^YtHS01$O_~ALE<=FZ64;*_E%g)t+jBusCNGC@AaHIVSjLp;ziR$131V6jH>tp7r3o`F{Ahg_b+yjF*dpklD+DFwwA zrvk%Q@b@64rJYl_VaVKurdDdTP3$7=sMDE4>esMO-~^)BGx^c+$`la$LA^@+S}LhXvT zKqG@;!aHsb#YSht+UmaHcP?el)UoWp6MpM&=dQ$QVU31g0_uZq@n(1C3SZZowsPm& zlzXkT;E(6G_vOryCCzDqbqNc%(r)psULx=hazyf{O}DpKh|eZ%CJ}#CCP3dW)ha%8 z$DN;P8KV2gkID_dd6)OW4?pv7q0+LBOB3yN{*CxUJQfdtlZI+JN47$yIw_y%ctF^ zQ0BIgj1nKPZNEgmJDPS3)H~)3NY!s4I~q{i&a*$sPrD0Ys>?O=PBy3f!S>*ey+KDm z%e!*;numpd2N-{2FHdi!7T6Mnw@H!rV;8wsiE@4}N43IKrPf4dXwRUXM8e3Pg?43OycFpkDhuMBfExTT^FNMA!w6@o$--^6Bomm3@7V`W@ zH{Hy=8QhOq8CktQ@7MAGpLfmCg-oHSONJsNjG#z62w{r#+;}69_p%Vpq=qZ zqxGz31C;gIY8II2olXAPiVOjbK<;vTWW!9PhdvnS!L?C_I!be)lTbUsWK~RtXVwWJ zh$BsJCj;pThE(apzkT9h$J2QIt@az&8$J0d{HbUg}YOI0jsY8Tk2kW_C5m36fQHnkyfx4ejAggOtvI>=etmz6$XRt zBJWgPy*a1FEJzT_spbF+narky*L(DSQFC-%0krFM<{XZ4_N35w4r3Gy_&-;v z`mAhy#^}KPA(z})XKuEmhpzG{Y^7Z7JDrKCxd#-aEHj1B+Doy1R!VV9&(L_g`+~6s zaS>wf(CXtD8pBcym4czM5>FogF;V%|7@}?b1*R>>@v$Qkd(9~;mgY0vI_5m>OVNa|>Xr zc5_o=Hg{sclwWcedRrkUJJBMFtB8nmR6#@=4^d4o?IAMi-vee;Zv@jduMxCjw2LZ3 zOuFlbkUo`>Q>vRiHpU*12Wmw}UvPI!H~wwm6=awcTa1^VAG*Z*kVyJ_R8TEdm{o^V zbt`Sx;>0c7{oI`D0!!T)hfx-zF<31l;n`f(N8xx9#Id^a0v*0mSgm}6wWW$~-pb`l z+6iC6kA>ra@Os55g3}FI2g|U0{3HcGp8wF-2o68iSfx2iQ=>)rtJ?E%o&PaI|Js*ai#)B% z`Y!Q0Qwd|pvGljxCCNql*}VKiIxwd#oQ{6>#7lU2=FQjvz}D{HG2TtOL@$t;Egs$i zOFAZNs3oPr3~!2OeVQ*OS%0Hm757cPGs?>MB)oA7PB_0I9{_Uz0CxzaH|m$z1SYsA znmDlAErJmmbnLHRg3s$AICn)fjwSIZ{w45b&=d?iuU~ghUx$=ggp^qvaK^(X83doV zGuk>14)M&#N#eRSh_a7$-r5tK8kCFq3a+t;oP}%Ahi83|HsXOcOu>ATDkKpjD6xAeB@S4v4yk%K&TUbTZ(wP6 zxO9i^XMFvN4{Q=WaER}Og1Q+OTr>v$CQB0@GU0xSDiZdEyM{zL?>P^^7hgm%kWROS z)QTwvCYpiX_=SyltQl+VnGs<}`dj3SzJlMImhd`+8M53_9AnDn&I_m%+UE6!18V`R zWdXro8k~O_LeYqK&aDB8F?HkSzL;Ag#7w?%VSDWGtHpiL4*vTdkKZ`*v+_6l9SH#d z@Oy9!z~0H6-q69&*us?F%iiAF)YQS$iQf2slxj(NRToQ}|2h?$qB86LyO;mgV@?Wo zfwHyqm!K|Jut`cKP?`e))YgswiF^W224c#QTckXf$(E_Ab36Ro=XhGuW`Zn$;xKsM z=6;Az>~~9AgjgW%Ew}6KeCzw_yX$^CZU5){4W-YUVz>@I3DE~>P*29S0C*A62dlpw zV<0`6i2il<$VrbbL{6tq6q6fykW^;$u3hKlzzL-B!B{Ty)cCAl2u0m!&XF~67@`I; zSe~;6IU5ZEHjW0uB55a*wytuo?Pd64q~UrtW0F+M3p&>{6-{P0VHdU#t%?&hAV2UF zo!M-?Y2|84cec}Z({yuIV`n7v)a-%zF+jQE2!yLTQN7+Ac$bYG(_~uy)6JxWIm(FFeGzMJTw-_9d^+g;e z{Uc0+7E`18A`ONGkb-Dun;n=9jF$xvt-)}rpkZb@k<6Ix60>@c%YsUWq*k(C!;9Rs z_xApz!vx&eGBHnHwhVWKPCKo;mG5X2iqWb#6Wga_T{+hduQq;J`>@GVGFO z{NNxMjFIsiDQUY*C#zK?iZeno=CMo!j8#xBTT~pm7x#CMW7K=_nL1;5y;te@LF@p# zF;wpe%_)elFihk4gU{Vs$6vWE&XLbMdGX|4f}hVl{Dc1lE;SQeurQ1D#2&&YW*NX) zl3RKPy%s@zHljM9H-snW4b+O^p5tJLup^6eItO32ATf66@fWRZkwdPGC`LI_@p*@1 z_#?iqk;nS!JW`=X@JxMUpu(Xy?-bAI{Pb+yD4*`-2W8r-JXlTm7LvE-PuRSR5cIHS z*|stL9J`wEF0Tu3K+hWXpK#t;y%4+Zf4-do7od-0#nBu+BxZl7mGiFv9n~DsvS`+D zhmT!w#gnA-$BXCo7N(j08f6Gix&G4H!hQXRxuk1%2ITjQLktf7+i6eEX=%ao-%9tN zkT%W#PsLL(bg@uzvNZjl>9Uw;S*QU9gpgf}W-aO$2)amV;aLJGIuSv7gu^-f&7z{^ z5OL>yK%Qg_#Mc>y+xz}IUj0{ae~^6mU2y@3mlGq@+J{8Q!yKE&plMINu}YF(Ok_&Q zxNFBg8#6gR@xauY%9vI~^zd{#v-7kXg|ezx)L#NQ2!dei>UBD)b(SrJf;hEgrBWkr z8ja@QH+*dt*BZEzm6xf~ruUTW@*PFcZ`#;X{>(yuezkTKZ zytkaa$^SGSs%`%AgK)l6gKZ?#KtX8qsHm1nNWg5G(P*mk)J72KDJa%9NHqzO8XOxH z=CzuuFLqM7 z@QWoVB&Zm^&G{?q>7ab3$H#pp43O5#cqE`5NTOnb?o^qY!A|BX=D|ENGv-Q>(+06$ z8mB#3@^YD$F3cBt*>&m9x>1&4O2nADcuz2u`D{pGIx>$JuB44)sM%}H9!F;f_B(9? zgDPsxMvH>mP^VyCMrN#NsLWnOI1+m;vyj6CNfbajJ1tC!$e+Em-R*W@6jf&J^J?bo z*?`WgVl6%iP$e~VqFzB(EwFKv^=(so)#Fo1of$Ep;l7yze=f3DpSyIp8_$d%xACC#xlOXNV z+oZ`xWvSyrWzpf>R8mN>Di)KJeq9eO-YgM~sn{wDJYm|*r37QIqWj(3P+;iU&Q{BG z3zsxpNOfv(p;R$Qfd_noO;J)C;z&mL={9;aYa`4f2q@5K!qOX6hUTml=hfI0u{IfG zg-+J792r=r4>U!-qKK#ss}5#yl;!IVbp=|+&>!N#GPhOaxq0fT>MY*z)jWDuXe_|a zQU(W>4EtFy(zo1AAWUzp7wiLX%{d^qs`!-c6JIUgG1@({9;w26r5SMWd5udrpt;x3 zOgf=x;{W;mdc3HTxXq?$u)_mK=@{#3u26JuYkyOhy*eV`HeN=RM?HyNmr@dU>rK&; z&RRHR(HVe<`F4(KCiDs-=GLhYC}l04a}6#FupnYo02KtrAiPK#mA z?q8guhlQ~d`o+u_M1HQ!39E$Y!0$m(!tcTHiaoc7-SPNiSslJ+yDW^j67%Tn`eWA? zfYln(iWg#nV>W>s?y#V45fTMOBtcA~gLNXh21e>iR0zNB=hAk(NErTgDMUqywA+XZ z?<}Vk@<;`Ilsx|(zez$O$j=9rLIe+LpI9Sj(7&?E=o8Q0KQl%Aa`_GHL6vIoQxl|A zNSfm~X)ykbGpRVkFKY0j|LEG-dV1UrAZw;4>F@Jcg%Ux(?p?Zwqh)SCIyWi4>nl-T zV`*B|w>C6YPg=1GR!w=29-o)Adg^llp*4{xvF^C@UR%D z{w3FrO`OgT*(NJD=W*jk2Z!6;UZ~+3v---@jPdD4k+ooEj!6%5N4*=+DS9N@JL0Z4 z$jdwCW;0HFXN*4jHvi8=VTm5@X^x?)_KYt#wh6>>*7LiEg;Q+~sA9D25M#JO;on4% z1yAsU%^dV%d#wqUR2a_*4+dU~oDLuAXz?+m=CxPuKOYh|1~Yv3C9j|VhV7_Hcu`0A zPgoKL0D$WMQYQY(mY%BZ@JoLh{UQH#{GY#|LD>335hX9M1rw>zpau(BGImpF*Z`Vl z*(}m#*{s>rf!+tIj*$P&P7uHE2iF&FPT7z;?yKeO447zO_lzJCxy; zBQtlOeV=iE_ucg~ul;#k0rBU`el$YW5^3_<@AHGFa`+ouhfY0DR-h{qhK{5~;_`6$ z<>S!@jR@ZMCdkJ#7Dm@|A0HeBH5Y@&=bR2N-j-WPg!fVaLksEwGWymuaO)*1A|leQ zFi%cIR`3@P#>tu}KU20NQL3C0>^!f${8~)K)FG7nh4F156cf7msfQLgzYZ~~{KPGh z33hNoBIL@Iv|Qw4d5&_BqozV~8AdePV@o2K3$^mEwN93@bk=r^mQbTxd_V=PE{sr8 zbty*`>C%f!mpUsvRd?;_s`K29Kq0nbAZkbkp*(C0doaz2TvI%4oG4oK4OBB<}!iO{`i!#Nk{L;;L zZniCD<2KG86n6rE9npj+3;7bswipPN# zD~wMnv=5}Z=^i;>-)%~gy2SP?r_}rZY9kV5Dk3OS1_LYnI_yX%$L6)QQeL0qjVhLx z|yj;aVRP*iz+8cghxicx9j&5uU0a%Xqu`qf7ONZPJC>0xMdhc@vpAi3mypd{!L3e%Bb$cILd9uwMRHh1f4hF4y~&Cp>s1>!5fhKrdw=q_1-v2uM|9dw-8WPXZ%2(C?U5{&vd<& zZU;bYD<8U9*ue$@7=rVxZ)c(2=tny-c^u5Ud{;)bTbVEmzT-{3+zK*>s1L7HqOW}K zAAs+^xYCY67CzzCZxGz~j4uad8;lW!Zy<`9*nQ>QzNW($JzCd9|? z?ncl*26?>Nit=pI=amO^MwcYe;ufoD4O0%UC<&+tHbmz?;zed`i2F*Z{nixE?}5e| z2f;Cii+d2MY;8bvJy(B67Gz?9^y8WJl_c~P0rVHA*pHrCgO9nPPP_*mfDPXh>iz}8 z{fnmi2NZH*{D$iIPpq{6mMs_U17%Z8a&})*{y)!r$(=~)9fc;3<43M_9;cqq=6pJ+ zs9?V|7a3%7lK+`} z7LKU^MB6BW!yW>ojvpRguXOl|G~0^xj91+5VOp>;Is!~MXVQ_npunB?NZmldYaNqu z&WP|J-i9kP!NQ>^)PyB@K0F}SPoBU}QRZ6|)w4a23-s|5mns9-WIAy3RD|zT$k$X1 z8(Be=BjGpo6st@O?yreU*J1Y@gbg6 zVwiD`)Pf0{tirK*r} z3%-flO@0m%s$$JxRXsQ8o^D_yyW{31MYoANjNG7YZOE8kqi)HkM5a3`xLT4pMS{)j zjsMes>$s|2{~pBu&X_8GBO=uQ7rOGlOqpU-x8-qu(;vF&rlpgq11-QrO){3$lLHM) zL>fI4VRLq!LMbf}w!`b|o2CwKW;r><2SKvY^kg4-_SKRBp&)-{JP@K;-%29>2VL>^=Ep5a8wklo%01` zDcFqz#0&x)YeVTn2sMp{;H5f*#WXhJq&sZr(@#K5b8N=(07Lq78^8809FB{*YC>Yt zbv_!5p9F*Y6>8cR1arK^s2!hVKC}By5E*84sU?ix$ zWK?2SY^+>kta09>SsPt-3Xkie3`5L5%s=Kl$;y%xFwF!q!nfqP!fF#$p$G>_WyF+| ztLO#eb+FVv*qm+4_$EVpnz&dxYYhiV(J5Qjmz?A7!e>)tM$l1xAZ z+o}XEAUNe8+pKs*ugTkHmigkFs}v@Z){2ScL3T-(KrMP~g;{gbpu>KH2_%GwV`o)U zKc>n!xAhIdqCfZmp*TjU6t5V89%BUUN+_phThh+HIDX{Z#N1xQl>MonZCpl&`_IHF z;F3;OZ_=#XHuTI;h0}m4_BZm`>DE8wK;(n4)=Ixg*HFMfbBj)9LGFcHLRI9@l@hLY zzEL=Ez_F3Y(DI9ROuP+o-DA-OCnw}PHVBL&QT2*SFe~y+psj<|N=8H}yNiCdAj6~q zZ~!xx#3!`t5u&K~%yhv#F9{7R`xDuaL7ObjMi}Bl!lqacNBO|}uec5n-Y@jw@3YV< zT1z3QVQmXp6OqvQh6QaxYPZXEh>L05xEf&H^cX$I2g-xY*%%rSzv9S*xTx?PbF%$` zg{w`wMdb#lBy~I+(GJxyp;~0PF;G9!ay_7vn!Lm|-!S^N-+)#@WNoFkQjxd?MGJaG z{tC(ky~r2o3MQv$_F|0CO+f4|@V+~cYn4Hn;+P)Pg|sW=zqm+wFZzz|=!!B8&lY<( z&X_~?$X-7W#JNYiwNso#|+3_!qzb&O!>hi@=x_nb9*ga>bE z>h*%$EeU=0=|g{Amj8*mpYx|1!MZ@Y8D;AB!D#pCz5v`d$TJM|2qgMK=JFRHkrf7M7;nR-lLw*0T* z&b(MY!Tof7EVAFC761WWeY$k>-hV+8MGXLXGIY#|m9eWk^TMQ{00U6r_n;wH^ zlX1li<*>-rC<_cRi3)Y3e+beM$(sh2LBrxG0};bret}dvt;l+-Np_p$<^X>y7c6$r zGFAUk{P@V5c~AlZkTcu!@nqWf^|QzOwv)K$?cq=!0B5w9_)NSQ*0+rgtPvOOiUAMl z)d~anfR8>j#BHmu0K;zL%?<>L>R{j7=&dIjUt}&IG8$hDVK{E!t-fv3s4UV@4+fe^ z56!GL_}&V{`yLsSKPC^HANuf=NiT{Zbd{LoJ~W!0m?K(0JWPM$EhSn%>|PB-{JsU? zI|jp#I&?SW-W&5z{Jl6NE$Vc3BZ&kjBw#m1KD&MTB4%>|npxA7>|k4oCCfO9s`m6z zWGP8g?#g5bF9jinyiPI#7F%Ry@U>@mRZiuM?(!7mvsDE3e3znvG`gxX`s|L;Snhm; zn*nls{yvW)+^};NGUD34z134xYg!Z=(MJ_T~K>H+$WG7DZ@IgfNQnNM9_t_G8Qe@CGbo=Y;^7nT)aEQqtIJVEHCW2lq`7 zZ$C;q9}c5np;eMu<0_KLa!7)M32sW2p4#-V7#pGzee9W5xkQMrwPFrMyXW#Gk{gRB z<$gb`KbFvz8p8=yMY${n3Vy9JzniNXH3I1=j^1=vR|`YeDi6Q`>2Vg;ajVs8rPcUG z-}2)WdhK1b)pXlv`|?ti)i@e!jeMGFrt~2E7iY2Syx)2gE2U z%wBM`Hq>{pJL*5oKZJ=J@+9{K~{({O2l@LRgAq)#?lLOcuFsPTEP z`5+Q!7BtGB?sxEF_Zblh#^fQ}{27g8rX_p&9>H$rqoboEq&VMLAw*rgrF%4D8b{iv z+UdPT1}LXx9*ZkubITZTX)2RgYP6fTfK`WvrFInbYD=XhEXvfIjNbJHnvBL&W>%ns zG+JLk@$GGqfq|H3P&NC}!M2`6L(e`n7_C(zMzUjDT88876`1ZiHDa+D6EVy4=j%<) z^}3oZRvVRkvDNnKHZhOVmrk(a#WVRdT>e4p zOGKy7;i}2q&3N@CpPXAZgSFQ8<5D^kk!o&~n>e1{A<8%E(5qxJ&3XF-{Lf!B*Dyf; z4iS%Pll(bCy6jK64ZPxj9+A95qAQ7A@Z@wIhA#Yc_K#vM*Ase z`$=en2C5z6uf8DyOg@M~tZNEffS#Oq{Mo)K*2UZgOsBR_+EJMMm7UB2E&U%CIS%_x zd~$z;_iAptmNj{{k`D=bj&1Q=1_Z3b^b05WZa6PI&YY@!q^do(@<|LK*2zQyU$MeT zGLbMxkT1iFR))q%6HjuDv5_2^hiCfT2UdX&$%)b>NEcrbp@>L@kZs%J)ZFo+CteZJ zoO|q{3&2)I3G#z`W$KfljA1;o0h*gge9k~}hgx|ZIeA3kclUXQQh4ay$l_)y@p1xC z=7|7Flmn%_0SbuJ@?_`^0j*vlY>64Q?BJK~H^lP|EGM4Yf~dPql1Y*dkCYDC_MEW^ zS{cJO1(5Ad@0-wHew<~mrmFLTc(@*^!{tg+>y24*qHlBtYMQV+NL`9`{iN5fdo~&a zyOYXEucF-Ojeoj%O1tNQUvcI}HPLwSo=Kb`)U4A*SI3`uY0`zKd@|el`>S9X?KKy0 z3FnhyeZyPgjBwD;Dw85SkdU8Jjz5^&&faF6GHP%jIsKp0~x2(CvqWj&+BptqkI#Ily~4h-2-~EG{nwWA6go zFx9*{SOMeovf*uwZjpBkdt5j_Z4Urbb8unC&=cQVUNAq!fPstVE_Ce#;dBMzR>rjw zg5MCJU|o5F)AG8D!nNb*2rX!zmp4cAH2@gXGSB3otPQW-7;?xl5d@jMq6|9b5^78% zt-o=NCp(l48mF5Dfcc7wd)_aer|kT(B=#}U6{YR(EI$gffZW!lrw{?{ z@6Xk?MlI?};R5}#IQ0mDQn8|B02USVgX$bJVXr?FxgDj+&^3-py+}6kCDoK4r*p$m zAGa%N`;#g~a=-lgmZj$%=jxJ+rZek;bDq2YM|4V(uKQz6*Z6nk6VzG5yh+yCF1pJG z@7!F&W`vhSAg?fP*-;@`?Is6wT`+pXDW1P#WA8x2T{ois|E@3oBeP_M zU_9l&bEy(I005%@qs&U0{FlnAS^gS_F#N~>$%0h~sk9Jb^WtF_S2kAE>V%4AMJ*B0 zt$uHz1i*y;!ehhNe0N_}H~p^3y<>SxQ;a#cdtSt!pW4|6K@tIYv$xuvZ$EoxA8Q8r zf4*P9{k`vn5g3m$=Rt*O8fn{teE=Zuv!V0!d7x<-6j3Hk>ousZ2!zJU9Rk;al>pR- znCVkF>rqsupd%^)X+Y{xS5>+YhTys|`lPA!;0l0iK_Xr%B3A>~VpfMIv8i@c?$rS7 z!RKS#gLgWOJmqO_!S4bh;Z=Q#^2TS5o;Y;{JzUf}3yL4K49Q$4>Z1pyh@pJMQi;N1 z0{3C4?!=AFQCMKIG$-uwSfM_~7q*h^WG$3EE&Hb|-kCE^!`)x&`o}R_0=ElGSd$ha zvF4RlWgZi2&|RrhmT_oPW{}G1wW9RBiSDw@T~3pwy{uW(;@lc_5!cm5>lhBRI`i@p zS{7XD=jr&%C0;xau~2X0YL0lcot~C7#wk;?3)RTXb>>&|P8TiQXTd6>iwmbIAn-af z=c8f`(QHuhsV&AYs4;NLOylWnBn)0Zq@aLeHPO`b)&eV8jMK~WLB;v4G&h7YXhK|U zW1+TL(Za5&m*L)@Dq&urq>wo$v(ZKSCPzFc9nR)o%D`VRl=WK(H9w~l2YpsL)$*)N zF+e}9%3v@-r?tw=kV&4e!t+lXpXz*BD`3Pq)x`w+Q@PThUTOeUP$y5KIn|eFdB zW>tJB<={f@P65v6C-)mC=s?=ViB-}r)|+b{-Dm}HhLxUieA*D8LG z0M07aycp?hXvJbMC*x`N7dme<4tW}LXCBjRp4H~CA6K%H$wpmW9BVFAp*KvXx>d6W zZr$jtTvoH^UCKUpQrmV}2}n}Ko~MvyQDJfOlax%q=YLGGqqi6Lq_(pdT4pr!AzS)M zcD_lfBZS5MvBB~?^)0Y}+nREIL0a{GbMu!+g`=3jhuM5lsQ z{O2ct#c_s}UR0!?jtX`Vp}!wR{wJi@h)RZd54;D^doCK{hxIIxjM$WQwP+18H*yQI zgzpBwFNjWN;_w~Xrny3JR}W zdPrZEL@jM>|3l>8vPk06`L6$%e-ok2G_xO3V7&;BCpfGhiHOBOjQgC*&gWo7=c#XB z9Fz`sKrlHV*2Y8e!Q~$%llQNaA^=@QEY6WbdPlMM)Y$ zrp}OVq4XL{hQ`nd^0-IaGuuYuvBtI-hh}W@RzxMO^x7|jDaLmHMbxI3b>ez^Kc|)C ztPo^-e<7CH(!dH>MgrU3?XD)vC9%lK-;r}-3?|_UA~(KfNRm>ZHCG_BWkcc-A~zY1 zWsgIw_^U~siQHi!=2DZr##Z_af$oY6Q64HcCk4VT5omN>x6q>4iqy$g?`VS|wA7+4 zgCuvvL-k0`umH`j)qnGFK(u0i#NvW-fVUfL%QE|ZZ$_;@CrMej?@NN05f2RcPvIrl zjh4`>Nt-#M_m9lKkQ(G~!GuzhKdtrlb&p(F-1d<^_e1)e=UwNHm!w2zu~K0hV|h7i z+~KWh;f_3mNZhm8yqBgTO-D?pf;)eI+xC}%MxIB-M)cWzPbVF--D&(@PrHmS5llnl zys4h~Kg@_fh%vD2)?=@5ZaW&Xe(xrr?_U=CUI9n+3o(>nT!R3YYoV4lqcEd7v9cC* z>Smnrbkz<xklT5%7&*#)v0HiCR{i$kh8qVR zjB`$E9rMrs!czH<&?Q~zeWd*|(;;vG07(9iLiZme(0@st2BZs$Dq45%>IEsGrNkN% zWVqY{q?o89q*x*X0Y+*-co0{RXsHAi*LHm;FyN@Ei_R!!4j@W`qc9*UDlYO+*qz?I+IcmcFX)xeB_;K)L>Huh&oS=WgfE*KXJS@Zu~W2k2kWd4F5LsR7)E zb-FBW(?(&o-Lz@lhIVbX*Rd15^)2$O9Kx+QaFlG~-Af{ME7xY5A$P+us#v|DhKMrpQA)5g&_@}hNk z6dp_wSg?N>Fh*c6)E*OYz=GM)r9nfiN2APwQ$vjnkJ3Px(`inR3_m)9#^O--2^4TR;z zpn*A7YPPG1y6biU3JtZT#oW1PGS*GcFIav)jfdkNEsA}*x{X48i-{LSt&G?AMJ;W$ z=FO=$3hLx#5ly8=Rz@M2v6DUJPbQs*m;n_pDjk)2*Q&<1;eP@R0L)E-0pA zFH!Hlj((w$%xob#I}7^?=|%*FjN}n-WOUC>7yo3ct|zUYr9T?Feb-A|sH8$xu`b%G5bA49K9M#g9^Tg! zWnN29UU%^WMdQVzE{e(b zvh>nL^HrVd@wf$`mtlmiB`Ppyps^-3TLF1)7d(bvR$fHcY8KY#npQSodpFCdTFI(v zMHMgPWF!@h4kk)C&0K)|9NcsK6`2kfzzin5y|&gUnN5m9e_U)Kffqhzyv(Ax)MU+> zo@OkpGsFBFt5gl-1!*BaZl$8vJ-03~AG48&}Ntk_87htA4~K+D`k6chrYH!~RD9i+A<+MDeZX$?Ln>pMZC}#b;$sercdhw->XA zX;^T37yhBcj&-6I!JdG_{eBmk5)`)RIuL0<-#~h7PtFmQzMR8)8Z3|eeve}$$l6M%g~fJTL(7ZV^3 zP{8a+BO=_eMFz;yM?X$z@`nnIFy{ATnig|s(_gf+4ek}$04V`-Rk8_52;I0whpVL6 zVx=@wuAgoqBJ5dWw539tiHdLs$)VL04Y78WG*gSQi=U^!f2E12+O{&Xa*cAj^5&%2Bq|0l`-S4Y2|%T)>d)0XBQ---`ZKzvR@l+)UY60i>C7 z$jtzq8AkQ!M64Z@ciCJ&KRgAen(F5H4qytWLba6-iF?54`?CgaC8P0NiE>hJVnpc| z(Gk2p4&y3*C9`p7|vB#4o11IxSgbKBr(WMtc z8Ewr@{89RfD6tawgJ7;8=^&2!8G}U11o09PvxIOc*B8htcFQyv5ipMpu(q z$^`Zg_46h#2v%xqaKsn{$?YDuNOIAO*YnBs8PQpWpqBJ}pc}Hn8&OI(BcPkKA6yBw z>w{|>c6)Vik39adq&vdYxUli6ZkS=>H!eKVT|DC27sr-S%mH!-_}nF6Oj7|AFk580 z!d|5Z22pQxTb`{{^)n1NE$k1e1{HHmdM>a@sW68In7#jpuyYEJv|Af=Y}@MCwr$(C z?T&5Rwv&o&bdrv3+nuB{`F_monQLax!8)nyt)r^-uKUrwsG^J9oaxFDPC7I>R_CU+ zRJu#>$ES2EQ`?p8EPU5Bj zUGvA+ z+b;K^1FM>D(Cx{dB$3};50^3ig}LG03VFHn3@s1JH#5yFe?r~)&;4Fb%}rdk-eGD! zRNupEAMFcMgbhnml}2G@jD!^8f0L@ndoF;Ah`;X#Yb z*Pfgy^GXOKPsDTxme*1Lkl#BJ{LJh+U&C|FEA{=D&3Vr)t&F2XWt-G*XOO}ZH~#?N zH_#&#GJM3fo1)0mpydFx5l%uBhcL?Dm$Yrq#0irhIsCTL6FgtbCq{R&F=1h5Gx}qV zF0bFC!o?_Pbos$HC4OvpcT`!S&H`?|O9d%KgvU zBgU4kceFmcUz|L^$)}O$Z5|sxP{zec+)x8=x+JSdO6y;-F>A)Ox$S{{>bxs1Eskl)YAJ3 z5&FnvJ7SprYv2U-^8oH=`IEq{!G}u6Edvw67e?}Jez&fMCZLYdMQ_w5I`QI;x6olv z=(xihCWfc-S<*D{(F5=r0JbaC4YJ~ zB~d^gnh;`5CyRJFYx4hyp+KZCVrxh5(A(q{k`5&r(c@I=U<%$S4NrC!76XlpmIT~vWXOB*{N-fT*O;HOvnm)ZC=!N zs;O#5}kPNH^JmhB^oI(cjI6w1$iZ<6Z7?W&lxW2Z3{fjL7AF%Ij-+3wg9)^>` z1_Gk{|AM`unX`+Pi>ujxC$qjc9@-N6*G#Xud$Vv;0+BFjk!2IK=l}BWL<5_^kEZ>O-!OLbB;m6I<3Q$gnf-xt2yC@Uk#)xAzmObjwm2rm%oJZ6iJ<}## zm=CohgS#cCNVkWzW`$8=*a(g`*a1Jhg^lIXud+ftMt8McpAG{!#E3m1Cf<_$JuLHm z4D|SKzXHM1ZHDI!(vUpUCPkPYH36n^7aK3~5T38WNHP=aE)~3wDlfhWYF;Grd^7d) z=&zWQ;X^NUh6+!mAxk*e28E%FsC3m!4J3KH?Tl-+0@oyhmNUmmX+#`j`@SXQq3LoL zEonE(U&`02i+avUOss}*IAwWSy~H*dy1b?|(X`SQi|WxqhJHiT;Dz6kH8uHbX^Ys0 zaL5%L^Y~tN5&0sT^aYk`R~EWs1Qt{Hk_Q-^PG|F<&HN?h5HZIyfKBfqEavHB{jG&; z4eicW;ev4;@f%_AyFc=O-CSK=-C=yR`;1yQB%uQ2t-aEbtI|mV2iV#+nX~CgVM;XZ zAcg8&B#CtnxlLm>)j}PAVThnlr8fva;SXwuDxQVak_gz3I@BKYqFbnav<;&kRQYl# z1n4Na4igu91qaTQaG;(}JeBV0a@bWnoMFfoyzNvOu06!PzJUZ;HgWv3#&>H`XdJ$9@#p9 zt6boI7*cudrGF&MvbEba#i%d-=2(kUn*0Sx#nv^g=;J?p;oHAjQP4RoITw(*L1djt zID|XhA|_G3)HOP51iryt!ZNVgAeP~u4}f*shza5{Q(Rxr+S)u6L;dl%U264XQ3law zR3}xb=^@M9P44ckVz)yC>pp}tYZ4yetZY8H_~D6co57%KR&riC+!8X(s1DlzYu3UZ z{x0!48)7RSD;@D7^MhDbZ`k*49^&yMU`v{$ERk?lvuJejQ)>%{eZ*Wh6PI0ObfLwT z?!bGPw~#f?rGYe##nBuOcBDZPE_&owQ1d>g4qUCVC2F^OPUXHJUj6P8r~c4|Q+FW8 zfhTNz)oY+(AO0B6s}bJKKHq@9j?k46thCUU=n*MVZ#@tUb&nYC802dLQ%`}#+2&&C zro?oC!u-e`Q<|c^l*h}xJ@}Y^XcO|f_`YPIoXPX>8s@&vfo6P65NfY+vN~gPL@QtM z$-|ZRwzCiDMVH<(Dt_&S+{+VoSz_yXjhu*hFa3&!1x{toT6R8#EX(X-*C&hT07(WB5{_6s(VOXj-@=? zQ|@2kEVT1+nA<1(a79y7A6O!Df;wVvceP6Fd0Jb_UJ_nzHDZ#$S82y|1qSv^rz;f# zU-j87n0;2;jt;rU!RU>TJ6Qsfi6?Jszxc{>IJ_aBX{$~6@*hzFtAOq*@Zsas8?jlR z#)o72w43H!kLm||m)`a%54xvDWJle}HIQDw9+G^Vke7T^?iAA~hb4c=8fU!fCWbI1 zH*n>yOf0M6oAdtqExy#X5P8>^YkFp(=DuyrR`6FgYbM znV2$(ypU9+WGES#ajCSlC=E3gGJ{TZgl1@o==6RPic+>{te)m^$EIwz`?S93t`(-g zD6_&#?8=+wHLX1{DKIsO$x-a9aGe$@+wXC&VK>?!^aUnxgBNg=M-Zz!@P#Wx(o^t- zNj5vwZxV+kJp^}-V@M$QNkq($-~u@Ah7Qo(g(bZNUpRv`O%+IJz#M0;AEFt$PRpy-J?M(&5xCsO~H`M;w?#25)c)EBXYYbp(5e@{_3Q`>8sB zB?!^ZBY2Tlpzc2Gq4H0nEX;A7I)E(*k&Ukn>3MBpYp)>vLw#ZeX2bxSAL{RjYgp1_ zaOPcD(^`Rq49s!wx(QjJkQaP^)XrYRDfG@ZHuCJ}Vv-2)`8AZgPfiljGehGfnQ)Y3 zJ?@+e*E7i?YXOvJZ8oNn($-Y=y|?e}R%qp-jUDz#=8on&4!E>*)z=nX2 zN^Su*nX@w}Vog(q^h)as8`}}I5p)y<}2x#T`(r!4YS)+*D>-WKS4p)nsEtXwI>gx~o_N8g7iv ziDqL+7-!c+(_fhJGu-$YpX*p(d9dFJlF`ELfFCcP(+KX#r%RF?HP(mh(dJH%l`~K) ziaKtkncAMzjU{fFt~>5cN!|wT9EnDzunievswDSfe6G1H*d*uOz^HZOfDbR02teC@#*!1%U@!sVk+k4Y?WUx1da zoWZY4UYb;kQF_>-EB!-b!Gbn71hU!{xR#eM9^oh$hld3{+!AeM!Sp z(wGR8Tb^Q#w^dD<0;jO=0dMrGFqPG-+ONejXC^*pq)9(g_1QC$=0ZgAer$I=rYT?wE9_wVRqA}#P-#F;v_c>fLq=RcUE_YU?ja@+(6=1; z|2|RtA5BsJB(eV|>#8&T&ju-v?|k;(s`jeu7Qrl1A|z<2ELTVnk{>uwNkH<(NTFDe z9JJhG3kj~_=hB*Z68IdBgy86|o9LLTy222}9SjFK2iO*9xigtA$188RybnDZOG~-u zK^ZyzZaaQ2Tkl&(TN+PG3O9K`4Oj-F4QTI0nju{yj`3L6=tF+S|4pwSbs&g)M(~&s zC;sr5)*X@}&0$ADPJ$xQVW~Lu7|jyYx0D2^Lqbe=C_@L-c@ahs&y}x!aLuYDMf+C7&oN;ZCY-o)m7e@zE_Xp;m}+=&#F6R7ArDPa8pfId8Y71hQQu|Z zy1iJeE$*tlZ{0zqr;ZS3kA+5=MBsvucYoQC6%C7F1mIIO9G)eA!i;LEPEe*xMb2YF zYSb{F$uPtF>rbxMkSdZ^-h_D#2Kd0AFnjRAHeEzDx$9}u*r_mxHZ1e>-qQRWQ49Lo z7mo{b@o9YX@$;!e^b&3tbBAR9it_iEGCshj`w$lEbQ!@n<+5vcx&a#U+GFyrM`mEC z&v{-~wzs#5;jDYYmdxKpz#iqNx?&W2(4gUL+m4pkY>AbKM(j6y0CU8lwuu)BOwL`f zP6c$MlxYk`q30W+tuTcPu^M9jn=v-kqqXQ3sk%A`(Dr7%IhZrJF-AYl5s7mR3~&H; zB5Ct2{^K}!P3oCvz0(czn*ce&ael!^Y=6@A*Bx8e z!f1imt{3{vuT+9!u;nCY&8szNrChf^a?_!y%_HclOB}BQ%J+i|fP@LQ*6X$ywX({a zHvHn0LPSMt`^>_&kA{`EfJPZ1x5&ggewKEX6gtcyo|%~c{O+DXbaubGCpvOl!c86Geqc}NnAfIy)w z)=p>F-rnK&C!IEID{Z6sa7g%}P2OI{95VutM#&es;A@E(d=c-Tf6=Py%zIxK|2+EU zZ?XNux99Dmu@>IVoouF2Chbfdre(3M*z=54yjO;WinwFcEu&7rX(LXBSoA0&%~)wB zVt91Mnp9!Yjj|(>cw>w>V}y>paUM(h9sVPy1yss4++n15cmKS_^4wY{dO{Z-Pid`Z7*O_EccR#n;AV`6dZ1tpIQRB zeATojLGNHKp(0$S+8Y8mJMX;`A8g(8c!%!S_IWk9=c14P$e!=roveVrZyuwhc*y7-u+xMJ#9hmkeo+ z4Ha3#h9el6agn67C>bRca(Y9FsHv({_>91gr zJ`vq!voi=$`a1};S76OOxO1pLLc%wAUTd)cC&2d#4(b!7zk;ltBL0K41AOW7Kqasm zoq}kXl7|tHGq) zN!2$QU%`q#fUF(D9^(85$hK~}Y=H^ZJ=G>ww2QO9g8*jx#EbNabUG;bgko>)f^39W zUM}r~SK?Hg?l_HwN<>AZRPiKuznOI(E**i49N@~yv{w2fl4?xJ>P8I}I(->MMUOkH z!uimuoI8T#GPG#|yIZ)e3Gm$1_}o}&4&{cs(%!uM2O9wF+>9kE->Gg9?5?*Fh+8lf zaAgRz@Ox8)TQqKfa3>7Uqt8V+T?Sc94rn69(M=m@-(M11gAAXPZ=z77go4L4gH>+ zUWOm#MlCfxMv09j65N_;Vnxn|$0cYVLjX%^q{GpWN(vocISF~3F_F=Ob z;*zV_Vhl63!+6tPfYLYOx)}!O$OZHTGoX3gFLXtpm@(k4^J83YxxNu_`!cX?j_JL) zj0H3E?t3p|4PMot`dhhl2QHx$$VL!POLcj4j9DfOwA<`amt#Be)ylBLe8i5} z$|ZpyKA3MSG(bu>DV>=YCuL5nIHeHL4r1lU8XdPxZ-N-v7n;;f*jy`BdB+?h=v5lwo`9@GY_!yC5IgKm?E4HafIuvs z%oPA;bHplCl(`ZLv5AkEBofdhv6!1W5(ZdTB?=(aGl1 zB`Z&;M=LvP)0ZaJqW{CD)Y9%PV3~X`MAdZLcv}_6ublA@sMFKQwg833xcp<9ZI+rH z3oqV$lP&(abHBi)KP20OM*elZA%wTg{=+n{(Dl^BcuL26Y-Euusq)>0w*5-&b(wv_ z$5LCvS;v^+3qjcvu!fyYITjY+G$o3l(QjR;6hrnvL+KHyNRmzVj&_DzS2aZ zkim4V;oVd(w_9jFulCY>AHpzvm%_8Wm%>TBwj4C2A4#;j@m6>{<`|S?@n2F{M{j#< z!5#qSFo^Ck+!wz)x5*B_{_MXZ`T>NEsJ_z&Z_qx|<(CxsOdk0x3I>Wp>6>>FzOnVU zbbnQ+A>=zxZcCD5t0eJZkfT&_y0pO#$}^^qz?=P^!2^|pVO+=cAg9( z|6#9@H?lMPFLh0(|O=%^Jqz67FGjk3-LUx)+9ufT{>+j z)>kS8D&k+DpD9k-vWKTe7B4-S94zm>y#oBex`j%DIIX-0_nfBVtv-mOJtK-&v#(JX z{D>5t_*4uzx-2Q$%(|kyF%X*uFz^zht?*-5+GAK8*VltR@-_DLr}=HKzPT zT=`F~RZr`c`C*wbrSriVyk#H);phk=!0mJzj+6oWv(Y1|vAQgOwUNbAu;jlMV-A1X zKZ>*LiNnDwv#(4ZC#U3!TEVv7>t>a=dJW@~?a#Aou^;!siRECxP)H`86{tu+XeBU( zD37Bqv)^Lu4MJ>}VMf1{b%?~|84~%tAv~w0q);8BG{wP;>~{inRV>Ujq^n+i0sXJu z&BE_!0M@&Z*Y3NeMF;EO*f#$8mE?rYjO>-oEWQ)p&R+i+3Y2O|J^Y)-)8i( zg$5=ulwePm4MSEHMNxtxN+i`RQbgL|Hz%0bJ(Tlh87f+~j$U6E6T1-Bsji4^wbo)K zR!(15->hD~Ze6za=&E;D`f}MZB~ON9UGg>9?z-7_x#9cz`Pbz-mtx0vml()}%>*rN zH;X9Eq&&_hm~u$|3?NUC554cbNmPsLwuv@O@2k$wUw-f)gte*_mwgA0-ww9sRqcC8 zFM-uQao~Q|RgCw2TT>j5*>d`KKr(E43Aa0S->XCLl5(K$D>nTM)}V2G32WHrx=nGh z#Sp*k{J1B`_2({t7vcQXn#=mOfR|w5u3?oIrGF&|E4qHFD1MTACt=QE@}LojKJi)v zn3td*6_}UmKtXE7p4})<<1Iw(mS^UsBk$_6 z(q?&qGS#A?gCFTSx;CpNp1ww>_b|)Kl8ZCiX3O55F>gzr7}J)u#fr6*ciJ!`yS&#q zGg2HIzGMC=-Kx~wL$$OkFP-Hs(ZY8?dKnic28i95rnkvkP1W>XElIQ^mr3`Su;@Cn zZN!6@s4i_2>yq8LZ0MxrIY`=#TkLi&%aqrVCr@lzltHy}FETW~-B_7IbPQH>MFC6B=j`KB!Xl$5y zP%^CIP{xwD%$R7Oo5{+Ott_u`VOT-z{VLE8fp=7_SgKVW!+XBUzT+hI3XDiA1NSS`G?JD!P*=3JaSo z7VGNt(sYdS_^>gT-3>v)?<(c!o((YmIqf_-@~ufHXJ~{u>}@q2>L8qXZ@Dqzx=9ol z&WsjY1nU_R=&|*b)lq(q^SnoK_@yCox(V_mr&7!pTgJ#U{^)`&DpWOHTfNOT4)gxU1~)7pzP z0vR*ARF=R|O=gXWpsUIn)aEn_QA>2ORL8hWvjS`jEKSI?-)rF5qss2I7n1%G+4nQ( z=AFrZSQ}92Ak+)R#$Mokfe2Y|^=r(9@RiZsKKlC+%{uTctTJ@5^m!G3ySNK5-yuLHI{$zRp?zbCbJabm9Ua4)n${Gv_&hBFy5}B{0p=jkW!H6xn$)O^ zKHk#GVxTnQ9|U*3J`pz&mOBHlHtxR-mGKSjNaJO&s*=8$5{&3UxUy86uhO>;{oL5Y`ukUBmT<-&kIVvI zM~F-L?RlIF8ost_YxGMkxOeVQKtaxM)0B%~&B73Eb`(du=4eH=_h_yQah;%-ty|%W zr03!7FfzBfJ4RrR&sXC7&u%48M3bA}?KYx8;aNQo(5F9`x8nAodBRLjjxSnn{)8N# z_1PKJtrv55#O=$S6CS!_`Fs&_vm^C+Bi6|;W0ETip)rV@>YT{kPnbG#j=&rlrCzgX z4G%&PAGGbDmlzzG~JT(Azz3iH;29w2x8M zd)5vDRh*l_S)sjt8Lmjl08_USrJjuaS&L#!m3O9@VC$VB;o?|Wr^X^NM*1;N{yA~* zA6$W+x#>!7ef#Z-F3&Z z?@jXNSy3`Xj>T$_9aEbUw5hzo`E!Nu>)zE7vWygKeW;1vm`6)0m>y`x7Fa`75w0f}AT0?c8r5J(krNYB3sa^JjeXcl}Qn-9-r!`)jK0ndQa3D>_ zdRfMXEY}vuS%9zh_j_wn;b5)Sz$^i{I&_@RuC(n+mr9)C$gbBoQ~SG>p>tKea6$*C z3W+eV6I`jbt%rrL)SNNg@*rLXdfzCr;=S#i_NfB0olaJ=?K4 z;Q7R;?4fMc1$|2|HVDDJEj^;&4+Y_fB5!o8FKWh`)QiXz{5gG#YURf?^#l00xDTIj zw$h7d$rafne=2!jnYt?Goqk9{>x+!yO^2d9!cA8OjkT2i(htdGC3T4q1lTYoJNT-H z)8106M+1Q6Tz=G^m$c(8O`pi?oH}RsfYV2cQGF5G2!QQb+AaiR7)RZV_UZ~bQ0&Z zPN1YcB1J+^9ZDgT+KvcYa&%wAzal<9GqLT-fKkU65$1+WHa5y9202V(oDRIVr&^iX z!&!?N9jyRsFOU!J2pxLh%o2_b&MTjvSD>L_jF)E|>6s;Vh#hiATbp0}^l*3pp0PVR*9LAr^AT)*M(O)A?dnUF> zf4WWJIrUtegAdu@+9#+dTru@5vXR{vAEV@4ZB$_GWd!AtSgpESA69HHj5B6G`u11J z;r~zp^#K_D*1vD{y#JnV{d0-k6xkuMSy1E#1<87pt7{9 z76vi@ZCo8Wj1faZYB@AwTu(7UjLn^1lN<^AN8!(MJOBG&Y2DJ&$=|5INo_vlpkPpv zO-&RQJG1S5YZm!^U!UIt%^pfij9|r(9<17%i?`TizD@=sCpc(W)V-$zpzwU2SRy#s z@WGyk(N;S-OSj*7{abW%Jf+D8tjK>^M!^ zbu!{~uL+VF!EHJhn^c)agYd};4HjIu_WQaTobK5vHlvUIP61dY%!Snkj#v;6Pc>C_ z-o;5qIB6v|+`I&@cJ5ZC4CKwi+Xy`pFS@>gom;sf1QOdlPx0Lb(nDNgDo*9>hy)CpuU*U7>WL=XJkEg z(n)#zeZCOxkRwulT_y=sZe4`ib_*||j?zd{oLSAO<_Hb`j-lL`jc@fn5B|;pB)Y)) zE3~edlms?9KFG!JBGoj@CZ|F;vJ&tdhDQ~#a@ag7+cYLZhxw^MOoo#l4W&*s%#9%v zYo36s)_uvDDof*@2=v5NEUe+A5yn5#?aoDj`nNm zt}8}H-E!I+E_!^YdiVVU$>Z(MA2@A-jeK&^i_@9qm7oo2Zuy{t%Q9~08*!WCO3HEg z+^O9<_#Rxe?~+Pzm|mp|_Xt8eSE}zm=KYcsK()stA;BSguh15fu7iG)j2&L|64sLI zR&7*dz_!>{?4vIi*Nj48+&GpYE1t!>(uvJM#<&9^<-M_7#*472W7zOJ4E&>Joh}~= zm>B}rlKz4;!Fe%xTNC&V?SWkK%FrqK`yxr+Q@}s4N>9LbOLyr7GR`6d$#PkKwMI)m za)|C+lXS*jhk7V3yB8eq6k6Er4kEM&I~zMAHbUuN2^-{e^u%u4>6VBYaO1>Z@1clv zW|yXVz7YJu_8+PEatPRCfGbOl>_CC|_`4$zPT73=gG(rbyikeivqRz!)u3NKE21ML z;u(*ezZP$?j7#42wAu8pjI7XDUBxXtwO&M!;`#gNv3L0kmJHH*q`>0SStTo=KCkGm$hlkEBP+(4SuhG`-ae# z`H#?E%HG`JKi?2?aXkn@jCjNK5dj^XuTTgLtDIe2Sg{76**c+p=pkvUtfLYxx4^!X zCYg_EGW0J~Tkj9Pz!?QM#V0>ZZ^_tnl#quQqgFyYp!H$MycRNaC#Xac3^2UX5{*wq z@A8OcAD2p4=VV~vpQ~qLLbR)j3EEkQBylIi=oV1#!j~&{Eh5 zK!bzT<7~!uL6*C2ZH?pGxPtygVDnE&lh_?6kmFmBJb?Uf1U8P&4z3OiLT;{>ibf_j zX8%2&qyFZDx`NL4BbH8!00Wkhb96UDE)7hYQE_k|j3xn^+)X+(75fUK4{aRX%>db`wp|a=D7UR)+OZz7;wTrkV&*9l#$onq z4df7tG+GL@jbOyoi!r=%^ce-0Zsw^LM#HQNzl%%qK10-tKIrVr6L1sqEA9s3t_P@> zaFF_gNR;yi%#kmo^|jFVgED{+@-~^sA4eGNCTe?5*b`A8eT~8NSthfGa1j23I>G?* zHXF#lTF8T(f)-v|S;UW@%{=zu$EL?RuLU`?xjXCp&Fb0ZsXI&-xP@%6Lqm8H&%#>E zV(kFo8Sh~2_9o*J#$##o^54PN1T(}85o|NcbY>O_SsuEb?V{_L(g&R7r=!dH5H6zy zt9@g7kI^P_S{^2&lN?=ROh0^96${$%ymYaor2q(@VHd;S&Je0VqJu~vZ~N^8Qc(d8 zA0=TB(oa5(baAfBQFX)y7+NTmX_VX>rAY-6++^_kGkO)#9iEn9jphrOR^}?E?jhx{ zajBe&j^r@2^gL{7{1$fgS0ffEcaz~2%=%+^OGxW}#Ab4wXS(c!{c`ZJL(8;adP`7A zYvCZ8cZOu}LKG)a$?RSORnsLdk$F;M>t-9rC9rA2X)5ozMmvw7_Ji>-xt!kKlr@HSNik0s4zd|Hv@qNhVX z3+m<>$m}~eYNbQ9YTVUEO0jbA)hCXsiBn;!_P&EHwSl_eHwbXKZOV^HXb1|+k!{{V+7Qbn#A!Dsxvv%$AFcq2?io56|W?x*l)0NVNcu`UZ2$b`pBF%`CAaTUKm+|mvDmq1&X*#U8&`B06Or!j4^kOl-hJKBV&P@#ICNCEL~j&kh!%|rq&=|=la)kpQ2*KPT8oR zUB#J29m0K8N+y}9o1T2~!`3t{Ny>{fBdl<6+`b+cykjW)-ZN1J{G1|%XUQC(7K;;q z|6-1c_-y{VmmD@k$eRC%_hi1Y*A@AGDS_AZwlu=w1AJcOQQ%iGoOg(|k82q^OITRW z$)ZbqDU4VE_vj75obvlkpBAF1wtX*p)M1EF?-e4MG8Vc8ocYB?-V)ZVF4{7=9%eHc3Tcu=SW^m#i6A<* z#1dOq16mhz0AFLb(>ef`w((3r7j@<@c1Zlzp1>j;24VEpK$72`Wrr7x%z-_9c?0dW zxibu%6Gyy?=Pr9FaK-GZ+3@`v^U?43 z8AtVE5ewqq>$Z!R6JjNOC9RI=vF_BWjyY!g>{MsXHITdfF%;c?Wm%G-=)*s-hv#IY ze@Fm-P_RGmYu+si&<+AH4^Xt-O>`$+62`dPzH%701RR_sI+~n}77-$iPX^g)O{Nqk zavBKppQNU7EoxiRoIDcL`pwn>gKKKE{Wg#8@HDsF422x4?>jjold%Ni0z9AKZt&od z`b{?`zmrQb7OG~6jt2GGe-qR0fH=5xS8Vp({!6LrAO7f;jfTShj>Me5JyWdzua>I+ z4d`&e0bp1%S!9Wa6q_mV$SzwMr z2==L~OiXbqBDh#nE6b}*;?psk*eEI(I04jIl2x5)G$Li0*lg?#SL@-h1cL;Nm=9lBr%xzFg z9O}elKXjBCF$eQeRCVGBqW2|>sh_maLf|+uoE%WTW21V0V%(0{CF(8Q&qjXS*D>(vA^c7rEcmsM_vHsyH%<7KYYjKI-1eK*gdAh5QtPmp;^H}h~Xpv%rhcZKC zI*kqNV9pXolyDXEy|$A)i`&~ZUD#!iJryULRmK!{ZOsuZiOU9~dMG6%C^>DhZ2T;}rUPQ6|Ib#r`GzF>4mjpN!&RLz}5#$1`>P z6gy!Pf6zX?8T8tfA&M@qoURMx_DdTcs4c9;6C~D<-kF^u&nRRB(^naY|1j&vQQko zjl>SELL-;eF$TN;I=*>rG_eAt{*)1Yx?_Z>x40|0rQYo`=>Dn+(n-H6YoHd>Ij-&1 zUAT;z7#T+|Ha;E+p_jQ&Q0R%3UQ+UfuqeV3Bac$ytld9R`T)6+xRdck8o~ddjxsp# zL+?wRIw^jo^T!+sf9=ubuP^vY_G zv7x<3O}ulv`8hGLaOF>W)7IOJF^J6~&kiUyg1+SI1QlAa>&6Z9XTwtGy~BxqsUg ztJB>_vpCby@a(tN2xRw|$$RNf)z4;W>)%!j^hZPK#Q}Y30=jd-Oz-Xi=@+z?aSC^G z{8~DeGWH^G%|D)5$Ds22jY2#dJ;RzSXhP=pwfdEAbIJ|#l7uSNsU^$!g^1Q(po84h zn5cmn$cXX8AosfgcrL1=TPAtZT*;j17UYQ#oIeRH_*gP=X8TAS`^vhvw0QTT@9<`Q z!pHD96xRnQv#Cc@#Rs1j1qY1%qC)0%Bf401>vZ3ESbmhagf9(-V4u zIqq=HgiZ?Q>jhsWkP^xhkDwtzl2R+f>jVW`xsWF3pdm*&@AyK$XP$l~m=5AnJ*zn7 z+{rScpBrIUW)cjL$$B30&#?ge20kEZIDa(mcsWuNL=Y~g|fib0r;F2^L z;#f1I@a%Y&_!{4$U=eN}8{&+l(EJPlE$lm*h^X|;)g5DXeqMIMu*d9X2KWnOj`s=B zp41(vIayiZ?Q_4`jVN(+rOr&_Ja9w2b^_Bkc9XmR3a*5hb6eqEiPd0|7{Ld-!|7c; z1bFG%v3ba5@my!}%-h;$RnaeQ(sX5Xg)uBm`vE34FFLL>-&0OZeKMpzVL?4^hY=u7 zOhh)Y{z{zZV`ad^O>maJZ3r9Je1Wz+?IhprOW%rA40Nq(_yT4I7XyPl@WpqRjvHoZf|CCAl^#!R)};oj6Ifm;tkifp|NB zT#@PNQxrcR?~GXwdQqN;pU3E25wPSB4 z0soBg$9{ap&}~~s!Xx7fn(|IHlhdJTFZ;J$sZ&L%MKVkAt(0(_G^V_J8#&{BzXe=WOu!Km!3SqWzoh<^MWr9qjGROkDrB zQ#Muu)(7nf-QVIy3qlG5N)!qVZ0i!Ds6Y~0REj7o0VyN^Ue!pFe4^i+V|p57z_?l! zjRp#xfkr64)b?U# zyzGGdJv!{TsoOTVoAFzFq?=Hss9>QSC=+h^u-jf<#3@J7@5BHn^-v>`eB?DGu`iZ5 zI6ut(0ps!B32_d>KBf_Gh{7P48+k;t(XgYuK0o|^GZ8=HUNj@d{`WhK^;|w%K4d1D)^Pd*>26@qe>X+2)`^8le0vEL;`mbg9MHtXt& zre|pt?C#W1k5*}|;iz+I+`V)hNnv``{kXN8CH)%t=fBU2)+Enu7JD|_n&md~3P=}n z&VQj~vo0ohXq-0tz{v=8bD6no*!XEn#OwDkq^?(9_;qSs_1DxXkR^ME_vr7WF4J{| zX%@xG#mecN59^iaw67eAi;%`p7^7?6i{YAOmwV7KIXgU5WB?MC6q=lmdM%SLVO&Hu zkbW513`b9y8 zf}nH%Kxsxl7_pI$iLqU5XQPE&s*39kq*$5}YetbdalwjLb=K`HYh;xz=R?P6cP`7+ z0=(kRYqtOcK94;xWpR!R%O+*dl6=ndePgV)5ZrWGZf(-dtugQvSPs%C{H4E`5o6Mv zzT1}xQn-IqW_|)J2Sc{k3{NBF#WlIttA~`y>mjNgoddTtN2^0xgEtddzD&pH8au8|#4v_@K_$Yt^*AiQbXgE&K{=H_vIit*-8 zrZ^(~zrX9UXO!X|%FT=9p?mO^Eo+4wSPQ7+@%*>2BUMopjmjg+z6vAEzbiE9o6(u8 zM^J^}syf0hsO@*lqIP!MsqOdLq8Oa`BI-@A;-c#Bc+mUC2vt9je_#EMVne3Y?_f+xIxwp&AHhAZ}mELiClX9BD>Q zx3vN?fS!R(*?CRm|$T-hGqHE=}gGf+^fUS;jMkFr1LUiv5HAQL@h|Z))oW{h9|C zF<+%}ZhN>f&LR5Elp{cZ9qOp5SbdIZnnDqQ(A)^!!de`Nt#jN>B5QKs?;FvY0e zi;x4yg4Ue`ZmJ?mu(xqzA z5*J=kO3C#3v3HTN>vG&1k(K)X1GV{H@UZtHgVcGQ^4S<`LHDGA-AQ+dWo7zddGJR= znv?_Cuv-f6fne=;z__s80LDi@aRKF5)hwc_i16n`p!l1NX3j85m}kXdyDJYy}_6{kv;P`k{Ldh8R_l3DvA+4 zP(Kl5AaaY3@X7|>?;-5*@M@6$ew|HeM>yPaoSm^vKq#suqelB}Pcc_Db7l(2RLhiw zE9j<9hSV6P?O;AO1Zrl+wb&6A^YI08A#s7c>n}BzKan8lJ{0uMGduq*ARLf%L_u}nE!ev7~d-+DNb?SbL{ctx5x-R~USvcYymys|IeEXaF3 zpT)S5zn(O`)Z#E9TI^0EbDswAB4SQdnA4qwVRGQwr$%^$F`kxY+D`Mwr$(CZQEv_ z`ako`bg+&4c8`h(z+4Fi|HCZB}B$Pw)Lu}Zij?y``f^? z&;tQ}C)zg*jCNad%na2R|DiH#;4p7x0m8!##k=WDRLuFf~GY0|U1sh5cnLH(Hi!F|H3W&CrS>2H;#+u#xfzRfo5A!sCbXjkTy zVS~Hh(}Pg<<(HK_eS8Xj9h*;Nz-Op82>k&O+%A;vo-e`Ax0<1fSG15@d_Y`-x9x1d zZZkQ)TEyH;+GdSw17@ofK7H(k;$~h7cz&wv)R)!WwiO-P{R`0-*N7Blp0v${P@48@ z*U3R$!|liPNlCvLS|{N5JGRZl!P{pMPse8v!$Tyk)Q%<{#<`{*;u=Wc8bjh4&f4A? zo^cf4VUvfuVoY}M{JBwh0j+o?i8=eA=~ANUh8I5zDFkfN1dL)e^5NQqWsLhwLeqyA zYIDkN72g$~6~EI#rQ%x9Q)tksR8eMf=9nEbCcEybt5KzbEeK=iDV6J1+O4!|d_g1VY2S?1w6?UI!l= ze|WF8%?n+M$GgLiamQMhFlS~9b&VPVs_B`%3UrTOk$+B8|NZ%&?>GzCld)^S$wnC& z2#Dc-tW5s>j&roLwQ>BveP>bXe|?pfFnw%n<_phI*X5y#XcKMI$^0k?(86i0f23#% zOO=5HyGhVr1+h@Fv(GYRQ7CnnXMlhiEwssO23O0Pml>CS@U?vX_+Wm9zic?k%pkLl z_vb&{d^qC0`QVvxpYgmQ-}$_W=m5g&>-k#!lQMxgOr6h3n~ooTyad%5%*b7*jK%@#0?Omd()5sRL1| z7sRhtJekumzf+qHrUVdLqz2i^iR*?WeQWPur8i&?Z#tj275e$we_-gJ#C3LXDaVD5cEaow1!0 z7z(v^qOsco)Xsv3Sm?auGz(;NB7NGm*6mf?d?c8xq_REji%j%^*MSPP?GTb7X^T~J zf-F9QH#-JcVLnWiX}BuohnW>)o!auh75d6Kflos7f(-_0;ihHdDhZX;X%rzQauJM&>mswoXZD=%<)Zm_aF@ap+H2mL?@yEE_u zEYDU?0Cn+X&xK6AjO(c}s{(fNMmA0EaDCMlkIzb7bl#>>EF{Xw@Z7Gzcu+~z*P|-b zXC$J>;uZV5dJq3~IKtP0Bh=U2?FV=2-4>W%KylisSvqN1f^;LT(FSpWH1$M6YMKbS|p^Uw;WAbsSIiCi_ZwWtY3~Fm7I_q|&{OTpp4oPek@q*~cS^34R zFI-0s=J>A#{fiqFIq2$r7<63)OOnHQv`Qd8gFPTR^Z6uN^#waiNzAYpE$i<1sq(pa zz-vv1^Uc_RtgUh+=2_E2yO-XmWrR&6Xs(`A+UmI0UvcR7K6z{k{y5HdHm!(m^=(qS zEms87CVtYMQPhzlqXo?g`yBZ8k=S0a6bhElR-15DoLn;Yo6*poOQ^#NfL^-x65d#YWcqAqFNPi#&p&iadj2rd&dDm0v;%;#J#@ zMAM|3MNgmDZB2sFB_zm@mht%A7_qm^YX(vzus{uk+Rb2{bu4f?P*{h0hu3VH*@+~#reHW@1@^9!9B>uTEK@8B~U0K zqKzTl%J!c;F8y+^zJYrsSJ+GTmiv`42=Y7oa4gw!UjoFE~Ml8^LZPIIX z90qw7D=U}N?CNx&A>NL=JEH5cF~*zS^!Z`LRTu60=n}Ir2-CS6IaQkLFJLnvZ&$5icE)th69& zf8KdLXMQIvJ@=xNzsTN7IYY5}4j`*HG9y9J;E7oe&UxTS5k=3h;W^`p=?p5k<2*&X z51=Gp!^RkD z1bidpo^&D+z;U6?PjyCBW1%9uLn&PR5{&Lr2liYCI4GH2^!iL@?EYB8qZece?_iHvpm(dI?*Z*1JwWphwuLWQ}Tyv3KL`wuG(D z!21cLy8_#~RRycIXWmLVow8uDEeRJ-5VA7}QM-?Vum9x^ zlK*cW8+gP`#>K2p8XC@^0Lgch2=mKFfDYk<38%7XP|P+ot;aucxDR4G-Yu=wRF}3 z$qN;lVzdZH0KxQc+G)gV_vBjW$?k|w&>H5l?c7M)V>&pA!$6K?)Zc{kX~cs>Pz-{c zD%0Jup0x4h(&N@iDq5gRBh+duu*>}P@8^--TAx(ddOY4P!z61wOwrB@%+Btjgxx=T z2NdnY%DmG)x6%GmL7iFEiRP+K@R z!pzmt|4OX{d0Scbw9<=bkp05{&$m-g#(91%z`?H#aPZUpkGGS9v4NBEf33jG^|e({ zzqcndI>!#@j3qVOK?S8Y#-#hz+vU^QVgu>?8Y#^+t*D#n(^8DajZ9MAl@x_RDN(=# zRCvIHMHgJ#Y7YkQNip7ywyIZJsXUf%s&jLyKd-A0d;^gT@U+zvwgsO zfs5ut*e)xCnXeJ;YeGMSH}!&k*6u_&7J1J`G|lrJii9-6tL-7;Aly;H<~;Yu_lb$% zJpK-7j0tn=gp%8pKnxLv3FD~Tqr^AS*Fz1yYlKHGdI1#`UNbeiY`A}m3^f(uDcN(! zm+j+&&3Q1uZZ`ryWN-Kqv=gy^%@Epa+`~& zv3N~~m%4Mt7y1egwtb%swH+I2VeLE|a`iIncV+dGd+xd%c)0`lac=wu_z3>K*!^2L zI{)%*d2kMed_wS2P470Vfp=791V(aRZ&Q^sQks?v)QJ03g#)Bl8&hBB#JFEuc%c*T==-Fi+t02 z!6Gt^EpwWrdYGN%I15d~LMMgc`t(m=!-MU0MJ%HX4rb@ZDV?#*t!BeHdnXbrV$B@$ zsupAiuiR>F$wns|0cXsH{C?KjsX52yEFM;Q^0Nf*=|AMPO(M%Gb`lMTi#ZL-nM*;2 z;4!2rsArYalzxgyza13LPUoD*+30z7j$#8l31m9YKL23V+E++tOiIysgqdqm&bJdA zrtL*XCQYhOm1TdQ$~M^JY)*nf$#R7Y1=^ajFpVZzOhz`?V{S;gBr%Hj&=$z~*&gY~ zq&4P;xoHkqLX{Jl#O*+jyl6U0aG*9}Tr~dLPOY7vGZ;CL zHKM8S*<{hM;xIv(#66IdVzDW7N%A--xQJ6xU`8_tR1(u_+Ou=Ok4k7TCPU3+iq1o# zq4q=qksvm9A(fOh5qp7a2vHT+lAC8y+7Mk?zibT7xqqys!K{1afNB=6Dh3uzWP*(A z|6$t`ckLQ(Br+~-`a2mXNRap0THHvH_qe=NheZsvM`_4EI%>7>K)dlmJ;Irbt5$ae z+{yekEjs7G8MmjG-WiaE<@^a{YyR34y+e?9(DIt)pH7U2hTDS)VU({0UiO z`Py)LwS0&EsWP&2>pJ%V@2^Jz!}d_fW%gPU?Yp1 ziuhht;A&1?3Ij#thg(llHlvQc->N&COO=H>m0efbgELEu^AIf3SVMh7LxECXrlEd` zS%;_?)a2`BL4tI$Ud_Q*t90L||4;0LGijzYaSfBgfKzNmoqOY8^vnS%QAKX5uckRC zYcf1s@!`oI%+pi(+3MiYPxp)V&^AjbDr!8ELDAf?vnVnnsNpGdEisx{;w@IzAYdeBtJQoHkTDpHY@#ums8_pTd>S4%-WAbRnm^w ztuZO7P4f+`-kghvDLYkMC?`;7-;8Yzh_5Wi9-qfGve8C)QHLuVx;BKhk+j)eCxbM7 zRs!cL)S2g>kM^$<(iNsQ9IVX8-V%lK=Oe`&T)Lj6wY1}r&8TgamQS9ix#qJHXh%3F zkCQz65u(rTKhAW#|+$=xu<4WH*-mEKo@8%mqOGu9TeL{|N){#5PqY7;`){Jt>Y7D53P3fYBP#`ou5 zwS%yz)#L?@F>~I~G|lyQU~8HdApMId)2rRi48Op*(_B9~m)Jza3n@|OiIg~TEun7H zLH?txmEb04(%E4e@~sPJBlv3V*TCDaoWKixzF8>G+G?v<@b-p`j8@G^wbdWV{mL`E zu+70`)8$@3sHb%x@(6w!!-%eB#C!G8kNGv3#Mg>qvIp5g2VC{jY@0D=Y>q?GrA5&+ zbO)gz7jV%!q3Wt*@4rJ91k@Fasy ziupL?xPhBnDWd}w0$yN2FBA?0Ek-W9fs^oB&{1Ex1)he9)*n6|(9d5CHkeebMbAT& zK9rwgVr9`mb&_>L$dcWZ&8MucP^tZ!m0~4Az7ZTcZ>4aJxZ$WR!a-{|_rtxo&Q03D zik$Ht=!-Y2!IW{BO|;E#$QrxngEFBKIHev=TY~9OQW?99NWE;cEm+;VSJccEFK8%e#hkikmbPStN8SP?A8xS52mz6T#{Nk3we9gp23ce z`x77d>apbFhc-DRW_M62uw~|D@C(v$V=y-9T6FE{iBl-){2%tk%WH`<$42d+22|CB z$SeEp@m4;dD>1Aq6vxS?Q&kkKEg9w2`E3;=jIJPj>FlnCVQm?=b(L*o*9jg+*a8bC zf73$jmm5f(@6z+jk0&l(_6}Y_PaHLEk7MNiT2*-kRH%$upSrW$1dVtTpm+h{J^ec6 zWT|mxha|js!xofMf<3W>gJ9&`_kpSkLQEn)N0_FIbmQ1>!%lC9_bDghO{MPLfTwDaXs93&Nl*sknU&px(QUF#Ez z{JA-qlv(BmmCRjUSNKb3j%*Xagt-HkYYC3q4}3i@yf>T^029m|`Ja0!TN3R=Us7li*hG!FSmg3UA6X0w$*mZ)XyJYrTfHy!k$tp zGZ$3>x7&6XDXF7IFEZzCwkNOSQ2%rr-kJ?wsL!mr2$m~$vLJuh01w?vN?ADwN9BxB z$hK8+%eIL0vWcB*{iLiVTCgyIfyu%~|K#cLMV*0GLScc_GJo?`(efRXbXw6s6^8rd z-2LI{BpgM}DTt3sko85(iokMBXz=c{3MdO&0YG$D3c*jV6e4ZnMre_b*0kyK zmch93c)lo~^_=>|vbv^7|J^ZebA?*x;JkfgFz1BdkbU1tpdfE0z*ah*asGd-6DLtA zl8u}{i~^@7RI;2BpCxCg z%_a~}Pqt5(I!W9ZCAwtNjBUn_xF}wsI9?tI@_GKp=2PJ}Sp@M5>+eJ5N zEI2JuT~o{l$zzu)t1I?8?u8oE-r)%S zj35{XRlKnJOP?C$Liws>3wPN8d$;aR7ok@) z_|!`1f|6$^iH@65?YwlsS3HUJEF4TsJcZf=%yR(`ZrpxCr@G{v?yC7uX$am^6$H;? zEpdSjB5PmShDx8Q`mTqm`72k+pgd9Xr!q=|@J`$&Q>gxMEF}e{LRiQ7b?TwC2gFZ= zehj*hTP)p_q6Y2Cg>YIagEAiVv!l5v4jXi)u`Fy1U4Imfhva={Bfwdd{<Co{LJR z$^mhSGsI`Fd`)LByt=bm^^hE$Wn;O0V$-lYx3*$lEO)-B$Ni2^-DQDuws{YgUCz|h zs#8u^>%congB@k{$b;6~aB*?|caq#3Q1jhEdxrDQ*a*2C4*%=L>d6nIj*y<`hp7U; z^H-iky|p_7`V9iWp}8GOEA}_NGE=|fr|*yG0PpGmrN+c&fir^aSKMw9`ini&4N-bT zfQRj%tj*}kBc3;ml<@iB&js_5z!&&Vd}xe`1uUyo@iZXq=+~>fR*uYA;KF$#VF+z(ly$-rhs`D%&78V5*nqxQP}! zCxy->WZR-wMzPK&|5tlkWS_lM?X?fkzB>0sSUv$W8parBp)ohNFB~=!skT%hZ}Z$e z$bQ2xPL|+|<2rkn*sbK-Vhyo#f`s);|Lfnhqkp+UHt)b~Hp!-KPP`(``Gs5Qj*ByV zk@#dofjqrWOLD-gc_Wb@7l!TzX<_JRu!n zo+3RN)nIk2u^9fOoV6w+)acjJs0nd2C|1jii)go19XWhCIr}@1tHogtYsg(|r)@$^ zcR0MX$=bLu-NCQp|K36t9_imWuKTJP+%dzye;Lh^xKA61&K{L3jphmGxfgG)7w*+A zn^XlEuq<9C17GR;vkzlVc=LD@N6HuuZDe-5BNF}AnjdD+BN-1iOo8vivw83S3%i6s zDst&tEbMirwA!N)xUPWwz80oVndl^xqko$s!PQTqBQOfZDsL2Se2}3VlyxtUOnWg3 z&mtm#OIBILXH~nGIy3Ifw%qnKnwx+-X8Fcys#)j4Mu&tltDL$KkmzXk$y;Z1k*=-6 zkL(^~)j2jsy|91qQSO?onI%}H}jO&L+cOWD)|Nba-=`jw|t9A`MiX%4FzbcEZ(~?cs#%+^impV>F4lo8DRO24z>-khuMjv-&wll zh24SQ`)L4k5OTj6b9igW`AHI(WAU06oI?$f>|Bydz}1vrbC#B`t2wQ0PgBt7CF<8q zMzg^n1(lfTKiwcYW;cvldN-(om&t%0q2@dhd-MH! zmJ*}c|1i;<*7UJHn?7_>XQ30Yhf+_gH|8=q&ZmG9n+)GTHQ;91LAM1;wRP0}BRsPyYdZWpxvA1@%vT^Kv~ESO_?_!U@zoi2e}A@D~ys zre<>lSW8t_V+~9ivE$hH)jz_*(*y-}kAsY8vB<7kP6Yz*!x_#3nOEzlT`-ZhuGQIK z!eQb@u0-lA;%+VM<*QDG4-m9$n8Z5IMEdWc0m7CGwXmlE5KuRVdFXz=&J3Mi)dd{A zD!5Qj%~Y{}o(>G_TRpXHb+WV6WSR^lM?)>qePh4y1qHOh)~bfvcQcKoO?)@0cuxXY z9{QDzvW{WrD>SN8$F7P|3F)QeT%5ZlO^Q>=Z+k|sp-OdkM)OwG_JaB0!g&b3pasN8 zLgYSXj-tI3#80Rj(Ob(-^giBCGXI@@9oU|Pi4&q%-fx7ykJkciz7qZ0y0v_Q=R$1& z=PM5c#yjNnG4!Cy>ZIncFnfte4_ov~7xi%CzaZRLjlF5Gg> zzbX;Qc3^D$YOJl#sb){crClhxPHlpU+esOGs=V}FX}DUuq;(vB@c3TN1o%mNu7x`7 zW{)6lW0uCX#DAgD>MUM1x6pDCHA!cp0S5{fW#4v1!R|UMz{&SvgiV7 zgVDbO>bJ@yn-m3@o)Ce@{97EV{_3CNWv>t4dN~s?p@aBVIGYDcB&A?cU6S^9(szP!yvpaf#jQ}PLFKODk-dVz!;G+ z8`c^Vr@O29hg?rEo3&sW1NLD=t}4gFw;*yc?g_)W6kc%*eh$%VR*-^`&#fj*J{CsV zx6Q8)qn&e`ARyzgz8B)~%-s(}eZR+i;?fsPwM4*(dU8+;R=S>*Oe1IPM>9sum_T8v zX_z#MyVUKSmJff-(_^+wJ4eEaNAL$U2%vycGwWq#4%fsJSdvGDYP=S2 zX3&d-3Or1et-`huWT#syfK9>9HJ}RYy3a}ys{R42cE~^*f{E&ZVW6#O&l(9)9Tb~# zL&<4$6syEY%?m|{VIZuaY|k5smP!a{p=D^!M@s7|j!?i~qOugL43+4EW%HMb3?oftJhXEL#1%&~ z=EZb96T^Ra3$&mV2ZA}X=E5~yM2&2)hgk{3`zKLk2H2QDip^6E`>C+Nq;QV14`WH7 zWT8EOg%i{y8YJ<1H>98}YWefHf3b1Gk*4J9(XJ6OYt~1Q0+d|VoNZ{qfLar_CRq(? zF|(LDQJT?);q@^7RIH>roJK=E+F68fQfdOa zpmP!`IRa(}1%{uZ8c-+O1hoj3jh-_bX)@KCg~}47GQLqi`~*;lDbiR2rjhY!r~&N@ z<`Y~dJ)ltpVb2iH`<$rwBDaj!!ncroQM=5W3U_Udn^C*aUX--1sDnA0cu+`|s39bu zl0Q&bR8#ap^$xI2PA^|~yJgQ_XlDvUAw_O+*U#7)*x0y=^;eit{DiqN`v^NnfptG< ziax|abvTL%-YVH+{g=a6zdKDdzS507tEL^UT*m|A$Q<62sQXdCxD94dgOV7kNu=Xg zCo`M$9GK^11}1tFJU;3p+l+fHs^Lr~{WPR;;4RFY!UA8FY-zWJldIC6iH5tA*yB1H z$8k{{usll%oJm70J{BVcZ4i5&%+*v?w&?xjjk&pLSb0~Cw5WNzFQ4N(u&o9vblx)N z_}0j4^2fT@jj+Qr9eR?x*la8`{BS?tHm2^k21dxSL-@xoI85>FrSC2Xb}H=_i_mgE z$HU;wqYb+I^cH}c^8y+|F9N++;W>cPn0g6#i)___3xD?vs0nDm^N*gB(bXHp}n{8W;8myd33Jn16w9AQX}jA!gTKT_;J$MD3gi-K_ft8QHCQb0>Dr$)B}_ZyHZ6=xH1O3igpM+cJDSn6va2D?NF_CBwdwiegJ_cJy63O#YtWnrw*qZN z{#mi#ICjn}Zzl_7ISS~hdX(J1vc9ihyQs2kt}=_K8K;NSmKr9H-K~g5Or&G~0w+n+ z&3X1Nc;;5RB17*ib)>Oxa$esrc=UC6leO(%Hne>imCpD$cl{w>YkW@nDKk_g7Z=jH8s(YpS!W(H`x?{7IE z@;e1k^UB6Ne37=)D~df;jDhxm(AG0c{uda4v|2Mwh>Rf&oy2E`{2oh(HApL;_9@8a-8}P#*S_j$?I^`}3G!v=!|M2SrRZ zxlfe}ESZR4eQx_qOQKUEdHOen3MlB~)X@FuOXyI7SWJ!{d-XP$_bt8{4)?)T1f1rY zQkY$ZC-+1RHtFm^6p++P_0l7gkz0&bBiV>8I`Vl%}Vt<>NJ?N?MXXh#e5D-#A9?ygQ#CZb2BQ?eA-IBk#a zhpp{R(ABtfaNYf>5INrC{YO``GATHNH5yeuH3dHMSA%zziT%U&k$>R$bRwB^7n4s5 zZy;4z4l^*Xv5IB3(0EUQND=1M$nU8K3@LoL@aFw#9eA}3V998iRMCL5G0+GW65YBh zZ%A%U{T~Mm8 zmmmf9)xl97VXQT+O?j4_%6buQ>tUdAcxvo_C?%-A;?kVyOS=)5QfLb~uST&o9y|;E zOm}RwbnMK1<4ioEZlT^!hmNDsHLR%MI*wLDlrmYLgnSUZQNZnCt)9MgOPM*(5D92K zuu~keW%)$2HE)gZ#PSK*v3v#DR=g8)_mUmha1$HocunBpE6Kg7V=VxPyYRU0zm-5x zT;~<)1D%sk`jmV% zdk=0R0-p)dOkiECwFO{X&0@|QWo%rQZi`izOi{F0>^2ZmC|Riusz@%Aorn#>(~W3X z24@-ezPR`n2(5Vrkk_l*d(<{HnLcDDv2_!ZXioBr{(@9{In_{PQMJn09CYc9% z$?3z~ktq&;u!Z2M#IkjUk6|BjLp$+0LS%OLSDn5B_aI1ri z-3{O(2rK?IO}qhAXet)TLtuWMxr#a}vBFq_d{bDZGAt*`5GPrTPlzHc8BDf*4^g$v zhoOQ`Nb}NZ3!Q&l9T81}+@0{7A9f^lhC?bZIazqg8Q2ixWz0L8>qqA#pd=mn)9(Gv zZk#VO52fp0M$Ts-BauQ96q)OQ`}eY6AgL$0^Iv@Jyn-VEG&NYMc~q%{{%KBqFsuM0 zXH~NAt+a)#mBGv=nVxjZEMv;L@QE@PW|WP--g?9m_;X2@xb^NI*QKVQ>$XL3IMJN# z!Mu=!&1caMymnbVBPvg?RS7cK$~p$6g|Uo)PVQmhlC*p!>3Mg-DJF-5K)3ZIfRPhS zyO+hkBVuZJLNVb_AHOvN;VMRs;kP4W{fTohRNtH=f&$E`PsnV)%dXM0pyJFepUerdAH1 zJR&IG^U-EdJC7QQiV#L3kR8Gcv5BCn4TS0o24N0+s1vT9$-Xx#e&DSE#?`C1g%3@JoY8|hL{8bO z_7FN$IgM5WYNIiXBU2S>(`BOg4of7pAWBf$w0UzfUfz%a7^~Q$zl|$3#4xfcAn#)o z5)7NkLm-oCDiYdJ8GELIJe{##^BZ7syKh<1FqS(@_Fo)l5?QE?a{3FqRbORbfEwjb zTDpp;7a{3in$VAD?{5}Ez6>YSgftnn ziNzrQ1l%cZhOxi6s9ZLA(|HLRimjTVRR6>$`j!7FH)b>Bi_LMz7JHCwb(P$`}D$VTp3g>wQ$mC|;?Vuc(! zU5AMi1&>kEL>1=L!PiE+{q0$D2l29R8$$6^@y-}jZ_fo(Z{iqxQ}wRuvTcA%%X>uR zHj6u`xK2eXEM!(Qh$O6A4G8Qh=?JS2{xxomhnK7NB)0J*OrT2M261ypcl`+-9v=1~ z)ZO1dZkq{_C*(xq(z2$51wT6XXBQNboYy9$8Rf$B5$nYH0?h_Ly84EN{h@b!fh48vd2dypc>Z=V`_|h7MH}2k05v z3S4JYPvbbD6Abm+x+X6e8qQrD&$~3g|BCk1hGb0fi|4;Hf8x1arQV+5Gb3L9wGwmai5N!`aVF}sF`U6Q0NpheY+E1PwbNq95P`uY` zi_Ga}R6n@)Ut4(cGGLL|ia$>-wLujR0v2~;fA+(UFrHb){}@I1U1qdG3>qLVk#Dv{ ztUO`*yacL{9Ll7ao1n+l@z#Z6dI%|2KIObGHDre&mI?Hm;~WV8z!B9m`e7gZ`OsRf zn^}aE_0J)XprK=_u{NK0DgjOvG|j%*?^u0Z6>yXz%_fI7*Ez$qq6O145~-Yl`8MDQ z$VX4}uOM%S<2rZY(ONp@m?y(BPAiVJq{wA{VWAYMM+Aiy!vh|y5k>eq%5|2IDR?HY zO{}fw-Z-w_r}h0lmwJ|8nFwmNe#BZF%hOHbcDQg3q;Q;LT<5*ykcdRnK)Sg(;6J}b z-Z=-q1v(F3`HoixCq~`EcrHlm4e+Lr+K-ItrI?1PET1?GJmhCkDNf4!W>i3A_fc?H zkbXndC?B4yz|f~yCOK^Iwlzz(Av-O}wfT*VM(Ho6aO)6`6f7rn47u0ewk!VCAK|Uc z5c^PXG=$>@>@uTp$pf|=jN9FK2dl4Fb(BZ0N3K6es%VfIBB+>Gdox)XEwwOCWrNcT zuLZZycE~UKIV0op!v3DDq3=eDYWG@_Xy{v=46*yE@wu82dlzaCpYMJt-f?6sH}(mp1(b(yP6R8F*`h_@>e zJ^IML%#I_$MbH5_`dMUKw0RY_N$+3VajFU74!?1Cn4MwqE)gJe%HrYe9wwPX-ey0& zC^jgSHbxBNb0lfb|0jD=rD~kQ1sGV-|9dMbX6z&&B>kVNP}TAu^u&kl z2V0;zp@RY;h6p3cT%$#oGANjcxdMtpMDsZk+?d7a@D^-%*N&ED%`?-}&&LGjCT7}5 zFNG{V(*_y@V6HS?&u2i?z|4cL-1q+(R-J$F16ITCG_3^M;gAu2IE}p&;xIXn z3Z@Lea&9WeK@)}Aqbdmm#=`f;+7gCHa2hG26b@DaO%n9iDz6H#6kzl=hHnU1^=A>} znJ90<=EK^K#;~h&7VTdFZ9@`;yB@W@9Nu>0@ImYfp#lV42P@sAf;A8Mp#gE#5uF8KQ@J%n z#a*EVwJ#K$CSeXnn}u4|5p1s%>d&AA2AEWf5(fAe_*jT7;r;cHt0-C$LPQmq206}d zY{TtTj4D-NKXw*b!J3i@QUKry&+||+Q>#Lwh()UzdiXOVbJkMH(B60~UzA5P&k+T$ zp(Q3*AF}IwzW#-1cHf(@z*eAozy6IRc)(i;l9}E|>_6nmAb>nMJTIM#Qr?NAjZQ0O zQz8PWJDI!QW>lScisx$dkrW(qli|6|1Vk0fT?YlLBS) z425|~1}K-x-1(9dR>$H@nb<{gz*Ne}4Vs!zz;#XhU37wKRa#r7|3UT3xVusYTM29m zr9e~z2`**_!8{*k{?#Ahxap{9-)4eopKOX9Y)XwnXP|J|VVUZjPAOE5ph9C!QKfG1 zkWxu~D3g|1eddt0!_0x&ZJQ$9oH3WXskzd`MQVwVBo-zQ+8#A0_!jdz=$7+3{YS)5^|5)i&pKKBD6A%0xDll3K{Y>3YemlUTzDadkEzHy#KVfIzaK7TeMh^Dd?6r zK9QS|iHRpa_W*NVz=9jK{|Al{6+dse;=j3*WxT>%>o}`_b0_B}`_J5Af>H&@?}vp9 zBHPjH%wPs0QDdc)^TC`3SIMhRZxB8Z1~4f?Zm>1fW*C~ms15DOoVAJA(hm+BqJTe9 zAEQn1kQ9dS%C-E}2pTQsD=^#+)#v>AWh$N0>9W@#zJ>-t-)gUaxDzynjb#fXn<#n$ zC)M^JRL-9=5mWja8IIx`!71xBQ|c>8d}|HWk1EIQ-My4EmS-6S#mQ(Dn?eDr;}Oo} z4Sd5Rhl@S8I#E79-oN25#H6)iDG~)C7$QGMdpy1h#cvC>MmqyNa&+iROfrChQXW5oNs-~bN*JJGO$V4u!AyF)0U-HVFBE&Xz`Xpm}5Xe0RR2##?F z4sJv2+G5`mVZ;B+;pbU~%{R##hXg3cg%JM}%k3p0PDyrvHhoGtXeVJAgaUI;p(^a3`w{>pR5635U!P z|1Wov+Q=;OORwxv6=ceiY*`emB@2AWeBOVjY{xKpkb*8&h}jpt?f4)}H2sSzjm9qW zes629cm`yg15#sO{mM5n^l#*OxfnHt%}p2&O@X>(oMRzJ{~AS>tDkH~AQ^BoLSX=d zPHTOjMSnrMT^XCZvKCIcnz%~bOS;r8m22S#_62tP0bCre*tu(?$wswn3mp@s%94tt zt{5(hPYgYzPC>+Z32{w4HaF*+WXtPo_wkZqMM7yJv@4$rlXG1bWb_Iy$JKv@&GB6s zFyjIeaFG6Y0?xmu(EnF9C8>RQqNri|n7SscSvCJc1BOL_2<+8GYBm=XMpBd*Cv8W7 z`5{>sWZ)_d&c;1)Ndu`|g{E1W+f~}N+huVlm}DUVGncFxv2L;Cu`)8k-=Fh2^q!s8 zbweiL_B@&9IQ`;%^Wu}j?0(V1t_wCB?dvuhAjNvxZ#sCprpe<+zIBQ^Q(|$)!bcuF zGx6$=9Wu1f{M&6gito;=8g1sF9;2J$KnibhPm#7evc^rG*ktCQLh#-4_tnMz6By>s zZEoN8M{%UBdo5CmQRitNmA3uC#{OF&MmEv_;CobIX2xElIX-mQyHEN&w3F`Qe+qc* zMB$IN9t&XWJ|p~vH~t(A=(r7cYojxqkD`RPX%gI!T zag?MX%=L#f0|NJ)F=WRG;5^G8fls9IU5MqD2m0cn!CuB`pM5ata%cMGweY45t> zcX#saaa4QeIG*kE4jeWsWzyhC6LWb`?XP{eb>)EobLKR*7R)(?tHgC{ZUi+XVr6}^ zFvJb+CY}Cs_slqXXUPbe+d`IJKs5!VEbe$5(ioIzT5mBE06Cd#2c3BiD@|u%HOY-! zAz2(=1P2yXLko-+&R~ zwC@N; zAw?C!T|VpV>HX+499g2MV(2Q5FSo#R8iC3klw9Y zu+87w=;o>Y&7FE5l#^z3-w~JW4L%kYjzKzC>@~_pOkEsckT#cZ666u`&Q}WcA?$mI zXY&7tvUhCHtZmbED;3+eZQDu3wr$%L+qPM;&5CW?R>fL(J&mp3?zPRH^AC)#*EkRC zhvKQRBD*vz+Yd*SS!c&haiQKRDbG1&oM$rH#UxpCqtRBb5;6+fKZz5)HhNXM-jX8c zj9F$0 zNzbnJwxGvhWoMpW0(qU!7UPy#8bz#@L@7Y@%jRAymt}kgVP6sF zkzI+~`RCmlYO^Gx3Z(Zw-l`E)>9{x5!eFjWDluf)r~pg5;U4 zQ|+m?4{eSn^-yimirX5VL93DL2c!%6_S0O~o4Th0S=JO6hiCx^YqoQK zc0Bq*D@9l2LX>7u)G-7mIbnwAh&!lIOha51Pa>Ox%)!tgW+lnh&R6)RAxouI6XtGARi_EqIeai#UEgu8(2i!% zd|b^#2Iw1NmMDX;Dt&{*;0 zrQ3mWI|f%~O}+{IzJexe>ryyex$2@lHGQ1 ziCwYlqLvS!9+R+{sbbZnuwW$a%FO3GATbCIPv_;t&=xXJI+F_sWI~i@%7g6W!4iPG zTY=g0p5M?c<0NTM@9;~0C%N5B_0O5o_MePA^9(@y{bZ-JP;cmbsBkOktyNz`efKR@ zOVl+bAQtJ&sFQ5k>GC`M@Gx}2d=}9D!5ioiF+XD3`7wvnOx19M=(v_Pw1mwqtc@K_Y?=P$>Q7dC0eHcvpMSBw zJb<9@r9L)@`)mj(VZHMEu6(WgnZWyZkTjmpi}!FtFvL2DhC!@HFu3AAw)-91no{ zxGB$%FK+bnwOhUV| zk`B^JSC0zHVr1fUkhk+<9W+yg!`+ zW3ZD{l71rmZ`wC#2H6g*nF6S6yua;0Xd7Ry@q5jhia#)|^$%qK#dE0sMyJI>2~!fR zW-I9Y*7J%1G-D+ewvmIuXIVIERB>ZdYFR*npv4h%KZc_rk~+H@j$b$#0>?)~@SuM) zi-J2uqh94*UC>PTLcqQPvrqXn{Tm#wPIx2kJo2%_e>lP}|2V>%|2V>myXJ|5kZ-cY z07rQI`T8e|UhM>HI*P65Uq`s>HltzrKOEt~U7(h=BcSsfY|sH2`Fb&lmL=?25eQy> zi!j5jYO{YFVK-GViN0>MOsEi7hXsW3n5;I2;M!{b{}T7r z%Xj967NTG4NK4evnz;;6OO!S$H-PL=bT)p(8YkT>SKo-xW}{Y>mBy;4c+*tgNMotq z1qE2bAyFa79OZlDSiTW%tQ!M9u>1qltN_Sw#d9$rtB3XSwVrF#gRv}+sK_XD^YtM-3MjzA7GDTv)e(PaWR^8(Ojf? zFq?*FSsY$6hX0)P!}gmYuvZ#jT4{JK(Q=Pk#||eiI5mNb%!@zCvg*WA#@k!CfYT3@ zv(dj${%Z;^4&5Hf9@<>a6>@F*%3lf<(zd%=b-}68TV;r9EJk)fge!JKS?^eM&DbHU zU=UaF;Z=3B|6>Zj^clqS>Dh70u_&c>8ZDiP`g$NO*Vi-ajyIPyv`kPwJZw~XNKJ?9 zWXaz0G5n^6f(HirtSj-bgB+hLTf?Q3M+Cl7Bj$gd49k2nT#6;=o1$yI=rXJ_Q> z$L=hm2?W4n|0L1;&KYZs9QR3XcmO1!bo^W`Aln|ZTHZ=-Q^n}Z6cDb=^nsC2pvc5B z+dORkbktY&iebQc>?@Ao;>PO?i2YVae6Zl`dkm#RJs3$WXp`FmCGxc`mm~LzbHY2A zM8I?B|9k9Li{zi!FU&u&-`N8mik4hx7Jm;&s?F=d^#SMq68p6P#D1X}m;te0?B$}P z1-3^T>)ZbJ=<47Fw05((e~bOxb2%1D@dia_n9|&i`2yW4qNP#Y z6VAE(sZ0yNiEYK6tMFHEu5*x3;^avDHHCTqn!@brI`i+-2BzW%?N;&ULt3pu1Gpd# zsQ7ka8{g3~H*w>yVqULeSOM!lkzJt3G(vujn&8KYgc_r<5i4Z|wIv-{vNthrSwQI}ppew|U@{w=5qV0@ORa&r>4? z0SH4T`S!x}n`c-_?4Qsd08=>eH>XU|A~I0|R%m1^P`0_(vsd^(rm%2M+Ar&nmmB7b z(#>2-VjE*#Hk-cC@`E>XXkKfgL=61SZ2>VRh}Us$jHvv?eHW%yK{}}@-xvjS4uP7k z_g8#j|743=m1+iPyG#9qD#p=QX}H=hcWKY~g6u@&f)VVXSigbm!`tes1_^YtJMCqA zKZBhi4gQHJcM4^l^O{1Q5j4=GG99w5b?a9&=vCAAY}^;bCqz(8_CSXNaD+fx5#WPn zMe1$e7Ay*M8D7Wn{4IlJjxEPcwExcco!!mUCIqM(bO3dO_5aK^`PVo1ziAwPN@kc} zIRH97%lAo&N&)eFl(`a8p*9Jr_>}RP+9^c={Y2b^hDU*n#O$o36c11~d3kpFjvb$~ zoBpIz^Gh!F5@Z)Hmo1m67x3rI9`2rL&Nfy^l-)!C;J1rs`ss~l`^iI2@8kNO{=0gh z-%A}Rx@9e5Zqkub(=!X+t-&uI{Hj5-8x0G-J#<$ktK<^uAztL4h@)?vDsbMQIfs zD67{)evze#v`k+_!l~^sZ$4Qlkr%C!9GuI8eI&U}c}hS@nE{;WLJ}7C5)nDNhj9opj!dy6hlg!Bps5IDlijO zt!21W-^ecItvrNOQ((6UV4F@2wOW!D(EO_7iJ??Ck-RmoNISb+9aE79D^DLvoW?VS zTl%}nsu-IgKS+$)ZHHovFr^~@m{lqdt(hv;6l$t*ie<(%iybJkmF8J0$HqD+uF9N} zG?H}CC10dY?S<|&E)ar#;uwW@-&G~ira9aNg5tFE9d51rh{WM=M{ zH^B_Y1-x*IPLNa<1!wW*W5;hfNUfV@oV5%LoE3>|wT^t~wkdGjC&D=KqphLW#8KP?%Q?BUzK9Jb9}KAk4*HFEoi{ z!Jtq4u@oOw2%xK|VW4!WtS?MJSpFRqpJZtn2sRoTvGRn^TQYbOELkCGk>O!na!^07 z3&=T_^2RA3pJHQhh)_lVg^Vm<5CsEw8?f?Tpa`jowwJLrIg=TT-clqPe(4e$4K-%B zFXV3q8K7y~Ws*0g<@qVe6LnBebOLZvxn z%2^rDcFM#BaYaZQ5vjQlcY}ALuj4L^-eD3o`2EU&NAqIT{DD)i!h{m}fWHno{p=c- zFn3I&2c1+QW8NSPb511Be0xVv*nUDlaCcCK>{Iz);>c|YtL7D#6l+yhWXzr6h4f|~ z40p#9a!X&~isGk(yu)%<`mckI;clo6VaKF%;SQ~FIUb+YMJBa&wcs%uwMW%L(tXaL zm}nfW^wEdZjf*7;N+Hpqf#e}OclJfKEP}7rc;||xeCznBS#6D3-(c?Xs-e;TLUJS1 zC6O2n;ryxT*1n5(4Y6)9d$+O&`x`X&7K54^Ya2eJT1I(WE76Czc?e}TJ z(NDI|x*Z{Uzsk&u=|c=F%5@#@^Mk}>P!*d*`JATIt&QynjzaChjr+?yzcb5 zh6H1%hC3X&wv5zZYyE1wmR3DbEoqMHB>^noH~L&D`QI`bLMaA~6B}r)6g}Z|Pazf2 z9}jTHwe51obuJ$EuV{Y$$mw&k*BNClYX;Ax&U1_Ej5PvY$z*kG!DYCr$3*Gnc2FPf z`eiKzou%S2vxw0sh@Ln01L%zJ@o`^(ZDmk?vshk>8tnChBW-g-31(I&qr&y-w3YDp zWwR056V2vVoGxpwSZkFJ6drp&Qm6hRO2uZ}m5X)UFiIy%qE}$)*8DR{hDq5LGiWaK zvV21u)rLIkAG!RcXlA^_xpljvDiEB=s~zqV2JzQFOv};VPZrCX_ILO7Jn!q!Xi@eo zKa-aOAF{5EBiIqIOm!J_eiW>eEeVhC7J0#6k**>%+yUX_S88IkzpWs1VtTBBDdRK+ zS`ZB3(*0h2vKQHpMOR}(`8w*R;!89z%sLiQzgbo=#zhLW0D+g?W3wzUg1SfjTi4rlL~A0TDdV{qNG32JMzvm`p9!iN%x3FWD=M-}l~vqZbqqrA{V zc~Y^0{C7~*A?CS(AG+E@;v(T)dv2+1kY=KJC9&ly_3VxPv8tHSN{k>Aejtfa-a6wC z)v!Ls933sqG0&KAoGH1&__XUVS)&X-ymwhqRY!^x#(e`ZeT_lv?i^E1* z;xgM)flXQBzY@)gGD}Iu5Mb;5!bJ9ygcAxz7`~NK1~M1<#T1d+yD=>&XgU}-0#Vm9 zogE;$86OM;{$QAa`bcYxBMD&EHXtx51v2O z<$MeHiwhAQU(llExgfG(}y>#)*Jg*h@*}7?EW&m(OGt#G;_OQUf-|BVpw$P$|)^M zmWlEez;-~wV+(Uo&=a(oA)6BBJOj>%Xn;13zKsBHC4g#AGrMOP(Oqee9`OJEMsX*a z9-K3}xei*ZU?b|gEuGQI`lRWBIHK6957z=t`5f!xo?BY zuX4)_nqPl~f+A9y@}T8eQkI&+gpNB-L7S~O(_F#LX-fqSwI+v6I)JH!5JgU>V6!m+ z4cbu}mB68=p@9PEGpnP_#ih?jraF(u^?VG*DKS`src=&qj-8nBsv)Gi%y$z0XP+5$ zHwrz6dMWutPwk8N=19 zrc}2t!qTGn`P6m_>!{3J3&<&TlC7EGiKrL26O$EmyUvYdf&~pLje%N3 z%!IB{c@Z~iq*i>S>EKv6r0|llol{@5%1~~N0O}XA_H<-`N=Rh$3bF7r!m__cz z3kE>+9WT5%m96rpSVlwjrR5KgphV_%dsv=kuR5947+X#i?BR^{q?RV(z%%De!6wk? zM)TFG2QKZdN((d-3Hl2H63G{;+RtHDp&oH%1DKXq$Hv!{pyCwpg2Uwy)K}-OZJs#^ zox2!~UuZ9@6569<&F-Y@8B_;@b5b?e8H8R;*z`kp7C{ zZ~Qe3u_PmE0n%9!WDkKGcFX6q!issn;GO75=0J5NO$2NINtt!G`M2C0&>NU>>*{uE zE)`YkFra2C~y=E+lzRpe^v8Kd`GiuNbSHTyS-Mi57|RWbsf*xw_- z2_;i?xc)e9xD9d+a%-9<@5Ug=<;JgsLV`uc<^?wJvsaUW3J-++fO2yT!P%AP$qyz# z^U}+LW9UAL8TD*9p{z}I2Zzktu|}T!r`lhYBLV&`?;n*zV)p+><$waH9B=@YBY$1k znP=i}xw+gQlkji3`3Lkr<>tRC#{rt_q}^S8dvNv30$KY%>XsLBMTl}k3U+3~DGj2}qB7Kw33Ni3@lnM8V@x6;hhU15 zs452`et{!vnDs6-p?U`Vm!Hy@7R2Xu7SLOAr2Il!C%(w( zzgK!U!I%;RXY{s)^ii^LY7+Wb9W{*qI7PMh@_Ak!4u4TVRZ0g|Ii_p48(}9}M@uZ3 z)yG~A8qGzbT;h-;i|P^pqVK-+v2HH8vVd;0S$oJU)W@>y+dt9wGfh3*L!f4gJdsPL z+b!ey7WIFk@8ka&edqs)VgFC`UGwF!ZSimP-Cqmt0j^{0HsrkdS?^z+g$O_!j|@P+ z4+9Jqv;052xc@40{QGh-pk=1oi^jI|v(LJjW;Lve1xTEdL% zGqF)-N>ECX#1os&X-;!~h^8wW3y@1D8B7^?dvm}I-z09B#VdL&EL2sGY^En>k zNg0cr#3PT@=LdprL?MK2<=ZNR<~>z}ZPnXsg!Y?Fgi&9)!6)IcUGTdbVN`nX+d>4- zTO(9{MA4cbPTXO+70>B_rwgJ)Jlc9_&Iu|*F}2ZwR>3~Xn#g2j$`=%tJSslx2!%$c zZzYr@Q(rl+!;bo!Br&h)t|kzIOBI!6Kbhv-ybdp5gM zv+Rn>tmI=rwA!^fg(aK79&0!Pv;d05--)=}E8m{Ux}8AZnz z#l8sIy$#R-X`C$QKfo!)GM;CxQp-cFR!*q$J-oW7K)HPRc`k?}Gi4^a$rp&jx=k>l zbn3{ofxIQ08D;~?r|i&h7E79{d6sxl0X8bia9Es^8I{J!L=O`N!qcW@_7956%Tl^z z7}CE~SkVq2m%u9Q9fa3##;sELj1|3|<6r$FQ~$6iq)E7V=(J4H%e3v)M_8zS`;ZhI zUmM5Z7*Iw5c8)Ke7lk4ZD9qqJLxoa>6hr&JaktF)jIcR2N7`%ffHmo~@-oGvx> z?if$e9o&~3o}>e%Sjt$6wYgKQTot2!Y$y=h;)ZN0j9?nQRPvP^n+pk?bYFgLYY{|s z{?7a90BTc?g9i?jcuh)br(Ap^$ZMmSo{)&yR|p(=P^nitle=i$q1TWtI)3?7 zS$&6P&oWLa_(wHW!OkN6`&l-h=oT6MY;(nv?xhlS%GDle<0jD+e!SEQ7Vnylk6QTQ zQk&xJWrXT_b@Wo+F%xMB6P<;QUk#L=0s~rlC`Ugz(7x_1E%fd^@a+|$6bR}J8YHV%*aHvZK2ICq^X-8{w?7Py(u zFU{ake7(7H5twhG&jx1$1MWYKmN=SyvQ{T^P`|s>X;8Rr(#F=p)`9n60lOne`~Y=e z-&ODq>%!pZ#WmcKc&uk90W-W^-bJSNT^@sa?Df>N5C>*!O;Ppgh)gj)nB`U%i%$<7 z4|?PDgb9r>4`#=Cc@!`TdPY>LVZMBa9Ig&pPh1nj^Jw1}IAvoQ}HivEOiK1m%9FwU4 zIHNn=?e1DSMFwz${I+08>(^fm*2M}mbB-&q!L5T7-%k$6eNkvnhS;X8#j_kef3Sw^ z|JlL(-s`l9vGZdum5!T0X4z)}HD`!G?%6(I|C?FJ zvylS56?6m_n~koq zPy@Oaiq?-${*C*pMq#I4J}Qn+0w!ZDjBMDL;S`BZ==a>Cw}{QaBz_Tfa_!vbt!S=? zVD5VgW4NxOaH*njN(YJ6BfD5ca`Qe{lM3w|qOaZ+V#k3 z#%rFo5_h@8?p6WP@1{dOw26+)qSqx^e3G(^^}^QE3>lbX9TTMzLv|4{?XtIf6~m_s zD75bBb*ar=6DyNn5xS;;I-c1r%4{a1WN9|>D~cOW^gUf6_MxQ%kR~JZ@72AlBM;~B zhJs;3-hxUm!V=wCj`4t76k_Ko()nFns^PM9$gGlGu!IqTny}en21QtOQbaD z9vK~itY(-EiYTUyVyM>f5wzU|o@3P(k3Tsv*14j4mk~(Gx+P#Sgu@RBDvA8QQ7BS> zq&9H%@nd@;aa+`ROZMy3Q|vSP_K^#}5$Mfs8|6m*Z+{hx~MI}6d# z;SPCh0BRY3CF9?TVg}bIAE~t>tGvl2GD6N!_{P#Y_=nTSCO>7l>r70EW)k6;*qofL zH`=GqX3VcAKff>XpZm`w0W2x&GqDBQ6Rt`2%uxm}Ho><^@6CsMjfGKKjvDdfHf@y^ z>XG{BDJtH)0dU^LQFde3nT~Y_hU0?@x3rXxmkiy=fMWZvJuk#RJ8uT>0)z0x9AWsP zC-1{lzDRur@5-Nle;Izc(?%^5WAxG&ZP6dzg=lZV?5k1vR_(Dv36XA&QisXXoVNUt z7Nn;$rQ=Fb(B>)5GZA%j>{LENY0P1Z2w^TDz>w1}SSxEIK)J|ah}iS?@X%Ffe)Sgo zA#5s3uRTXA{P_qkEW1^UT0|yo#aNj7M_bo;N%}qnfz-f6GUYTa9Xep8uC?rG2zk=}m#e709y0cvi2h4O0K=vBBSr!WXUOH5AjMU?Wdmq9 zN)&5?J6AXAr{XGNdU?GP6}MTH_EsyY?B$MR=`Z0}1=8>0dddi#ZrtROF$QQhs68q( z-2}}99J68iozLpu!11d@)?!Y>ndh5-pJgykNV_f~n$b$>J@hL6I2l*wnA3My=vI-V zAh2cr)FPNXpmh>m3e?g_*^N_h>pYVt+iut`fXgrmg|u~@1o@j{0EPvm7!+Dm&jBe0 zAA<>mV9*X#jcyHg9Np@uf&g9_I@CYcJ7sh!Hk?jvMOubKmDJtkFQoB>sSXMBkOn%N zTTY)%7vj2qp=E#+12wh~{R($lqDtoSW#@7X%|9syZF}PftVq#KO0CWaZI*FW*(l6< z{3mO>W&;bgfn$520R|?loPGeocmBq!vv?Qo%JLcbvuxJ}&}G5(@D>~B0Hhe+{ubK{ z^RDAX0mb&#g#ReEkE;)WKgT(EnV4RzG$>vXE1D~+|J!*lBPSW#3(0M& z8>>NNrV0I6>pa!l6A{Y_p}kxi)=9H`IkoqwX{ZI$rD(9YJfG=$sVX$O5G`lZx9~I) zJ~(U4-F~QSjbnu)v$Y5(T)v6>ExC?)!C0Do*~;mmzCwGn^C-7zlhi>WnFA@c_-?Yb6IX}ShN9l|W?J2! ztd+h~j@*wAZ(IW=gdKeD^lJ*B)rt45Nhdar<mL9A_VH6lt8RX3Ru2(X8_K7o3PuD!&)n*qw*KB zLs=;#c_=NQbSJ|(++MO-IklT-{;n@rM%P#Xw30%N*q;>XCRQXZ0 z@CU~aahe?nsZ$S}WEN#a!x;PLAL~c}yG$nAi2FO|71xpXUUp+s|MMsW8DOGV7%)*h zIwPWmSFpGaUU()PbeQvGBe<~PG8=E)|D)C6u)T&if&6M|wr_pY#s+ zd!rouS$=$3_3&mCqIU7$5-1^ydB15OQbLL*zU6iL63u)&!Yp&{3AKfvy(Jj`1!h_X zWg4bWrp+KM(<`m|>kywOF=Ib-g|(wEbza7KqJ(XRGhJ4!@O&ll(Xc6|%Yt4JfH>80 zu_^FkYl``pC1uu$n(YR!o!^3w?kN~!#nOJbXGv&XIVVJcjIRQy@PLXej z+Q1;paXu7I@_{>M6xGU(dgs*6tD>3K8)~%j>(pdIXWSotsK;NPlEFzrfs-MdM>4BK z>(i<3mAa^g{7hwNBX!JP9C6u0)h^g&3uuk1SrD~FbBex#lX4UHb`w9}l$l)sZTIp3 zcT>Artmvp&Z1X}@^Km<0NZjA`KI1>>i6m!ZWWdJV=$+|N{1W>zwih{~oSf#`BldBd znRC-SiX`uQ$^*N0xUlyEw>-fU0@5p}SxN~M`6i}h!o!XEldfP@?)9T6i{zAXFNm-Hw+X{sCZZ0`;ovcx2TwvapEvwQG2Ijqd`NPW6kh^chCtB9VnYV7u3*sB%D8?HwfOTkrKW&JxaqEgrK3*HPdD<)wy*8n1K{~sh11?(6 z{QvsQvdxNIRRW&bK!pEw+zt?8__t^Fzn=EdYHxZd%cx&dd}$`>6qR8_1tKU<#n`hv z-m_5gNni|u3waikwcPzX^&RQiqP0h}Y(72P(LSX%Yb+%RYylxd%{`C9pMoEMyi85e z)zReny`HAJj&rV`vfg;jQoi1=_d~zM>?AU@Git@sO^n(%0O|EsA;gS6SrPJ(Zr6<8 zhyic?MmKp*h@cP6H&DgfO4y|aDCT0srYhdFQ4rmD{p<&CDKIsoH|;QXWzQvPKGZ>W zp|cU!WzV*NO1mz^w%TnV1J}&}gT(uf!PtS5-*N}LrBuEpd&h*GxI=;)aQo=b#d%+n zradXEcKv|KcyhSy_+dAp=dK9;vgg780kIYr&~ECkEOeVq)fvXL7BWRoK{+|B^30ca zmjMb4RY@47V+#jG!A(kLvo!$ddUiz=?_%5x22=}tGK*0=1CPB@!G~u&c|@PD%8dRj zg>rh!us4S#5-Op|07`Xhsx*iB!YrKK5C!^0dzRq9|0H>9G7J*krJq+a%UrrZG4e2& zKqoBR?zO_oZ7bYOkEPk;_ue4ubrc^|^lL$1Uc-?Ln78#}czJ?N(Nyw<&?ui#G5I9s z{=rTXUtwvzr)>Zn4Kw?0>&VS}_x`3tG zrbaNw%=)#4TS8Dm?&}O{b5m2R8N~>s)CPkir7ovthoSmEoa_vmn6E5Vk5xct)Kkjf z*UYA<)_OopTo#9RVNALsyi4Kwy>zWOl4b+ zDprFec<1e~R$+*(HKNaqN4wOROZBy)WkBDSfhr^&e7N{^W@wFArgsXd3;5ajOp3yp z2#9j`{w4H4L9-$nk zNU3i=*~%I@bsgPPefI7)E{^t9s_1U3D1wi4f}P%J4KZ?898hxC9L!~T$Go<9hTdAX z7dOS<;YIaJKJ36cx+O$yb$GFO&h)yf4tRtW9a3#c{~ffmjVd5$)D*|+9mG6w6+HVX zJRP?A*13o!v|tZEY~ki=e6RtrK1}>WqO+^g;9ITaE+Tn4Q=u+*CONH*r-mW%{6$~vO;zs3|LQ=v&MaA z1M2J&+f~L@sQZkj<6?hiP4>jLY91InYiLd@A?t_NZl(or2a{x1{CLuF2fJl~N>fXJ z{=EKJD{WKMLG-bDa=d2h`b-7b`Ax%pa{tbGhXDFrf|MS9)1StfGERzT@L4oySYOZRnpl_rLaVw#-C+^y&5v4UlIqL-X&;= zeys5%(>R)`BlB;mTL z;)8BLGG5*~0}aC!p~jbyehCcU@PKvggfK|M1AwIVhoHVI47^-+9?tE^BG7LLauCU2 z_#%i>Gec;%7)I3y>q3T&8)sU5R_MT>%+-qD)-i7I&S+%I@d*2wTCSj4Y#q6;?a!C#O}cVUQ%JLcuVY+!1+xxu-YIU7@L+We8SQO}m&?L$iJfhQNz<56WvP-U$FMn$7r>3Y zF_EnzdNQ41$G1p64~{-CskmRjsGvH1NdB4yh*#YYvSm{t%3ZTPJ z+2BulPy!?jSB0g z5>R9lL^J}3Rvi&ay9k;a&|3vB^zE{Se!&RE>*3=)Xw>Yxv9e`HaHGJ}6S%bB3alSo zsTl{Ogi->KJJty~`w=f=!vGQQ~S5V$s3F=jU8EuAFf#O_anZrdF|RpQJ=w z6OEbRX06`{wsXjG(6@WX|JRrQSu-z81b_&r1|S01{?`Lr#=!kQS_A)bXD9y`R6rR5 zqG*_iXfO%ZuO%r_`5Q6lf-ucSmDGbcnh@)aO@fuTuBJyzdL=&renutk`7w1f&zH4- z8>tA8m~mzDdc5Gge&d_=;wJa|x?jKkmNW1qc+o4u%N1uSfJOAv_9Z7txCBV!#4#;l z02Z~4ARtYi5myA_!X<$%>k^^^` zYbP1MuG(!#1Us-hbr{7~is$vnt`27>dhZ(*pZslpKt_P68EQ~yNh2n0Isg@L@b9RA zGtWN&Q~=eDn~Pr2$5I_ycpi2y}N0s{VmQ2CxJ?U&B-pg7wP*P-eTy&D1Nq||2FB;&skE*XM zqg1REdf9zs1iMH_C`-4=a(#5h)1^^u;&CX~f(v{mq)i?$9a_1HYEMrkp}sdOAgSf4C;T!JV^8>SYUVJ_Skn#% zz`ogJf@x9Fz$?=oL%j`CJk-T3XaG|c#+GdwUgHZko{in(z}(ez&yE2ywvZ7vgsEiKf{B++rYWiR5PJJ~My>`I2U7rI0JdqFq(blwEfRnjz{SN> zp?~g20c~;V3;}K*NBVJbA$iM8_@_Xw@9N8ojM7|>h&OJY#!8^Ux@KKt1Xy-=X5U;h zHw#6%sqj%-P+h@@$uc6zprlGC0a%zn;#vq}_z*$2?-!J2XoaDx#-S{ImyHnAL$5LE- z+iQ9v>l#;Pk8~5U=l*_a3Ir`~(>W{^?YD#-aA6gfqi3LT&;5Q3| zOpn_ueRSvB+tJVcTz{3U23*dogJs?BRf@7`T7j2zWPDc#m^;|5eb$vQ60`gS1lRi> z5FB7HZD4XrN|V4j0h&*0Zz)yPf$6$zRid(CvSCFjiM1vYvxF3qI`lW~Oo`Y9`WxPx ziAB!H*@~c?Cy0mkptG@&B<(fH>?sqGGae$Bzsvw&C%Lt;8K6@k5C8_3`4{Y;~UzvdH|i{!8o+@b?*8DH{}Z2IE34p z0k$fNfJ{5X!`FK^X*aG-SK#9|1cypO`6c+okbtIM83njy7I=g98=d0L$<@8)Tyxbx zO6}L80b3Q02MbgZb~rJlB*V>jdl$s%h2}-uQgxPhhKoUhoj(R1NN$lr#td1bn3L@< zk?8jNZKy)A^UC*wxa+bz9JYcxqGj9)Z~12# zFRqi*C2l;zs=DIw<+0OaC%mND>O^|g%cE40H?0Vlz_^wrLJgfQ7_S(6$;aBd!)%CaSFZNLt77>Vwq$GQ#h7h0n_gI3R=v1f^`k-jy+L^udFw%U)|KX-Vy8RHQY9Kv+HGDaF;Rq7Te*YNOHQoG)Xwf0+RfikV6PW`LdFj`G=@ z0~v>|+fJh&(7(P*+q{bFe`k|y0q+v~|J%FtUk}sd|3cO_^rvu#oOQP$FmKc>MimsS zZ6rWNjBMPC!x{abWbM}fN!I?+q3(1v!T099-SV{h7S!|ga_ROB{Kf{OgFz?Orgh;P zEHmLn{B;ygJuE^iO{fOvf%1t#5M$MzBnF~3EFt|iv*XAJ;V(m3PZS}B{;-0wJxz=% z24Miu#=u&PQo7=t$~I^=B=gQlcaRpR0SC*@iKT{FGN)@_u zM`(%hTM)qhjYoyT3VjIR(8h*GI=~=!yIJn&fI?Hn&RScPYR+*j#k7d7~EPk&x z52@C#uT@4lf`$a#^iD=mtZ}*ga@P|SR;7t|u`jO7@6V`c=JQDmh_h+Ef?^|Cw;ENX z!Axz)!#?fy(8^+vvDt!+(iO@qT#i8%`B-{5+wlp2w9^n`%3+SizKd#8cN$^f#eEkz(I;HjtQpwX*9{q)e*ji@_a60m;b>->% zM2iCSUBbVo<4_v4WeEm5hPR`{pF(Cekq{c1;r0TAqd#~36!Z-NheQMHpabv`R;09c zRo`9wj|a(<<}0(-4F#3di#utpv~y|AT}O);y_YD`F&WFouOb$0vwe_w14!10R|3sc zyb=0BTXFlupE&~L?6n3<=N!ocrf=FBHxv;3EVC(oRH~3B+&}{|TFN?+h~D8fX@EK6 zK(CIX?PZ=ctR|E`cJtEh-j$V;w1Didd@Pc+atOFx57oQ1P^wLFqLU#;Q1H*E?4rD? zf_DBI4b>jgRo(lBpKsX#1|#8>zAyDVx&Xd5I)JZDM=l-DI8oUo7ok7Y0<&x?adtWG zc%ljc-Nn_(2yVxi%+sDfORBji*EQoVq0wx*7K!1drmJ{0N42apa{7`Vu#k*Xaj8TP zwJQ4FT)JthLR$wSl$?E4yYNT<%ci>Vt3FcZ%C;7^CgX5g;!QcXv$*8dGLaRH^U#;p z$z@}K>HGQ%a$6*5S7xkSGm>BPvcuHW-b!Chpm)QO!gAfWLo-?LAAoTQ*v~z%TF~q) zQEl>zLB3v~AE`iKr<6VeNO{DM)ZVB`W;@3#Vo4llb&G15Z_+#gCo-) zMLxc71H;puU?8)C@*YE;Q9?thh6618QQnRe2uM$%fQex0pso?taMtl}#t^s4;>SkM zA3r2SXM2qnMmcQ+`KeG8#Gr5lGL<(Llemn4gOSNRbfX?nmKABsRnYziVA~7lFJN02hyM7MeC=RriyT6Z zF3;c@k|?npv=aODB8=}IB0Afa4ljR{m4#o>ndt%Iybv30=FUECK6+LL9+wK!&PwA1 zZzzR zMxxUx-O`j%ohwf+!CnisX%Xe=T%$Uh-KU;@h4(Z%z?bt1y?zB4Od=sGTt~0nk6p-J z0T8z1$>&Fp@sz)OD)rvq{Y9Pe{RQOSy%q-Xe^XaAC7S@7%9->|9@v`HW@6~03t!|{ z3^ovymOgS!8D_O>wmHVI;k%JA_N_g6wIh2^?Ke3}_=w|&*KIHA9Q(zcJtMmKgHxM; zm(6b@_h=Q*S7kFVa1O0<0D>f|Jh5nw*1t(!=;cQjkhsi}Mkg{~>HE{U>3&#_Qh++u8p? z*v1ABw*UKsrT-37dvilJ!SW?bm1V+hgq_h`6F4xhA(z!~!P$Xwh_j&gQOLu$ zJ;5R~|1BryR;;GBl&{!CR%aWR<#z23?9a z6_xHxUVd5sQrsG-Mr49QkelBw2N_%pJ<3U}+Bhk)O=6jCd=MCk#CT?^ znOnQ~zFHxPueu>$0J}R59d)^JEGB}V+MG6wOY?1M9*c52esO7C&yYlWTurT(a&tW@ zW$#B1kH5tsSgN+eNOLslHlrn^QM~5*G7v21F;+HwIDp%^>@PjJzT}Z^kY07)d^$Hf zkD$`5)>dK&v z4v`6ga@zqdAndtsX;xx1r`4Z$w@TCsQ<_^<$>8~oiomXnmy>wC$aKezw@c}szd^hx+PNDUc?MLO_Y%t+{-fOEp zXQR+AE1bFGhM_+Fxi~M%GfTNv9JkLR8g#G%@fYJgLW9$lLz`;nGK^}-bWEcjkkWLb z(mtZ=+`CHIOH=W#IfU^pKcw-lKES~fc`JnnX>5pGIVgefS`}TaMNRc9EJVQ{kXB3| zO1S5C9p8-QilrZAvvwOE$$2iE#Mm}6Z}v8_UgZi0KlwC3Rh3Ga0<-SBX!1Z%omP zjUJO=H}`Jg=D>Ask%sCuE>(X}JO0jsIE|r;TxlHX{!%Ml=j`?tZZLWOI|zQW-mmle zu*%=Ft{rZgn*WuSiRh@q*3+-x2dZr^;MP$)9W?dt`Xr z&CKu;Qn3@zd65wfXO+oq(9ZpxiPEQ^e4^|zHODED-qDrXPc>yQD-oeQp|8YYUky<} zb_e@*Z6WK3Fgkw8xLyelRK=k zZEc^99$;^L1-;-%%9_<08XGM#vjcXiEi4YRst6Z`;H@Nt9hm@`L`)2m$@SDbU$Sj! zS6YByaQ?1B;P)2*jz)@fSZ%nHE_}V6j8~lf&FcqDl5-u@Oze(48VC?#i^gVkke<^S zr}K~3ch++vBu*Wg8;_|QFY86A@`y8u4>}}r#@>f&e8W?$3_eS?{rb_`jAR>L0*_+* zhHj7YyQ*J~tuK41{KpM}Ox*+2d^#`{3&=@KM9jxnxc z1G)*By7TtxPbV%{n3BGs<`2>j(lP0|afP&56k~f*C+C<>%p<;hGiq37`Z)$m6ucuK zL)+3Zr5ff)(wldzZc5|DCt?C64jJA&<{qJDL|U5>W_PsRy7v^rr(9W3($^#x?!%X# zC~*4Yrhj70kEFAC7Sb=*xZ=CElK)682n=s8!alKDOdGHe@j!J133dl-QBkau(5N&5 zEl-nPDqsIPAkX1F)_VODcX}H986= z&h`B0x`ma36nX) zZW&FH0Om1^iHY#@BH&)bP3Q`R46McTL*@mKgi{c;$Ws>)(-C(%0Rfw}!w-P9Z=Z;! zyp-Xhg=%G|9V(fclektrH=&~3% z1w;t3&4~R7867n_6J2YI8VT}VX75G8jO2)@TnD9FwKzFgMTURlLTnaC`8d|mZ}v1O zZU9I@XNL)y#DgN!O@4D+)2W}6iM5pBETSnVT`HQZB|V*sNcB>s+bycH=;(}PfC=Xp zC1g%_3Ts2Gg^44!Z%H1zN%Gvqx}+6jBUvRzuAh`pU%gISAYQ_ZbW?IxoechOwF19! zCi%^lo+E7WE87D>taWCp`QxbEHTcJ=0m~KRhWZ2=b_HMMLuP(OBi85`2 zGuRF7G1j@2iM`ew3VZI5qm!NzKO@K|KAh%v3Csc5AkVhkWX0VW~3M5OkdL*y&P z`=>--z`4Qp1PH=!WWHzvwf7OByW!UgB5$z0aQjYQ^45M8c>sxEL7q*e3KFSr?T;h% zoOq%yApJT0+PGXEXPrHKW^OcO>R^F_u(rm+LXtx07oUh^#0B~=d-T)aGujereWk*f zBUaGdJvt1^b3co?<5!&8f%r~QCvwVp2$e%zTZ*SHSK&6tIBg1$PB*G9ZCVnVu2gDd z%u6zHLZw9|rc4^0vU|6Z4NTKAnW=}X+G7l{ajEj>KDOXTYLcCtH#L(S%#GC@OY<(^YNP80;)c|z>Y$M3VcTxG3 zQXV$xR^fQfh8=k)Qr;SD714A99?YP4MwrGJ=3vq2&W>r10`JgpA&UHKJ%JZQE52?J zfeWS;_M%#N0|YicKrNN@P;LWzDO7$|0Yu)9r=lb;GV4mOJrvqgpbla)pf0&tP!GO0 zzHk)0(-FYWy?w7`c@Lo~=-F@K~*zm7$byPVvWiNlHy0wVF1CL?gmC68oH;>wOiWAEi}(eVulj?&|w ziA0VuT8-x4SXADTP?(_A1%ggyzW4iBmfD=yaGzcxs-FwXKER+MmO+2p@L;~PYhjxq z#w9w}PEnK>2_9q49HLq5F~J_FO1Jm5Ph=DQslWAnx^aKPiBQVd{dHIEy zgH|KLP<2F*)$hd933i8;#9cmvJHB%|K#nhJ6qj&RUQfs`Y7Vr7vh_-Av85EY z{X7(&>b>gy3J!r$P?`Hr)LGiU!4-gX?6oYk9R0t84m`H`vc=_ka`V?-%2oaO?9LJ5H)AE>*ugkCx*N zDF#blq`1WHfv$GR&TI>?E;*)FE-wrh_c(={Pj7qy!ebumb2piVhRwpWQYwyLt8jPK z$cA_GXU_Rytdqo~^te8>m342h-a`(06|?{Q6`+bC0d*Q6%>Dl7F#l;{Z|(8#Vg3mS zbFBXeb79aWu$Aygq6kr;vg!#n4HqFDEdy0&HwQqN!vVtFe)DE|dE2JC+lFDo#;Wyt z<7LeW@IS^LXX*Lc{Pq9C;q~FoGDsRs=*9bf^0{-H{pzuEx#!pO^(pWJcO*9`oJ2jy zbqg_yD%G2g78RJ2GRFysbl8dCfAFAPF-nX!;+U@9TY}b(#+@RZ`v|oQM-=lG?5@Wh zeTbPN)uW!4@=dU88+LDy&~-mGa8s!1pzsI3+MN_4k~fFs*DX~W%8;J7KJE?mTRxWDxQZnHi60zG((>Mck$4_QfI7hC|LPh`3f#osGt< zWi7gCDXWuC_JkrbKKoD0lP=4PgDYqgEAd&GaCn>(=bzK%zc5$X=t|Savn({k z9aSbx(Dmn+noRLYbe!$Y6@REISgnW&g|r>1X1m2XF7>=j%S$VkIa&0p5*w5CdZreN zREo>5>m6;&$?1^{M)!!iO03*(eA-J&WPlah z5gckRL8kRn@6AO2OuWHwenCE?q~nZJoK7ygSZ$L#FW^h3{gK~BNZ1%^>@C`2Dym@5 zWVx$c+u2!S3B*gYS!FczI(nB3p@ZqFrN#T=L?4u_IM#aj>Mh z%r!Zy>p~N1lSW5+trbX7Gu-Ouh_yUhvqR8~giIGfdPhld_<(TDghd8ddkf>1Rcdux z(PxSHYh^PI7+cbA-xAqLRBvB&My`Wr!^YA>NHm$HPVP*FT7MBErzz@Gva1%xwTAkF zRrbIyNZm8|eL`*#$pgi3FX~|Z!<@if$on8f-zdN@c)kon z2&7H?tp=o`>@*lc2MEZ5zQu48tq4w~#(&m3H7iEQI5-tSAr%xf4x{jOqC*At!F+Q-QF4b!- zOH!MOSB&H*C88^llx46?xp%4Ju}(hrkr$Qt3f-zA(v;DwZXDIcZCEM|jWfU{q$!*D z-69>BkLi}1)NF8d1k$rA;Y@$_**^7G?V;gSRVf=^!r~awvx$YVD=svgt0L3QW;&Tn z|1=UtV(BaSsT7org@_HG5J~;jcSSuD%ciXKV@WG?73ctD$)R08x^411<=uMHVe)l( z>DfV9&SGtl!5MJd>rcQxpZHZcK+&<_i{CQ)++{ZE$TyJW-e9MQrXBjEu3a5w6|azo z#jG_qsXL}LuxgK52j{)b#wb%p&5sijpz()#WC3wP(%EbH+q(kd`gB_giqu24 z2o5sWFBGgYOr&;RVqLU|E*&mnN3I0?p2bcnQBFt?3TvMs6{u3rgZiO=@*RWgpL{>K z`d7Y>z39P1=yJJOeNzhe3LX;5`X8_KO-Y;Ai#Iv;^^C4k1=& z9qXw~Gy36n(RXXxPIVrN!oDL$!B}d0BOc>mEVV>-=YHnVq=j6t?{Ygb2pQs8n+dzI z6g!X?Mlk{M{bkH0X)9c=rW26w`T}3adM|atv-meGpccu`fOJg0Hw~&tT*U^V?0Gu2h(;Y{vC+( z{DCGKEaEjYj2vjC%~Yao69R*agz+wr6p_fe;^k0P`h^U)8b4&xrH4Msad+a`)(vc% z3)9+*d&4Z(E#Jn$?w>h?h9FNDlqozB@|noYG!&60X1fofw>Q}|(56NEu-cRYPO*8oI*E%;}x?{WfG=Hk56&*Yf z#bOfeD$SEcq)nDl%i_~dV-Wpf=Zs5OTai&b9h&qhw{DHC;vVtZ%sRpC!lo`1C-2%M zCheFBpg*SOgH$Jf+m+It3w|E^p~m?HP`}TA_-p+I0u{oCNu(jQP&pf^N zM%8^2v(1wgQ()e4aS;a-CckuTmt)aR{Nj6G8P(l^zO&F2I1ivfARaJ6xV)}kdd!E=AjxDzKTH%Ot) z)W-er@4vToy)41~(SJ5RfKS)|gPiz3S3Un0@oL)t0R$!S`lSf~f)cF(uY*?MP(n$` z06PTX$u6yNGQmd#fP$Z|VQa61rz4F3AZFSN?A>!#^a`3j=6ddVR&4HMdgsT@2;Wk= z-!tj;q?5pA9_Ik|`pf$6>viKR{@+tS{l6f6%5S8R#)y2FBZ0W+UP_^8Lel(c#A9Qp zjttZq2_mZSEdg4nOpqWPg==6G!0P;TT(skc>;4pU7f4%?Mht3jTd@Z}R*2h-_p&L$ z?`@OK1sLxJDdG3|-XD4b2(aE9Km{U@AARFTT~bF&{%`Jn=|I-d`2p? zb}-$3p3CTD*U9K?^O>j)hkQ)H?N6n1Q0qh}tcx`|jyu!7L3?Mx2PLY+2+)~MtYCUjtgyq1uP7%ZH-2Cdpl+k~mr zSTLHil$v7$9DSo`M~d|~_J3q6&^*d-BEDp|E0?<)r^KkdGZEnCom!&`PBdMY+3uX+y2<8+FOOWcLM~#f;Q2n{L&MEJ zPq1G~7P8GPYL<%GX?X;CP9hp(jceWq%*m$Ev|_A5GVJY|qYxrCv0s|~r&92|RP{Q$ z231END4qQ(fL;>M(|Zue#9J@%EnGKFOfvYRLkl;{qP0~QHLc80$etI9$f(Jvan~X% z3XXOwRhR2Z2TMXJ@PU`m4{{2+f3n~)nJIf$h<#7RkSUQuvou$d2RqqV8G&w2>9tF} z$L4+6mh@->UNKxS-$wOHt~*cqF1zpyRIBr*AFdcJKV86M6k=R=D5EB&Rt(TBI}3Bi z5Jl<0QWY7G5!&owFnu8t4Bydwafj~ik%083(6|}@6WHoEmXf^860D?d6NnKX5O8yI zt2tpzUr2sNzx8r6d3~ifzSGkdh~kXiFu69eGqbbPm+gYj!<`NIym~ zMH>r?Tx_;b!imT=pr9?fP{^)YE%7+H3LitJkjtzL1lo%(%S@PFkY9%skwmsAJ1R(C zbuvq}a4Kl3dCH7^{W6emtxQ+-WhWWUq@HTBm*j15t0!pBId{+GkF~p%__HFytt6V_ zmQU?b44rM`Dcan0?sujXQ%U!!t%KQ6h9)fmle%#@Exo%icyz8eYfD=DWm!}kJsIk3 z(#5iNgI&sNhn{wd+erDAf_rkRHApG^W&D1JgP({tbZT4ESiy;Zx+%q_L1B+*=o|Q$HSA5oJMIUThs%q{`Mb23ep?zMy`KST0@eqr`nE9LlMrR`ZsJuM=7xuvyX%3XZ16+=u#2FwHTD5o=5)K>R>-g*yKK{kFSX`IAp$T^R zWnzAytDnD_d+9*(+ex6H5Q;;Pu$)xbzdKm+fn6Ea^-*g9?u>P2FGO$)vqu5}a36az zq4{Uc7nu9mf5tr3oKLo;st6fQ$W&DAr;?Md?Q3XDf(JTyVN)140?XPcW+Qp}Jb1cN zqK8xh*0taXE_o2A-ypy$QWvoH#f_i2(O2_+#qFwa%6#7gaW?y2@9FP86PKJZUd@NI z#*^#g?{gpEa@8v^e13Qz^on*C70iV=H*>cj$c==2emVnm6T^tlC4NLIl|z>8fSfTj zDJ>nEOq}eJbBnFOJKhNPfqnY%dLQ|qZ(cNoS`yUOgEZ9a2NLviV9uGG!h<{mX{#7k z&(K9KKMo=@;nGgDh5&<6WR#Eeo?6c;YKxZMHjIaKA0EPf+_ep>!!yOcmJiexzLsN; zzPZ0UfCHrM??aX~0?fLf@f!*O5DAMoGDe@-d{J-F#GT>}68||I8R(gPpj^)-`>yl zLuNJ=T4~z9vIH4Mt&$zG18wgtBfF3Td`Q~~qG&IeZD`ddkQW`=#b(|^vmOJoe5`?c zvc9)ErE9s>YdNrU@mN1Z_5Wf?y@m7?1|8>LI@Z|DKx8h#H&_6v;+Hc)*fx~&T(Qyj zQs~KykOE_KU&}2Y5R*EpHv3VTH|9a8`pnV$KJ`$?o6hPUYpB!}{{{K?NTiD}n;ZjB zI}OnO$4FHEe=%dJzZ;_-WBHo8C9YeG2Z#7WD#0gA-yB2|s!A4+Vn&1K6OD+g;JTKM z@0q$~XNUY)*=%VE(HesBa#!`7L)V6ivEcJGu(GjfP6M>z+ZTj0gyvbEMs;?a1BB0M zj@|p7*+=i&Pv6GcuiN%hpsFFiz_Y%5@TLPG3;g7hd~x1r3E(xt*+GCe>v3M(8#y=e zWD%Pmr7sVbpM3fdNQY>aqR68GNnyWnftPL?2`(4Lh#sEp;PFiuvEwOc0AqMY3F|Yl z%U3f40Hlx|WD%krAcE6}GlJ?t?`wSU7R?x3;H926Uf`vlMp@u3-*3CZhT9WGWb)P> z)IdBvZ2vLB=1mf=`eqtGl(Tv#jA(UN(A8D6uMOnQ6CQPs2e+HzLm?KOwBV)b$pCT3 z2e-R=7YWQ}#VGE6!_D1aYX1w<(PPn0s>|Q-QH`mv=&8_!YP<)%Qa}{O=X`sD72ip96q6!toFO)!)45%ze1h_Yg^(4rqa-x zSM00EzJ<0bi*9H#jG|Fzl=If9CV}rxGcBJ&vzvug%0=!jFBdLuCff)RUo+$g#AP8N zPN~9{8>;^f&5~MIUeQ~8Rx$K2+hID)e3WOut!hOPS)q5@MjiM~gO{-p2{M2?o_js zy_9h87q7@vRaTuno<_R+DE_F9-R1~>95_b$2qOn=`mcJH*2;{ESzN;>RsU<829{1T z6w&VOmgD%#F8lFyaLdbe`}a2%kFb4RtBidccUx52Owi&%X`GiFOI9Ia1y@Z|YN=2s z3Qw2gyO|v$%IO-W^1Uo7Tvdzp_%T>BxXr6cmExTfJf3OLZbtjdh7t?ATn&DhMQlA2 z@cI+X@-0;Gnv@3_kmNCTs)T`nzW4;32Wo?Eo0A7BiJ$p8wn5q~9z2GwnzmwDit95d z2;81^QlsEHC513JE;<1;Y+s6_ad#Q91cwyddZRelz7>aNYx#$#Zy|vv$|d2k`V%`G z_o1=+`>Wi(L947?WaJRe0AY00_^#5mBb&X5oOEF5?&Uu(Y!4NT4rkxWVtXO`I81K! z?>Xw(1wl(;RluT3V3ha2-N5#IRI^pM{{;9wRMp&EYB?lSeX1i}4jHW6+gTrUOnQ28 zyi#j?&<`k8T`w9Qe9IP}7fPq@=95HaqR}#tv~g!onl)l=G9_hFbrBsGsi>m{cMFmx ziJ{DRC8TI4D`*d{Cv#bsv};Z&Pu_KQL%Vp3&Kw@D5rr5GsBpGchuIlcmhxcD9BXOS zv}%z{UJf&&)MdETrjM{;Kb45Qs9a>a?ys*revyj8w$n z0y@TWJ2!0+;tv2*tVh&dTkIBdXYd4&y3}eAL0TWmAGjo7MGHpvqlRJ!2`u>GRj=N4 zy`0)HzTEPVEkXX$P4)ep8sVzB4sY!>(DoD-bnp`HKYao1BJd%jWu_?N^;CDNB3MP0 z9F{4{3AxpXQ0eAzx0MsV9TYTl;!%&9^h}d&CZ*d#aiE)eR!cEhJ=DTz3AAaeA(SUW z^ka+HfFs1uyyS8PX~BrLE(bkgu(|+>bh>?422T7zX+=)5T^!$*zz4!(X8Sk|s3w1BO%2Z1hS93to?KyKDC2nL~_DDiqidjJrbn^V2--n6) z6@MdDl`x7K2)*%bwM5?h4n>=iPwoO6!`}tzW25Wrt9#bOGV>{M zH^nrS$j=x0MTyepUlb69xF@j0r7;R=dra`cUPX3HV-MAALlOcXxceh>v^PzueHg7M z=TQjODEvsB<%||twY?RLXIqlIKN0rR9_43E+b5$e4S&(PI#=R;oVq;`@}EhcLAIXt2W#GK$6?88*EG080D14p*HG2GcT%V4dA8i^A$`vrb)BHYQdCI6P7bAN&zuP zEh)FFO8KNLER$PX)1)XiYDpqEB0^0GAC6qj^}1r>(vi`M30ak6l5k>KMkO@axJd>c z2DnjUwhptLsj#972InOjq|P&xsl#1+Si|j1w{sHpTQFJ6Ng!qpE_*|y_ToscgD2OC zA*sJHSPR45Z7FT73ArQD@*m|1eXGK@Q}SVA+d=sOPlXf}TIkjE>-a%`A2j~nGh>8G zV+1pvDrSdX9_BqWW8CiZO>8Kl8iW)%WRI-NwS$oSOE_pKBlP8~^&oup6J42MXrrzR z(FkZOh}RWqn08B8rZeK;hkCR@T+~&-Px4!nJr5#RxUU5={J10i@Nr>_H}f_j?{&4pT{fnX@?&Ts~!dS{ZLR|01F zc6k784Z{bjI!v=6R-8nkP@WIl2dO&q>l!(b>5usQ%FfjLNg=W%?tNTAH*bNXs1Iz0 zCul`4?`;vin^K(DoT%$#xE&Qz3*vf+XiMyxC#FYtbjAbPruxC6 z`rhIYGI^zU)7(fiL|Gg$!8(}ftD|^w>}=LDwgZ;V0=v_CV15Y41kJv_)_n!~C(J}% z+MS^Pr}+^fwu68$XD|zvd(=Il)q#jcKo|`?tKN?@GvTdMJ=hr7%kQgq$#<0EPoDrU z6&(yMzp0qrhj+e-m%PhAfByF~VE}*s!2hA%`@hbF|M#hR%zvZzrlhlp|Nr>^cm~ zehRPi`d}U$pvDH1QLHEFK~Diyq6pzT0G-EIsKN`ir27qFN>5eR3@Y^aAt!vJU%;W; zAm&;23q!^=EwYx zw5*8^)SXoHdPa=nl#4;&B;QFWoT}@4lU52(eTw`}~{dZ_utKpS^2|C}%wg>h-w4B9CawlKA z`Z4XlR_K+wxwL>4`jMjh5M{dNeC_0SyTkd(CXJ6o9fTK&WWLOB8j^R&CXzQ@rX65} z-r8^qe`)=UhtcnLgPJj-Mn{G(Y=xFc`k@Dex}=_J1uC#7Vfy%F{x?Tt;f4adS-4%y z9>dGR&C%W6{I|pu3TpIIYQ!EF@2ru&*LLx{Jt*IqYP|cwp}dexdJ6+!hMsG>{-?cf zRZlI8)07)|gWa1J0t}UA@_R`w2YXiD;!$ zY(&U+l{6fXB@UtnDIv9ZB8&?)6+sD8s%^8HYVo`;t1neO^+C^5mY z47r!d(o#dVSW0xn3F_-^QTP>iavGdAm;%n;-C`58)k0h zno|}AM;(Qo*F!cHbvY2}DH3+32;Jd>&;aPZxvl|{1WFL-V+C^_jT>3=6YFF9>^NqpwW8Umj} z-~#G!m_tl=AoYk}wK5)6rB@6K71+(M;z!sLS2Wc}f#DJb>C<=)Gv5L}YtI@X1{ zMODOlih28@a1+k$s_ZxuodsP9s&|)pmmS|FozjQ4_Z@3OxuYN5T;~_qJS&9A9kY0O z`^IS&k@xs6Z;MS!6)iAg1njAsy*uRUrnwvs{QmtT28niTp&$Utxdec6{ts|0Kw$kp z;aF<_$>WRxn7$CT{rj2VW7mI45e5bzqf@NWqWvyj6YMKuOogx#lr%C0{T*LK6kjd% zO6M%I$nKmEXAv0CMkMuGd%e3V@NyV#^%isP46ubOkni518OB-!&ro;)}!HhCB5_9_oJmobn|z?L}{V9cT;K4ZhQc`v68%+ z;=#BiPy)O10v-rOr_MU{haZJNlS19O2~pzDS920gJ_hDp$Zb z18h+q{i-Wd0Y3f46$-m z9VI8{dcl}&gT%*G8U}7Y!Iuc5WNiN@;Tewzl4}WFxl%;gIMcbqN{6W3s#xi#N5@&l z__tWAqI2u|d9=&U7B38OPYD!M`rX9 zLk6cqVOowC3Pa`_m=XvS!bI_=FmzXh0BwHpZi-Sv52Sc^0_#PTErT&X?z(B}3X{(| z(?T_La580hX|}Sl++dwHw8CgTc_x<*vxO_GwqpLfuymTWx=1ue-kx#;R-~Wx8igfu z1p^gsS`K`G9V=Y?B`(3mRLL_s{SajWQ-0)fPkhrkQd3=}jxC2}t6+NgPO-9#d^RL&TsBsqO0~0oqz70!n^E{WNU>f;+1U+$*a?O zYnTs@z*u@T_da56vpDZDz@;ef(omIjZ%h3L7~OJ8^`SCAZ-fAMtJADB<|7$*v6K-z zTbJU<1CPJur`W&1phzdYSr%LC|FDgq(1AF6<_<1__T+6ytWwt&=t^kCIpe?Ic6?)I zh%hExP^oLYQm$NLbFxM6R$#!b`>w3pl0LI(JY#8?H9vcR0+)OF2J@$4P)uob_s$Iu zqfTK^z&Uy^w;;uoIGp=iZ8mz$b9v~c%=II$Ne!A2C1XPX&f*lLJO=lqE%Z9oxvJur z(as9}#WGb^>7X>l#r&_(=|>c?bD){{$K_;5kq$M*S8U0}Bx`zmJ@TJn1Y+e}Gih`6 zrOu*@ZL0?rC@bx5pU4*5n{iVMdPFT)$#ftGCzC#7I6ZwLTkH#Fp*AFPM=%w5 zJV#oA_RhKULx#TWEQ#gG?!o;f^9SX4{45q*`^TPDD#~Ix3erx1{hcZdcHCo}86FOR zg9izDurHu3XdmaDB-%~JfSul~e(Nv94L3+@uz7`(w*%U3l*(Km=Gyt0r zkLBknkmf84jwx7!CN}w~lfs#2N;!0-^1a3ZK6YSu6s{Qdt+#LTb1;xZ1#96r(`;c4&+@ks@u4cwBy7c_SM6iF}{D3Y0Wl}UtJ z-*amJGx8P_R81>=7YQ45>*P$#=wD z5ih|Qqf)#VP%g5D_w#S0+$3?^{fXK^S^W6tJ!I3jrq3?#@>Sw2qq@=dkwbM5Q5tF6 zRb`-7mLO5aG?T^%C_H0J_y*Q8-`E9(qxZDW&CCoUZ`99lj^Cj2(l{c=z$}ax65rtj z&nr~4Xz+BKI5Ln`lQc}(G+?pu#5qDRi>eXER$Q^wlE%@InUDiR~19!I_PG6PLbxIN{F^!PER_cSyV%@;^yr47GEq+S8y*8 zRb+3nMymR!o91U-R1K|Ca^dy)x^OL(ubVi5*Kn`5MlNE)SCzMQcD4q3=a0JP0XT}P z`W1}}7dVVzhfwIpQp5x%jU@m?33K&$S4@%&b8@|8u-Wh0sv$cP3cg#CKWw}S#;xVa zmO?o!{hPL2*!0kx5@wsf9Wt$eECt{t9G$4>nfSk~9wy&=|9mrE4Hpx+v*3Fnz+Fsd z)RBI2*QN~+2RdR{QPUJNmOb&DF|5A%#QJVdB~MPFNBjjw-y=ph3T&MwOZ;#;3Yi@G z;PvqfK>I#u`ZR01I;^`r1h#zf)*0isx7XVdz?iW9BzF_7uClS%Moz-#t#3B*kTAWM zx|e#hGOH;o_K*N`tuE%5rk9Y#bS4_l1@BZC9LgKftD{z<_9I*j~} z^?9*rxuQA}T3QCD7_2L=x`Px#19rWwQ>p&-b9$xar3Anb;rbUt#1y~~5vW)^y`sl~ zgdDh9pXR#t$ld+SdCl7P`}=vQ@I&EH4-+bS)fk5yn-PjHIJrw<{6rcA`nQ@AEJ?7# z`(V7B)bKJRF{Zj z8k42QS(x7Ob?vgUmef&9*xe;N4tT=NLVI<%goCU$PN@}`sC&O$M;gIRbY(~pIpc2h z%&B6v!^`L+_0J1#tvS0y$6Mm=@&-EQAd^m&mbmnGsqYNV-`NW_7U_X%S$Q1F4Dy^m zfMR@GYM4_z8k?cgbIABngB7;MA`+`vY|&a#S&xQ+tN3K35~JqIDt*e zZZT^_k{vpNJCiK(7)KeGlux$>%uAauHeKitFPZ69!t)c_Qm)kr{`$4RS~GdDXK?MJ z+=_KJ(#Ake)Bn5=io!afQ+IM4+Fp=BQ)n=XoUGSfmd>)wYFKdk>7q2oGLSl4A)mVC zvK$4y!oX5y@M#=e@lz+4r9>u!N*orPKp*jYZ!LLD$;7`uEtrvkjvm(T_op~Kv$ry% zcgTv%fu$ymMx29Nj(C@FQU=@kje@Nj7l16%ZZK)NN)ef%%g%+0&pu%)|8(gX#IAnb zv%TN}+F8dFh`K=C6TL^X6}xA(?j5vA-IK!SreRKF*ACg^R657nVnlb3sa2gwI{ylt zmsj7RNAdm+x!n=znVa?T{Kk3^8 z9$SkXC5&V5v6uDQ8LcUn$mDtYEmGajMRj7f8fE1Br`@J_vBt8K*8VID^pT%kT|Q9E zEUVBO$9LXQe*^`?^EkjI-_4`yUyhOia27jxw5q+Z88*-1T#c(HJx~w1F*T`QVI2I{qACwdR#S{L+g34xaL5TU82N;dP z3hCFTI0MttBaM)!r9(BJ!h@8NRJd9F>|^?UzOTq$z-`UU`IQE`c`w4!A<(jFe;bjD z!^{g9p$oqx9=to@mLqOb4>_hkEK#a-3BiAqXDj@CH9&c$50Ct(Ja2z(tdHB3{Hr|U z{i{3&aUKE6vmwMAWUkekQ!+;DzsmFWf0SoL>})Z<|18fU20#)p|EoM_Pj(04)6j17 z%W(Wto^MPvVh@A=vplDZ1jAZ2IBD=?WDIvCo&RdOC(2l$pBE9`j(v7A$Cu+93N@dk zW#=DzaBy;-1d*^LShY%m*qmPY;UKKjPabQ1ph2-p-0!kBo5HzAdRYgOODY=9`RyLy zn}Pd=aen&a6XGuM@H1Q=g@NRf?4c@ctc0sc`m7~syy;i8>gt8k0Imp$dR}5Bxq!MH zXN0n|jzYLl*Uywnb*3hj8r=z-=OgZl^Yh@ z$XV(bB2w79bH+H&U{UrUxUe;;AVx6?YHePL_fm3LN591OS)Lwo1+l&|{+?Lu*b@f_ zJDxN*ArAt_jd`|pr*~Pi@WG8em%0eZ$Qw_q!FdMXG9e0=I6nT~QHllBUC!g{&r0R8 zrl=7D&J-=K?UD5pJg$3ze=pDG`jrM4fYrM#fGqt#fUf{~_5TUJQnvt%fb-JrG60hz-}aCc?%(iRb%-Za3IZ$ zBgc>80K(8>a?mmyjtkdT@?Zt7ZTUZhX=-X)iMb$#uzR%ks`33$Tv;v+>K^JiAG+vT zi8_Q`#rtVU@Vjcpp6UZ0B-|l464pN@k*&m>(YNDix0U~xu|w}&K}p;tCc0_DUXShy z8GWk`D5!la2vruZ8Uojz8eEkp)tW4+yyn@TEzgq!_aa?o0ut5dJhW;rUO>~3$7N76 zSR;k|Z!Eg0^~!5$p`G-!t02oqD9ux3GRi02?3jL~xybY@FNSJPSh7_sqOWg;I_QRH zFgCI+*2P?j8P1xwl23XTn@3(`wJBkw%0YfdT=zX72Wla|fI)ohh%O*>S+c=Dy`RNX z1JkIFR~>1%{+238eZ`jMH0_`jqDT8?_u$Q5n=oj%jk>blu35fzGR?2rMJ;t@ES#Up z9}QpVsZK!^o!O-hx4gEHQ?uasi2~65VI;xBVv%i}ch8}3y$2Y5^i&$1F#980U zpqoiAX*mfzCd#i?+#6$$Nm`k96$TRGc(Dm!)n|`UO-J6k(qMDRjD8`@7|{bzNtXBX z;IUmY`=2*W?b%kN0~2f%9X(Z9RyP2Y>2A*ADCv1ila$gGhcFzP?h1pr*^|h;z=XkJ#CA_pB|_R`@c{R$HX0QDOA3ABlY*b zDSaal3fhA7#qO=TY7aOZ-(^vIhi?ns5PQ=m%}`j{QDaF5gaC&mFsg4cqzL)9_ za;@32!nDdow%Ie#NP2n3X}H!gYLC9wuWB5mJ*w?}c9x(6^}h`DgwGrS_Na^FX;MO% z-?IRFl#-KohTX&Xhp>bt$V6_o@vMFH9TdPGl@@8n71a@Ths6uQ37zwbN_+Q9u_6Wf z=eYu+B%E)cfsAUjbRUcqWY}B?ryX4wUW$YIlrohWa_PYo>B5gwhP`An^39=lb)OpxPeL?1ijtYGmwAFEe*s6@IR9iy=vqcJGUwLjJ)5X<6JYOjNw^a)43 zm0?iYg4`2r5yu=)lGLyf059#}1HemTttwxhIkkRp)Y}nt$9~uUgO~opS{hWJ{~0^q z2z%@g!84o(ut%Aid$u>`dP!iO!HPI13UN!6ae3NCBIdfxgt@wU33_{Sbao|3?MNQZFa2;Vhzk#X`DoJE^=b@NGwL2rY-Wf2!-Tp6u6e#pl zmmopvTdj7)ckR^0QiOGSy1Nfdl1`G;jPb%K`XTdr80#}_LIklL48HH;<>`IS&LS0r z*@f)vn4rg9hBqAh2?(7t#9EF&b+P4;M-bd@k!=*+?oJ|(1A-Kyh$u~_CPzJLmiVsN zRm=jOJ$1mT(fOx5Ra3_$^{C0{)$?EAe~((`uV0$|fNJ=EfKCBK?f)q{<$zvKqg+f2 zT1AT*fKG7%JgJu}ElTHP0gAj4bL+nH?4_gW?5Mn1&*hZhm7tp7D(k%=XdXSyOfRv( z?ptKp^?u)Z^~yc%mjCmQu^brIZQ+S2X!!)t>~^IboH)7XJ_sX<4ZNegC(?8rK|b?4@_y5n_>c5 zm?a2W>tnj%9G()g)JuUT999!7e{V;rkyc&>!z_f)WGdA?5wrP0{JhC9^TEv?)8SN3 zLdbQgA&RTe=;&1JCQ63nW-!7FN1UL#a)fKS{Df z-7$izIce@d6U;#ZvF4W^@sh~7XM9KnjpLze>rWSi^>>fQE>6Y{gDeh5vO=9HxsDc1 zdi;1oq9E6n?PKa}x0r$_hio{ojl1ElvbGXcSCy{Hyk^1+^J{Gdt0Hf8HSJsPhy;5& zeck>NXFC?HNRsad(2^7!9mip!P4-mp(K51VF-seL2-c%4J7<1Udg?VD2xc}rZH!-` zCN1=9B{C8`i)?j+p;m9~yk-Mx%Vg80Mqm~DgOqE{u@iZk>{rF&?AYZPvA+vc^1|?} zl)5S3J=io!aWWy%uDak9MZym~tm=){j3x>m&$(|$%`ilBC`lTi)7n&QLi34(eOzrK zKqG1OseTb!_X|>XtqvIdUxd9=m}ODArW>|x+qP|68Mf`rux;D6ZQHh)!3>?qUDaoI zRqyV)S~u%iH}e_mo8uqfcwcs@X&acFJfxh}&b`UEtKsd07VA9~>FP?a1`W2&a@jaG z3)QPm4#RQZjn?M0C#emtKegiGV4A2mXYTm;r!NTjXD=A|r7sygZIuT%FCy7GYO$S0 zQxpBrE5q~HwIMqd0Lhj8%0G8jOF`zSS5dpcFBlT$yM3HuS9 zYG6YBu~PG>wrk%Sb>FwIOl8FEB_;ajffMSFQ;hx;E||%2Dy@hNyh~kV*&Zd0&!}iQ zcL-`h6D0BBi&fGaj$@8?oR!)wRBW&17`h+M8F@?Rvi^PL&f3!fX?@3@4vKV@`VX5H z)2xl+S&(*Ml>{y<%JDf7)k}M|rk5VZx)o|W@0qIZ5r)J+ZO7T}k=o)`DOVCj?jgsLkG|ls zTg||owkCeOy_nh&rfUY@v$7K&wpw}zrV*q<`4#QN9=Vo3oB&u(}cu@_KO}Xa5u+wN25w;h${m@R3TSY=pFk=%5`eSUhaYmH|6%Kaci zT3EYSOarT@bi#eBFSIKj*VJ-05$6LE5S70yq|*E6)1yU14Zmq)KCtO%Kk>@GxzQs%VBe!jf+is{cdw>f$O5!aCGb#E zE=7JOLf-FCunjg??9}>zTg= zRO1YCh6xliZWTOT)=R zkQ<`n-Y2+QU=Bc{RztZ?lqZe9PBJ!>yCP|H*Av`C9HdTD4{f5?jrnhu7~(feOb-QI z96d7Pe`ARS{1;1XSzM>+`I{vsS6%m)C6@Wk5+m_K?NOiALU3t(sxD$x@+GODh3^@Z zXicuE))#JX!K59E^Ys8)7>A|%HXPevOLrSBA`KHYHCF**InuFl;UlG>T2}(6rMFZ` zGYv6mr&uo(6=a=Zttt+%dShoZ8&sUfn=;n{E!`TRT#DG_5wEkcjTC{oQ1 zM8Ey7IK229hwcFXISxNRkswhtyOl--R{i}H+-pQTHCd&mfnjor{+Wj#!^i(~9-58^ zZ_LihdKEh0`m9zF2ULdhpS}=RpSK3bIKM-xtG;Vy>#W?Nd(+5QQ%mqQnxhorsWKtm z2LEM=(a7Nb9P$;rSX`{z0x#SapHxcdpTA?jEdFO6Ud1S4&96J)g5Ls7{F%c{J|}m< z8Ab$68_AJ}@>Dy6OM)4}WWs4-1JW24)?1rhQR&OfKyIe!pmZxE%1mTUsTbX@hn~g@ z^VJ<9=i(bONAW^yst^oi;Eo+d$I6o zXJNyoYZnFP)HhIjRNVVc2mB^O7q!wnp!BU-u+GxsBV22I$2Y`%zBH)zda2Ill7--`#@g9E;rE|%59Z!d-{&Wo0m}C4^7AuVOl(hQi5>Henk`QvM>t*=rj{J6 z8){hGvjR^(l$(zyc{2E^&#X@Th$_@#d92krprR}^vjhPP5~zhW`a)-Ol7;IcqGc(f zb?YqQ*lx;?9P$@)mN?8ld$P)z%;|&k|6)C>P ztP~)%FN*VkBax3!fA<9NjF^qHKR}7p8v)SGElrXHIVp9j0pTozHzaJ;+n|t^`8auF znyvwQQ|;%5>^%#7V++I=SRjb=HN@=kdsf$cVZt1@3)+XPBhtmlCv6b9<<2+Pq~u{PeK6O666fHS(4;3qY<(fE4WZ`nlKg1;!S>^y#f ze?y7sCU(higS&r1iScZNSc=|KMT5tA(T3u1x=7Avj4=7c8@U-d;1Q(_{2wSWzrQFk z1oD;sf1t#Q{-VU9k!)W8|9_NN{~5qF+&@raZmXWa7uQ6rS?-pY8=n~;XxbzFz)Z~a zVt)>?uEGO5h@RW?_o`33xL5gN-!bM6rw*sEnECPP8|?K61Ju(>4kpJi-X1};O)`Bv z;uUjlG2jv0pjq{d@e zLOk|Ig?p{_V5+gfrTQj?A+SwX2xB_b$hrvW!=?v=DBljr%P(su3wW1T|#8` zR!6s(8+eyN@V>BEf&5yOY|t%l*>g)zSDF6qfwrbry(DK$)!p7{=ihE(A?>7+!$Jc9 z_+kP8Q2)nsLv;gZBXbj{e~`qg)IFS4mT*6}Co?9EnGl!|7~&;B{EW#Mi6l%IG1y39 zhlK)xw7iY8$95T*r#t-wDi)gz>$F5FT8*eJjiS)}8LzdsTF{2Nnj)^wI6oC<&d%_8 z*KeNx{$Bjx!I@v%P4`aM&D+&Zm&Z}XNHz!q^w;e^%O&Ly&nCfu9;R*LUNO@<5W82c zM0Sh&EYXaQNh@m0n0*Jfn|rNfpxd~83it$%2Hir(L@oaF=&dxS@6m9Vo=kr>6Hc!W zQQHpyyx5f=6IL}a@a=cl^u_uy*zg_>Jb!rHHL}?!>?2^_j9Zn1JB(YkUHSL>?7A=l z0&H>a^ASJra9j@*>E-%=rO5Fd59gvc+_iS{>hy9s0cyC(X1Uo?mXa99!V;Q13KU98J)|Q) zJx=NrN^VqJ&%8^#(OiAOk{B`CmDc>2oUbb+f~~Zsfg1;Mt%ekOV6JwqvR6~RN6fFC z1dYhqLz|;KCuzN2Qr_7-gDCN3!!S$BI4T)p!b__ZYsQ6pQcBPi>2kW{ZIepGz9`&!b05`PwSn_&^{aU53YRm3CDAh_pt=~ znfT28INrAS`NC-u(SB}c$*0-&)l8;`;-^GJc}!Z!Y&l-rK%==r?4AVMg^*P4j@eDrImN0hmBWJA zZQ-nuD43F7qd^)1P-LdC85a%7aHnG-j^#%8j=UBUq;=N`?(6v`+D+Cbfw}m@<#z~< z2RzUu;Y4R=a>I?n2$I?jB!d9LR24Jog!6g^8TPEYL_y{VT55Z%WGv7HXcTzfX3o~nJ9qChCMy?<1gKtl=8IN+7 z2BdBK?b)lT+Ky)??0Q7P=jcU^5{?+Z9%)1Y^+S=kgbNjR&z9r2(Bk5;OY=$c$dwU= z*NLTfwwLNu@|Az!p%GEf%pZp34~8(?7sE%`x5LwFTn;c{CL7gZ)4$z9={@eTc3ux) zyDvnwc)Me-{JDkmoe?JPiS~X0j()kN+kV*N?K~e=1a#jCfxpcI@E*i4)9j~V$b(|% zd)hpWDPn7%0+<=yeEL*&Pr_nzv ze8scQvWJi%qw%Dc#<<$F#JcY9;!Huu#zeAx)&k)FvX0C7f&}W~`=UK0k{j zCMIe`7^EdV5NlR8#%;rx*Bh2r!1FVq$PhLwqHdxX9mjd72P>l^LN}d7b9;?cS4*=c zzq>ZAY=VViV4V2^sxHJqBqo>%ZJY-=_B<~77mpYw&6prRncLo#=CRnq*|Nf&G0( ziWsNyo2F@KZX~UeX{#3=y>9@5aFB3n+)rhWKpaYK!|9a!7MdF$kLfq}X~{|)bp@sj z`FeOAzX1h2ww6Z6?~S^OvMg*jv;nyj@_KE&JRnB|H^?qPCdHmpJpf2Cjz96NJH)g= zk|%^^?zjsCou5m3?MS+I36d+8lmpPPAs3lG*b%cVt5d1Jf(0uRGCryknI6|v+Dl0~ z^W9{S>$KH^73~86V6{VZzXO3*+je+X=8<`Z%4SfGy?v+m%WhMRjtEF=xzily* zqN<^17Jdhq?}9mz49kwRb%~UH^!LFOt42n&VM11Lfc?`E=Df-v!bHZ6ax>h;0)g3Y zi*hKCG(YP_+5oxZmWB{k397=aWuo{Z*D34W%gy}a^4H>Qru?!KRCd)V_D2<8?i=ll zKLPp0?f0^}4Ebf)5gF&>>`iGqbbpqM?~CXQxK!FpH`(_O8-bD%a?Ki-~-PdPJOM`PnzI&1vrw^$)f`A{#+Hq zG|Q{_42Q|LBrFdKJvR>}lfs%idNxDQMa!>1m}AAt4>KfULORGAbzKH6-CqTQ2j=$6 zt29ZNG4z@t$8UM=W2__KD#dV{!tBwpcHL69cXQO40_mQ3=94Y!6v)kQfYhyT z@fGsBrdILpKKO8ij5&)twNZ`C1DPvrpOd5v!I{hg&0`N}I#Xn-QqQDD&$h%M%!n$^h$_g4-QMid0S+iBf`5iVVy`N*vtY0dT)@(#ev@N279$y3 zpRVL5^DVKpNqDmX{7DN_-Se(taz81?pj>i)QRml>VSW(Td&{4spJUu7J@!qSKqvQ+ zmZq2zSgMv*_p_GxD zHX8)dKhSw1%c!w|J}o{t^7{6Jd}Zuv|Fq#V)5xVNk3x=|I0*$jX!Dz+=Koisk3kL@gkH$oiPh z;8e&Qb6TN}z~PMCl^ljxDf(U5!&IM`7{Sl%W4qg|OS`tlnO)2B5I!-9wlGMx~GS9*|uK95-Jc7YGn}IJ6dzVr9oMt>O=Nz+k|wvsFfQ zeN@xu4w&%bg?%S{qipe{H>F1{&7eI$E?mlrud$6)h*91`9w^-i!{0ckn^Jbe)Lb^$ z={=l%gfqP6`f`IiSS-1Rdo2mZnbO1FnUK*PdxSYxeZP~Rdg!`#8b91$MfC>z{nU4K z2@7@|Bz*J9+a7NB5^+o@X4PCuEX4X2LBLe2ltBkw+AJ%8Ca>x1iZJ$x7Ey$M^rzQn247pp zA8*9@22f$WrA_sf*p)HO5}-&njUlBj7YLi| z#nu7Ic0^~gnY_G5?b zd&m_0-NyS5fL>(}TcdxGc}4w;%&V;wA(TCkBwek_P*g#Inl_xsGEPmmpHTB}5l^u0 z*~{u`Nw4_xm#<;5YjIHB^f#FIZxPQr8B|DsyuoZH`*-$VjNe$#*T)_Cuai3?1TO}? zD7HdN20%oC5^wo&LfrruXHJ>1z2b0d1i{FPR5+qQ9e`kOXik}N!YU$qQd>}cFvQ@Z zez<(ZAOKU9O*lj9YY<4*ihWxIT6baJ&a(Y)T?+WE9D-se^-LFe$n(*yAOL>(i^R|f z6xDrtmLdXP=Dca6vD!pY0=kXzOi>I~>G`r#`&L2k&YVYD{>N~jLal|uOqf&9nS(*J z`GF=OJT@h*mnO5z>lK~aD(05bETehqSz+>u5@uYd6$X3IC^T~#JM4ypUglI$@KBY1 zt#v=Eb0ry}!%}1I3^&aMH`m0SN|h+!&Vvi+QZhk8rEdxiTy<{_)hZFEO6_mZx4^XV zQ|5hh4~ZNxUK@E%sh(9WEy3Hxti;ic_BbPRK1Bs)!emB{+Vnnw>oO3n75+j3eQZ$& z&~VZ=E=y%O#w?hW7Gyy?q+4D%{d#b z<@s%|oMp4x;$H0&HfV+-Rm&8Gg(wDShq~g~f>S!D_V_o954pr8E-N=EidqrZ4aKPF z1jU-}yPm&-B|zF;DTle>F@-`bCW!O2xjiB2l4yQxX`!53(+CrIK%(=+u>!X zWTj~Z>!SYG;Wbw&Q&+5^f1gNMDYTr7Sy|$guFKGd+G+j`DbV|**zN7$DL zoDEA2|BKIk7J9*X7JdPGR`I4(e+s!p?M|kCLls$;MFyQKAq@k07XutEVsQ|DL4!Xn zlO`^Cc9m?q!AM~$L+O8(P`F;ay`8P~&zgyaQQAmDfGdaydw)okx^p6Y$W<47Yk0&5 zrIl8BYe+YFv`W5GWlLz@kwjX@%$g-952AE*UCf?>rnZz3lZ|7VgtW>LDIN&96NV^g z#p<_m*KM9Ok-$|ufjU8L@Q2j1T&XQ^nhS|s>z!rdi*w09?ua$9D()&Zx;dv)+0-id zTI6WzUNv}1Jexe4QH|IfBr=`wq@Ap~77cuI?CQ~}*=b({Q)A)yEAX%P1{|-+ ze5s8winY7-t?p$b;et04Y-LbsUFviOeTHs7cU(lRZ$fxHlLTvUTcb!ND?oE3eh2= zQM;DZwZI~f4IMNJ8@YW$Oo{AAjZvgL(W$sMf+UiW_HzX}eemS!3Th_B9gjwWr}8Iu zpJOEOF93PW%lHjdjdL)t@3t2Ny@Nv@e=bZzmj}!@%h0BBr45P~GQMhid}pJh3uO1y zn*r&&31~+L?As6BPp^|e?v+Ep(f%vFj~pUu7`=ASnh0&1BStnXpZ-`;Tc^~^|JK|a zFJs^l+T5LG)I!;Pf*AZ9A#42<$;OOs^eY`utTWngqkju`^QLeEK0ycFXJIOb1fMtz zz|7~gB%3Rn*FRx#VdznLlDDj5McO~be)w?{b;aqhQ8j+sl`T0ICqp`+A;mblAla4v zwS4-wDlXV96RLW`ATSM-nMv3rtaO^&K3d544L?cpP zIy_IzRMf|6iknNw9vVV*sYK`!4yg@@TsKHruAs0W)b}FcF&g}Gzlxj1ROi3) z=U-1atdx<}F?`to*#b*Ri1Y}MK$CP>s#^z`+yV@5o|o4v zx@0~8KMrNCWT1_*&W@~ps+7#E(MZDxW=&@EdQE$6pLo=ee|@%M8gv5ugAu?r9GhXhSN)^AlU~eqoA$BtvnB zswD!YsyYydMzvy+EM|sl3pO6WzTSOT=u|p!d$|P%Z>xEpvvIal2EYNmOZSOU@u^Fx zEnVdVjbF;yDvEw<5&oAei_l9!P`E~K_KFYm^ZLaj91cras4v~1i$E^Plq`s|SoS!G z=dZ`hV`}lJr9;E)#93O!yk;rF)p&C*V#kAcVt2vI3TYWSVs=^$FMzGUm zYeG{(pOyM@0d6Y!+MPDPyoq8rqpdP`4yiEy+u}96eJur~F5N5Kl&M%oly$T7X0St?RP+ zHa+W71E{{%Ga5~zuqsDls!DP&d8itPGKm~S2&Jar*Pys*t0A&mbW@sl3!Q;EbQK;| zp)TqcBIF3jH8>vS8wQOj7uQO8Xq(n%{}px}s@+y9GBNMB(p6CDXfkya9wWp{1R;nE z4V8TBBR2@WqrX*>e&22HMCuOV7pNZ-Nn23)@{`D*btNYM6+tL0>SneOJNtCHF1D91 z(|DB6xWLN9DMBxJl;gPAxVV_Av#&5~1Wj2G2K=6>WAgCjDEz2G_q>@2BfW*P29W<1=uRX{ML0= zP&kh~Xq{{(7nux$q|G6;-OH}^r`>+vjaeRu_Jlj3;g4j!uMevo3N_@iY#3wJ^I$4(_Q()Dt%`IvryRwd@ z6_QF`_UWmctrI_Dd&Bd%z)l-hlI5ZE2>H_Pc_ZJ)?E@D&L*9n>r#R)?Au0InkOX6p;5EqR z{Z+8IFz{&fH-mQmW>Ej*Cg|S`QjFhrW{Qi)*(gtF$T5a1Nc+wps>@fZNWxB;p(v6& zsNf237o?KpfhOtJQs|a8EZf-fXr${^D0^a#tG=u<{0f}*%QP-C*(;f5q#Wj7w>JoaF zx@cV{k(q=;Vgn=B4NjIT-~~i$HbGWkm{GpJ<$OW9b6NH8(rUL-x!7`|8rGCu(rbz5 z7M2h%WVkq+ko1LJm`g}9W_sH{xyB5iN)A5WHL zQ<5sC zYfe_26sLA?8~(>159$vO9%f?D@eHrI+&7zVbTb{>b3LEWD?NZ5!ThgtF&VI@euUC; zXbp%SG_Uq5*%N#?&jtdcsG4fn4$w8@_pqAXloM8Zo!8>Xww`yv_=25h1KRgG@g|zd z-{0wS*IwPe+iS*ua~O$m`#2r^9>C5-*uBjK-MmDaWh*mB74mA z@2-nv#8+;xBq|%SHDO|Bp{>BItBhl0uB#{?vpLv|PiC{Ip=IIHiV~Lb`ql}L_sda; z+)SZv^X4fU*we~U-^N+ZyH2D~#}j$2IXc>v)+A@-Pnxcxm)mp`mdv#z`bM0~1&;G- zjb>Rvn-oquG>>p=C;B(FNaN*NpF|%yQYdFZYz=s(OHK&&;qld zvM^3A5^>jPu|AE@Z&sLFZ745W=b~A>mf1Gmx5Q<3D7O*o+oPfw@YG^eqn>+#^m*#6 zE;2ncxKOK5zJ)YRUbh1#M#@>3CGkF*l=&F}; zY)yC5={twnD+vQ;k1_(pK{o*)=YH1!;2SLu^aARO*<13W3XrphOz%s(fe!Qn>x&Fx}v`5aa5e-$Q=+Rs&64 zYYC*3DVqDkiFsr!JY&>7IlM>#*mP$azD2Et7FFbL$up+e4S8+8VRpdY^;HV!+rRG3 zBCaT_efj)c-DmwuBtfmoyfI4vr28zy1;aD(_@2Z4?cab~`*GoS;qW~GqcUJCquEqd;zEX6EeK@ROel4vB0jawJkOcF3mwT)m28_^u?icHqLBj^d@0#6;a)oj;d20+1<`2^4ORx%Mkl4Pji?)PrYBV^qjr@4cC+C zl}2>|tEKpS9>$1EE9}!&ezE(oUB90yH4(Ye}-Fsf~wPECkz33Av%zs zt7nz6+vQ8+c-|{1>Kui-$9*~Pn>bk04cB4P?_r^`BMeTs7ZFAO7;$cD z3oyKnHM+JhYO_*#WSZ)#k7~P6h&VQ-nj~eoNPVL}GLEhi`3%eMSZxa1c4uhx>+TW% z^vjmMCLGgB?2*@eex6lw|A5h3;dSQ`&cZLKc9LZpw{Z)b@T@w%x8G=pu5DP*Oy_o$ zE743{3G)qx&Cu5~^C+$xJHg2J=q8Oj3h75e8#ld`n!7-~pPuVc8=rBS5rti$bc)nN zd%rn~$-$Ca`y6Ql-*5hMxo(npaa#0PF5h~dMSfN|*@F5>-HZ!PJS~?N5U@Jr5(ihD zmQ1tYo^Uo{fG^W{C+`|+`)HKb)z)9FW%9ah$l9rOY~FclzUknM-?8l480C{W2zya4 ziE8$an=N4=l94VSL2Z@}*Fw%+4Shvd0Lh`{ZbMIaZF&b#qd0(FxIKv1Ipp`mM!)Lj zj<`)$&TTS;?lRlv0Z*$XyggFEWT6`b`_RTfxn8A4{Nr}ir(#8F;FGr+bHe-S>%T{v zdC`1=$=%2+NxZdD?{Db9_%+y9ib)*dF0%q(r2T=Vqy66~GI?QY{NH-j%lEAYo zotIaNxT#}1&DWCI$r&;)FynrpN=$NFDeWpu)gL*URnTqKOnN3{_oXi|WKlYulVv{zQu(6q(sZ*v$@#yf3-F}WK%GMG zup3V_;B-{w5yg`6S77Fvw{a&~s)|CYp?3^iv^I246YJg;H-i4b$Ol5YdQ*$gwx#SD z^xB5Fmelrb*^fOH=$bJ3BfVy?v!8QCE7n-(g)5)z@BX8(5WAcEy_jOYQN}N!W=oxB z!x{fYNXG#?W4YP5(s$yl6}HX1%u?L*+(pcRh2fV6aL8?Sa0M8e{bDasb=gt7pFt#6 zDb){D8qf18ul#v<%a=_7SaymI>Yl67<`RRoUfD_roTs3Cxww@4?Tjt5KQ_8sT^sbY z85iTU2@U3U47S3azAPP{H0BT3!|Y3r>uRy@61G1UZ3n}?)G`c_YrRroGkSW8*WSM_ zw42?YL7A-Lg|tl~$0-T%@-Kb8G+=AG3 zUU2Jh2Y-tFxRlf<2fMuwAqPj`Gfq}!i2aQixx0m65Ge1TZR8#8hFI()LV!wiX(IFI zb0JQq3@?r2z@T{KfwWa#tSve?_<*k1+r<*a~C{N zom)&4|4rQ{vOB(;AiF5#xm3HXTvYzvn}FCI(myIH?Fy0TIadT8wTo>M+Vi@(&JZZF zp7BE9l0OS;V!Sv@<-Ax-)yVp#@IM})BX!^Enfi0(D+j*a~Qv{afnPOn7J$s2xtUqFriVt<5uK@bGx@6kaj zr@^ra4Mk%mrN*EVdpseHk?6247-fnP#yH;jNV_Rkax$NTd%G5eJ@*iT2CK|~rWr<9 zb5-aS(+hi*&-82ttITJoXX3$=G_QJo(r0DTigZW2P3Wrj12?!>kO8UHny4)O#Vc@i zmew-&^u*>e)h1uDe8GV05~X?;?#S^U#JjUU`qYrwEi|9VUF#@&uX(WrO)LPH4UO*UX1%F>Fj-G+bJr`m3Z|c)WALgn?Yv zrD$lf44O%E*krc&%5d!mrtvXGGQ*Q_D=bf5lL-gI8_^8~YZd1L-l$Y`67B?sgn5w2 zNGGX;@4t>ve|0e}t5hB^j+y>`R@HMb=25HzL~@)(^ zi*jt#)+uwY9B9Hu{e2M|u{gX&$n67~BzZ_=2V%9o)a_iWav6>c-Ga{}9DB(25bq!* ztEN@6ESiZG5^syfdTy60+OC(HPmZ0RGIc5+K(-;u7ZrBt65dQO2d}VyqJKB2E|EhS zKP%o0&R36oYlI{ybY4QCQ!xwx2DpM;`Vh=;cnoq}9D*7C(+}m<8EmHltaQKF^ptx8 z1fv`{m?09;ADeVXeg~V*c`!h3JG-|3_8 zM!~z4Cy-P~8fn9%<;~cJ+}2?I3hya67ik7_UbWqRc!O>^tLI-A#aVj!^U^o;s}|+| zQ6K;3q7b&TwKXwvwy?7mGBL3IXYQ$cxud9~e#yEH&?HDgU_;1D{{qU;)u$}#>=XJ$ z7Auer5Wgr+-Z5Yrzm>+oj18nwp;o2Uv|Qfw{ejw4C8V9KU~S`rQgx&5)3c+s^QR`~ zWKzcTM?$Lao6ob$^+%SI@5!cq}^bK#=B|q7U4cE5|%xge~aT68>HL*6;C_0zuRqJhsV7fC7YUau{mc&a9$%+?|JxlP@yEa-vG>083Z40AEzGI%oOn^&-e!3!5pY9)TYj%X=aA>CJ)^{({ql2uKew( zBw|j|ElM`p9VcQA`W+}>-n1|~M5rs4DVc>yj2(uQD^^;hr7bc+5sa?7aJ0LLy( ztL3S}po&v)m8)~ZnecdN{3MlUFoUe@Ossctkrvw8Rva3^B|3vqN3qqD9N0{|H`;0g>(C>?V&X4+h?78V7Nq&{*u^CGB;;708Z7mpT^A^ei4OU&%>tk@1u5(11 za(IDPf>9hgyhWHczQ0^6pp^0JA{TGzz&#u|9qA#Z?v7xpP$komtzh@-qTMh(n2!`_ z=Pz^Pi7$xLm#T{vs7S>qme=X^*}|TpVahp|RO-RQAnJW=D<6vqW@E?Ziv=`+sT)xj z)D}Wmmlw?|Oyw3(wI3^8Sdg}WAWxD7E^(#Hh-E1Tkj)=UDOIsm7plv)4Qt9b=)kk%tUKab zvVP+hrjh8TZ%cxml__6)ub3BGZx=j$vm6$Lez{B*alOKjPN52!roY{pU z?ML*{V3#{*;;Q{Im}2aWIitie&SYDVVolUdbWm}Me8-CUV}#upXP`C`_!Nbqh6&X5 zq{&*V=%GrJo$MgvB{|e?;svgoT38rg)TbiyrcZy)iKv_Y0Q`k;PVwQF;j#wgIM$o- zI|M)49Wo~W(2U5t&M^J0G-6Nuoz=V8pq2I|^YDGl6Yb|PeFptZiSsho4|P?DO2?C1VH88MciDZ)+tjH45u_rs@qin*%=}eBX1Q+6YQJ}=)J~KZKlUzFN;I08>+<~6gstXafOvx~eTbgI4&%JbVc@0wqrMG{k?vaB*2;PzQ=TxLm5J>n}>>73<_(SgpX z!w~kVoQ-h0TB}fCZJ2IBC3IJ#G7*G*qqzyJQg+i4e3Vm0Y9JZ;>R155^JmKEP7L=m z(q$0@yfLd)?9QZlW5srFKYRf9 zL;#z>S9JVvVy0UG?r5}DP!QIz^bmZpm~TW`x(RfCu|#u;Q*Akz_VwjMOJ?uW7#1k$!}!G5g|py|TvI$Vizwgg^F5rFpm zb_^xY-$3j}t{U}W*43~Ew&052A>B*vj;#K6h+zgt(-RZk5uFTYLu26n#5f;8EK&rN zJ@J+{n;vU@Yw(tBE z)CEdY{%|sk^b_s%vlE5>H-S1g)v3&BUU-7IwA}Iw`HyJz7nwku1Y~2tpr3cl8plzo zyFhvfx69{#*qwL)X{5diEgDnHSfDmc;b=eZhw_Jdz8uq#c zKsLlrZ>b*Q!=Yr&u|Z|Dif`!sboU32OMp8ppjN?w48(m_vs5}Y*-n_)p4;rTL39R} zf1NuugIGn!xHvLBwWv0H=>xbSM!u^1rV$frA~(kvjTC4&Y|kl*Xh>dBfPRJf$2Z&8~*zhNmk>(Uy^L%CgcVf5JX2hp=c9@@H-7Qq(VbksY?Z@ zEGaD!g=(3`C}i~}u{QaLPZE|aS|fE*iBl$0lWLx+*~3zkYJ+KZ!K z0nV;5L`yoLUB^g7kV?wMnq6GU1lSjm{RrBvnv{Og;>llxRh8(aOpF;M^zaLukWq5 zmj^(lT#$Cke;MKaiN=v`37O7V1{O&lW5key__;EB*wA9m|2(FRM4%71U(+hk0s;*% zD0+;<3_Jm;PLRPcqGM7m$hXIZf@I6UzDCa^J;`*Uma08za}4)+EGE1Tz%%z@c=r(Y_%rgM+;akHq_y;7lAUqjkJ_@AK^K~<2V#w{0@*EzkZrgY{cUUq#%apWi>#ahl8>& zONk|JmQJ7KMnk(O%2kW|3CK*+i6y%2e836jIvWI+eb!Ok^Jk zY^j;TMv9}FBz>T1OoW-b$R#LJ?ZGDTn|LQ_R5UwpsfDhnwI4OO=$<$#fnkm{d1}d{ zL+_kUQ>~tX9G*QF7zoL0c5>)Zv%;G6K2v~t&1osLbb>e{APS7AC2+Y`pN>GyR23zp zUBEIdsic(=NpS}zs_bD6sp}~tD;)}kM@NBu{2bjYe3Q}Uk!0=+Xs6ddE3juvwJc2$ zp8C5eXaVUx-CVXAaw3qQ_5swDVglG>PAk9A2uI$Ps}P>E>^X9HS;bAH7^VCf`9&We z?@h%7?eo~XJ^&aMl$^h;r4qkoTY{;nX?wIC@&hLW*LR>vSY8U_nLsu_b+qxALW-3n z3~C%eHLT(aB@L6ev&q;Qv0Ac_$+Jn!Y?;jT*&+9Es~~j&X?XJ|Cuwapi}Wq+IuatK zt}NL+US>XaocLat5R?n%hab)`pFS#QmAq+Vs79qDRHE~V7pZoFV$>_sv0-*3`CVdU zT4_8ZwERSB6sL1CsAMm-oR_`Wa0v(X0SKh7YU- zb{M(_&Pj7Ek=;hluqP(Yx9c<$T$)e3@Ez{^TlZ>NeXSiF5VKRJhbxY)E9JJM4t7Q zGTSyJrlL2rN4bqQmj)|s$F%+Qo}5q6K0NBEs;KJM=t%^z(B6fjVWq;aXtYx-SS|#W zY~NfovS2s1kaP2pb2E{%6vUUrZufYZ_Xy}?X`nzwt`bt-|E|;t5o@+9s0lIgMQO#E zFB`q*VzN(_sz1R+DOtsU&6{OPu zpKdSL3dEqZ$k%Yl>0qUj8yH26b@9|T)_`Kj|FmQlKt&XNkQaA>d{?w!@+_BIl+wv9 zb+fekP}`J)fy`73Kgp_iy8`+ z4>Qeapt;Idoiweqp~N`<#mvzJT)o=}hXN|z%C$S4hA-Tc)=M?E00U=iUJad>5PrwoltZ!6U6;G`B8bBuwoN8_kV@(g`@zF1Rh{o<=w( zq)XwdKzZGEp&q6~JdVI>o~Rw|$dYV`$%zr8wJ|3(Z&1awpcwbN-egPNoVV}iYU7!# z$MS^rg-P?{k%pkt>+%+k%0XmiFmDc_O$kF+ve|^qhU3E7N?cXa(^{QzQWkv0m^3fw z)&mk5=LHJmBQ={v3*lyV3V>)b@WQAT)BLZKDFF$-{NW59EF$V_ns?1nG@3EPU!dTo zjHCVm4HQkgu{7f}94rxz&~Oumt)Sw@jPZFFa6Hq{w@jUBL&Vctrz_ikWN*o z1rde?`I?cCQR9ZzbZ^Jsn?~;45jm)Jy7fL=nIXLK@-Ao2C&km}15g|R9PqPNIKqH| z)nFqM-e_VDgF(a0HhJz0Y@c)moZ;a6_in8rOm(j(qY#Uv+ZC8C|+g<3gZQHhO z+qP|^>(#tB$;(VK$^5v<$^CJYlf85Hy8EoPwjgMHZ5GqCfa>XFgWunYFe&E}Cm>*m z>la)i?^+-CXPg&=_ z5H6)Glf66|s#NEdpXmpNMNdwqc;m4N{5js)25*m;Vc{v_-h7EI!ns`;!!=_j>LPG~ zy)|V%xdCMTHuwMSDg^JBUP8%tH9Y=){zsPgfA)g@qbe#p7#ou?{%2i`kb>#s|0P_` zf)$D62oC5i6q?$B1ctzN_F$xYz%lESne5I2U4I~w%Km!(GpmA%dj06d4MGCt`N9h- zXt%%?f7eu!!k|4angw@XmrzME!Ae!xK&#wzOtuWbN9hwxmmbk9;s|eFQaehEtg+Gr z@bPE-;)imPtkqDN(`*VT=?{}Elg61MEruuj7mPlMTONG??&RbZq$SyDq<+ zHw*b+alpZNC!gv!v`hPjc8vcE9Pod4EM)6!M!0CoSgG#>vz7O4B z{#cbX@I&xJ;6hG>`h1lr6iZ3cunOB_iJ`FgJ#06|5iXLOttB#Re~%_UZRAXNySMp% z{I0bW5S#S#O$(Jl(SrN=%z?xR)=qXQ63;-msZWvho|_G1IEnP<^`DKp`YF_U9=vCA zcLKAzZ^JL!i0#IG;&@u_Y=Sj9Q=B^3lqS}96_6Wq=`&pWwrtN92NoRN%7!{6g~|iP zh&~q@n4#Eu0DUb%c*fjjY>*BDCl=|UUS9}mDOU{F3Jq~imnJR;e5Ly8yIvX%k!y|Q zP$&CC(Z}b&tc-dA&W*#~d?oJcRQLXR$FOk!+aS7XJ|}(ggf<82F@>Tqsz`CZc5;3! zcT9PD7rSBEX}!o+A@vP_F1}t5L1O@2;lFDTm2^uCsVQA z=DT&zQ?FxcXetyT;mQX}@@t(C_NQQf>+9k--~EV?uq7YfO?K??8jPha}2|qy|dVPqA8WCLSCQft)Kj z*Jj=LkdT@D`P_Z4RM|auKSUDZozoWB*PGoUG@|@HMHk%;HopKOQ`>?{ItFMiWMpqs z@|XfT+k=why233Z8m$a6z|e(Z z%S+xej8WqCvJXP`N) z!j8jN#(}@`>E{x)^fGUM8`nEtE0rg`Zoc#enN7E^VRvBW`SF{#RKh7!#kE}pzFlQ@ ziF)^(Lp;eTay$Pa4RID4>jhDfV)uMYf<}u3)sRjpCxs=SH*3_#Tll{%RR=3I>O$$+ zzQgaau^9TXQ#8u9eKA<2ZIJ;!v%Ia}H{Rl(T;e(%j)jEGR3aW7L2jT=p#=?Zb;5~Mg zS2k8QYF1k`E~>urOm|#&wM(%6dcXYAcNvJ8I2N)p+}mi^l!ogLGzv2ry zQ-@$lvR|8gl-cMUhTWf7(#d*6!rM+Dc?uPeGeeJdnC)xX9?<1ZB6$iAk^10-77ZJ2 zW#2)D>|_w--t`sOlb}}(qouPC8CLv6?Js&Wr zAGf$>g#%h%-~se)_K8{@1Cz;aJJdF}RD2yr=+*ZH%?u%IEw1t5%`L9!;Vvz%$>G}$ zw|IbS$6GEsujxTZ;D>KGV(SYrzSKe#uyV3fVnAyjRZZ%sX_ZF*X6YO;~3g@26xprq}P2H@Rs!DlEy zoNy_&W85T_>bQx`*o|ZEys5=GkE`#v3C)lh{~pUoAq=crfXyDC1xDPDaX8TJ_By$R zhr6`CgoH;+gdT7=XIuwf56CLQw)T($+;<_}DFLH{cp7BMuB8;mONtQ95yeyXqe^ZH z#f-eg^9ghNapS2%)Ei7YNz|W@GR%QR0rujb;7&J0#9`W0oyqS|{lkd(Io~5n9WR_F#yseU+7PPoh~Clz~=K!0ERQw9t5(MHA4M$l4@oW`rO>K4EPK$@NK8?n5HAtcTUpOp{ zi63rL6^*`+zI{PM)-obO6sQ%@0QB|AQ{Ok1dNAjQP*Z`$vo!t|LR4Y()zRsp z^!+k0mU37?fRGAH`LMYasIzYvOK)Gjh2=C9#JOK?-lH~FLup*r z7^ps$M9LA~y%3MppByqhy6F;iB^7OR?{u89N_hH6uYMBqV03t*i}QZpw3|JCXRu1f z)+i{fFC;zy^G3@Qohj==5STqKW?TLxgV<)DJX$YKhAR?U(9Wm$>zx>1{g88QEoVa>Q?X2lu z`;p!SN2D^X6{T8@!|70&gs7c8W)B61%y~RvnSrL&i^HKnz~ZL{eP21eL^HQPygX8!JI^3*?OMpd_8R%fwDC73K-LArpRAc^wo6S?QSHL z*+NBFA|Emh8Vcryg=g-@&(>S08?}y{iHO5w4r;;|AJ>oWSvZD@C%Rmk%{^^t{G20O z$PH#h0=>{5s2e2-Gx8YSUIjVIfM?;1tAw@N(dkIR)RwYa)ZkoD*e!GJuqrwQr4qO5 zJeryF@yab`STECwUoh_?w_U|*MA+zbIU5So(_U$aDs~Ry9%gI9otxIetUwTT+l5<7 z3wNpbt#MKsKI8@!XdNrZUYu$e@E`~@YAP}ljFxGEiJZ3&+-T$-10jisv0$6uI?y9a z9bfg0EV4^h30$5q94$DZ&&`#s@oKsxi-P64WQ-~vR=|QYzICu>$o}<)^Y=F?Smsbq zRxS#wAG*2~_3+r&C(-&a?Cn3J?>6p=(IZ!*#X`Nk3_}i>wfff=#}Kac1<5Jtp+HI+ zls{`Y-6=S^DlY!iEm|ux3Wu6jirg!`S+Y-d?-xkQ)Ii`C=ihON+*~kntSxmVaj+st zu*8A!7e-6$zTUpq-mo=!V{$*Hseceno5XWgw(6KB;GFVmmR^6}dts{akU4eBn~ID(Sf~Gr{5hEu)vck2 z$gK2NHDG_T{(CU(DiT~^lvrA6uDtaKj;y^{%^uvR03^19`z7}X49UcG&HiL{YFnM5 zPYMs|DOD=UcDADCxYzoXAjKK#h_7{*A-dWL3;VJeV}6c!xeruq4*Ycnnb@OXv4N?w zikcQCo`lsD9PU%=Sw0Wmlf)A5<-r%3!hD{!AcOxCKZWq9!cg4+p+Sua!C!pKA;0XO zS!N28SZQ@->PqII491gzZO((Gy2mVe^bm#QYrp_W+!$OA_fs}Q3sgEtQ#?s_i3pd}R6|7ekcfP?D> z^E(}2>!65eEjbv(Dq=R&W)vFU@7}(ncc?7djK!t3U^bS>OWSxwSR%ENzT_~q#LC#I znfB;nA5eOyFQ5xQ&p~wPgA8IJX`IbGl|d%a76en76H=Kro`NQrbD-^aqB$zE=35y z7LEdkGj{_>+wnlZMPCNSDef@_KWyl>$d#TcQrP8&e!ktFuho?ODxSm=Ei2{3)xl!yQH5pph+Yg=ge=PuF{5~?D`q2M zR^oz2E?tHjG>^rY=rMFK`k~1n4>-izolmIF=+gb5k4BG;9@twT^1_mV=?+3if6u+! z8`xbAVRjD}Fs_G}b}rA~`VRAeUp%MXd8hx^n_MQJ_mqb>>tw37682P)y~?6TMto-( zYr-U#*lLi6DBjUt2w`^ObnC%=kcYs8zzLE+XSQEpne%OJiftM~OnrajYPsJ)iq+P^ zGJhzAK@_1kvF`60tmW7kXA`i+9!~E0o?n3GJ?kJ3Xf5EKu}~M0uJhOWm6uMg*aO&rBIWuF%sdAr-U0^l5cv< zwBBCM`OA1rw_)#R=SNSBH^FH<{um$Do!rK=>D7D4`@cR0m@!HRc3bG z{>dU0VH@)<8}3<`yZ`wPQYeA(U<;RjX`FPXl=I&NLL5INwSS`5nM0}}c@5P^I(~Wi zlFIGAw>qYkGK&o>cW1mf)dNrUv)Jo!OX_6lT7*$cTVe6*Xp9&d22A3PU>xz6EfbDV zK};PbDZUtcZTycHN93Y6_QWai8OxpS8*4C^z5XDFm{CPp^ipN)I|rEpccIo4amrdR zGNn#J$x&3O<{OL`-hWd)(KqyThl~liaqx>Gg&0(cTIO}D@~1DH%_B5ka(fj_CsK?d9S0*D2ibh=vdHD6(RP*v0W!S4_FVqB;keFrd9O==1r-qg(zSm`VL1&};f05?O zn$PCPrdHP7Xx#BJ%BqPRpV6)uYoMPMvd@AUj=I-c8I-+|30Zs+)Zs~dRsC-yh2Txp zZs4Ecce`+|u*XE`w&aFBU)GU=#^R?jU)VLkc1bVgU(0&r`$v+>1cTT!$X_tN+-dZl zL<4>K_H-$3P6nO=2m@khxUX?x>pEHVL!@JkLkt)L^D6eQq%kbO@y8_Vk~&p$};muqCM@Ek?s z^O{8-+<|2y^5i8b+7=bL9~K$8L(GO`cIPCoy|p8zwi&etnN}fLd1_*^C6OQu3&Kk@ z&8GvaZ@Js@@e4*mXyueaX=N1O)5}U7tc5&NtWz;JP1V#D5?IZCb9tXk9MP;!N9s`s zpR2^HAJYTWeg&w<@PlX(QT*2F${|AumQ-C{2zmfEnHb%Z0fi`1I*XVFl-%k+xy35I zPO1ju_-dw`qz$D^Mx2O_yriFQB7@S3WC;b=lp1QY-Px=Cx@bqX_>)LdD);{_&A2I( z&T`=~;SG?(v1lh^SU{YT;*n~j&4W)SJu^4yr+71)OH^u$P0Y);$ggZ2Od0rdXeDA9 zfGFU2`>M5}u1>UCWaS=}|2)uJ5EP4ALx;buOKU zbLP|lSD-$FYs?_hY!v(n*Qls+<0C1g9PS(h<6zmSZWwZC5J*V3V_&`^UZn~vj+tLK zEd@J2cUO;X;gmCqWS}Brobdkb3}a#DfNj9ob*5+ur!vS?uVY0Zh1bay+nE~{G{Dqy zfdkH%FyKVnj>VX9?LB*88gkDycm1L~QdkDBl5df^;7sRSlq*=)g;GoDI9fQu%{IaY zwCK!bEs3lY9Os~3hM;}{)hvGD@ss;_$?D5cF%oewA!+H0+n}l_>~Ln$jW%;WFr8XG z8<{?vgEq57i73Z(;`olsdK z#ptsA_2ss4nDX${Nn&IP`dHW-c_fli=&>xu*YdcD5h?3H4Lm^$O?3t&$*q_`gmO%s z=}lT}Nta&e9p_)trkt#LZfq@2w`T4kD668UyI{QNxx{5QkS`ANmEw5b`ZLn8UQ@T6 zUlxrq#{XNa#Ra{8Z=wC+WqO5mf>=q0-!$AKlD>hB&Bev~$EIT0!aX%$R>Bo*#7jK9 z``JjlSG?SIJJ~98dAjZ-3%LE(3aeXZp$_1>y;9sR$~P5sKesB*LZnWTX_7GADxXEI zNn5b|=6`d%xpu2rGS8|qE(GzH41w!=E>n~DibSkHN_aF2*xJ^A8uBLHh??||xl47| zIY=T;uvcaF*1c~gbOwW|c=(Y^xTjq0@>2k(n@L@ZJL}?5Y?C=*3BT0WCBYh#8Kk=I z$DW6m&Mj%PTDnL)`(+@Pe=pV;+9)>$vdiMf3?QQfxbZ!U3UkF=(uT?IhTuYlE7Aq#p_p9X;-G#kJ>}3D#^*pzT*>4 zh;Hez0fcNmsT>awO^R-Ug^VL z1lLn^cD7<`zx=7dx{liwuGw-%)zv>?QqpI0SD0;Z&#D$!)nZt>$waCV=uA2)g+$?L zg7--XjmRV-sN1;nZg|Q8O(%~U=@3_7}HGgl;HI`F!2W*qdZZRckQSD(R9Z-ml zrd5FiAd2EhF&Gj**8aRD#!weqscd)`n`*wI6^_1Ilb+eFge7{Lk+rT)VS8(z(p7G& zF?w2vo7>|kgq1|6$&$p;XGuGXa7m(6Shk zO*Fr7LU_xkx-ii4Q5GgW%oy6B7TgnOB~6Fw`sKSpZ;?*;QzS&eR36Ql8yc}mO4D|& znNK)>@zk;G#qF`X zFrH$<7&0!T-FSyUc8nr3dyWBpdkTC1tjX3bT~ssY(vjMVLd_@KVa3Tl6YhV@uLqgI?K&QyZQ{NXqZ=&ACYfl#M6`=buK%NRzTfT{& zX~?Gy*NMhif@U+J!r&rIYE+dQg=Jk-MqOT7_~loz)6cb!zE6~eU$8PC2Ums zPOJjb4r?!wR98yXvb9-WZEAM`J~%znA4Yy+EZ#Bw z$2%`f-p>Lba{feg9N&V^oU!!YTI|oVoQ-RCR^34u!vyVmO#-1KZD0+!)--ygw*!2xg7RXJ7R(VA}QIE`s9q5vB9GlyW49U6BbHQQP)6T@e*L z;d0&S8*ySl@@r!QM47@^$t7=iJ9Ozw!SQpFYEEst{?2$k6Ld05JpZCxko8#6e2V`K z`(+4Le1HpLtHvNc@dt&v?Leb1iHYlrf&I?qm{sV^$USx`T6AXI3EJ2l$VZ#%YPI#~# zTny0bIYn>S;@)@n)>Sf#Cx>Li0CWj+^oV6xz!TAA7RGg&d26&mc;m?Ix9f_Ai>BG< zjH>UyU*r#4sxS{20&CITO{iw9`UC8wNCp4gUXd{5n}#6{7xx}G7Ig=5vyJkz2gr@x z!$`RK|H%HD@qY>U=}yE0TbT~W2@00&BVaxI(H>+OR;l05Leml=w?_!OCT=-u6NK*D zi`eM8`s$z&0+JDc+(J| zuVir|G2x*&AJJm2BQu#P?f64+UGL8V-WE~$M=zcXBuhAwlh6y?=J*_@e`ypQiCD-l z{TtNlrK*BEka!TO#9l3%%?+OWjoh_1;FO+vl|Q3e7+7}C&1aYAk-K;Ud*!AR-*W;m zQ+fSCYX6-Tr`pRYJ@-{WO|a@FF!7%cHgqx!dod=DDQTX6888|7xCz(13h#-`fQ?ZI zmjv$q3i{T`N&Ca<;{hSUYsmv@@PW}BHCmkd*_ZtP!55mrPy8X+v@LDFTkzJ?EqZSkE5UHnyCjH{sBncOOtRJMQ}6==PoLpvFM@P=1bODdL)lb0uOs ztmR^{kwN)RB2bVjHPIX*(N^H_JdzLpQ4^#piZ=B^v$md}<(@W-61AgND2ra>v&cd~+-+G*qZIs=!5bCS;6 zpfX0CDL@~A!NfB?k8e;`JMM z2RZ*%S5BJVL;G=xV&jhDWChuAS2^sQ=UbN}CSkxFkZfwFLu{8OY3Se2BHJ741G_V* zhsgU;)A;SeXP!J>w`jRq+%?4G7)v~*D%$A9o=Jmntbm+?lBXuBM={V7a8mJZL zpNp;FO#73^)GiG?HG$$W2h|*INdPH`BC90WtpKc8VXhv1xnFbx>Ufu|1!mgMlJay_ z62X|QHJm45>-T=(4Ys7Em(J9ZIvGHw4 z%6ghWx1K|(D+J*&@1d7kgjf_tbOtYZ{r%VlKX?_9g8k4ye_`rz(b_TVV1HrpALrkX zi|ECB#eeP>vI3OaJ@I(_lP$8JbEaj??Bw0CrRS|9mzQ)&(PJ%XI69a>m(z&$Sn>Bn z4vdf^-1o$JtQH;(Nib{`KgoV(+cqJ^ruT%h38{tAd7zR<nM8WUo51cVJXLsAIv{}%ZA$IW}2A=$@?xrv8|z^3WNqa!&?dmM-L{X|A;O&)S0 zH@iM4L>Jv@%PZhcO-1!R{}k0Ri6dI2ffk&PCx4eVNd5%}K;P&SNx{|GYk1_P5plomR9_GQh>GYGy8=!2^Fqvix zlfTcBA(=A`APe3U({|7uNJOS4Wfu@r2Zoq7=ai1)^w*E%OzE$sMxP7c+hPo#Oq+qGYYx2Y(bu8QVRE;z&^A+wWIlMEya4a7x8w^Elf0 zq-w&{mpmEcJ`|o6+bF-c;XcILM)p+O_~t!mxw$*k!Ce~FcZZB%`L55|HtQ4~4!x!8 zi_!UZ`$IWmxF5YKh6cDOii{?X&qGapfRT*3*e_%@1FoWacsgowA3ovW$1-{Ak#Kko z={Lr&@BaArafhMElHg^BptO*2&r@Z02MCLwOfA6$|zc$+;wU&(Ec&-T2S#Wtl zxx`HHgn0V}-x9kfNY4{~U}O=${sr*miXSPxzuCkv@Nlp zVB;xsv|6gQEuSsb{7--uarPRm??sZS!`v0To7fd%_Rd~Y03$9mPsXD((XWC#^njnOS;>z|gX8fa6@EL;4{SB&hJc*Ejp#ep^sUibQ( zxd>;adNHnB>ezz0DG*uWY6-14ekonFlHo%75ud+vSA!$z^!O4Tb_ zbvBXQwZ5Z5UqsWl;}*;0C8B?3*fili9`O|}?f4&)ZrxG_-FJG#)DL^!_!#dht@IV+ zJaEiOHL^YuT00Dx#}gOHAZlvvb4Zs4OcRS+8pSRjQ$3U88gx_?jFqoL_B~KZ$i+%F z(mxV-#+@2Z5@pW{vc_LbyITAS%v3btlYk=-9tDpEX@2X0ruHZ%#eWZ zu?59#KvkaKSjS0YyeS0i#Jz$<7{n*y8+deZ%{YO0c!pa>y*AzdQ3k1pu$*+YCF({M zlC^{0Ng(qOl$D2g5Yi=2P^TYpf=C_;!XO0~;AZeEA)K%Vc+g|PhwyR>!$zMfapU?Iw(I6BQV9!h7s*=xOob> zZ0~FyxEwmOU1~}o_8?A&usjXj(LPpuYJeZ`DP~e_8spLoO!?XCW6hNODAh^Dn+#dzodGI#|WipNI^6pzy{12)H4uX z4LBl@sr4$L_-oan>qUOjpeNd;LG@u8z*hiNNN^TwgevAEfP)o&?H9lsy1(mZRVA|l z?E!U9>eq7@N^OG>a}s0@*r`{~Ap#n9jdsZ8+zc2~q)Put1*(c8LQ<0!!fR>p%#YcsRK{O}b=F+Y6={u%kXU?YVYNxm(%8le`dU-+?gn^dak>QZaLaNuoaB zqOKxwjUuiqoI)wc(;kBovJ(_kyu3G*f<%gT>eut|qxmTXBE_t{!%FS>rqJMA=}iA_ z!EL#v=Ndz-w?d={HGmMOdk!arULdP`k_PiCG1OILtlF^qgU(M!@Vkcle1j(as9mpB zs|+F-M5zpNK`6g-Rk#&y5fix6%KwgPZaPT~NoiFw@wKIRbf$ny(e02nYwTi#<&lG4 zYsoG;1Ig%ZNTi%mz6=+3P`U&D8DPU9t>?;bNG_eRVvySehZ=}&P!+G2XV_FbwCP~4 z{;VVE(1A`B&i+%5N=dXB;YXjm)Mfbp7CsQ!iRu$Uw(X>pGqw(98kR{!?eqo!I`< zYwrz=L7rP2=#3#7qAgGIr~h|tFwM(1Ak0{In1!<(say&fzTno!qMR%yid443G*Yib zVh|{ZKQz3mYA$WrD%x|`Qe9fFQ#4kX4HKnl!DQWl!@fe?37cJu5D`|(b1NEQGDp~+ zdG6rjq_^Nv;l6by`<_w0g>onzO}h`8O`NP=V~vlK^Pp&M@{%DeYq{+kg9kI88_?{M zqrA){JAe?^$0yKqC878xWOSYtl)HzCL5UC#ic84#Op_3Dd=XD1I72qYoz8ymbTT}U zYP!md2u*+@9easYJbDnqC7xOg_IoypWHW?hGjN({RMvf%SvDj;z!}+mE;nbR8Ud~5 zr51RI{=3%~3dbsl+#@VWuEBNec<1n$>=HxBXUIo)xhUvSUne}d_g6K4nJZdb`10sc zr-0@YnfbDo{jo601*u$3GIwtrq<=wFGWj}f5hQI9`5@*RPWx6adowls%dnSgTnwMG zCO@^i1wQxWBlG(X!?M^@Z#|H&88M4K=6xp+H0;)^9~Y5fxd@lX{XuIdQmiNT!{dW5 z!)U1n&y{@oD%hpsISZjni{HEVgM3G%n@TU&-5kFoqZ}L9(KlEQ?Dzxg>P8xw!~OBW zN$>w}E2JmgIC75@^fqZ*p+{hQ9SfYzRT_ND9MyF0++`HQuI5e4T|1^+3)nF{_|9n7 zy$;!Ji;8mUkN8mq+C<^az_)`Az<0PEhWd~|yco*uRXP&(wn4RdyCUvx_c09V{-uw7 z%XRj;a!q-o5cW^aZz06HC`*n4J@@5a$y*U*T$%YJ5`!R|V#6#K9PVQ@!}%O6+-qZw z96j5y58s!b;`OP7$UiS?DF1pi@8E>(Vd(>celO`!2FCPWmlsC&RVmiN-TlQc)q~@U z4_w`$v-Q<2K^l@f zd32vQQ>2z&0-j73B^&cAdd|n&_x=~4vO4V&utZL~P>}?7R+;D1W0R)vA7>caMfnDLzkIyPR}2!4vuXmA}}!BG8?nDDI7a_rf;+c{(X15mr)HBdGT_^h5oENZwNf5Xnq?=apF8b*}SAe7lpD<&v-*)@Yf=|a0rguHE ziW?|BUht+&rB(jv@r*dr`EBNhTf>8=R|kE=LF5JTXP4)c%r&(PPMQ&4r{>9uQRN1X zUMrM_Ex)xw)xfl>^|54;*>B)**Gug~`qVxZlf!mmN2Q%;g{AH67E5AsU_F=N-~5Ct zNj>sTW4dQLw^)|kE9bIYuZx9$o3gfmeefRZTXy*qw3gpM3G7eqrVsxm zc;|(dfLOwO=x^b_QA(E>T|#|8=Uli^i5K%%MVPg;-y!==lXt8O0aDP`14U8Tb}Y1g zM_>%~)r>jS@9Xz$a)x``NRJAU)&sF^Y9Lx3-{F|K0~s6&7^RkMaySJ7wls%h(2ZVZ zP@Ev;BpQ|^paX0hg(E@YFC~|!(1Bu0OaKx$&E1W~7%kr*wlHsFoPM>d4yyd0${j8H zREmO+HDB#PEJncyaB^8R@(_gt%XU{&sR$&IgiD?9Y^CeaomfEMr~!@fwj{3*(v>R2A!1*3`(zW)jbNlW$sXk#ACFmz3% zwiMlAFwulMV3|ta_MGiokk9kR8^%-V|6{3El;WX%KlnKe{(94${d$uPk0<-uJ7Q`5 zISILsK_`M+xWWUbZM0awfg5iB04aqmJCO60YGE9ZE6@ertn@DX8iWv2r1Wc%zn3k3 zMdoH|1|%-ruY5A?c4`T;CZ+7!UZh@ILgM(*{hkpWH;reVG85?oMJBwaePW)=jI24z zlO0C+?@YB(Y`%}-E-2Ug%_~s#>Xtpiythl(bK& zv`e(a*@p>E&1z(%nC?Edhz0}f(G2oYkDzwCF#Tbs$}1!Lp>8iVpD)QP+X)dL@#Gs$ z-2q@m$_x7QK+VqY155INO`fbf(B}hTY1F$<^p$>v#M_7VZo4M^EaXFhxiAW26&9w# z!vyy9@wlTM?6^;zQ9I$65}n_y0mP{vES)dXKnb<;)vOhB*=m$y`k*wU)VA_^{&OUW zNcOXmjZ)vclwEa-%=S=<>6GLu0%NuEL~RP&+?ch^N;KGbEhgmT)TH6sO{xkr0v+uL zd7Rh|Rh0x7UC0ogW4@_^^uoisOrD~_z*b9iD#~Mc@+)Db?9odIRT?FllO5T($Lf#d zKD2;MG`q;E$%~$8g(e3@;bTA4!yub6FwaClPzgy&5gdhzww|w~FJ~f*X_?bX4%3do zB3hMNs{QC3JgbOpjf+gxFGm)U_TE(kynhPEk@W)}o=hq*N)_>xy?lVC6PILX7n5Z4 zf2-;(~Y4(80%Z!_ubVx~;u&#EUyTU=>Y!z~kX=+ATM4I>PM ze5gkSySyry+-w0&gfcdE6?~>jPeKi2p`bI!O%-nnc`*G9EO4a-c?yMA23>f67#5te z1t_Re1^Mcs9+fhvtNb^8=RDdQ%o!or4lt^bP}+s{EK;LXbn`x)pPdcJEfh$c3e>)S z^`jCs$TzPyc9q~0b=s`se>7+u<9gDUCpKnEMEL$t&SX47PSy<0w)51Ct^GYu<8?_I zt@mJ6Z*>z+U$G9xTf58vSIm?)V2v$dfNm=>$Qwv#iEOSy=Z)L2OC=SYwY2Tw!)$KS zMVy7I+ny12m@WU^?4u%ly;_s&l(GzP7;QSNPaL179_jHz$074|&GrqXWd{mwkjWY_ zuPvD=HiQz7-4V0bjg0!8JafF%G17Gc!OK?{!JlJIVota7cwo$v;oCh>GUx&4`4=F_30&Y@o760#0H=<{719p zsC`UpDuJx2M!oS!2A@~>30GXvH{GBSw|PwQEB%hSrsP{tZl`#?z5{r0GJuUjWVru#a1D9E1dI6T4*sq8j+|%#rWZ00aV&5vqLIfo7q_tHoPWoy6KGpo51#}KyT zKAdliGI>iN;!osFk4p4Vndm&dFthoX^GRmVUr>k4s zO)JP`1NG-d={1MRTjncovB|8mqEKoC zPpZM#0W?7pX&bUI*Hj@t>5*pNdN|%_ucNG+p}RN9KK@jq;@kuMreiEp1CBR;qpbNW znB_^<>$Q)9I(DdIH>aZ;4`KRzAOh9jZvb;@RyeSZq?q+U&(1c?;^U)QCCTjB2vQ@h z7nnW$lelDFW{dN+HJz^6=13!FuF*4V2L4Ti{smn7rsf$P&D_evz`P(to;@eD*X#oZ zBoy<7VMwRWeD=wiW^H?utRej*0ttOv4qI%GF#toFebdv>8hEng%=4W3U&sWsl-3U7 zdBQ(tiCd6L*txa*CY&ou@w(C7QbVS_Wzcx+psn`nY?$oi?wK@T$3s^Qx2#ckIx(j+ zkeeR2{#*~G4bgLS@9;#9O)Y{j|UGL zf+l!N+DAV(!Aj3`PI9qA;hzP%+VExN5qpdt2}Uo4j9CA~t*(fe7_y~C@Ypr2!MY#| z;1NplRHf~jKT>qayghZf!|^H#|9E8oD9Peac1P|!`}IdntR~rgDe1mhzebje`@o_p zocVF|WPeGb9QJOPXWVMqXV`h_L@#O0w5qdFLtt~Rk6%>sOpg#9BZb+D+%pVXk`D>_ zQ8k2t)Ln{4?t4_&D-v+#2%^IsGj4%8)V3_V#*854^F`ONmSC!`YT@rAl5#Z^ofYgg zZr**n{oZBct{biE#+tYrd-kby*q)@NB4>l^WY-fP*ED*KNXL93@z2h$`W4g~sHDT@ zt{qRu2R}Bg9u#G`9^^M7snu7?S;FY&>6(hqM&dhdkIATm)H*Y!0Q3`X8x4&Ha)=gg zIy7Rt?~2`Mv*GqSl==L9m!*Q^3WSjB)!&I92RP9hswphukED8v6P{-46}>T6sNxGm zzQJ;oNAEH{si2N!GHbb>$RaMmg}PCWEqF7@r<%Q$^~aRcl6;`(79p*a zhrtE5HB@yK7Cv1RTdMa1s3%1UWbo|>eY<+1%uF@-3`fR9y7+C@e$yhf!0lg@@d~SM z$7z2??KOHUW;_XPAY#|$@mc)>goRf~v9~U5j<>_Exp$shSF;UWPd&DB^Z)5 z(6(f_D7P_SPPTK=Mv6`Ad2eC$z8MmnSOqZN&O2MOX&a&jwsVE|q2Vwzh~{3I&QVDE z&F_obUDqDtTTFYaOxSJz01KTK%SF@}>O~01MP){C6i>OZ>%j>|p~A5#adh1g38o(m7r4`JQK!+_dn zP|=dIP>7gOtuiO`^*IM)6~*7ma^D<20{}^IUUda}U{qauYCjOZ{A1#c@e&#G?bm!lkE+i~@N0Sp*3t z$NmG-Rn^?_1}EY!C}re3z9n!=aJa>m6;SA_whI~gBg#?IY3OGXJ?HJ@h4V^A$vd?e z2v(cD+l(ws>Ug*$V1K^sX|`|16G<*F%{^G=sxB8{QtaJR4?EqkOA^d8qP%gL^!^&Q z!njHS6NzeNm|{dM;y~~57#37NQU_0rVuIpG8rU={X`WQ*>;$OLd*#yqb1I{8~yy-1B21yVmY9a22k6#$Sfq&{{{A=)CMO61Sk~uK(eSpqD4ID z{E{~hrZq!3G=D#Nk|28%f_o?NnlPWgaR+kf>~7TNP+kzqs}UsIk##2OuERF_{NK%k zpi&ZM{=;{{O~7k@1#t-+JYs?bRh4tt72hxAGB@jU*`$G&H0{F7+93 zzp>_AuBPxyoBGYTG21y|6!82|z8-vI?)uGyqJ#q7sfkpM>AzE1%6+nF9lMj8=Xa@4lVXCYW*D?d0LFS;EUhS@9 zs9uJorcA3afEyXPB6-uNL`a^UBIz#9eBItK5{f1R?Yy>;_(>&UsAtelowa=2RD*eF zro*m0``-@+Ir7?u%-vVdtCYFtZ?)YN4I_!xoG$W4UR^_loeRZiopuv2$R}&X=wx>= z&rodw)Fm)tn?)|brCFrW$mqa=ksPLP*KKTB7Wuj*ee@NpEaBiMk^7_YTL}KWa+cQW zLe+VSs%q0Tjde;8A&uOIDkgUkQ%56VA$xAW8 z`DxD0h3KQrvTcd$4&ec;HB-BYhOs)?YweU8@@8pTx!_n=8dcdJ!W}VF&!WRIJS+Fm z(p70~GzY4}*e+^($_4JFyaVz~dp2CNomC7&#Sl)g`proLJ!0 z^AS<4;uI<9UH(98cXv}@xhVauBE(^A_}I5#IUa8HJHo#S^ssimN7#nd3qBq93;2k~ z3#KmRN-)qCSI8R@39t|0^>^8FYnZNoQ%a3mlONN-W#xfNgFhCWN&0)=1i*S=4@f6YUE<`zXr|ARCWHf zoP3sJ{G_8H8m7^LHB4-m1NJ4?e`a%gkXV8E`ei);kWD!((PLpOLOI!!pbpp>gr2 zR@#@;*PaW!?f3;?8&qU9=u8_BO_auc9-!LH*gb_v)lMb^9FtChzr?N{s=H6~QxQ=& zWph$j`+Q4L{3H5^X_#QeG|P2|JEV7)QyAU$+AFw@Jf@Bor+Z>LnlZoqx)V{22u@*4 z&bWE≥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%0D>1+d;giZJ7s_0K3j_X9S~I1 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 HIGX Date: Tue, 16 Feb 2016 11:51:33 -0800 Subject: [PATCH 420/826] Removing the Zookeeper jar. How'd that get in there !?!?!? --- third_party/zookeeper/zookeeper-3.4.5.jar | Bin 779974 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar diff --git a/third_party/zookeeper/zookeeper-3.4.5.jar b/third_party/zookeeper/zookeeper-3.4.5.jar deleted file mode 100644 index a7966bbbce49344a67438bee8bb0cd1fd4952eee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 779974 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

    N}hR{0dLySoh!!h~(WZKRed%U$2{V7gCf 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 z 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

    r6T9k`3Qu3|?nk%Zsi+St;89-&5yV4X1>E zoDJQnyX@P`9Nut1-WX7A4*`AmaX!e)rTf?CzW}g)o?-QHiWO<>pzXaomcfFS@r&t{ubJP4f}1tTO>03`wv{?9;(f^B-g z9gN#)6Xq3_4-{H!@W0qfD~dFOCC8>_Ztl}-2M236{M~+_vxByvnv*A#w3wp>HU`Mj znzR!(SmpVZMWx1~BdTFCO$RaK@L^8eP`X)&ZH*+CFM_vP;RR4vSA-sgl?X4N+>8Dz z4_&AD0}Ux)3Ts9Ze}lRWM*s5a^K5x9pze|1z+059BH;3^?iWz3985K#4aQ$|UfhS5 z3qw5A-$VgecZ(A8i@buOCd3`)s}To!kx7E+zeHU&H}rQf{EAuRp){FkNl#Z4&4Gj| z5=bNm*;62{Y=y(f$K)W47}Y=Zo^-osWV45{P|FO)$Cu|AEe&cDwzfX9 zYQ!%XmX~F8Mk-@*$nEyg2gZLqrWE4;u7X~7JU4FFI*j(O`Ey8wJ0;2+OK8p~z9nT@ z&gvlZBJ%P_-e6MQvc=3P*tuCCwMswBXkHoD;~?j>jX$@c{%{Dh!5D!J6sx}FK$3a+ za%MtGz*H?75@3f9n{^)A~eYczgYw^+R>kWAm*;*b_h8)0I5-lQ5IU_M`6A-SErv z%xOrkB)KJJLdnk3yOTvi^YymJ)>K+Ptfo`RY<--M*Q9&B-sw9Hipvu_fP(vF&$*DEXtO_~EqqrtE zDVIk(1_V~6y9TDys)-scoQYYiU3=bxzuOi4>TtPl+NZ=A85m z&ATFuzj4uIGTsTM9H?qhrH^q+jbg*9y*EX|JFG^^mUTh8N+qMlY|W~rm$ofnQ7mx#u*7=N>_xraf1V4LpoqXKsyC0$_r!qcyXrB= zy6S<}wY!EMuoSA-u4>z|q^7M)YK*aQj-vUcq^+A;K(DTDQckm^8gytSt!rFmZFchRgJ%XN#3yh+`Pq0&CB1eN?+nFyTtFTM1Jw1N$ct*3eE#^%XeyUVVMeNbU1+0%aQ9I?eq zyYgoBTvxlxyy}JSDKO2u0ug*qyyd+pnPB5wKJ~pZ%~=G4c}d>}yugq6{fC_aQ6UeC z%0D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 z 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≥~8+{1eLeQVE*2r@vTgt!$s3yjUW>X%@2_7nC{(=nnP)mH4BD_)hA{U)l~ zZ)d#dBu!VF1=NT1;4K&1nVS2h`8Z&_iut+{ktBIntFepBeo_$$Aeu9jy3!MuvlwOd zq15=U%<`s?rF5GgwA6Dg;aGTo6bWZd44EEhq|PQvZL-;@q{nMvxX7zhL(G$wnPRxA zP8H=Bw|{I1o0*M$NvH(uRlcU%(Q4sX4@&dA%`#OsZ?T4*z#Thth0iQGGTP4S$3kM7Ik*$ADv7k6bzp_?%T~SW_z6m`>z> zrK43V?J7&pNhFl2{~5RAf=SehMjEzc43_IovKwpe7k;Y6z3v*1cq4lgRL0T;RVLw% zB>(4%xCuqGfMg?%a=ya!72w1x0jsaLlyORA*Lj`ra0&@>26AK)luAm4Dv3emk=c^L zI5+l@^s%%(#b10S2RSGF61{ftXc791=n!dy93}Rg^dicjn{}QAnhPTmMHjRR4=!sM z8OX}9`z%88jghFA`ii&TAq;859QBbs5CQ7Sv1K`Aikt*q%Mu4B(uF=ne2b(wtxNmt z*B|x=`KNwzItL>izx8AFtsja1fBI2zHF9mi)1q4H=$d;|mpBbgjgyiiDT0%RY~@E#xqS{5F!|{99PN zy_=Mr6X4%1&~)uM*K|%9ZJ`36o7rR#e^VKiDj8~*s?N0t3}{x>tI>x(a`NYfpF2>b zmti}69n_bocghQymTfg?Bf4+~9d6+wc21+KKJ)k7>V8iD@fz5(uQmklz4|Hrnx9x< zt)IeUv(-jsk)i4GT9?=%AX)i^wb>e?6}|_{N_V{agL|#XiXVE?zGHZQQ93DCcGx0Y zzRmMV)@~Wj!@_%Rinfx4mfpWYiRJOEioN$EEvDYlza`8@gA#40-Hnya3DtytUmOHT!s2QLhmHVXj$XHl=|z*zcz7xWSAf9F;FE2QcF z(-i(!E2ve!_!qC@i;i?%P7_s06_Hi3g@_usRbi=$mz5P!dLLA$I^z%-ebByh9elo5 z=lv6wdr=|Js_#X}3bb{GbFuzYDKFm#1sTAT$Nelav)%KD*T2tYhHt$h;OiBvpU@ww z7&3+Ekr-=uG84J}28xMJR64Q+F+)uknkcd~yxO>jd^i?yHRqOoXVFn)t*?Hs+_SGN zs4{ceku#LtG&G_H249%xIC#v6Uvv=B0u~#6Bpc&c23r^^4`UfIZWuwZeyh{Yual=t|JP27`p%!sW4m`NI7r6hZ=6-2U_2sNvtS>Fy z&oRB-79;E#EpWf2tNEWd!v}sEs4wBQv=yh&Lx(BX3$$&Ew(PTJ#muw;Ve8?Z^|o)h zULj2`_pi+&FCStN>%-Md#GpJ)&UR0tq99UQG&5cDE80xZQt>>SRb1F-?*`JS#3z;0 z)K^3m_-@wZc}a9-*6U!Z)v!*{8A;{mlbFU?00wj)NC=1tBwlPqp1g65Xbfqsy1YG; z@G*}Fia7ruB?*FJZv6o)JoxxCPU>N&$BRg4ra&P)3dSFX27G0(bxE_)^ylVvCgboI z^%uR4NjyfQFc*%Ew8PQyo0v+CRR{3b)SRj;e&u2|;RO4c!6D2FMoJeAN`U6>X)Sj) z+pcmcL8xPiL9LT!?Q*fZ$|^06+2+4Xxo&jCngKNjr1Dgl-a+Ju{`^XJmGQH;9q|me zW$`{Q(n3Db*|S7}M)N^?!f1HFpW37!I0JN@;H{yw8#zD`2V9HDUhodjj%~pMC%-E9 zb?|=g`JA_D1E(~@@`}j0eTMlM2rZg2`>~|)3*_PB6`c*hfZLm+;r_|Q->erai}>Ne zLvk0uvw~aQEyjD!Q&1jgG^51|-fg^)s91gLOEXoj)vzjZ*Etva(UwJ_+GTx}+Bg+! zv$^GJYN1z7Bk6Lke1Gp6t<8H^A8lrjBF@hsUd+I}!%=7W$NYD`$FJB;UY==Or#O;) z6?_jyx|I zbVkE7v7S~6$--*ujPxhk)HE%uC0;;xut&;>+d#o=r)fy_iaRLGLkZNb7UNh())h5^ z89Z7-f1WlXgUr_R4BPO>;Q*{ezS4?Eyl3xk)L)%CDB41)3rtGd# z7J!KmA4_@DRgX)4ty|NTY^LS1*5+^9JD|f4R0DYHU z`@+0pDMcwPDDXdr;6`q_7QogW0ygq>z{|ThEe^dZ4qcXfMq%3}CwoPI4n!KD`O zjgo0FPaEGkDO)|pjYxQg$efW#cL1dBh*_OF(}f(S|7;*6qymjW-DrXgYfA2mPz4X_ z{4h%Q>$B?aJ8~%Y@6#^Jto9`cc#Sx9}eKYNcR=(qlnzbDMP225aC4+I_ zGeAesyCvMbiSaf+d**)Ja7=Ef%mc2(X9+ydI4BV=9upJ~Lt_PmX7|}S=0@ER(&S6R z&xHdEP?2q0{^Ydj>&~vAKfwf@G zfog4?T0Yy>mFlM-%BZ#KpDe%e*GY58-rgTf`u_BIu{<14PvvC&>E->pLg@nulo~7< zP~qeSh2O{e;9LwRxupe{uqNDdM3ZpJ8BLh~z#JMfA&dM(!QzA;9relav<;4a#g-qI z!s5iLSaYP#pH8rXk)I6=;tZiOXEZI3mOti{8CjQo0VU^*a5}d1q({Fsp*O*}H^~<- z5yS35d(epre`r)40&h?3GK*E7Nq~W)aY~Ag!O~3&a<5Tu5D$@R7cG&mg-F;{HgA?4 z;m&vGBB?W6NW-ohd{E^r;K8sw9rT9VUMnmdXs%r`glDf>8x~B6#$f&?arMBk+b9lF z(@GH3PdN+$=?^Cq1{*;nXgJnFq;cvFs9(>{8#37|-WtPMV|r472K*4Bvsb=lzSb=g z!qczZRioCg9gyDYSNV_}u|e%t2gR^gyyHfz*SLj%18kM<$H9DRhLHPj*X*N%J1q3e z2+wQVuiSp;3k=i(*2{$gD;-qs+`-#y=7NV_8|8-ZJR^cVtQ+`YI&w7p5`uZy+EdrB zm2dUo*zb19#S0StPOc7^3Dc z-qnY*FBbAeB{*pF`|!*qK=Qw|2xdn^pnM61;K`hfKh3>@;jbLnevv^7DBBf+bHCfA z*I%tW<&8k~l@8hV)EuZ_^9+n2*xiG?wFeKV9T0hu0WUD3!B@JaC{9XN062qeIE8|_W4Vkgfk|by#|^nmLozJIHPNP?Dy`UdZySwq$vcffz~D3KkpdXf!56m zs`s`Dvnc$KM#eiH#khJghIEe{m{99x_ytPy3%cK&7u_fQq14%{`<2dM{Oi%c6~~ka zu-s(#Y~zI4qgrNqYbN16F`>RqsIU)Ob9KUvC^(|SG7OGzi)gAXA+Vl?@gp17DnOgS zn&7r_!7LQIw_Z`AkT6%@tK3A zQo*h{Tn843K&_~f5R6}KuW<&?B|;s=V638yUmBA2U9x+4S$Q2m6bVX)!Zw*kJ`qL5 z9of=b8~XU0ik4mYZ6uYJ><+U46Ygt*)w9uir87$4=wr17(=%F3{5j`c&@~iYc|vFs z@%MEZFHNG)4;~UTCBB6cD?bK!246O*9F%o-*OUqT zT9l1Z%IGE{t1t8l--}zY8%A&@IpuNX^%|7$Z_-~WD7h5R$q$F{E5>p2B~ryay+4-i zzy|UA63`UJ{#K8#R_a_E*szj9AesM(9*<02qLqw{JwH9pbJ3ABWwDL)7Ct2qyY>2b zby!7RF$O-wI0+rJ7Yd2J1I}4x@?39xcpBaSCL&tX)$^H%q(YKq%Ebl2694AB;izl| z)_sBE5P!hb^vk}qIEL0zxF$=TqoCvm2GJW+5WbkWPr{qH*0C# z-huY7_3~$W;U?vC10Y1Xd;oe%HKK%5G{Ryx5?*1b9_^z#K46Fg3F3K|_Yi^!9&9pB zYv*ig`Ht>`M_^~=j^HIe{$}3?rnml^?4f}7H9{*ifZ+B`^-#Y>!TW+Upnj>0FWAfB z#vl43oOWOv9E zSJ?I8_Kz|+yQNPY`!ciYHMcjoxTWqcA0pUw!|NOPI(wnZUjm0x4V?=E*H;f!!$?ka zD}quRrJY(4gTU1YEjI};A~~s}oC}l3M$cNqF!D^`>Y>DZgrqtYy7|z+ z7y{=SP;!EJRh)wY|L9FDieUji(z9_>a%-iZK_)rt@s-ko8^ogONLOikRwav+7|7@x zLn%RH62~wZ|7DzuSsw`vWf4%%Er|3ARkD~)bchn}BiFst;C3Fz1#ULy7#7Fbl)rU^ zcfUi0HO0~`jvO+x-FRkIeUQZ$owr_DKlFYJylK8qmErKFX#6V0hxa~5PKuvGxn35z zm#QPN_B!U zue(xgR>tb0?*J-S#fz@jON* zRnZ{Is?3!%k48Z+m*kjG_G%fK`a?H>9J*i|yg1UujT5-wv{-OzS&WDboJj#gtQgbC z@(o5CgUs09`-}SQ(x4k$xlp2@aUaa9JbbDh2c&trXq-c{>6I-;^yNNqh zynvo&IoH9C@zwoe&n%--TW+F3Wb|FaD_HHx#LGJ_U4RzoENywQULyb}dYrt7m&8;C zos@85O(RtmD_Dp1yL&=0ljdeh&6cU#dyq9AujFegNn~rR6#w&vgYrRr-)S&NM|rvI zMg7FAjDc5KZM6X+h33-I0zW^V}6hh!?Pwrir(}mEHZ9|I&?nh@{aaYH&rbYb?au zLU{hYuEKClANb`T?~pzErx8^-iHeD^!$?x1<)PHMHKV>swxCP)7@g2_d|Xm8y_@6R zNfz`P5~QQ2(x&Uw5@peP)OdqQjneYEc2i=%*sC-(S<@Yo`J+|O?6~}@!)w>WAv$2{ z#aQOI*DYJ@HY|kXWR<5k#Wli;8x)?12fz`2eMbG`cr|+NU>eVq0_6Lt>G9IYaT0~P zHs?J$?P*;pczFgOe!J(sm)3<0xo{q_=QZg;8Y4A(2er6ndB9n!zCP3&2CXv0tza3y zfMN-i#zoo>e`d3%7f#1MyMxOeh$A7Oio#t&{w=Cvpw8pu`C4&fORi2aY<>c3#)3Vs z*^=11eA>xrniwGEV_IgqIYi8JJwDAK;#JN>i`qgd{v??Ef<5rV%H~J>=8UpIA(^Z@ zqwZzQ6ZFgBOGI9j7Zrn)k1Sa0{4J)OHkY2=G}h9pVT*cQd|gy<#Revi^{pR%ZQCGi zje7aUcF$ut{aa6tJZ^rb=^At_((M90q^qfMJa8Jl+B8YRL?#q@DhUz_IRjcH zuQF3wUNpzg<~omZd)em8tusSq?!RAV*qHX)-q%U})G8F%?`{;M5(J*RQOU>IRnU4U z=9&(E%ONLLb%X0cE37Nj$8T8Pv)ggq_*n9c2$vr|$%oH${v`7w75?${#o_BA^zM$5 z3O>FUtGag99JXwC-F(+mgTLPzXL^f9q>*VQ6O~3lb5~I$|OXtOT z!X!K}NKZjLDw`ps23f!>Kdl?^pyErL?8dD!;K&K306b`f$Qaq@l^Fp22*53>NZXaz z)AHt*=rJBdT(UNK293vh=Zx(6z!1IV z@)o%p_bAINx6mD^^+l-x7l>xi9aFmgig5V~a=lJe5DzCP7RnN`pS+OZ6x`&Gkb1lw z1?6ADu71*sA=CB8>>L%=6?=z}-qV)&bx31^$7&bp2$OoqLV-c}HQWp2Js{u+!(D#P zI8Sc-9@s%Vk&r2(ip#`?6uX_aCxnEjX0Sn|B5smPyQvr%(t`oM;JMVJv;&{Z0G~u=PfT%G185ylX!TGaNS-K1V20 zFaLD1MmwJ^S0_ugS~6Gstxy~1&oP(askx580^K|Ps3bTD9&$<$U*`!C#l`|6|4!S) zdSUUbW-9@iV{DG|K?rifS?ZptlJQ_C`-Q~0+C%^|$Ik&0=F{bvwMv2^ZtA8(qS&&k zf0?>j6#8Mwk2T+kAdLW|_9CA%5Y-Wn@qqjYt;>QNUG9Ysqo{hCV^!LMN(F(3^CqX)s zp(W2!R^ZXQ5jZIGrjc2m-?BgFO`!-M7+L%jRBqo%sY}9<^E-IAqy}DnLguQ3MrW?? zXys*N`<4BI8gk*pPX9}Il)^*pb*#Y1&e)LLosq)Hp8qp!UXz=)I5XhVCUaP(+RA-H z@|8cL7VhH;@rTPAvE#fy6!$XUzQVI&cGL*UGb2J9Vn(Z!Zsrg-dmI@^?LK}(>yWqb zx+CK;IeZ=y{f@kl@P0Gc~3&@wE2# zlE^pW7VbPB?K47K&qLZOxxrq&^}#`EWuPD;~lhbaAi)1tT=^F(O8le7`A0H*WVg@~U1KBfNS92?)Se{XK- zW-Y~a*%*I$URcvQ3(}Z$nygu2>+H|pizn~0_CmJ+DQ|BZ@>f&_`m%@A-7XhdiaA@B zZE+VEIAIWFJ{BD=Z;=hY9G3{vGIwu-D)m<@T0(Inlr&UcF=MSZTZsJ^z~!i=iEnN+ zYn|g z19*LK&MkL4mP4qHBdqO+j}Z<(1kWwdR*=M=yf6Osz^WldFZiEmpe+XWL3<~zOQg*` zp@|WMq%3^&zVM!XE$y?M+x8bk;(Ze4$BNs~7nuB(T=)oDeNJ1epbEO$+t?#(PU8;r z{MIeNtk%y#*!1^Zu}OQ+@g@ZQa@M!#BP!43dysB(FKGR?ZjgYV-4FqdAD}x8zJP#M zUl<3a{I9Mf9nYMJD34m?Zi_3}8=Z)L$Q9j}Ic^aCCi(D$+U7l!3*46}ZK%pRkCMV=FU&Z=&N=ZK6?2D|D55 z8B;NR*V~NCUQvB_WP-X|)QZrPM8C?@F~ylpzuflMWLm9fm@}k)BO2t0B8~0A#%p($2sMurCgIvOzs*!6)C? z;fUYsY}Bb-iS;qUEjQ(159n^fEk)v3fc4(&EyfsWf;ZSsU|*1Z(&_Lmc{ezF(>dV_ zPq&;yR}tV-h-X+lY}xiRglw+%MzQPDX^p)VsAm;;F7ysa%T-Q|DbizQ% z)-am(NOykorj!<&5xD!94@e!tJ@HqqDI&QEPrO%E zf=d~f9ZG_Je^U~)wlSp31W-beSD={^Oi>EXBOFbj`h6?bijeYga2ZtrPRcr9UcO#t z?@5T^0Zvl)s$3~$LX9iju>db1f3m^@&B{9?pc`N~k8BBPzG!Tn#Ap!|+ZjfQ$YKUu z+Yph2AhUX}QqLV;##>ErDBNGUOsl;1v=scHRASx2kdfbPtAH}lVFt=V*fBvKu5FhS z?m}c8N4xj-QdbIu2mk4^Gno!fB!L0~3PSkr zE<0HV3vmZ$J0n+Dvwt6*r)l{3elv_e?cGzd@?r%u$X#jHzW;e-;dGEN_JL$mDZ?{i zYh+?zG>Wd~E);T>i%VO{{jT(lswZga`Dh1UH92bz=psb|&Dv=7>Zk2D|E0H|0naBL z%?Y$daE}E(C%Hb~l#tx_JeM2Z=Z{JjAp2n#WNxDuMu18v8ByG-r05z5GqUxrfeSzFKG=z`=%B8!k(fk&`*FW|PctaFxSdsEXnaSb|iQ}zxonrEviU)Gd{jni2Xl9RD< zrz}|~VVzgkOXsWMj_)e+&|H=af7|^$P2?^P2Q15md*kVFq||PKio0Jt$aJEeUtXW% z##vcOw8<)?o4D5)ur-kmxihOG#B*VDDYo{7fE2QYSKn7@NpJ`scypu5TQ!DcT{2Sn zC_CjCnVDJSDX}bZSYOXAwO^D+U~Xw=-72ok3-f7DH68H6FUv2i$k2oeZ9wN|2vWt; zZ_g2%b6+HemOESchNn%TJm@-moXyECkK8$~?Z- zhWWv5ZYCFTk8YGG%{2`*rlj3A918*{)u*u;Wv!cV6nkA~doVP;6cp`GjkTI$r2W0P z+1t}BMwOvL*#ru`fZ-_pi}pK!2GasF{cnM(V@d^hZrUAG?_k4cn1qD>!(U~3qJ!#} z?AZE)+(3shPt>UW6*GyYq-8h_CPXF)RgnamA#3FXs&ZH_GNbkASciQbX8fTxGvA1B zq@6m3 zifM0xi|+9qZ=D2Zo!JMF0BN==x7;Xx${m>hu%lU-cVRK9auKd_-F|fT?s!G?@w-_z z);WDm=yQ9~I?f90OXD}g?q7&vuR(`-=hCh%$99UEVLCdKon1xiT+L{JPg)W{4lbjP z09YX9&gDb6pTX<{MuCYo`@t;dTrpGO;p4Ku-s}US&+La9v-U5!$xbD#z9BE*zKlD_ zk0j@!VR5Rm+%JmUgGbM#PnLv0CGxL9g?B8hfRSe2l7JGJ%ggJeJC6?$af~0YwUJd= z5>ChP@?53nsV&w#oz|?dFBP!?->+(KoprJT114V(J|`PY;q|sdM6{=<%$Lp5%vxm| zJYp4P%Wmdvg^lzomA2$hb8V1iXd19WSt|g{niw@(B0U>FlM(8>LT+dpQ)aBh@gk-f=>E8}K_kw&lcmH*VTBz{x;enir3 z@Il_SfM1qF=9dyQ6hH%#WF&QF(Q=vq4dXeUg%VQ-D^`E4hFUPBBo`10r^k{f z*}e8#H|m3EnR^A^t|cb}OQD0dMLby0dd5+7CF`|p-J zTEh}I!k<_DR>o^R(KshXRWQ5(tMLyGvL4nS6{Md+^x+Qf1G3N;PfFR7-bfkQ7qjYB^+FzcZGT3NZ-vT)== zdpHmik++m3Bu#%w19BLv#+cKYAlt&}ui*SIxKb^H>A!#l4WUTMCibvp3yXP4V;gN> zV4~d|ag`X>wtzSfY#WPtUc}oY#O%RNTzgU+|9rHW4aT^*+}=t-2X0+MTY5%&`nfjL z({!trbW5rTy4S?)4~FTz6*4@E23usEzjiE^^~Dk=Y@nY%sa$h=7ANKT1%x02FeRK~ z{bTPN+GB2qnN&jDG)4Q8{B8STY)kvva!w&_P@L?=Y3zjrv>Om~93eNxgek7c4)Mrf zN1?n%p?z4u3RJ-AB%ov!kaF|I5%{FN?{META$>Gio|~pI4ei!O$&pMDNM`WEe&I_b z^97~CPO=K!aH4j{x}HfS+fn@G4?a`+iRLo~8br$*yt0SUcP^{_HWWU8fBqXbPF_ku#mw2=%vr?V)BgXZ zboBGu_x%q3#H*{}$^MZ=YKTUiRo{x4p$ ztABwFgXSV`NC?3ydoyEKBa)#@>~c$Ot=VPV)E-J|$c);RwJNWKj&S%fyl+edGp$l! zwZ>}h60$~40?Ix?4yl^qPfY#IwD#|Dq;3>DNwpV&4GQ(ZDYZ77jJ zn0ds0lDh}{D2JJU5uy8Ql*TWmPduKbFDqO~`D01-sbh-kcgG{w-{%C2S>RaXA~ZbA zhNQt0cIL5P`-aF|I3k>j(%F0JhfKyPXY1^_Ll}2=WnmBOxVn=2G_V+Sy2h6CY68*; zjEB)e?b#R?gVhrsC4}uXN1cF&B)|;{)$~*boQ5L85@tu-`@|{=AZ!ZhBiduwFX>On zhwv$lAIW1YV6-U)5GC9|jKv+@n_Tv`(FC9|?hwzOG2V^ZD2Dp0xuZnBpNrLLGS!6s z))`w$4`yQ{T+7JGBCXV&9Z4*ut^r?Q;4L6XonHSnIM?iO5BRy-(50^dkjrYvK4!w* zO4sW-^v{mA>R)X&n4CpynhO$QfP9)uQR%h%Rc9Mv0tq=fe^v;)js6Qy_g83$%1h>+ zahUfZJ@`Nk^yM4%g0R;DSuIS<#qy9MwVu!EFREATWw@iB&RAE|JsU_3$K?SwR9ac? z7YG&fUN^=#EZE;%nPT$;xY7X72%3cf-ZxBnRFF<=h4&z5DjYl|S?fo}q2aGCl5kiF zAj=oksv^`wuS)^v=Q&ANp1}uG)G`SP%b@i5Cg^ogyRY7#;gHip-<-Bl!`e~hIOel) z*5eBDCF|ZAsUBt$spHKepdP`hXQs2y;#(Ca^hmt%O zVE%%(J>0_79je0J9A|^GLi<9=%t(11dFzLx!+m&?Wc_owF*c0VS)#qMrP2E`Mv^J<$qR+N$!TC3K(X!m-%8iznK&)+Rrt1alxZ7BzTY4uiSjmKrK zViV81Iqq+NdszCK`-E?2Y@4c#{Y5fOa07AXJ9Jx!PKXouG3d4RazzP5PKX%U#dX07 zdCv1xsg?2*@$XWE)w}sTXMbhJ#Izq8<6Z$YJ|9j2ULu9q=rNRAM02>%X;x-V>%XRPy>uKBbyB5;cL z+NZEb(BneJmJ=zbHevYrL=h64fjQr#`$OCN!OSO@RE2`!V!rq@edjo7&bdNN}8`*xHdDKl64jM zmF>LrDsajChK{y4_AnaLk@djvf&h@C3@m zcR~L*2Izlu9S3{!e>9ss6)lHFLBvn@oRz2pc0Nf;i2*?vF6nJaRIokI$WU!0G|>{` zw2IoU(r{C}6?v{t#92v2M1*eQyAs3$x#6;4Y*R0f1E ziSzWeZ(HLgz;AvXnOpLaC{vn~#T2K=djYCTugD!M*R70FRzftXbjU`cLhWM&q3o9r z1Ll2%vRT95fu0Dz5k__C_RSn4gZF!T!e%_Ku)v<09WPw`_82unT7UlZvQ ztkHRmR##S3W6t*oJ-((2s?2-u%+b-UXvMvJ6JTjyUVkL$AkmlVJ480d44;r|MP%>G zgi3gXTYIK6cYk7yokJO6E_D61XQ-qZ@;ze^fzQ}64A-Z~$(JV&CY2Y61IH|v)E>h0 z3stSU8K7P+L)dkguEniY8OQ1icIu6$?TfY=gb_ht;e^1Xa$pQU$RzOgvvoCWyobLe z`PEA(p=|B`0fgef$Rdhz3bZoo*esdk3Cdny((h0-c9WSn)4$G0IttyKKb+gRm%1Ok z&eCEa{r&1w1{4ei2nY%a=wEYea2(nsPTxDAtM47q|F{-i{#lD^7xpOM8z1w_hH-P~ zD3_wSO=Z9fdahu5sg#QLA}f8oh|^)TB$OPxNcU0}TbB)?Ukbm_hCpp%aFDv4(reI? z$7ywHzE%!uAO$S=P&N)pc}7#2{Jwk)xVxg0eZFD0;zO9$+_BIm-aQ~0k=i?XK+Pua9bnM5_X81YIJ;M3}K>a zcf(gpGS9xERku@uk?X7~mTbk9M{OmhF;yOq3)BHQmiR`d`RU4om@`IE1rZ;7o)8w? z4I@Kjp?VUL)~G(6WAk0pm`vBUMVIAMuKdq^Hb04(E+q@(Ilb7+}uJr?K5+-1&7DKH|QHsor`;X$H@bhmX-1>T-nyMa(X|We91_k zyjzTZ(e2Ig+B4^_2e?1j0q}U{>BMLKwpCa?Nh%-XQ`6NQWF!#$>o>KKHi9zn>+9u> z+tR5kqY7znBIZA9=WGgzi5ge7Ea#r6VQ##ue%N~fB$R!w@1#3na&~6Gda7(473+bW zaAj{m{18%F0x|yr(WLl6rH>!u2MI)W?-bnx{=B1myd{{M4H!mRh)at5a6-KSMUr?& z6>D{>L3sk-+ZVkt{W*Y7@{Ai18AIW%&Dju!qum8(g445#@%3R~S#(1+rF^}P?mgLI zaS&jtb80Zf7c_FqXKM`4%sV3Jr3)F;x*?>4x#fN55#b{kg*)mcySTTQ8D~jLO(yb% zyelwpC}0xnaKIT5NwqVG8xTqz5S#M>t@}xX{n7Sg3V^0KLdaiSKBCw!6`fOhs)CyK zoCi~O&n?G^)*9qyOSR9rpoGHWnP3^*PGo)DC5j@-F!I{>PbHJ)0@3Guca_BNd7A&3 zl11I@9RI20e{Yh^sqEM7VbfzgL)5CDgHaU)Deslpis7&~4F@-y@>*c8Z1c8*-*7$% z_;U#L@qO9uB(P>}t0RS!r$|{&_H%{@jP*&@`hgSJ>0o_~~)geR1?HQZR;697nA&TSn1n{WKOpy6(X0<}@8W z&9iz4>3H(VS{$5CXY>sW2@H$KrGM8twfA&6T@~Os8_`v6=-E$xVr0jt>@!lN(yEp( zeBPvp8B>1r+DnJ=u?)?<Ucs8x3hs>8WOTzLyJX(cl*Hs@(awN&>*E4k0sMc~$Li-{=#JUof{kKZZ^niUTZ zYKe3u?S&?*6NU_Tx*TeQ@Y$_;9!EzD4r<55-}t;JA8D>H0|qgLtuBMr3gwI~^4S*y z-|`d5$E}|SiZhk#Sk@cUi>?O8qMN_nPCXTCBND8DA$rQ%A&9W#JhDrTVzhg46Wh_* zU9M4A8A(4#&`dMAk&frRQ6_9}R#-`^uJwrE#y#H(P_h)$5&Yfa5MumOIsdZRT1q~# zC}C$>TW_Qc^9d8s^-mr12b#I45ES(w)P`FrQ0A#i^c@qI+nI!#nv|0G#2Vv5QDsLn zo{Q<8i~Hf+_a9vHuTebu4G}6OnSKG!bPowjUWj@uzeiHfTL3Jf+=VUbLk!mpDikvo$W`Hb?L}eX!`rl#Kgqw^T8qpP+P-%ko|Da zEy>;p<*(3cZ^rAgtx$wErn4bD*{I2u*e-ofCxt_EUP%}W^_Jsrs;ASo>Ow z)K%JQG^y=x6RZY~`bX*72P^d3&RNUdCS|dFBvN+Gokg1ufXXp}M>CHWJ-KB$x9J7A z_pGwbEot%Z5D6-m92Qp#w~&+bG&*}bOC7UluqJaM8VBoi`Va9rIz|>xjM(i|txC&Z z>>|+9s+Cis@ZI_B&f1J+fZd#}imj8ANyb0`an`a8JK0Rk{J7;nVXJH-K#QdHd$w&IWBZY-<>G+IKsgQ#FJQ&Uy-B7-s-UEwFM!G#j`=61n*aYK@7Uc)rnnzz+G4>xErheq8bh+xaE$5yA(YKxLEc z6iP_a={m=%OME`&Vb$$KY1z)0l7bdXwDnH8K`qrAE4?nguLE z17y0uxG|{GOyL*p^Ob{DWz6Js~`E?t#xQM5f_s!$iD*`l96dfm^hlTuLmy zz?(qF$n%+Sp+HG+>Zi@v@+}l z5^cPL)}p*6gs&b*3V6fHT{{B$(<-i#SSq>x^@Nh_wtjI}IBm!g92kG+g$V<*gH@n@ zR?qL+D*6>_bSzs1NavZ_i~)ftsp-(6!=lpQy*KXta^0Devt7$5=gk6l&t!3atXb&1NX5Sq0Aq3Fw`8`lB|vRu=(ZYfW7wOR+9a< zj+h&eQgJ!4mS^d?&&CT~rfBpG0-iX?;lk`d+u(5>YKv06*}CwllF$0gDPB`)(x0ux$1E1Dv~ zF3ku9{!%aku@|)Cnsqw1D{qADp58O?Iywe_JaIBjF(2Unxwg;dQ!yhf2MRb zGpwl*K~%uLgywYBbO*~e(zcIhp+5!yyah!?GR!fA!kmzTf7Q0l4hlGhGGO=0k^}7BdbO5*~&JDw?1~ z4;;mj5?Ad}*o1`QV%7%YjB2LGGfoWtwnj_o6Yq^m=@agiO6imCol5KC@1;uX5*?A3 zvP`?R2CVP2Fzbvzyg=>yu}!-p&a+In2A^o5b|TwC?%XhMDa4U?C=YdIc4>)g_jozb z2E9?64}F<->h{!p@^SEo!%TeAL*mBYD3;TeKPs;5*)F|-Ze4v zp!XjMeFXEDaT&8mYk+DPr{u(2s#9?mepe1yD=#&-W4Stcv76@8$ry>=^3hM%|m z`E!P9p=B2I$l7f@(VjY?=uP2BZBG7-H)NeJ>S}iyLgiMVJz}SWJm`q3KbWTaWSG~V z611%TES%c1HYL$xs5ClVtS7Iv*LD%^tNj% zb5JYT9_z$1iVl#R1S;Os+B5eNB5kpDM%o{uyffJ@3YNq;m0}O_{`%c@CCgjbISsE> zzR}Hw-0`CBo)$>)atp;6S!0^peS%llEi`IwX0nW0+=@hcvFu|knH-04AP>f=d}8ny zq;|z8ktmp(x-vVaPv%OT%Pq1sx)h#Peyog1KXuZr1yQIVq~; zeyXTJu-F}$2aVXZl>u(d)IGU4GTvkPw*uTppl~Yb^J2o3NmP{ex)%nGy=I{h-*U}^ z>x4MPo&M!xgU`0H{MlrPX3{Vb+-G@1AAotcBao@~k+v}c<%#ah2;*`Vqxdp+jA?Ee zuhYsx>4zuxn!Z1yZ@arnC3tF84RBDPRXTia21D?fAORfQHU}(Ay<%)jX42?T>*Rfc zb6i8Ker%$n6e1tgbOq?_sKgOS4$AWHW=Gxli-jcDR`Oj;cF8edv#%m_^?qXA<15p9 z<+iTKa$sJI-*FmlmClBx%eis~#W?9{t^XT@YDbf4I77-y6l$)NZB?<~xv`}vh!)FV zdZv3eUebxII~_Tv+JuNvn6Z*;Dm3de#uE?GJ-DleE6AA>u&WGUkG}ZjK48AVG0Z(m zBdX$AiOp7=#q2UF0)G7Klbw`M)3pcH@-loH-KQpT6}5+~uy%s4r6F)2-eF#Jz>gwr zl0`3Ykz&^=nBqec>GsrKR35VJFr;(8WNG+$8w22&`zV-c=CYm!2LuXsW@~O&tM}_wVSBmDYJs$mVwxb3+Fw}%Q(P34uEw6(? zJati>ma!Ftwt+)BGP}bWJZ)*4oH5>zcG;cKwC0n06@L%rhG$QnnKZiR5-@$Mr;@TH zhN!8RmhBjcO-vSCVc|_n7F&3+!I_o;WDCT*QgIE7XOLEuQu$5BE1F>!tZPpZ&xO?u z13X;vI~c&-+2d-D6QGeMjeDDuQ)T{G>2P0&$R~!%VsQmp-pH9n<8X<-k>kZ%hn6RA z9qKQ@HA(+r!gKlg5`JRpFVLmMU5BJ6_bs9?;5MnBVcW$%2I?L-v{$)BMf zm6b7Iz)w~L6s3F>6;PopyPw@GJnJat9S6JfRX zCZNlDMuq(j#amZ1+lD2pd!OzJjM*_hY2^ z!v9afu=4lwv-6k2W%OIc{O{@>O0M?S|DhIgl(en?2PrjcmSoGOomE=%DowArB%T?JFmeuTGhIA){Ur3WZmHp`ZnA7xleKn;H z0Polq4pciF?yw@dhUUOK3cD3uk7pR9Fz#ig<11j%9DQACM8d_J(aT&)e=M=#LURq- z)w7*ui_iM3-7{2QCRKHXS)ln3T<0=|1F?eXoVv>8WusDNBj;LJXq{y&v3^-e>qa(I zd=W3a2ry4YtCSclY>+B#4oARK12VyIdiX-|6b%|K`iqt8os#{xE&w zq-h&`EKs~Q)MjuK`uIIK$@8eVLXYDubKq;Qb;32MdeQCp5`6MmwDTU(T06dUIRM|m zwbv?Zt*mC=8&3EoetcDMUi{ApCbthlK1OgZeTLiY8<5V!3sdS}bq=aMw8<;tmn_4( z|5X;CCgP`v`V9w#UmYp%Uuj5XQ#%tOS2HtHCv_*w|9_@3M|Ia3Srz45u5nv7t;A+S zssEu!YDE@Ewu%CkN(yMj!e&JP`p6QS?ObYW=E^SrE$?kF>j!{2k*kpJoPmq@4FLY) z=fLA;+g8dthft2}HP`!wbJyMP_++iG?;m16&IK3T-i)E%>;(g3-99ZY)Je!mcKmFV zso>aVd;TGAchw=0l#n`FZd$O5=1^hiD@27uj6?fJhVLd_t8JZXwP?U)i`2H+667rM zvc)!RnK@)gC4O_H+n8N7_XxeuPwJsdc0LBzfbC8y)f6jbN)N1jJ8gR|wl2Z62JqXl z8idr}zH&9D)f>&FkgS zYK;?!GVq6E9BaR{C4CoDd$+B*a7!i!1Z1#L2D;*;;D{dcX#un%FuwjUu*x@yp~L`F zpXz;N;Jm`p{m6hS4aWLNYp~eE;DF1$$h?0)UnJ4v8ajRLB^aAZDyjc^4m0rcNq8Q& zTGX29!v1)tMSOC|54NZLTceMbwzyesP@amAawleNh)k(UYbAQ*U@3@O+MD7I*e1kj zxF`YfDg!M#&E@u}6?+ePD(1__1gX|+Q5-9^ev@3LL zn8#7(&Ctpz{{9T|6)gFIDe4v8^b~cJ8-9;$M<}~5YDV52oa94ZSbj%Ggo9iUU$8Yr zorME;EkQ;028-zx{Pl!U^z@H;B8C)X=~GqXk;z~G964JOLItwBOF^DNL_Tjo>9yYr zRV~o8DVA7J4Mdy6t2!)U#m5W$3Zf123H)UtkKvpiGAAzQ!*!f!j4M$Iu*@W-kz+>5 zD{CD^H%r#bHNtk0DPoY!B1;yPV#{`b303<^Q{`J9M?TRDwrkAu{;r<~af(QNZd*Xyrmn%(O%k{cspkeCZ3Hj#){^P)-(Yno_S26p5r&1%s@invdhpL}~a9f&vkHLqi1(D$TV zOtNFj2=^G@yB%ZPj}gt3g7RwR=sU^wdQ#{6>Q~5`${C27J=~c#0xL89K6p;bh&_Tf zeg6q@m_xbWH=GEI)8?ZqxGDAt`%{zq=(}J4R`5wE~^XM!k zT;YUo4pBZ_V9<60-hc)!5X8P-taavE7RXm(q@A(%p$Xb&(iG;PNUR0VVXn`2e#Q1s z;0r>&ihr{?RG;pOIABHb#Fh5JMePm1+7$`e!BQC#sW!r`N0Xg8Sf*_i*1%q$M#R_5 z;{efD$UpQEy@!uAe|SSPwAtMF_O#KnhFy{l@-J(E#^_1g!eo zC?WU-kNtO|;eUp{{xe|Iw*TjH_}R|tFFgmcrLaIXNrAFlObqmqi1JBS3J*rf(z5A~ zyLQX6`+F671!i$9jF=1$s)93O7>_x&zYq`r!ACI}#N2&RHTR)jerhKKZClp-QSUv+ z`_r3O=1$qAXAgq& z(;y(0Qp9Yt_(+Yg8g_*(1I^I(_&i>2fS!{CZ~iC@cWae_#?4LO{zXWuh%tN+zuoI4 z%wu9`Nm*nEU(bBaOU5uHsjV!l_i`ZN32LeSAG30~u1f4SXU>%i^LXS`Q8$?tM;9#} zc3&M>_-o1U^6>ZyJeTg{1ea&f` zlAaFclq*#y6#?5K1)D@N9xMMQam!Lc1xO9WQNaBZK^!6%qFX7_f=FGLM^W{``e?)e zUIcpTmPrrrFjAZZN0duboS`n57Gx&kLtdP;yWVJI4$4Dd!WE8dlZiKkGLd;TX%5;$ zm|3YeiP144A*V)5b0oU)H;_E2kU5Oi&oZepVkY07NV(ViaLS=<$H%vy z8=wOMP*vF_+%iK`=;xwonO2)@B{!YF^9||zkRV*;o{Y@r2J{eVf|->mF(tZ2=Dfj8 zJdmNHNtu_FX&gga)i-V^vd+nh=yuudghP!L3UM9c<2VUzIHDVAS6e5pqKhum0V~1{ z#W^!xax2lNibX8dfLY7nDluLQ3`3RY;4MYf!q{!Ugb|?QXCB7P){;1Q0rTDX+K5$j z1}fXc&OP9W`)g*F1Zh0jN;xV1BQIVK;aII5l4bqr<3l!GkWA?Sd5@j7F!GxAXbB4! zFjrx8InUoO`ap}7+j+HUJa{zvU>T{Tpwp#8=rUU#&&0i_ zFVTGNeK&rfeW&{>QuG853FwE>^oFSNSX910HZS56q9TbnV*$t$S&KhsP-DI*PNvs? z(Maf4VsutFE?d8O_Y>(UG6OY3C)gRf%gd!8ltN%fEU^{2H&@;Z@>e@;N@o{#x+C%v zb{cl3OW0wLc2!Nqj>rZ1(cAr-iH)-0hK?5|^F~*gV8x~Gz03j<+MR8C02wANAK{*4 zNO!p`b42bNDHq8YQeOYxX&e-3C{#WYT1tI85bKjn2!^hXQlQdc(;6ib7~E0~@JFn7 z(Oi@{`UOQUIl>WnLww#Kk{?#gC(PV`+f|5TM}5m8T-G~^Z(Zo-TyNL zXlALoPNxu+zh|35)dK*%qd?e;60;H+j3ncMohqqG+vMsNnZ|$A6j69TppN|iLW2_| zASlg}IA5K6ohQCOzP`Z!fNo(22T8**wO{Q)@eu3b_OpU3k8}SvxZz3_F$5OGCd4ud z^lw8Q{c;O*qEv=XY4NV0euPna&(aGohXFA;tP~GM0Kuz z3u}HD@77ewA0<#N3eqF8p(>;sh%m%03Xv->JFN=XK3$TTI@{ ztZSDLaV#ycfFyUmVMBeq)&j%y=CzmqN8BfOKv#t&t)Cm492KLQwN z-taf0Hdj;wlul>iI#-;?vMJyJ;fJ4tJi6zrma{b;g}1wo?nD+PMPoLZ19q4dk`kj( zIocLZfA20cmt&5+rphRW;?FUrJf{T#l8s!Wc zvsx0y`)$sd*V&)Gh9Mj)bUpyS@EI3Y@cQ}s1&lCAh7A8Y7Dvpg7kF?I{xY*}f)!A}UG^Z%>O;mDN6Db8k3Cw8wW5-Z;N3GJu| zHNOja`39xs0W^fX2Hru9zkma}3nWZ^|8b9feG+B0KyB_2Xuwft`jO5s!z88|$&P^F zJhA)uF7H1x9~??G0QYa)VEtAyc>b#&9Cbq%V~hV3s;ajCL&^2Kku@;Xl*E!A*18Nt zd^CbwRv$i)j>n2H2oVgUe`)MUs+M&-yb-JF8@2aoRjycq&$0ZKVjpa{W&CAJ&g=<= zK?qO6FS*V)&a;o%J*Reh-~X2807e~a&>5r8!Vl{k7rB_msi@b2h`IR2%42y(T>||U zqKY_54>{y0E{hxGD93|Cm$1k%iCE7W>WPHi6QFnlaEp4^X9Tf9`acJVL__3%444CB4qQ z(9y6mCcKDczhQ#0sZ14Y(`vVt^$JO7R+^gv92!scSt3G+%1y5958QmB;rvC`^$?n< zC}OS0#KFSsNdw~(78p+UYi2XwVZt%U&?w!WG+xezKFe<21Mh1r7MCA_m99MToP)G@ zmm0O^M4nG<%ASMwhCYJP<>wo2^ovegyiN9#qOj_)F;s<1fz=w!_VXZt)gD-b;u~?? z`_U)vo18{mXOC2pgPPuWP0{<@rB^sly66PSNou4%eJ#IX=*AxB%$v6k*XHR^2b~zc zk~v{Ehdx*pn-FYUm6K&l`4Q$jkY16s0b_X;p<>d^RWj-z^1@4X2Rl=Kg^fxNd`HrKgZO@;55FtJ+?lUSdy}mwgv;0CL(qQE1W!-Ia|`K^g>FR0 zO{jbCn)m~FN1r(lk6%c+)Cp>fR^aLi&tZ9`pugjN06(3gSit&LVc3DjfQN?&4&G>m zAZ{QwYWIKtw3rtpy#sOiN09u$Qhsxbz2m3v`4{&84D$S7v%gVq9sLAp{((NPo*IS0HBZs0HFVG4(0#1AfyfH zqq4HXf4al&nKWSxZOzb65Jd}g zs5DThih_hftwN<%)zZ?cX05BWX?49*7iGJ0>wlA#AtR(w_&MFR*>U#^X1B zOaOS^GcP&)2j~}@2Y(0u76Q;W&YwId{qUYJ!w;w6@28pXXY{*&^=-@`d|p5MbceI6i?mO)j3pR3d#wTXvTs;4T!R;(*^;ZG`xDcI*KU2o~utD;Bq!Q3#m~8H`KyLm(EQc19jP0rg7A2f*A2 zAm)iWvZ*UdK-W$#;{3@K6>Ztas&hY9;nBI|nVF|5&DKoYD$dqS?EI>gZUEhq6X@J` zCeMJHS*ZEvN+CYzl2aRPTBv!n^S=krDj%vTSDuanw2G(eR+fM46sL}iL(?`dwg9%t zknJA5)YdDx1U8G!v(_FtwQFGJ-Om5sJXbfaTmW3h=i=(kdA4%#3v?Bmr>!{$_|P@4 zWCHLO3q3GyTic?-&Eo-X9X>CQkEO`h)LZ~9Q_a`xX!O(&0df({gLmgf*|g9JfR~)+ zYf*4pBe=1jRF+4+t0wj9T&X+uNrkJB09R_(q-}s%L~K}8$$CB&YT*manQ>-Ho4lNU zT`2W+PS?@BR#Ai8K>N9tB&)x#tSB9<*9HJczl&ymP z-NgRcD7|^A>N45c7J7Bl-bPk(G<$CChB@_{yWiVMKF>LH? z6cbZm0jvBvslk#MJ;IDD=SqQDJIC^PGF%wZy4q+$@pm@;L)}8zbeu#BR_Q z_`=gAZLfN!8R)U_k~wB)YU}91gW^gp6zOuq0cSgP) z%+<+;7P}s99LY9El3ue1pz?R2Mt3dEf&%w_VWVh6Cl@4HQxjSfE08a9&Y z6hmN-x9p!v5P+Z@z)n~@4$KJAIX6&KJ&;o*NLCTHu65Wtqe)wbqMgw6k5n=wkN@j&O{mKoqMrbfD1XuRRjAk{fwQeGcnfNZx8buU~6ZR9Lx1YC~*?p zE0&~}#OoFl6Lq9SeV9b{Ay!rn{a;F54oRk{Z_`5Lv!<2l0WpSck}p7?yvPbWoUjeS zSzN3X_6Z7`ya@{zrdfrCrGTP2lbQXM;aQ3((P}ia)dVaxx>2l~^@0i;`7;IP!oOz9 znn)YV2*^u?2Zq|be`r^RsnD2Vsks&=AXqyRM8E*q_w8#GIB?vUrE91R%7i$u7%5@x zXpa^vB8CGfWQ#!~7q}~vP)~yk4ciI^Y6c?zHovm^$WV%#{jnT%oU6YE2y^Gc5C1nE zwMtVPV%J_UO4Ge0k7jSa6p*@B@|L5OSiv8y<>P1`$@hUlwTco)YHUi`-t>o>6iJ8v z<+ix&Pkg3?5Q%>UwjR8vW05VaKS|^g6UYg42Lr5Pwo2&5&OR7Bimly4cU;+|?L%ae zHc(OGlNhX{+frz9(ZwcZbbht_(B;X{9*tAdS!L`9q3n=6g+FT19$z&F57|<3lKS?( zCLdbWzr+|qN4=%aKLtgHXlaQuuZB|)(A^sux&j-L(E!~WF$XGS?tt>zdxRfoaO|rt zF}#|F(e>mB(Ni7nA-uT8#KK_%Oik1-E=V4brC7n{x|@n>0_KHS1lL%B)o=4xBN~r% z4=f2G20@(3OI?2mxP4GzVirhPD!ZXEHpGVmi37tstT)Fa8$?+UJNB(#kPx&R#$#4V zFXbm)1)Tq_bWF(2;QJ62X6a$*trAubhJ&b0q@#ECYoNzm(B$8TjI<9ikdc5`IEyGW zR2)=qh5%$`E*kbXzvSy8cp`(IWU8iRr2j?4hncV)8~}doAg@1@h0RhL<;1=zJSmPE zV7S^}E^4zJ%x)>bc;O&1)+E(PM~w?(3uuy^k(`5{(5yjiM-(^A!;J_%pxo2Eckngo z86G2-rPY98#Brc0Y_MZeA?G8&gFd9m5W@p*Nw9A-+wlScGBf9M?}>m6%>n5afg2-S z4~i)>%=zWJlw*>oY&S=uO+NG>iA(3Hsu%{cPS;6rPK?Mx%Eza5#xxXPb_}UJjj?dh z=A5_JZ@0f0%BK!@OT1~iMB6q)Gr`o0q!!fIyS%M87-yW|@Ke~+SB<%>SRHD-M_UW? z$;H=QKs_AV@1LoR@zZX_PtpKQApiw$zat`-wFz^JNw~eS&^aN@uX6Dum~&ldTeS@Z zd4>-O;sI6g{uP$V0La7MD zeGcC+t`f(6#;|W#(G$N`rfs!yUG{r!FUnN_$JwdLHNo<*sc=|!UsjIgDu-i4b(0d2 zrNE>@R#Z(O)An6clz)tA{4~_|ol~4ECvNpxRoqpd`+MOj{w1)^oM+B`0s356fL_R#+>?1QGVnL7(LF@!(#Ahk^`-26{4Z0OTR@!-EFPT8_mAYU5%o)=TpXCcv6Ewm`^WDx(2$- zGW|04!#gPpDTXA-S{N1)>+zYtau3~?J*N-wOYM%@ZSm_8-&lZMZWKueWb9ZCq**8E zNZqouj55El4E&sDZA4ax&%K{uaUdclB&!EaRm>PxP`sUlAwx>1xK*GrJD1KhY3oNz z(T-{#V4HLhY=SFYduCt_JZa!)$}$MD7|W5%uH&-3=!t*Xj&plFIHyh!SsuCXZPjb~ zDA{4wQL})oEDZ)FuzZn$V{=CVVF;OqHQWk<5**X=E$Aiq9;yD=8n+H%`!_-L7x8RT zny}My-g2JpGrIVfYUfYsckYSvzu{%YpR(`$a{o$y&Wl|lX#C2be6pFJ$!yC!Qt}{Z z_AP*yv@o!f*{QMu7XKW92drC;JYN9Y#8mA7fFH?$$~Z{lxJqkaK9*) zD{^V%P2&%dq?*4AQnRKg)2XZ7qI3ua2@@tk3NbETJr>ceCj0I0`iBZmw_}>j3BtS(2l|z!oh)t^l&m$sYPF0Ypa0br%ph+`ut`8JVi`M)$<77`F+Gi=H~5B1Dk762DAa#1d|>{ zwSn_`jDY+G;zb9S3Ow6O#sk$@j!v0VF0X2@$O`Q)oB4%bY>l{le^?*giKR<19TsxZ zs@eEHiH!ItUxeqw6}nL_=U+bD)@hyUiE1rljhNdobO^ihxZq1f(Btqnd_nDMdTB~g~syXB2D`VL%8OL6+NEk zulF1#0Ls4?IR9C@W_1lYw^tplO21xMOaR4R`@wt{~9`U&~==h{TS#c}k{dn}`-N{{tH$pKj zriPw|Xi0<0}Gm2?rFovq-YiO7r30Cq>Q@rHy1J65Fq@v%P=s{s>6L!Ray-+~rRV$*MA*30NfQ6jUc}pH5s$!DM*m%pXx} znbN6&;l;3A64uaN8kq`=MWwKD_?L5P>Idi$Jnk64v^S7N5kV3*$(t+pc;_@3MYP5) zMaK5JTz+EJx|2gQT9O3k6Jbvo9Y#$*pY9#%CuEjAR7;l{ULU<_%v~1A%gr!9TZkTo?{&v^RFd@B_5s{wnCBYv>#FU+2(EK=s3%4BK$~pY!iW}z>hzs@fQoN!Z!QF9rHu!S;f3VeL)18DWgVq~Ln0&0 zF<1D-bOes>9|vb<>bwt(p2-KdK zdaiCGL<-jfB?KRl?cj1_B}$g;Y$GSk)OK^0;)N$Pl_0wLaLn>|qHk5elU8oUII&~1 zivD~3gW`XTSVc%QeXj39qrYc;XJe4}azn&>WiPaG`Hr?t;yzF^7XM(J-C{4$&g-=^*~BE_h**pd;3zctuyD-Apgt+KU5H3X75yF9mjdB zhs4)fKYd@StZ|K9)Mc#ANZ(f2xlm6pGnyAJ7~6WZjI_pml`#)hYFRxr^G}7k_ncjI zV2_?(N@KfPa1kyhW%zWnjSfc{*Z%G}@|m`nh|{9s#|52ki94az1CQvYu;&YQ2anNQ zf;$Voj}wl|Yx&gduxYV)p>Z56hI~X=9s!uf&0jFjWeqr9#u-rv@rLb=y!Ye&?zD9~ z7vJF`sjYBz>0#sv75!4Oy0!{MNqc37%fr%_zY&Z9!SV5T3ic3d?50-xGPJBzb>(`p zJ3_LwW8T|_TrH1fjgc0c(wN!u#ummO;xxSsx#G-i5E>oR=gD_p+rAe z#1HLo10if!lgK847bdWP_l5eI+4MAad2@O_F;hg$D&U$3htbLJnbIQ%7h}$C8@Fy+ z9G4|~nLZ-b=GYeUBXGLOhhq8tIPZ}ua5TttlL4HQKec8y5YR`_#-YX-COGp)a9E-r zLc`x4vI`?a#QeMnCSO%x%MdG_N#VFYPE(+{%goOpcd2>V)W%3d!|_l5)-A0~edC^6 zL>)(S-Vs$G{xzHX6k~Ixx^@!~J||Aa-+w6ve0U&M^#o%+2A~s2ptmvQ1=%NAZ&sm-TKw#~(Lthj|>!d_SBQbBg;g zc#=JmJj;Sv&p0Z=d9tInBh|IJCAY?ZkBoXvw8uY2!6)!GrG9@^v}61L9wGf#uXWBh zNsZSNmwf=_;s$Kf*0cNt@eJk>mja50RkQ4j|4hXR)SrkpIkvorc46D1fKSP8|?hg%1@d_wC0EpGj^pgyv4~^Qon2&~~!;7UHs&dQ7ogQe9`aJ3mtJ(Yqr!t?< zg$=gg(UK#?0;Q&e5v3GTMc6!E+POS6RgqU!Ihd~4RYhFUst`k04t;5jQqEl=P#(M$ zbLaaVhK`*-YiCq>K%b43lhV}6No`~K2!+-Fb9l*v>)iv)i?%-wKz|N|U-L~WiBaC+ zEegNH02ZY;9#M1+AHKAWSZ6J)$s@~gxbq1_?kiZv|fgSJy zjH(cOpsNlmGf<-~{J{fmj&)gX+QFP?F35$6B0H}%DGR{X(56mY5An=N%OctL@AWdX z@S$8Il_F&?E)8IZw}_3J8sGJRQGPSlGQV6=M(d)63j91qxSrD94Z;Stl8S`8;S1Z{72e&w-gE~%vm4MR-S{eS z{G$DWmlp)mqpic2VfF>y-X?hX7(fI1F51we(H#SX9UTMDF6a&YUK7mgt66&=`-m?o zePk}!5`6F9$V5odP49Qb8waAV>(n`Sh} zk~Z5*Rh!MxLG~%Lyb?rBB4@QOS}ev2g=TQ3Mh9wEb4GJ7!?g@%Nvp=ke-#WvMTu%$ z-l1z+0Ftko%wV>xJlY@)X(7~Y4>So&XqBB&de1T^2a`%eVoN>6R!@g6u@HSKsZ6+q zrpIUi1}66%X&(|DI$T<3I?NQf)MWppN~&w=>OT%t_|$c+%BTNE{P20Uv$zI&mcwKv z5E_K2Yi9@n*YxCd;^wPTKdQGadV&(%sfw1`Y1{MmRhI!pEv3y zwp{Q95cf2iSMvw*u#WvT5^OE~sB%FK2Bo&^_{Pi{SOr z{(nIKQPZsR8?<~2$2r2`8f3va()A1gdc}D!hTR1Xm(|Clmw>|u(&J2nhO0v%X2FQ+ z1WJH3h*Hjpz&N2D#Fg-*sy}k*4JCCXlsO!F-Fnz52lU_e-nl^QnV+hEVO#RWQYt-*+p??yXosT^#%5W=@_~y?2Yz zigYCkS4OT?##~iawX1FSKVMk;Brr0)V$G$EsfM?+xLp@pEtRig;DJsl|7j5)Z0{Ca z@`S9e0S#)waiJldsj@_&XD$Li1AWH?H$z!^_o4e>p?Lr@&ZGVQb7I6!El~k!LAy6s zz?R~b-wM@2#cah>)yZkQsi+-;5wvsrZ`AT_aY-(EUYjPv#!8G zU4E8oD%^uGa`5;bsN!)io}c`qer2>?7Il*!YNWWE;%%AJVuoL1{g`f>IYawrLplJL zr9k#0%)COTsEi3{O-Qkbl97~a;i#e!RJ9fE29VJ+oF7aoS?c%NL2ASl#4Hd~_S(b( z<~WF&IN^|^74+ql6mHKDdjJz5!akST5wU(ls@$hnKqjHOmZ-0Yp6gDhKVl^_f;25` z(xkL|nQaxu*I(sZfAa961-iLx^w{7INk4X_4zcs50?4$~XrbUPpBN?ysW2@?kEFb! zs}Y*g=s+4eM{@9jS8KL}g8X*Mw3CWURi20=?Sm2X0e!M55b6yGeT=zFV_zWY#f)wz zZF=8djv6AG7E+AVBJH!3idcNFn1N49@=~jQ2v2!OZ&Ug-NVOp)*~{A;jrJp=`h-Pr zz@FcYJ$r@YN@+7?G}9o5)1HurQkMCEPEC>U{9V+!%d~*G_rjQm#R`aP zN|LMD5ba$6pQ4?kd8K-V93BPAi};II|AH~#N`bG4sR(a_>51r6yH9pP%VbJPvl}e4 z%!ZvQ4`V;wg<#DOAo>^9*9T^LD~8xN9_&$=1?77G)0XNu%#wy*3i%C@!5w}^$QSN* zM#Qp*x#2I#UP7hZ-}PCM>rZh^Hr5s?KceX3W#3g!j{%xN>c8EG(PFm-e$KOEy(A!q^zC+c{S#7gw_Bop~ z`rNz9h=cMuVOd%VmGYBuNeurJLBv<3IArs<(M0%us!>+-%~OF`m%#6 z@U-WA;OB>d092f`@rH-N4ilT*;Mw04?Tj0nncQwKLyz#mi$)19WC}Ix0{d(M@EI!L z+$r#!$M}pz^O%akc@c+oC){emUw{+RqN+$rFFss&I`PJKUfw6ykt^WyMwp#bvdGVs zLh+_iJp*YKo-Qcy=F!bV7jQB;D?NT0F!!d=fxp9A(pyImeXmGh;+ zU7$Yk>dNvjSDl%AvG$bA6}dmZKe77)eo5;~;9sylbdSdBP4&2>dtYMVmFwbFSa*o5 z-CO9+A-e{fUqpH2>Wx9W0IQ#i>6H}u6vR48usinjrYCXDX1N5?FG%+4u|L*$@$MF| z-{p7_-_B>fA@*dvo=dl@`seD;r#)DIF!?F#&)Q#7zqx-vf4cjU{%Gru{4A(budbEb zeMC{|>sNi8UB=+`3i&2Pcy5>zCbf9tvJ1ew=dX?{F2gBx+@JG#MS(d*OB?uphc1x4 zA@g$|DDNry{HaGwVku%nZh5|=;q$P}VjpaK+q1XXfq_3f)K>h*qaV3JT&_EsR`h~p zrQZMg_I6vYJE5K+?<#z4)hlKbc#(F0^Tl&(XQj_F+R^nu{7o68y4CNF9c5_BX+_G9 zBa3-OzCiv8fbNm8wzSkU->Kf$R+i(dS4 zUWU5Ex49kX14%di2$ohH7*{Zo+b1K6aal{rtK;-Wi0q!MO^QUDZa|oK1XV*1%TN-6 z;*)O)&t{q$wa7ibxLeHQ%m2JGuJ%+*gU-rw*wqJwe}Sk(Rsbl~5x})L8iaGc?A=KA zk15Ii&*3wfda0dfXi3MH&8St}+>hVp3~2g~1o$$oc$XYO4<)X$1IaPn?S10NsgjKl zEZm-_i3e%es|POsod$VJ^w94Ty|<=Zgy8AW`x`>@(Vh)D!36oIY6g>F*16JpBS5S<^+p-NYN`|U zwx!gzTUHsoq5!`v@RQ;A1|n8LUdj0f=X%HKInEbQ%`$&6>j!D&8ohFmUkK8N>PGc$ zzV!2);jLcL<@2RWlwVToWgF822kyt0>Nl2EnquEH?Idr-+tCTiCU3bmK*^E1Ib|z8 zLj&}}znzxA7V;_fpW-6v)GLP9)EX8N*n6q5tgxJ;sUOP^qyr~s^<&u|%=E5uU}t^W zd!=!f={fEI&l=$A%bfmCBJ7hGIRL*5pgrGY@_{!fEn!2P1GCP|T2W8*!uoBBywcON zkZDjOTGYLEKjGGITA87L%|buG;*a`Jm;OMTeUzpA!wWtl>e`vLEBH&U5E1sR7=Byg zUDW{BtxjixQ>AKC^77hnyynj(=+$~pg(>puy8m9#rs0XegNTdrbg8QgtJ3?L-{F48jbC;5EiG(o=I-o>S6j;=< zCVFX!vh-t#3Rb0uYLluM#gsOn)oU4BB&v#;YaLqjvgT6NKsIk=N)TEjt!O7z{N_{a z>ZNn6s_a$`<#Rr3F5k^e!8k4E^LW-YIPIo$Ol!EW^-OcQMl>-4B+zNi>j!f_O8zD6dL)?_>%loihFuk5;wouX z<@%_RP^-^QAfy@ZB$BA_gy2xdNP8-blwB1O<2teesXt+~i=|iToYPrH=NbL#hE#b+oKq+n6&yA?_m9aP1$hQY#Mf&{{NMTEn1#t68Au6`2A;b8^BY*H**@>!&Qq)D!=E@Fq9Ns$S-I4mji&O~Uct4Bm(t8vl|ZJpT`|I7Qi39$65wr$(CZQSZU`%CxU=kDkeG3F1L5t%u1 zB;M!Z=hRP6ef$gnlm8*DIM6qu3n7w3yE=;P(?&DNzs7Xnj{p}{8&VLN?CS^ug^qwp zL)Ckmg$`COY#Wc&Rmsv%HGfyMe&WA??CgIAxDZA= z(Y;**!wDqJwe`V365NnIsM-N(H5L;v@x1B#D5rC3ksktQmyB~kv&6bL+Bx3R4-3S`)jPvgnR@D*;Ui_beMmhU%B*F0>7}PiUJm^DzDjSxwy7z zYqegvsk2_S?$U16ZtJRb`OW=2bzsW0du#P_xZyPYI>ouY|Lwi~YU_5`ZXJ-^y&?Sh7e{&> zQUc6jHIEep)g}v^1&#sbl`aEVC3jDnt@YH<2b#656}ktG_6M|qCuuQ6r07|63R zgR`Vx67yJK8VYXuos2!o29w3{yW;zr#IqrS6KJl%p829osO)JTVoD7wf73^Y*jT8A za$C~GS-2s%CygVJ)-f3Ty-oMC)U87-;8Jgg0^_S|^KAl1Ev%dw);eImQzhS{dY8mk zBFRh;-geo@TWLS- zCnN3Hnna{c0&);n4F~$~Y~kDDy$v!PpH7)QatSr@`g*q(0|b9&$)h2&2Yb~U$T!Lt z4y!RsamM2@Q5z}gU^`&3ndk{=Kx{w)q?J4-58e>NSIvi(hma&lSy)&|8Oi+0MRC$nTHji@Mspk#cq8E zXBl-klc7eavHXbWc5yF|bp{OkR-`8|p^Q4?BBC%}X9!LyFU87}CNinezp%nr;6@^C z#;}(3DTF%w!kt`}&L{dpmly+tc@Y|o24KR{^1jeTPKv?Mtk9nU;NUW{a8K~QuVw&P zugi-`uVwBO**QSAc^((*FW0kuwJiHAIqTIt=fwK&Hx%!5i@p-$ig~k>dag~$=F^UV zD}jlT$pqrb z$X@wF>ORQ6ip3!!2Xdz$7Hz+Z#b$2IP82fv$Jv=>aJeN;9#lL8irG(OGVOM?e*a|9 ztX(p0Zy7`Sk1j*1u6H`p`=T#<6tsrYDBq}={L^~+WxO`NGp`X3spiMFnb&~KAEaGs z$MpB!T-(#|J>d5*^|AaVjBuWz_)D*dKFa&vT>NCz_{Ze2SaMg_ToEfDB)(A=Qx==} zzrVPAe--CH$ecJi(Hvc~=l%NHshNK9+^hBBk3F;N*~sC~xQV#Uo^$VBxv~&U@)eQK zScZ8;wzM1euxg!}zM160Eohhmf1%6FDyTm7ip`$koD6a1n|KELN)evy1w?iKtn~}z z$9dF>!x)bl(U>07P#TVIiEXW9$3ljASL*j1q-E+(%SB#_F@<4Z;voJsZpJ7oUOqf< zC!RRJuKfTsHh4$z?G7VAnS1?vBlnfOQttC$>jSC9L{z(CH{4)9(ycFF?E52-^N}$% z{L(Tt4SleUHqzZg&hphHYX^xvFNde<06CV0iI?S?fiGD)x+}gJD`!QXTRno@xVw`k zL`<9`%U^_l_JQkzC>^skg`|y0{6xz!@7Xj&fBM1Xi|+cstfSe9P{>s$jV*v>7li?N zlXp-3!3)w~fxru9=qR42V%-B`tcyOvlA+60GYyz3`iIq0=e;OeC!f$^)MASI2BT10 zBx?@#bt+WYepNcXTsb%7ouBU2NZI{xwXa}aTsStD#Gow-Fe9d7Mt(l)FM7I_80>0k z9%qP8f72z$XTd|3@dQ?#;$cZWf`!$Zm{m0=yxP(tLF*X9d*yncDrVUa7WI|NycxCW zApNw2C!@hpW0c#E3eZf?y2!qNY=PdSH4I-mD4&<{B2KPc@lnFjIQ^*QFuKA$&|GLq zpW5I6SJIUg!9_YJv=1j6a?g}rG1A^iaEpl$xT>kvbwYQJw?+Lo86e9B<>m1LqG^LI z`l6+*WzGk4OS5am6_E6>u7)evVgZ2Q*>Y^0Fr++OeKF9gVWZ}&uW?fZz^qJ@edohyB%>p&`QC8pO1z{|Ffq;)K-rf90PraF# zXb61fYO!B;D+%wt9m7~L(k?GRDtU%^5(=_nws2E;eR*ePUEZX^)hP3Tw5%8 zeF8ZIJWOB1c9p8Q&5icUa7FbP|HnClv8U9)op@vGO3qXX(*A`>k`b_C*4tmR$9QBW5~3e>;R1;v2w*5-9ZnJq$}_PAVmEm%~} zj32=vQF6@33nW$LP)5$eJTMyA^YsLpb3`mEP87QHGDA60R#V%N?Fbcp7jj$uekmhx zNC$zBqEgUvNnZsNfJVm`zayaHli1}{91%8@oewd;yAmqZ=S z3_iF|oGev6fW8rBx$Z9q#ew)-=hCm5eIt3I?6tDTlHr;~or@}k0}rR96j})Bq>s%j zJA{(q6^!)&mN^$%DCR&}|Ct^Iw}2WdDi{sfEtNwyOP0mf^$0A38Y$|V(U_x^o`0Y_ z-C0%%b~1H_m14!OL-_5tY@05lRT@o~5Bk8UDVZ`a`NpwET@q7OkZC;gyFo_ctl7AR zG-<2z!G!cvw+%j>9)s}n^8r9Jt_x8vmkNlORL)exzPuMN1S#U8G7YbwdL<&ouT7u? zcwRJhuw2wtUq<+nVKi@y>9{DQCm!d6fk6MV5sbw#dd?yJLvDUT%)tJU$;$0qk@Q#= zOUyx?`&7S%X?%zHp~PmLmM!40=Fy1+6A z2K_XXuI&2>V}^Y*QXW8Cj$je9S^6h}lm*5Q{0JgIN0L2qvaMy*^`OOHncd#Ckn9g!b=AP#Kz09YWT1IG# zfby*e++nauk1iM(zKLkAzg9?pKSGE~IoUkt^sf(@#bD6ubuN6kaS2UF-!uWe9vEpo z5q3a2{T_oEquJt;y2pj3k5g6m-5U)icBVt-lA)uR`wb%|(NZ~7XA!HCwWyq*6$A&s z4^}o@3M8KaMxg=>jVVeEU^W|Gqn%>X8r=n&32QZ&$CU$g>x5ZNBit3hj=Y^5wCDc( z102*uE~S*A)nRJ^$i816{K$%aLxvrWo~yfR`{msG_SWBkBcL@4SgP7**8-HOTD0F# zHcFbKrK9R{x%trGOwS{0HP@iZVD@Vs_ofwIVI4`vf8KjRv6Ksbjx$=o6A`F#I!aut z3ywEUPN_Y-?HpR%Hs3Wmx1JGPwaOC~StngZ{9 zUhuZgnEChm+~(u-!{@`$w;t^tolN&0z(A>l#Oo-{d)g`^R*ek0w2S4(99{m+gi<3; zgBrv|KeRyvhAtr*=}@iTD18e9dV_y3e( zdu;+$*%HGRTRs4tgqt&?{~0&SjXAu?w+mPoDmyauKxAU3J;b{CVnY3sg7ilBVrEa& zsWzzf%C?T1RcqAQc-MF_c5Pr?YK!IffTSAAJ#UR%(VnW^-WxtJ0Ld@t$K1Akoek_e(=^-Hj*6 zU5M>;ffD5tAfDaiwn5?XOYqR|vgyinj(-mR^28Uhg|@*C9L>WNCr^jd6}u|&8v}$s z>!QUk(X`*C)T?#K2lQ@~z1$W3pr2MVw|4aKk5R}pC{Hp0SF7wxcr>6#(SQziVnm@? z?p%0VyV-_2yov-oMCQf;+93SfMw9!-JD-O|cbmF25&qQutn;IGJ_{dElr|{kR3228 zEc2?|@x8QWe|_#^>x?C<%^s&S=X5>+cw*xC&gS1gG_(2GuCJ?5Q6e?w5kmP=1>#JvmVcywyD-;v0C3jxJiK4?nV+l(1Be>V(4@#cw$5EWI zPP{u#cN?jdNGE0{cyDHJt*}8-yTuESb}!+K=rA1#o#^Bjb0tmw!Ep0gW+bNC zL8%Up25n$L@rQ)x1M5qHKM1mS@^pL1IuN?~rR# z6wjJc%I9{1T(m|alx3Vz*J9yq%=drTZ5;cd075m2W4KWNV7IByix8?1HN9u-$1}VF zV@2cC`Ps2%?r1~XG;5u-tU6t`!z|KWbp6}^(*$@YivR3J_xy;zi~fiI|Nn*MK$QSCdFCv(tG0n`b+*h5l?j5IFeY-v0!2b|Vu;*Zg?1}H#<5)a^7}~+4 zalFlJ_oVxQ88c#-%@ap)gd6obZ1jY2NM}UuCi)ow69@9P9lVqZ3i)n5Cf6CNtdORC zZvD2CY^Ca~pzmClhEhC#G#oizNXIf6o{x5X!0a<_Bb)xHYoV=+{+s!XQ%LU~oxEi} zEh5BnteX;e)RAwTTUSTX30kXeDr2?PDDs+jlOeFBAR5pCISNo;64XWDUFvi-7B0UG zc@(LLQ%&QgSGVX!i&^9naNti|?h0|)UL+T~uF?*TDpDc^{unFsVsG`eJRO>_=U8@w zdYM?y97)rFork^|ZZ=LyUc&P#+Xd_w5r)=~ZvJbo#{zkKCo(MHC4-@($Gfho<;ogk z%`ve$sIG8O^U!j<$}LN|t-}^ng41&J?w~UKq0w$f3)zUn$`FSW7@3TW(PSqh%whuL zWom%tj_gRD0OzfRNhU)KJ(v0T?r~+!A!%^cJln{z92~+Z+e1#Iw+nT^qsAf z*vztLPYbDu)|;P|W#rtsv8nRpHRq9kS?zHr%zWJh26D)fAc-^O$@zEum=LW=MEauC zZagM?;3RXSEQhL;kCF;KoL7Mhy-?>Q(zU_5?~teFOJUvu%<5fMJc5%;QT9yS)v7nO z=Jv9qAL%aoK{{OX4OBNW6-vCFU|(>C-VbFK8jTvKLqF6@{Uea<#i3A`NS*A6C`Uj4 z#oEjQ>-PtssofD_3H$srL>zdo3V60>fUh(BqceWg12C7ya_{#F-un$Ef@4QK;TihL znxJ>DBd^kIJETWs+p)PR-I`{15HEHrbi-c;HpKP}->2%Dpy1Xn@=)X}qETt$UHa6Z zFIpCx4xhBuEdei*^wll=PiBzyaea6Wd3L*V9`L`zLgMA6zCjqpU&Q&JfpBk_n-9=r zCu$ROV05!~Eu!5ahw9pUAbHZ=#b=+N4`5~qAi6M7iQ`DTv$;Z z|K_V??5vwvKb@P2pUw^cf9k9M6U9wR>8FA7#kE>vTpx=d2qLT&i4os(=Yuf@9fW2A zB1OSNKaPRcZ);i4^8gPhR^GELc>P5&o+SYlyXPO0mUVji)n(`5`ShHP&krcA&liS* zhs_X|P)-ge*YD_2Mw~qW0d+}HB;Qxk`xEhaM#nrVAxW@r#9a-?Y zup2dTR0J}bVFmbmF14X(|I_7^Tp* zBL-OR;VT;S18=uwqp~_zk<5ht(g03fV9<9JC}BRy0#3JOwaq0|O1WYOD2`fn-IJoX zq^#w_mfZDPQjVqyZEEY){us17xe%dVD~WDQi&5DjHJ&Ou&X|uptT9IwHP*(&{(kyk zb>D#)^8OD3j|o#lZ_9$GEfzE-Nv)cLXo^jTA}at&50N@?3h}!_vEBVDaadU>UUXK% z=)_=K2lTJ$I(SJ^K`5g!KbU5Zl69C(-VNv)X$GN^1rU}D6-7Pffq+h%AG2H2SN|y>+-fzj)sH#L75pr6=|91YcO3 zXK_ZD-7G`0DQv-Nw0G!#Z3)3M5GHW`n11e$>Hn|z!hgG_fBkpfo76vfZ%Y#A0}g*# zY$V-LU@#se-wtW=FBCZ<~y@ebecUwQE(h774)30`{m*7ukQSN!$BA!5gS{% zCx5i4)T83@nj6<${`e@yoFafPk~O9!kt#pgQ~T+{MckeqbIORCD>0aGJ_@Y=#RW5& z>KnsN{ei6hK~p0aK@Lm;>yrgfKb7BAHX& zetkQ0VI;)G7Z*R_6b~ zCjXuLW}}3yg5o2K)MmV@DDD?tt%YqRBm~?T3Mx6+0Krz!D2OoRpq;E$*Rmnw%HH@Q z_W|m41*52Ex=@Rsam)V#y#W6`?wa~e1w zIG)zTBlUNQwtUAe$f}FD-((vyh(Aq-h$MWgjR1$5Q!(*e%c@VWVM%+bKr+jwRVw+q z)COu??ARuNk{ej70N>7swOR(OEZ!gEH*wQrWj6#ZEjrgtg1Q&NVY* zZ3juf|3LYMDk&CwB#>f^wG_sayRn3G_yRDZc&0R`Na>DP$s9SO#A{!~`wWG9gVcFo z2Gd~g@wAR9zQsY1*ce4D3rm1i26`uTo5o)Q^$vb<VTC5Er=62JwaLv8Ub28ao zr;pQKRoe*C9ot?PG~@KOdpY00YeT!s?kDg9N(jmiw42evQ#HTs1Utj?aQS-8rQ9?i zD`tp>jOO!$p2dlA#Ppw<@bQ!FHvHq$Px*#x^Z-sbm#tpZeFOLzCDRu3H)BiRID4n* zx;G*ceVEiocg`(rc9>!CZk$TuWs=G`k4xT(eq1r~Qw7z@E%_Kls`I1SJNOu7dQkY{ z-vssQZ}U`hf9|B5A3?qU)c^mlw74RaChKOoB@K!*aD5Yz{OC`b+3htWZwR*@%@w3T7lCDXDar zMJ~>{3|Di2A6L>L{|VaPQ>%~ZTyF7PLWs$w2-T@;k5xr_wFagA9k+r^73s&}0rRkz z7)%K~-(tH1Gleuklw`=~kO2)Gl99yC$EYs2<87I|ewT9VJnOOh* z+FauA!lR`-7*5UcIuw1A#%4HccbqJeL|#m05t!Y?9+pq8`F$leAJDetI>EqT{(Ddb za$KN{lTL+heAQoW&yit;T-BKb_SXjYf<$mFvH{)|cdqnzJ1cGEnXi;MG;`wHxyw+} z;a&CVt^OP|a(;8j<5O8H85KxT@ZTvAhvykVeEkAuZvE4V6|q1*5LIvIp`_?e5pvHE z3eOmyc-Z*p3&uZ$1Ucs1{w2ruU8s-0dw`$k*Y&e!Zee+agOD@ZPdozB{w#&smNp|Q zGULV|i9l=$v1p3Hd5=VWBJD9}sc&WowkPBWaY(-c zQ2^uZDvQS0OxT@~``5dVflR^eAIvz$pR&RKDyjBw`PhG>#{EdDAsL|XqR)t^Cm~c; zOoXEd>eMr#HlojY39|gQF0!NM1GcJ>8W&pQKSh6NO{=p`FPio~Ay;|UaJZ|KzzW`c z_nCHjs`2`MeWm`RrEVoejRY7tgWm%HR#^VZh_bU`kI?Ku9@>r(m*NQ}3JjqyeKhnb zE@>vzo9O=@C`xXZnExx#ufgP)rL(~7UdYo%gFcV~Qi~e8eXKdsJ|k0QSvGCmG#;BA zX5PLCV*W>dW?sufWf?|Fs|j}A-4r{zCc*n0L~m>jC2*<1AY8hzh46yaJHIE+er@b5 z==UaDfgbc5iLH?ST18QXp#nQQ_TOtRU`#rU3bwQ2B=Lm`$;ruvs;h`X&@Zw zyy)t#@UQ}xizIkrBz}1&2X!o^dx=W9q1j+0A-PC&95V%YIY&rMIGZpEkMb=9C2ER8 zkOe=BgLbbG>U^rgjqo7N5z0ilx`5^&5mR7Ry`W-kpb4rRWB-P1nR;)Xze?Z8H?KmU zI&V~CRbvOO^OX(Xk?Z$4OvMz?{q$JWW2TpnO>s$hPGW(YzDVQnlP6SL+Po?*-0{Y? z*!ZjjNVkF#wAJ27-Y+Vs?8v*S4&A9xn_-n}CsC_5*#rI1c6SRSDPz*Dp$vPpfvH;~RtdzQIuJ_+R$lc~|pKpS&U1n2P z0)*5rwga+nz_74o0_`A-jYoUv{M#dcsj@G_L^s;+uj}!XkX1*NLPHr*^@*XSmj>#3 zgc;Pu6)+u@oEb*mKk16jN1bn=?mj=j3hQj#vu^NB?2vh2*j0Oim5CTyyU!dmA&8N0 zbGr*slR3l@-)-+;rKiqH@Z7lj{M+E$7#gYIKd5oP{~umC{}VUvpBxTT$4k@ofrq>$ zt;W)$IhViWm8g3DhK$M}&7 zTBfuunYU)0*Pt`OQ7_a_KAC<2u#^@NM642my`y;x40X$ahrDInzM4nXvFh^O-~W6cf%}CsRk!OhN5iL)9P^O* z;fiEJ))_RLE*|YpD^Hzr)Yy8d7!>H4ZPXqp&`ud49O>#rW4{V;@fmoA^2TZX#kD)N zn;3;9%y=cPp={zoJ%VAqJn=5ha0R|1NJ629AAOZ$MtoP^NNfGhH^>WYr0!VqM4F;5 zf=@uH+Gld6>wN(>s8qx(f{&O!o;eV1hL#>2-yo5#ph4V?y>~=hCpUTNkbNN%dc$Pr zxY#eTG{Q_`9htlCe+}!I5*blbKhK%KA8wq$e^_DqpU82UYOi`I$C%$;^by=N`Gn;d zxn#D$i0Xk!E6T(}p%kjxC_2LV7R~)vEs-?L=Myw)*K8JZ%$-{6MbDYKO{4x{jcdeB z7MI>dHXiK00=~BV-dSlTf*S-6{UPpC&)!qd?%Uo+-Ez7hGk(YAHP9XqIvl7%VExiq zP_iKm{?-ur46jC%BSH3HE};X_KI3< zH=@CIiZ=lL;HVv(fgv|F{>=!=+8yWFTeeK$IU9oW~9d!0~QYEfhzdcEKn zKJtP@f$+Q52>5WhxcCY;vj}*D!)3P)UP8U(X)lTXa?Bsn2-&a()O^J|_b}Z(+4OhE zQc!sN9NiT=)Pi2Dq1BKT_93vv4)p?B8+FW#w8e$Nk1L5LQ70a<%|_9-F^C)`?N`Qq z{q2*G^$BQK4AT9Zlr_!n8r1v6jf}LW>a{-W44XzxyJs$D<2eF3>yqa{?1QjM{Hgf3 zbC-ZN6jB-X8de`DxC?)1c7^(*{Mtwx>mkvln86B9!u(bIxXGPfDl6}97b)8_D z6H=YEy&JAyY<2GE(iR&VY@r+I&Ti8>Ope8j9OTlHL5oD^egQ?K>J72 z)LytKO$Hv)1q2?TSdvzEJm9X?Ig3p07YK3#lbV%0VtO}!IIV^2?xo?IC#tpJo&g-i zd)O~U;e0qKEuMcos2%(2;M_RFFvt!kq)!p|(vz_z4sHJCP05OKXc==Do}1nMpyLF2 zs`emJ{8LpK1MaD^29y%{|A~$p07c#I6G0`* zOO%4+q{kX^nv+=4YAr~Hv9!s=n0syVn+H&yXrHc`Fk$hn@*AZl@KoZiNAilJnDc-(?B zK(G9P)SXwPHKl=6((Krl#v>9WSM;0Q1e=p{X|^TrDXz0Ld^>0h5l53}OiEeNRqVZW2U0d^hy%c8 zv!Q6Ixn9Q3uC{p4!hh{{jz3m)E+?W)n7jfCm#1zXO;o@?dxg;{Ul$XmO{F4P8$8oW zHyVT^dcMW?j!o0eEctb~3Y^Vtt8gi!S$*3%)k-0-Q&a)dgRE1?^w_7N%E04jaB6-dv2Ewz z)u$&84i%pK(Bp=f8_i=9>YG`_BPJH_MJecIW&jFf$F}CZNgKqZW)X`6tM_S=@Cz=N z8+wqc-aUNj6X-X23r95{>Zhu`G&CmrWb@Cn$3z6_I9x-5WtEb+wPa_k3qSV;#gx`z z6}xX%7wU{nx<>-nA1X-f`sz7H+eO*X*l|*tw0_fuWSK;(fn4?AjpHp_+xBFw3SOhf zzcp`XKS1I=pfI8;9=orr+RkYYQ*8lwHT?BNN2abitVhVjXZU%!i!UtPtcTP9e9i+y z0PzNP!k|8gHA^%+LWY{37g>xr2}@n%UsENIuFF%h=V*$GW2;Lsb$LG#!?|H}=7f^l zLwtT!?eQBI#_NkcID}#`|KlAnvh4@gF-gXl7le7zcD_-LV(#vtfJc+$e}q?ix+o0j$+X*g8;!)o|K6 z*yjp;3@0`lv=#Eo$912!I_=6!|Lu;Bx^iIL0}g6l!lR%o?DTOR86 z+m4Ig?n6GpL!VmBr*Khs0qbOM-7r@}(5gJV5C;%I!qa0jAm9y-ynOX1Sfa>$i~>h7 z5rkp;jE*jWZujecNK4F7`ZdFV%mUG!Kq$PslB9v$>4^(TxQ`~pMYhHXOxV3LfgW$wqd)>>@$gQI=}uK%T|q(oRb37J)w4asFxRKnp;5fN{D zbyv%AnO^qkAg)=Y6LEb4rLyH9@;mIRwqQ?#rSvEZ2J}>Y&n@=%Mtv>S;h?@9V8>Y} zPMA1h#LORhu$SdRv&XA7KL8uUFr4_L>Eu%03OtBl;342D5XTBX^13Jjg{^BZ&pxf> z2W?F`O5N)tow0O%Q({$PlJ}s=hABQMOhp~Kn5Bt4?KMYXRM)?R7}XgUJi(RM9)Oa0 zHqZGRNrP_@K-ozmOH`GoG&PcIQ6`uwjQ9BA>{uBTZC8xSRbtdH2-_6(7l`|toe^(n z@6QY7N9Edfn$+b3qxJVMK=or+O+SGo^f^4Y>btW1-Yl|hkD3?Y^i6t%&CBT;zB-d# zlxwq8*bd_$wMbvskVD108f+t+NKTxLu>7QU&=DwM)msFGz19jG~nB&YFXi z_QPbNQU6pU(?!Dn0Q{oZPYV*Be*mo?ZJprUX5Z%I?)Le9f!v|QJBS^81mLr6IXOaF z0;t6*UA-LV_*#+OE8?5ig7GSC z2-id0T0j$Ok{whQ9uvNv!coWf#Mg*-X=$lu%*UvCuqiMA~fR` zzXcC`98#0-yglLWn3h;(T9>L4t&YnjyK9)_v=6e{hyjumK)Q`aUSe7{t(Z>0fjHBP zr7G+uyiZ}acUu#TenRT9U*bHzw`Z+gwomg8%crr;AK5bVWxxD)&O^T4-#ecli_rgh zg$VtJQRRQ;4i>8DIL(Wq@Gh&8Xd#AW1OmtaSwL1AqG!l!&czq$|LPfp1)VS035hl| z7Tb};lL(&ioeSxDglAC7fXh6!GJ8?*Yo{J0B;;}9!n@q!ZF}C3tJJ&hdPDCaI^z<( zRY^`6=aQzec+m{;C-w6pE}%BlSh}TEb@mD==c>vjsi~{!pew+}Qf1vBDd>~w8$C06 zFV(SL*DY6%09v+6YMU%V)+Q{OO~#(0MVM0i%u#9Jb}<^K{5pDh<=lpBzzNv|G9S&22x( z35beF)LX&1mCtb%)f1KRM_-vI#$w$CgR7#JNu0nbi%f2!ZL!@2+}zqvZIJvhH*i`5RECDs-83*-QG>a1aZ;k@bRO-rT+T zM781MvXp17J5EN=zVORoea{twtj43FkhgrmDl~B5v<&RK$sTqeSG*F8ESqzlWqh&W zsJrT=Y)BMj@jl}B%%i_XSB!Mmg5t;i5O5KtX&_<}A|58*C$bk6@ub;~Th|Rz=?C20 zt8Wg^O7DzR2|Y=#gzoRsh6tLi9TX|Bzx9ZGeUS^h?h+8qg;M$!XRbKo-=K})m++}! zKSOqM8N0uzz?gWAzk|2>YyLqSeDaRIwSiJ(6u}Bl95lg7?jkq(8fcC+y=C+y_wuj* zkQG+g)e+{PGye{_BdWfz=Wr${UFoG@GTy^*NHV|3JQzX-clw}+%-BDk?+TC#16D!8 zr$Ui243+KZFU0G8#H{$C_C^v5%7SR6W}O-s6KC@YaQE2&$rz*-8O%i%Obv^Eavd;C zV@Q<#nKnss0(!@yO5->uNB>n82e5@mQX~$4Z3(D zwRj~hGEq?C@mOXUdk)3fy#%Z{vny; zR}&fa%=hQdG1&lDef0T(_jEYgGfN`0bgeCs!2#58RY>7!NVz04Y`KY1c@6F%_T_&| zT>VnR?MVWrCF(*=h3-*!imzDGCa!wZ!wDmh_A;Xm zu0o@lDB9TCT_8eb8jn<{wkgoKnkD!tdoyC(Pg=4=4!X&eh`+y@2qKd!dorD$Utfee zcPB`b5#b?q6gy0~8O0UF2jJ27)o>OQeJ`i`5G)+kk+mP_VOcX!Mov&0w~f zdX@pcHCp=QRzX`2eF>T~vjgku_DsvSA)cc+TRCaRLla;hUrA_L^xu%{Q);l zkKgyStl`CYOZ}!JHcO3i%9?yaO7#H7>Jb6e&Ds(i%@M@fqt3$`pv1nyW|TRUhI89> zn-=hm3d;Ayj`F?N!W9D+iI~2&rC_bQ-lsu4ojc}{REe|{<)Y3Gy@da-2?LBTAeZG& zug&HMH~60>4F3%{SgE@H102No&LlCJclKjLK>^kHSzT=csqt?B4HO_`D_B`Dgr-`* zWZSG;h4lmsPx0RarFZk*l^F_|dI?qn8i_=oaWEEj5Y0qH9ab>&n)Ma@oFbzwG=XyP zTH)P(opOEkPWhbDeR~gp2do%aKWqWV7E+G{=V)XMlnHgRbVAS)G9O$bXu4%Z>ggqEbyD8|eUvmvijGW}uz?K)W7FQ~Z z8J2_Ai)(UHlD{)@8^ngA0z{c9o6LicD$LBI5)0CB5}(HzCnGOwSo&8jETS!0$t~M4 zfE?=OOj0u!U|0`LJypt;t`@_PWz)keTt}0Ko<&Um0GpMFbJroW*K4ev7$G1B3;H;( zHmPuFYeCheLnc%3>0JTzZ(!${Kv06V!llfe#MnPb8>I3fFBeW1FES{jPqarD4I>$A zxJcV}N)uxG^PBbEvaRBYm&0XFwew;BYORf|_)E3rSpH|}K4-4Tiqf)tC`v_cY|7DA zYe10(8PSg}4@?TgSOkMXsFN!Mn$h4kqICUP^BK0MU zoIT8A3{G=C5Esc%xU{$5h+QoZxSImg#9of;4YE^>LGIf?Y(+0oBK?cy+p0$;6 zpcJHJ=cwW&uz(K>W%2O!E48AG{jn}y2#98{@}v;zU)RoVe%cbe8^fb^H^gWkKUkO zXW7hWcW{?!dL?tR$Ww{yxB)~tz8{ui`P2s?a&G;V_nkHPpmmTRVhfj37r#x47jh-D zFvl&~u5&{yV5QP<#iHI&^#$WK8&tf@e;Bqj%RK?Q0-9JGQKFxn7w;YHu_sj^8E)PK z)sugsdWWOxPWO4Rr^|wdQ|lTL{S_YVpDD8AG(lEI!$S;j0HVgjl`6(bK1C$Fk+(x$ z!T~tTjSj8-{hs;?r|rWRuCcp%EPon6F0Z1DE#ija)Yh!W5p#BB70LYItKjPZ(yRPA z7#wqUm)hLp#j>$Llp$yE9Iwh7!mk#Aq8DCrrye`pN3?aLBVzbr zuYjkB=bD}8Z0rCcMtPI(9CfpJP1+dXL($tM5USJ1^F)ePTP;6MXT3@Hrrfu52>-og}W$f!F5~dh0mxuIh!0OH= z%c>Xf4mrr6`SK|~n1h5OwIm%Qp3$cPt-x1tKh}lESbf8HmM5bwdv2)t4CeLyZyA7W ztn7p3Pm7D{Cy@Wg`ThUMAuLqZ`q3fz;fnv0LpUzdyH2Gd2~Fe1j-VSVYV-?+G!;WH z*hEov+LM@8>+bpo@~Yd72*r~CC~0XfDv{g*-G@ulgc|bw$@YT z|E|?Alj%Zw%j+V{>lv{zS=%8yDAPP)#gytqy1Ia96tTae8f-ixiYE1>aSf=NNsGpo zVa-Amg%6Uc`Apu{R6_I!FM@B!Vk6euf`gfeiw1Hcivk7*h67_XHdyM#;Q=3Z@V9r$ z{OgDxyxR#veJGXENVQP49Z6K`>^;qT1FYgi{MtZigsYY6>h_&NqwQi3sz$5r zWRHs+xU2gAQT9$jnnhuI{w^pxF&euuMSE-h@a3y{qF4eg#`J4F3I6lCh74D&M@l16$hPgqZIJbVm8TAaD-qPL<^t<4{G zFY~FGf7k8gTwt+iKfqbLhcUYciMvN(y~QTDs)w#0D6*t>6fuo6OlLm9Mbk{uXNnYk z|6Bj-0FdRm_Ycc1{=>5W)1CbPok>{ypFsAIoxvWEQF81=uw^etmein=wStel0$LpF#ega=`xifnGsS_tIah+mWIkT~#fNTn zb6mWkKRnYO*;F&AG&-Fzu+@P2Q|-NzQ>I~+bEarV?Hc*2g)=GW{kM#fDKmQuub_`B)$Sows zOp&x*;bNO=VUV0z8t=q9J}k(!-B=FxxfpLG+;wqz00L}Ps3WrW}nz@hpAnYzBLWPf`yMdvqN6`REIG8Bv`Bh0aQN~S;Vqz^Aa&e#D~ zBQa{|$U&<0RT@K!iQW{9_5lE*OoOG-X!>9f^w%w@<*fW3!d*u7Y@UhbHC0utyHfDa z=`_Gut|&FFP`kxHk|QA0eaTPj5JMlo7GyWOz(!V%YFEx};Sxh?-s z0d#FqhT_lr2WzuDPj@&@Z?@8Xe(sN{{1(%$MPLIt57_hNL`D%CppQH36h{AvN5>3> zO6Qoi*&`h}XAT1dFwi=R1VmW0R_I3%%Oyhnr5T;T(Kcmc>9Z(1bU5fP1}erraaO$v zjI}-7bv-7Tr6+%@rNh|wyI;?tXRBI`!jE8)GFls>=SCXGw{Cw%nR4|L&B7P4v2rx^ zA=0#wj3ZKQ#AMklr4+HzrBob=qiRbkVFZ~{bsk4i?!%4{A*m$?XjVd%3Y`=kBFZbj z^%N~nUW-ZA?nY1^J*&cwOFuK7sW92J@Z&Qk$p3Yx(8JtHxKDu`>LM%N(3`P#OUbHJ zX6R%S!%`^%|69NL2U!*FN!jdPCK$Hl`pw^t+B{*^9(2*d zEypR~BBH#_y41?!!C3IGf9-yVeggY_OTGPuc| zTrj?V+fyxm;!;cE#zaP~n}VFpE%?WauNk|uPCKhh{v&bEiMo<9`|+pNpPPplH0!+lTaA?dgCvV_l6^%K6Q<3xPk8cgO0X!QsX{)qFSNQ|WdH<0$} z>yNbsSFt;i(cBTfX#P4-n!)i@vTrzH+akea zqi%LW6Ad8^O!xKhZ8Xk7Vb8hKDR74{#~P;H4v@>{^?ls?k=>`=_ABxtt|S(8F^|}x zj*2l+SSy!wS#ZaIGLu0LpYhr8P1|SG3cdd~C7TF$bq){?5KzRwGsy5iJBI(8v#8d9 z@zyBHqEq@g>sPC)8~ zTRP~1U2v0DPK4@SGG~p`AJ?bNdrFGhlpIT3HyRzX+EtioO013zf3`hrt{w7YjdM-Y z(aa)KZ%UiKOy{m)U{L!)P_}&&fUR^eL7Rw&P2%V^Jn0r;*X^8rmNHsi21 zYuz{L#=VVx&veY|+Pw~h%_soA3*ol)5R~v;27upmD5&7A27Z&UVFP$&*CYH3c!Ru2 zR>wYW9*>z`Xpq%$*|hJ4byL!T4{O0o96fw+6G-Exh~yy8@SO0MJtQ#q6mjFR933Pa zbmZi{^=p3wGG)`%n)4lpO8?5Qlun z4#S$fN)Gt|bH8ceR3=Z$-+bSKmwHg=dlE)h3r1|6K39OL?zr5^7jB(D`+=#9?<6XZ zMFGAJea#CF)yof!F+UD{7Ut^K&7Yd1y0~>J8Y>z!Kl4zyb^IEvW#|I#aWWa8tcu8d zRVJA- zMN+Mb;F1#C*GqxU;ylMBi!$A70!x~B>I(~^c%$Qmwb_5y&)vA5UHjA~&~HH3uAD%) zfZ&WI=YDg+rKAwJaL&tcFq)NRI}}ePet)7;<|SS!nU(4@9@}w6>oqQn59k*y`4A0? zo;yXz6e0ojlizzt`__+QL8bu*EhYHvs$Nx=+j~$UcF4TCZ}aE!ZRkxof@?3&uPzzn zvLSj>!EgrFw=N2>G|#u<4ex;0KFzX%nnaapZj$b+V|hDD-LDAicBH2FbNG}_X5GM2 z$K0E3CUJ5dMRuJtgXc@W8Ot>9I#sw!+YTSe85f`Dp1MLv^9g{{5h)qUuz>=bCUX?e zJb@!MEu@z^s+P~{Dr4R$#b_z1T~t;ueiHt={4_Szu^L5vJ0sRF6_6;OrJ+FG%Ofo$$b-IJfU zsd)C_u1(OJ+bnkm``2QtW|YJryBaX%{E5j{mZou}0(gfSO+^6*IoE#=pj4apx2du?<+_SJK!bQekiF+8#; zvfoux93{eBB#*U7j%QjyMFzUT;c+S#FBwb%EXO6jF^yY>l%Y->JdW8utzDrwi2=(u zef3lG7OW~kEmjP%4ei`LKuq%vMh38BRSc$kZH5z5DUGz%(No;{B5`mLdaE!L$ zIExW4{s6s^iSiUNak24B=)cgWnaw%Hrgk~mc8?Tg2cs!`ULUlT*P?cJL=0}=3?m21Tl*}N+bKV>__GoBj;p06i*v}?`Gzh_SMoW zHk@d3CeBNfyfa*pRR9C&04cmtsc#ZNj$N{McQQfD#q>CA)Ygx1R|rct+a&U9*w~d! zN+yYhw<0hi?J8o{Eal84iW?|ahkL@s$rQY83K<5k-&9WR7L=To)$Cma8_-~1{(7d( z%h)4j&E&j^T&*c!}YIi0WSJaX0e@clJ`Zvt4!4K4ys6f%2BZRq6 zlx)u69F$xvQn8kh$DlFTK(}Y0Q6OSS1ees*;sL8fb&`q=&d-^&iH}=PVyAO0Pz3s2 zl8g(KLt-wq>be&-_Kqyg1As7+eyJT~(-FN*t3-L@-(#%e2u<5mi4GD$#;SOV2ggrC zNlccY=!xql(R5}S>-Gx5!MfHZqo=TNrOHfdY6G$8U(|MNUbv&hNforoyKW-&DQGlAr&{)PdE7Cu1g#(FiB(g8D0j1zudD8eVrIK3^YF2 zQ^{cQ0LEPeZByko{31LW47}ng)_uKuyWlxdL~u*>IF;2Xl8fe5{D)>;z@mFh*`%0G zl+w(5nr|WVa-(~lz2mz`lCTE;M^-7ZtqSR{EB83GNm|9 z;0&yn%h=XZ@a;Tk4bd-(h)kIT#JMI7RNmiLclATs^2}BJdFDIHoL@YTNnpcVzq7J*v@ z)itH*cr$36N7h2w<15Xa&FHiWX=sbtcO_R1+cx5z}oJ;+7N=mdHVW%}+4w*b745TeN2G<_IlT2MO28SO-0l%A*3IcYLgf?xN+z@I2 zoj7g~7L$P zELup9{bgxi$uG^=bGB-z5}3h7BY~^KVwP*gey%^2LGzjiLd~IkHM$d}*DyKRh&A%? zQTPy}#qpcth`~e4h4?1<`Vne8LUw^aFejnviy6RphLw-MSCte*X0>dr<&qiBUOa3z zfd*%I_RG{n_3fhDpn4q~#LIZD(P=I3l%4V3Yh2g6!i#^#Mu!eTi;%CsE1vn48(=av zTzOdh;U1aDqqrSVvE1{U>Jr-qaeoUK@3G*DyU;#Lh7Rkgfqa?Zt7Kw63oCNaT;qgU zJK=CSM!Z&x@|J6KBP1yA_ol$F! z`MAVAgIZ}tOz%fjZ9{DbNIf8=r)+&zH6utK$C73M4OwO$oxlPS89{KhKneve@-FiR zs+Ots@HTSpKCF^v>cqJS#PQUFFmANra&?-XFRR`X1G0pbVfkwfkqk;CO-eIexy{dVWGKBq|PjtPg=7A2CG7@_+nYkkO;e|odS zDLu2-0dK@_MZx|!-5PDrAa-GucF?TA$ii}I_-g=rqh_f=i;08GIP;}WKR^zX{37?) zsG67Mzse($Mt%cnj$4hs7NJ#kbkrnGw21t%<7cRIrfHISnp*dOe+*8vukCFEk{W9n zUtOTnRJg)e2by}QPZrS?6|1e;+GdBp(I4rO*)ed!c)B&-5Tx{Mqy0b?=zNjrFMSbxh={>(3 z4miU6-f?uX{-e&iZ>?XMUmvR>It&BF+JoxMPgp$m`r1JUO?3V%zNCGx^b7 z8|Rvr=@*6RA3~Pe0#wlAXGa_}ZjR*5%~r@%M)2KB$ZF&{Pv*tA0deU+h4rOWKDBX} zpFy)wHbSX3_8(|jtcMn9hj@7E3CTwb$=*dafF2!L`yI_jBr(`;5W&q594^_akb` z4>cx8BLTvM9_ExkAv=zQ9$pEhM>8^WU1Wl3#sX|bOAR$qIcvPa;c}@ynA3OR?iYUB8W#Q)pEE%{al5V0h0?+1Ez+}2ESd1b z1eDq0++ktz%)K|#GkpCdS2I^nCR2Rl6X3_vP*c3V;Cg(SKkSg8nR;?4u)L6hCa6a@ zp(s!%$M_rYF$G1Rg6iV38c7_C@)(u8sQNi|`^%>%6cU;{`P7?!5PEnH2|qp0qmHiapA*5Wt@P z49gw%(!gdN7;ga3hfQ2}13c2jx(D1uPP$e7YG2HGT0gqshg!<;&6a7Mc6ATlL!*Yp+S6f>IO!=97umRoL?W-1FlX?ehGSsj_HS|cg#KqDa}oKQk3m3c5)aO!q& zQ+_N`^)cqGjLXt?1XZR~)_%)&Fjc|Q5)bjz+rjj>v4cI73oF7z)_|A6L-!ckI^+*S zR?R$EnPFhJfQIQmxL`%hO@6!AQoWQHA(;f`ufSWsn1`p5<9y z!~K_&Te9;H_EX5}`l-)khc^@LgNYE4Eqe@PZB>XW-6eijE5ykz%s=J7G-G9aD+2me zs3Qm3L&S_F&py7}fa`C-_t^QR_E^ZP9%2`MX;Qy@G3>~GmBrCK+2PpTl5V|6@Z?h0 zVBCa{3-1r@8t@l~obs8eR$4E97B(4=As1{`J*d?$SqCsjIh)WZ!c0Pn*@kO~RPa}l zYZa)Wy=3Ym#%OK$HlF}Wh(S4J^ty3ThhCCFqqC9O-bVB9{(NV!BSjx<+jlH2m(%N7 zzS0$xH*fmDK#&k{!hSKGXL6TJgBn@?+k#9}SNb&$-$8}g0NM)D%5>~xU&@#P#iY-F zU<++w!<3t<7SwkG6z%K9zx*XfBK-KYujG1a@kJ2LK2J$=Q)-EC>k1#aEgdV87^Y&I z>VP&tU{$}`bE1L6)F@hg~ZIPL2kDi$MFMcn^Df3mPbU+cy}tm4~>yk(Kci&$L~ouzhAhn|AcJL3H=v#d?LuO&91wd)uUe zeXBt|&C~&2_cZJ?ytFD?GmL5U6!mHFhlc$I-`C$ueo)xmBjxT_*deL8J~ z*m+8Jp~`*t4&a>vmOHWzq@BX%JBI2&x--(Zl$F8PCSafZ%z6G+J#6bCr;VU)b0+u# zO?&d~5O*i8m^>PG|C>I8jR?gv8S3KBJKPSO*;4DaxSM|L4vf@!m#!q2J?IXE+WZfD z9v;6;GqN2CgacX+Nc_^!d-ILp**P1p@Rz;KC&(U2&wFo+A=!T>97QIdF)#ZFPh}CM zZ24qb1Lp0JqYgf6W6k#LIhI`RpxJ%)sj#7#8o3kVDyDyG4~4RZHU}}ZGNXpG!T?%a zaHV$Eh^r+B=USP4%}jMcmZx^s*g50P##&Uv=USSx%~Vx5SF1+KW&wx+mx}}-1<=ve%!d&vCEya%4Ae*qWukK z9$f?07}>#bxwFf+)w%u0mopnO!tb>n6Vi8$*vy2F?H`j8i z(CvQLMsE_BdUOHnSnBhS;MR)f@r^dIiaZXeRf}m-8)I^KjRK*ciJyr;>qdmMw5)5A^Y~`5V`G zmdhZ7Z%_J#NtZ9n9_bTdd1l)d|Ly;}faDd&JDPe9^U32K?psuQ5BeAUM__mV^oa)` z%QuhsH~ryEmsysmVu4wsX8p{9Cf#PmX`^D@JoF^5TZ7WO$hnY6gVVd#*zBuG%DQev za&pz9F-o0UU)LJCRj2HEwW8T7#Wn0?8Qg-`I<6&z!gfI>O5mPyX6V#`lr-dy!VT|~ z+aQf+!s4*YLL(KNnOHznRIhy^y69amF5tdk^EuvXf@M4(Bx5!<-~^ll1PU>g-U5TB zHy((F$>Fax7{@TZWr-eRGy<>&Z;|CMuQBSN`;9twKW+l(IJz*r2+<&fVHYZ-&zj|S zH5@Fq9XvoUB3%?75w%}-mZuicQ8TBAnV}nt@HH;cQG+=1K~zq?XvpjuJWO$K_}oQa zwRD(fG*L%}gHtd^7V^m)$@h+-Di!Ily;O%#x{?qa6J-T5FVh`XeX|@%p5-EP8HxCJ zEYVaUdsm#(cz=|ueX3H%-fR7^lQP#KtO+ZMLQ~@bW{D5JimNUlqhf|m7D#pFbp z+S|(pq%CGv#&$CJ4;z;zWB9E6P-!;t8%pYCn^)>2jaJVn3I6N8H!YjpJ8-IH`)Azb zvetL{!i0+W*NH`VU)4K{I_D8)GXmeH$YyV~77F+Ek(3P>->Ee%6E1ui)c^ z$_uKiuo;Ne(Se1-HxdvLqvcVA@|zjgv#nTfW~v$+iRvbA6Z6TxsQKF?!oJ0vx9SWrt#Gx+t2*`qJdh4umiP8vP;AVW!D{; z_7QlsMrsv%gU@xa{N>&@9lL2C8pP#(NQ<S!fP_T2Q|11n(-*F?bh!w7HMZs1a!H1XZ|L)O)w8)|Qdl|8SG19@l~!ctC=+$r zHVQTOUV7NRfY$#JC4QjYx6W%5CF*7Y12o@{u_E%~){q?uOh z6&A&7CY;gC9ev7@jRuirDJtTgu=Pw1dp*cxWC@4P*h6!BZooKa=D%EE7OHf$*6})v+FuKY@zDooJusvw4MiC;R1D49 z(Pj56xR>dkaO>kOm(;ZU#nag<8--Q!15m<|#`}l~=_rqb^d!b-NEZ$rSSnF5?6XZp z)lCicOpD5}n9i>8;V3brmBW$6t6`ZIjX6>zN{M#WJC^a>M5FdQGPAVPK?^;CvzSQE z;#rat{=>z3th>KM&abhiqsW%g?wztw&EiYWmHu?;32~47c6Oo)p^+AqiZhM2Tp%~3 zkcq2@jg4F*IfS}Gzb}d^y)z8?YIvX5$F`?gNeU+`&^jsMya_;(j0s@O8DaXouZlXo z>x#_+j;#M#Sa7j3UQ^-!Zty^$qrbXMvYoTLNR2{)@(6eP7sJB=)GmDu*Rd$%^+!~-ZgMX1NZ?oTE3fcG zy#B~}#ztyGy4wL-l;w>8mm0NG{~Ci|koKMY|gZ)IM(P=1kqDw{-SE=_5yh!)DSh)wjO%p<>!Thia ze89=)euhaBc*d?=o}J|}fhMi_nI-{|t|ZsI+W@K9)+w-)M0G5x)OBJnAwps0EK35m za_x?M1?{50f!zIXTsbAPaK_gDe+>Z=#2c0 z6#;(BES7@F`L>aO_fn@9Ik*@OT}J25Nd4W7cocJy_VN3888krwerUM`cKf1Exxq36 zJf1)`wGfTrtn`I z;WzWO$qBYHiH7fM4@`@|qUq)Uf2ZM$HSgq>Xj23}p7V^jQwI(TjaujCyh_hngE=G> zRpRYP4ZA+2;mG9~sj13?2Xb5V_zMG7Nv`T{7;|F+WrtGi5Sek6_>-ptU_EezQ zyQ*-HLNUpZ3L)!gbfOwWTy9{jG=>?}2!OIs=V^D1%W~UA1F@kAYz`9|hbSRYa@tja z)xl^7$;D(D+eR(LxZ6;UHS=3aJ#U@t&oRgP5N>T+<+zepa_DuP$Wb>gvsZ@v?=;13Xqwx8m(wX587cR z0q)E?+Ja>Eh`jEEzyM#G(R1_$(yBfzBX3ggRQXw{zq;r8Uf9PeqdD~qG5{Py68)`^ zie#5fysDF0nzhnuUgjuo>%=(~sYwS8NJ1W2BHHUAV-9-`KqX|QfZAbD{9{GqynB}i zw+;_{t#Vn>4;}K4R*3EcW{(ijRwys);4CUVG35utm5{Qg8CwkQrpph=<{V;)sX3kj z$;!HrBg_2N(0tgQnh;c$1hK(fm{m%}jr=lCMTg+C88x}UGqu-xe#d71hO*zSA9YWN zKJJ+7Rb$UQrcVr0mipfQ?*~K&tQm46Guxt=!8ifTQv*C$iB4wgB5_<#$2Dj7s}61# z?V%g12UOY#8y5HawR&{ybRi;~N4CQ|kTQN57fcAIs4>;l?Jkf+!K*i1iP{Tnk2Uiw zK{hfaDvT5!l~u%zfWeYfo*rQT;NzzpogPEbl4cH@N`%s=+Gm0$Lll0U$jZ7Lz1mUd z6;HgHQE8!R_-*+#saY6u4%(vlX4IBQ3b)h)FD;Sk$@5$2jNTaP8sa_GeLSt`kFnYD z6oKT$u5+0=a5>>|w0bAKLX4AWoisLuv_f~z-*>v@EgQ+f<*}?|Y;=4I`=4Sgum&qC zOZV){k7;lP1Z)zqgv@>r+vHO%obOrNYMH&JFwm^Ot+*V$XYIXghmoF+Z}bDZx3s>o zN4P^W8Md`8d%Dp#qe9?x+*R<+pKbmmZ`vopnDejG$B@o%{HCsHbucE2F`pb|=X?(C z!i27aE3ofNOuI1TxsW~h-TKJhndorn7E=B8X_AT5+`C6|HZ~kF6yRuZsnr_kFwjMn zJFunDwL$%`e^s6vezTMq%*C?wXM&N<2n%j6r{1`gLNy8GFnGnjp{q~aksl92ViP7K zXSqLAY}eniEUi|UDA^jtrmws$zUhOMvI!V^u(}PUbsu;uCe$9nWuLYypz|I+`8iZ+ z1>cb9@zBQwm9_rx?l%`yU=<*9-2%3rxTQe{w$bw*GP`1P-Q`MeR-fzxtTRabpB^Y= zd%%F3sOdm`Uy7T|Y2TfH5N1HwO}q_^4(9Sc!VX3Ut!1BOpQ4MlD~v6u^}b{_wAIV; zzNT~@9|q=3>f(g)AoFwr(%b%CZv`V2$G_*$$z%OgW~BWrXmqD`_}8crtDPeC<14w{ zts*4fSo?es4+)H?1Uf1G=@CUb0@S2x0=(jfiwkYHZ1E4MSygaMzG4;Vg?y2T%5Ncw zcOJ1Hi2o|^q{JYl>w*IUs>l2P5w!VVJ<|RY3jZhWRfEz~T6N}8#XgS}!6yhJL9Dbw zmn7g_whYq%>TS&)XkVZEP5-{X^&|IJ0Cca25lRBg=*1^$~@+ZGu zdy(CG!CU;5b^Db>dW%)s;&kiPRD{U(kuh+IT2 z+&*xZ(S9RA*66Hq+`}Wf4!i+lkb~6uXFzZyLC{@-Ov&>-2TZC!*#Odx@X|NSVOwX z2etQT#h=oMzBog>=a;(>_Xrl(d=-Aa(EEtr>bu`0LB7MizwU1*9$B{c_>w-K5qtdE zp0D|+?sea8CA?663MYGuJ$*ZoeinZ`lWCEE* zC@Jgs1-29@Y3j>^sR`>8jK>WmE0(uXDH+JoB(w~~qw`9r7~;=3Z-|j?ym}KcV>-qC zx2Me;sBVfBY$W717F%Ppcyt_?6Cz@*a3Y`F3JYx-1mzfx+|QLb&!b>6;0D*?QYD0u zZ8*J26MitU>=H}2jT37HkeMb90=2$sPWcsi(+1}ohiW$3Zcld#XADlykY>n;Rb};URT(7 zG9BegqQHlK|6n$%!$|g-J)6VIqfFaPTV_;I)OCDdML}$!!Or}6ZxH8=aJJjO(f*2c z2k@@5a4hqHio_WqRqm2Ddzc4Fw(xn5n@v|9k1OW_b2@jEuTS>8t_PHQw=FU8T%!r`jA_0HiBo3)sq^AMYtKP{`TxQoQRzxSMoY?{a8P=kE3Z@u-to*L^Zm`7? z5oWX}g{x|oJ>gc>iYUbmu?UeTX~B&f=LyLedYD79-lFrn|74btL$}`J{G~RAD~gsX z^5GyWHvF8lQgALd0=WnpMebZRBHkYN1&N43A?XA*cJ0Zf4QTx!)+3*-;Y31lz;U5X zhpWo(r_S*&9ZGAh!7#O)4no1a`75o7KLrb0W9*f?k{7KEbSZU5;0M{9IpPSOrRL)5 zl-FmXlTlcta+Zh6_M8Xqu2=!F%ArWEFG3asy;ItweI*{+ctngARv1extY~gMT-T&5 zZDZ|?XQ7p+hgC{OxTkevrw18ZC8`r5kvHdzNUcD`j)I~Dz*(!^-7Z_tQpMb1}PDa)fC#S27BZtZ?>G)c!7-so|Y?R*b2wx9)FUMQSU&Tj@gti zX!};&x+7W~{CPO7D76K_v!d06z?LWR+Gy8D7>ciyt?DEn%^Ud%AYT3P?v^c6A=qK$ z!3vcPOUzS!L8Q>5y69NS)31)_(I;y}{MK~~+~(tK%=+Ur?%!(m$Jkbrfe&#sG4zEP z)0Baa4hhVf1p{^4PS&9UtbZMm2B^<(sKVk{evL{8oJun?<+y~x2%6cnX6@DuWHC!1 zTbWU_xBfE;n^&rut)V%36kI@E*zBqFsHR$-+8Zn(&Jed?o58g!#Co`c)yS}ubv_^CY>l=wvq<`p;GvJ01$G1_)%POz90lW4Z$V_H!N& zG1=^fxgdx_cLq_5hMq4hc&oQ=Yk{4CNbJsGcE-04FoQ&KU9M?APZeHH2lL*>Mt9n? zPFg503YZ+HwZEKL8a?AA;E8T_aYdxp_wly3=tfuO-~?9f)-Tz(k~4LXjWkj+XfnrF(WiY^M65Gg79#xR4}zU^BDg(STSYW!zMeM7<)IdTmLYfn!L9xEEQC z-`s8f6F`V$c{HNcX6)bIBQxhQUSGSCeKu#18n)-xlZ);(B^vz>uEi6Z zqPT53*Fn3vYu+_-7tI0~IaX^cOFgE~vb1<)tSGeUfLdSUyC-*|H&?2^9g`v-sbz^mJD=vjt8Wi~gSU!7%s8x3j z1?@#{S}MabZfm-hRxY&%EwvVmgC*KMNz48Wgzp}fNHtDT!<=}`|KiN$NTpoq2uh8K z&SFqK_rseAhzW>1yP}#2NXs}Ve;v>#yQXhZ6FU{BHkQwD^gAvHJsylW(Nden>>A}c zFW&xyvl8Biv%0yaV$R4#s%Up@#_Cy+w}xP)MeKgJP~^-Z_;?|_T5STh5u(5YlIzE> z#h&A{N}V2i8IS5>PK$R1gOs_e2D9f~&&A@bg)=c~v=wo&^rAh9UB$0qpKh%kIKa7+ zdIOOdkd*g8@)u7Erpx!L8|m=dBc^1Bg46kPOnhbc93xImm~RmWgDzW;aVrTxAU*gsN@H`s$oU7lTC-36(#A(5i0Mtv~#!n}%R*xhgxELT(i z@!c7$?w4{K8r6JAbQ^%?4ZGzK=WLYC9(K3jdAEedcHi3#$FyzWg=Rp#E3(kc^jwq5 zaEnT811Cv^H5$BsRKEC@g-zzL1o_T+TW|J~^UN0dk}nocaqSJ;=EmB+xUq>w4oB^a zkWIO<}t#2i!g1VB}3CYfO3eUm3bq_H0r7!_^ANO((Jgf=}Mm4 zMg+f~mAYWLxQ@in*g!v8NiSCEz>iv_cI_-tSS-;+C1sDIyg$^4qUX0W_^?b;Ap*`F zWKXEAC*pq3EkBk`KW`UCth@k@i{b`*a~q)Z4r~`B!W!?D>|P1|HTx5p@LKQ}dM!;; zTQqo0_JB3VViXZPy0egH9`Bumrn`hZd_7HkfDdP9N`s<3?t<_T;e zBbpcqP_1?-!vX$^!VfHqA`Y&xdLVA=|nSxS_73CVwq@80S%%gKjVF4>c8h*X$~Y4 z*1tGZQ(nJb_XF35vyW)yZ|)}4B|?%Yx#UP)(*PGd) z@lbj}s(6Gne2DLg&-M@y<6D{7UK`_jBh~3{9Vc)a*N^DH4|VJ29f^Fd!ySZjkE{Pd zvEIYGY66*G-Yg@`d8 z`GGkZd3z_gzE_$5>^-e`c6xh5kN=vLd$|!5(2*LOm&19lub|!pAQJ8U>BxJJP+_CVPhsFTHQ)`RDxEpJM9=8~j=7gYZg{U7#0tS^bfD zN8LamQp%$Wz;%^6>yR@0psM+~SS2jm_!#wOOzp7I!rhl5EE-9{Hew&;EHb)~9;l5M zOmfTL+?6W2Xh>ZxTOgey5XwFM+ZR8!)Rdllwqc4lH1zmuQ3H&5TacGuNQ_km%ML-gipQ2YKwb$HZMfRwM_qSKd-3 z^V~$=Gf&7u%fm0hW-PILQeF#~ubkEo)O70#_}g210I?4qwQwl^*j~)m$rb(VuT&+8 z`y!0*O5i~>g*?%QN%m=PY;M`6=G=iF@l}mR% z#~`*Y9oL^V;z@bS(`nEyW89F4g{QQ*_rAca2L}K+XldXp)>e8uBc!;X&N#H_m{~v7 zsr@*GGg0*{BYwKKrdbe$wZq1VHc|C(gA2!B2#>%@5a?U)0l~FL==8#uN*NXUAeBPe zg2`RUurcOSK;_9}4NDorxD4kGdc3K=Vn4)e=RSh_sKc|T8^_E&@fI6cq^m}4A;DQc zEA5Hgfl;+`~0Y^9fK^fttZxz;S z8&uoQ7TRPF=ix1qdnajBbK$mdZ*Cn8_McP!7= zXSjcE$34^e=T;vbH$6}<^PMHaH{ezLkj>_ErSEp@Ef$sZ^5^JY76CVK7w0Vy@h=_1 zcNkys-t3z#0-ullP3w>NVr}^w&W$M~2m&mbl>h!)CI1mHeou--1n!oA%!nv$AZ&l< zDkxn7SF*wwQLIv+6oOOF+rQ|NO7?Or|}q(BE69~jpp2um@S*TQ|c;S`4Nda z?{gAsD}OgPeX2)ic`O88X%9Hy#6LKRD%Qv5f-W0$i+N5k(9Dh!E}uIv#ADBB%Tf;b zF$(&aAqqogkVj5j_<=JZT_Du6jg>K3WF83|Krvm&!yGc=q4x`A))7F0kefNuer|>k zT?z#bIP@p)Aw@(nc26`?s$U|-VIBsxEWWkplu==q&XDA;%PE2RghfbJNavnVG$WmM zShNZ}aVrZrnTT`8%aDbE)i9eQM9`vkTUopZr@8Mlt`c)9*kczD$-VNY8lyu|#a0^D zBlLS#qKSkFn@X9unk=$zk3(cWLq)JccK;NkDpDcxW&vOJ*i-2TJoH^FFCRY;i=P{yG>cAd+5XKNL!tWJep4(3!pt_ z8Oy*qw(sS3iL9eDWJ!8(2&}p5qhkCWSc59vC86fn%)eSR=h3k;7<z_m%B&3)f#REACQ<`z3;vP~(U9n3aC zW%pv9Ncx3``+Fh8%e*%D_O-w79FI*$>NLnU8PDtFA#nU>pv}fr2~C&zIYGX4WfjBk zf@uk1I_pIozf}S02fH@&POCbrKo_^U@sco?hygxo&HQxDF@oAyv1n1}c;Hxx_q5-e z#&d^w9sxN{i#BXuK;QB!o~TwNBQz>|BJGB?9Mc5w*`}`*fuJ*Z5tKaw#|xd z+qRvGpV-b5+jc5B={~!A^nSnW(;x0Xa9?w*b&a_uT=)ZvI8~;)WsK>;$8oJ_e(rXB zj%WVCFirVu)+>OQ?A|qptV?ik7=7W8tGO%aiz4q50amDtYH~fQO$%l~#zF;D0L<`u zattgh1PkRPqMQq|Uev)Pzmm{kZMb|g-Ra0+T79@qL`t6(1~{}A9GZT5mF85gQkJBI1#d&Y@o;llMa^;o`_%Tq^viSI#qZ`=9)tS>pw^(9 zlW&ghBpwkstV15S8vbp>&RLuLMz`=Mh^VpLQ_-9b{Ik_g3V#+z%cdMj`DzLNJSKI8 z4YRVM{-;W#Wv%?&eJb@*Oy?6%7CcK*(8Y=sK|VT#U0xGcYbesmiWMu0S%=WLT(%e% zRNxRnaHdg}b<`SEU~KO-OEx84DEmb=F34>55obzNZ7PCYu2mqCwH9_#uHERQBXtfT zbbZGmnwG|R{7w$goeFI&7i~p&_1qv>A_5PrvLF`VB!^LZ6|-Lxs)18G{}UwIcOt=A z9aP~_u)l|}4^m-t^mA8>Kz~4Eo^ftBMNHEqKUN zmAfyhX!IIMjJSHp2?b5%Ca4IaC{(kSBdu(Up?Tf#TN|RSq<-GQ*^3m%=ARL3{Z5wG z&51DJTnc`c25Gf+vp6|lo*v{HpgQcRMnvPn6OfwfBepm*ofANUX9)ANlI@%a@MkQd zp0}MwVhlxaSOU|9wX+bs9^d45b>%4#$eX$5ZNMld@&QG9+o5)H9PVT>Y({0%)GOgl zJ%Lw^Q11o#kFCFj*GN*^<#NdA>W%Z%6W{&{Yps#zBA*+JnmQw+9d$iISvy1s<;NWt ztKXG_%9{vsLoicu(z57J)Ir0q%nbOsw@}I?+?y3?5MAuMHttdjdRds|*FpB1Ax6jr z8nLPJMrQf%jy6}N!TFN)&rGl9;gzfK+EjyNH89bB?G7*V`daS#OmC7Dl&Y4a{bC4& zEw*`iEtTA=aJiHte=kl2TrKQ$pn^V$ym0Ax-pU9^%OglN>Rifa=F1P)yrT?w!T$2# z5-Dz;ecad!pXga59armhME>%}J#Lm5Z%YT)pp-qV*-&lU_O^7kGU*T7%ERsBd-`3D zHoq$Eaqn4dc)9A@)F8P%4JqSQAYM}QzW+4xYq9F+T?aa5UAcp3_)AWr?y^@pm1I*#ee6$$$R1?(mhzV1{(AM5pZTx z-JN}BZ-VG;M-awHK;wDH2cm(+FS)aU;yp_T;bi&^+Ki8A7PWQK}*gr z-2)J0OdW|i)ob~`<=&!)1@oe?8c{LOIT=}Lr0#-qe3*xK4TZ3EQDK&zSx#Z2(;vv_ znNDfma11Z}abB|?INrL1-euoHd)ZESP~(Op$o~a>&J_Far9k-F&m$J=)Oy5QJ`R6MQ9EACVSDqvJW!|cdhX@*e`q+fC;YB}9i<7tqW%}P1lhpa^4S(Y4B zgLlr+Fv(x5i~&KC1LMw#tyxyV+`u8am-vXdTfP3UhK?TFc`xED2Mmh=R!u1_IX0!3 zMEt>*-~H&N+=1?q+=+TjXe?gQIM$U#C-@?-vTXABQ`}4zL%Srah^2(9Pb@UQ4XX^w zZ%Z@tHIm{~QQ_IfRECY%-UvT@7T4G`vl9O45}!9FHfLHf_k=T+BxyulE>dS5QZ*P$ z?4fl)LR#v((uo;^voE2sEvW!*>CI|#%qmfULxvs{GmNE~wPxv+$*y`S6;8@i$#je| zQ|t}j*?7y37{}PkQ;O)NnKbWR&Nk+o&4^ABHow=oJCpF@Q$j0IC~jqVB8H_?3T5e5A(c$3HLLyH2~&O37A+ORmAS7zI+U3k zeL@rUnSJAea&|Q3K-Gz#t{rZ_rsoaw<~dgM$3W}5Z-i!M6ggL1miakU2(hP+XLaOs zYKygeP#DkETLER%u`Ry`lGaMxJ!sighEx=n49ZhVwMx{otYS0@Th5xF3L!U+Gtb=N zM(4SENR~C78izV#_x(B)OxU7Ajm{?z>-BOBD_Lqlj%aVw&1JTL^VvXY-@R#gai?<} ztC#L|XD1Qcp5ZuA5Jeg2{B6c1F4|wfn6~sY9YwD&yai4{IX94fTYZdXj~w2GO4n_b zV@eYMon>9&TF$Brd&iFybkECpCalA9WsseiErQtJ_Rwu`o-P?EfN?7bO zC1u@G`eT+89Ij2mIS8ANmrvlrU&gfa4sJT+UCH87@;2spnqQ9rsRV1>&O~Sg<3b|B z-Cj~Qzu;uN>el?!NM0f22|Dkwy)}PXEs-VE-i6AW^G#-L&4KB9NIJ?Xpu~2oKjfoe ztIW^+a%VMRYp^L(ehX5H$OKpsRercu1M0Bw^ohX zXu^YhDFlqRSX*cUqf?Jt{uvm1IUdh1NntK5ZHcp_$s?TD)Uf=503#9@JCo6-RJ2H; z1$3$-L%*ywj`b|h78_i8HkcgGVjPcf!kB^oRvtM!NyD{}P$<)57J;9WT-;{fKeFAx z`dgwN4QA+|r@c#-QbxJ5hm8WF@@DG$>IvB~7&j=*-{c%`)8?7#lmqgNf2aNvdgU3v zQUz{$-FW!+Hw5z?in4{+k@wJAV?DwC$}jBXz%@onL&fXniMK?4dRa`~9^^)x)T0cWN(P)XYqd4qrGJ(f z|5+8{@7jrEx#3R22L0R!)w9P=v({qYcECrAnENMZV%(sdh?wftW69Js%yY!RB+}J9 zdtsh-ME!VgP(SD*hk$Nb4R6m`GbP7$Mt(Uqo|wE#8i7ALbP77pZ#CATNaF_h^fVDL zq0siZN~}Zk91q!p9OHS!6<|6Ubb}+4Wn2Sg8rJR4-D`n=Pl#ZeyYg1V8>!A#Y^;q_ zl$gRg9NQG)982vR#qg~+q3m`&mSf1^?-6y;r>jGl__x=eM7)fi;>vFj)+kI#a}iI7 zPM2@{I;o>dL2}O{!00An)HYt4k!w;mB@jM~2<%K=91c=r*Gjp4N@!U17uB1e$b6#j zF*#G|=%r0K&&ZHDgl$8Kz>7AhF0hsv9J!gBZSFhM*ojY~rBCQ-meoIN;2M=Nf>9=h zPeH~NY3oiTRUGHs5Czz?iv3Ww{+KvOJ^%Gw(q}=7ZVP-#C0XrOxYbeBNclf16c^D-xGC zs5T9ODN_-AY2 zST3}kB!SW*y_j=+IWS*`r~-b-t@{$sdZlY4rhhB*P9UXTd+SLI?NA6n-Dg3tTR|fC z(JXp4C3VzLOSG-Y*{ICNlWa8Vk`)=UvoJ+X9WCU;!x)N*XPIHnzmf!&x=JN`i*8gb z>xXIzDT-%_%jB}(?PV$qZ7tfWTh>Sy^2UPDQk98`xY-b}81QMoji+)vK+LeJZy!@2 z7HdG=9Z%BaX)Cdp6nA@=d5TF51E~&&qt^dP5@93Yzb=X$PuR8gB$N&)mQY|V z=la`2rcvSU2%4E$@DRc`_ugSX!COpakv>1AW{&8O91jb?SlrSHYV%WkLF8*YIa++@ z0cq`pceAf*?MeAP80n+Tj56}5ha6@x+Hc(^g_64(S9kfrE~cmt2;Pg`CJhe{A~-c$ z`l7INd1a%@(4P*T7PA`~0b5Jcg}}rCuGCk0vv?B={>~m&81qeWG!rNkv7R z7BBPE=N(_l==2$lwkvq5tbTmf`%eR|0g+~@_ZH^BZe{`3YZ#MGHpwhKF)={UHGk%`k`n%H(9n_?M!ND}xM z`264pfjoN>;!KZXy}}Lj{JgjrfY3ci?1DJz&I{(~CQnpqX!6{mVx~|hEM^0 zhr>Jb(x`eRnRSNj)@HH+mXjD%n0+9+xu7DKDY;OauN^o_hVT=%bPs z71z6f*bOotd5ox0YN&h`tJ0WU!Fl6_rlIr0FU>E4i2+#$j&Hc1`ta(E6J|W-&sSl> z-m~@8niKfbR#vBg&mA2Po$hRdH)3mym4XJb51mhfn03|^(c(vSB5P1R)h))YgR6~( zyz~1q*sU8`ja3ea9x!bKiob)_;CjWtHw?d1h#)l?Mi~E^F}AQ6q|(cAX1zjwbV;U3 zPZ<2T2iw6p8ubco$MEWxbHD<^g*d?CA+)ii3NTLMPk!+f1V(V7UYXKwL#_<|gS$1z z2A~=w`oQ&N8N);mN@+~mhA?7M?4#8bwGDF&+d5Uu@RGNKml;CeyLHwaE{|gG3@F(- zIz+{F=!G6Wg?9X5lXi%Sbo@Jc)@HY6Y${#^qcPQ#miF@FYN0GDkqvf&a4{@d(&Iq%^_Q==CNZNvx%`CNlVPoJk z!=oAyGi;$K_NsMm?u3@*6JG3Y^>hcC??`&2?&_J09v5Kg1yC$J4~n&?pE?XSc<|CoAtfw1{b$VkB`GD8oMMM20L&bA~nI}+2st*VT}3n9N$!YUX z{Dh>Y#7DL@nEG-*E8j+F|Fg%@R zYCG`R#ZtpPgk$Oq>x;g{rq#{S!$F#>sZ8wF2TtC_qq=KO zh>gbl;OaMBqyK0K$hbT0VJFu>VyD2X*HX(l?r`k`V2%XN|4x1BR9Jq_pwn3ddS={b z83aVz0Ke>ntZ%IMc3|ml#Hz`3Xs>$geu|pBPfqaa#m&U0k!4V6*v}wod>W9a>`p6y3F}r}iHnx9B?|8?4 zT4~gq;ITxz-ovRjm3jyu^2+1Z;D9)7MPnaFb;-iD`-80!MwIG`v~0xp+_}n>8&YPJ z4oDpk_okH@MMx@kqZM!dcs4}CZ7$geqv@EM54LlgXlNe~)rzRMKp1K^wN-P7m!RB-vJuCXF-9Xkvk4roF*bAVWwfl3_u9iP%McJ z%OvXjc6uVfcin-%j$hV~ACF=7E$Pj;X*3~svJPs&JFU~_Qid&EyZ0{o*jKj0Du+L3 zYVW#;Ag+O)A6%A`a~iu~_PZY;a89f&ZL~m7Z-Icbt7hiYImjP%#7gXTi&2+NAaMRQ zg48jXBRk&!#bh7g)L-}SL<`O8l#u+Z!J*qI!>gGow^2fM&hckA9?Q%{OMV?9pi6d8 zx0gn|cmH5cOS^W*O>;CwFRps8GW+4u;gP+)6nmLPb>)rOr3#8lBxw6llnv811U@#Ytsf$Wm!b!n{m6TYyq!sZuL_HCCaEC!*tnx?uA$ktXZ2u0b~Z|O zE^3ne#n-y$1b-!JQlostrbCK>W2Ax~X zR7sGi>S-dS&7(2T?k)9_NZt;eiLsh;S>z3si67Ym354q$3al?=)+8)7!h`HcxmcAI;PY_QnIzR^_HHn`zRe_+#tba#V6Z# zC%m?H4^*imzvndLiAHP6&8uuXC3{zlfnO4Sd&^(pixA5DsW7i~alNaC+H9 z)33#s0TgE&IJ~48w$q;hD>u}J+GszU{S9cJlJu&FTG%KgKHnv1K1i-CG9X1w{WqjxUakp3;<9> z18}>Mju-XpnPl1ldq<`PXX4LD83%z}QB4o#M?rx{05wkR5-Wb1A~=27K9eM`DcwM<{h4 z%Muu`wp_pSqA;(z>fY-aTDPFsEwL-RUUlt&%p!TW%4TRUmIWMmR$mV>k4WY zKlc7c9#FD+(hl==(vE!X1PpcOxf=AwoARjJr|RCm<>lXb0pZ}`2hhb{-w*-p+m}v9 z$j#M(SChl|)5O42UKm0*f}p8ssh4tR2>zq&u&!*?m-Z%zJcrzX-`sUWo72_~b@zxb z)%T!2)34P0*FKQ?E`AXVT>c|zx^_NOY*6~{_)+#9X8RkpNjJJHnlHKMIv;&4$O0~# zk%S#rLvgx@dla>oFBR69en(!|e;s-v^gHU6euR%-KZu516b&qxkllgbZC#Ybfs zvVJ1`4r2w{=RM75*9}Pea-UrKe0PRNe0g<12qEeKDEhW)Svi4LL1nrN4;8aDFpCt` zRAlibrRC-~va!%j_&q0i9vCZ~9d;qyytp#~x%F$V#JN$&j?J%$=UBXX-pUT)_psf6 zj3p3LIiGhzq8ZT5c?buiu<{~WrZ;YkLeB%MvN?9hJg8hsXaNi!*m)}R7VY~Y-Pzs? z6$7_WdI9Rf5SOm9!&Ns{qbl}QB)2#^h+MHUr+D2MbwO8?&tbP-1am=LJwNRG;CE8e zS=?H8*Tt{IbhRrN_>_{+;zd^MhUA>ij@PdbtI?v|FIB65#Kk<5-<)}@OdV&r@0w!u zb-kE%4@bJ{ayV6IP2H9@fp8V30N4^eyCeqoW?mEQhFc%7(gY-_)<{2fX+b`$llf+r z)LlhwQxcVWu@XTBh>_Ww_e5Q{r=FVL;HpN+hGn+H-*>qhwow$E3LN|f` z0%w*QH%wixZ9_3Tz8_B%kziYK3;K@fKO>qHwsZi8F7y=A;R(y5C1W>s?;^&}9@#Qo8O#Q|E3$8XGcb#e8ww=gXO*|=Z5WFIP_x! z;e`hPe`U5xp<|5r79)zVZ5MW3ZL->-TYuY@r|r<99XX|t5H+lH^%eG{^Hr9nw<>H> zPG;8AM6zUIWY>)BN1x>RESQIZM|A-v5{wxFT}^2-4DI*TjLe=&2KQCQ_Te2$wHRU6 zRiu+VI^KwYLddYIg!)kr`i77+{+HR{s}ui*B<{%K;NCDpK#C6Zdgg;aI$@;ri{p2l z`chX);0KDnO2BZgep|o2j?8hslQOlGV+!lOS#Rx}g+X-upIs7o-2cj&X^rSsKFyV(GE1=ru zpnQ@M0BM$Reu98q04s(-Nzo0>-7Y^jj(`4bvM{OzHM%mDz_Ai`j^Lq+9LuRnrdZh= zUS}+|#m{G(!q*2Vy8NEz@Z2KQQ$Fqp{$gd1%e1*T6$t@FNzf%4j+{y_CDExuRVxDB zQjB@4mT-Axc$ZG@oaGAV6F66*oC^A~rbSnm*3PVH<*ZZTM`GRLt^BAZZR&9#5}tAf8n`Y-P6z&ZS2h-x<>uwQ=pM6@*}{4PV?vH1A8)lXl76FbDyfXsZ# zdJ^S7)b%UMfBQ_-0;ek-?GmZ0k@m>)S)4YaVI7((Rpj?&Fy0Nzr#24}h1acacNTtO z)Zu5jd^*l*pWQuyTacYwQ&sc1l;Jb>Vc$8sTR5g6{8pIEDv@5KP`5rF@joyb)~Vv} zu53ti#6YEE!I;vS0*rpwH}SxssUaZl%7d*q&{ng_?ntu+TYd-I$TAZY_!pe8F==Ur ziQJLN*p@|0tzP*mfA93nZ*VVzU4rPEa42SH4t?+P-flPOHd)>OGI2R5tJW0L5C0+m z+z@0FB%~Qsd@hDDlid;;zyBWok4sKuBG}@p#}B9KoIg&3P{8r&q+jr%MoFYa`xTO~ zR;qYb-gs6LyyzWC{)om+EyC-A_YKZNsIJuWr|oSDjTdh8^A%Tg%^1=A5;ju;h}K!U zvZGXHWJhSaF*oL$86nafKt>x1ZlrF64nLve!poC8u0@NYeAim0S@IRi2&uT0dsaAp zgV;ueD^geYKq0H(0KFM`7uej~*tt+VdJOt`Dh2y-H(5(zM496^JdwKk1b9Q?y?aZ` zdmJ?aROK-vk$pcha^@gq4`4v4 zu3dsR)b2hMpQPL){Acr*(A;nRNz)C37lL0Q<_@M8vhD*j!ybBTaGjyO7ykR5VG&_Z zNYJS*ZK=Bhc~9BnmDARkyKsbjl$Qaj%sz4sD_`DoY6`0^WZ7-+?WzZ=olBq3iam%?QWqjRU~62L4XV4vP6+jr2# zH(|eiR~@jcKD_Hbw3}z(A=8$3pxc}EQ~vzZ8f8#A{y5=%E0Fx6F@LFs$rq05L26(r zg`#(Z_X(bId|Pex>0Cdm`-o#0adur&r4Oub@Y+?7sP@{w|IF z>!2H&58O10!7iUg`2fl^fKx~6M1_vt0EG$XI|0G`IC~wLu3n6{B z`j;h(Xd`{B6snyfI*E!c;@Crx4+WBw^NmH3|G4^4uMJuUl<)3QW+uz%Ym&# zN5+0gKPc%Nb?pE+4mf6QP7F{t#scp8DPDB?d5bU}#Qw6S^DQG{eN~P#`SoZVYIHht z!|&o5Ld1kgK$aMwVP#(j+cLT0Yq%4c?xHLX_q5kVF^}2Ytl**p^`qd( zhd?Z_yXjHE6T#lGjVk-u@I@^=RY9I~5h3E(_AugbGoQd~`1Fcb4q^Hk|`v%wJ-L&oNn8>2?4cbyr}?TM)|*_@Ls6lhC`EtqsLP*v+j{|0z;LN!d&82w4?wfG~*&4B2 z3cSKa4@vgIB(I2m<5c;HQ-|`A?!QO;;~!wt>9f`u$f6$@U>Nw}$$%o1jYNLMtoaA* z(HGpf*mTr90T0~oKMHKZ;EWOqpg=&hkU&7_{?CE>zs1No>YhGm>iA#sZb@r7a1z3t zsp9EQ$)uz<9ZUr+XqkyfDAIp$>T3qCCd>vdCf(fFiAG+PiVA;XMojeAusFdbWLgIDUUUN5FzC?+f763i+b}3?$;0clu&} zq4r-Qd*BpkqWghy!7IX*w~GYD3ZaYO92M%@#j5Y2w<{yKSF~$SKdbDea1w=i(>M~! zTWS1J7rU{7uj{ua8#`z1Ca{k;w^z9(hwjiH&LZI~>F?VhecNE89ppI}JB8Y!%0RgX zg?BxP+Y#c*o4cX!*52!t81%m7@9?Dz3?n;0j=1sb@KRZS41nSzexg^yfR+~cc)!jsC;V75SVe1SRl@CCBTEb`$H+q1N<{QQIiaE67<e`gE#z&{1Nn?Ngi7$FxxRqO7t!fy2u!; z2u4k|vuvtWY`odsqe4fE^D}r5opvHB{{%0C09||>m3$!;MGmk^$tOzF_TjHdRRKU# zkt^-9W7zpgGo-Z(-=F>>(h8(wEg^88rBJqZ|geX}@?7QgFw zmGG|zn3n8TTW7r8%jPK?H4N$WTMdeu&xY8%oie9K7JPxUA4OszD^wc1e2#T9nr=#W zS#A2oo1nsQnqE_(vHCk{)VT|{G#^?3`x|ZazYDkCA3^}aTZ$uZ6+qr?bnM^J-MV>A z|D+C`C96asrP5#uO_;_`Me_IUZjlL7bmqiEV5I-q3+89dzE*c|2y2D^%A8-R z_1sdlp0>J_TgoptL?*G-D`C6X6IhD%KK`{or{IRA+S04Zf7TKL>1~sC2RdaQz?dw? z=+!V?jt?1D`^d!Q>JCYHo}<933c8T6)|4d$n&hT)U<%T!U3I|R(R{I_p)F^@Oe3^S zqFP|w|JYbOHf_T%u@p3S%uKpP3JD-Nr!(gmC@XVRrE@92Fp11zW@{RqKWaCk2D0zuNf+DYv_*c%BM{+5#6)h#EAR4pJCbd8uH0!dhO z0U!Y99Z}9|q|Jt4<6QLP^-0EJAT~qvv0enA9Qi5EqU_f{Zsmqj{i|k*s<~OC>$4|d zoy82*xWXimxiBq=G8o9bcdQz<;$Bz(kgbkh|B=%Onff@{aTRPe(LWLy#zS#kRO1cg z+7EwHu_h_^al-TZJ-CiMd5Q-B<|0p8BXBGs82dSUR{OxI!c712p;rS2CmEO14=QWS z>JSOB!DKF_bBBh&5Qx_3&?yD(OVZ*+MH@O|+ws4DI5XkY7jv^mmS`_&v|A!ywi}_f z7<(Yk81EjPaHc-8!W+MGM(3W<7|$VBV~t1`MjNO$VJvvGa&vZ79@xNHlXj%86fvu5 z&KBv{!g!syY;c}gX!^PiGA_p!GL<40S)!eXB2(nQSs|b_=iW22&kj1Z-NHrRud0x4 z^8P%%xGyt76Ba^yDhl0qg}XqQG|o4Ec!y~&BJjqFLl`k;Aj&xr!E}OqgjUKP0L>1K z03-}GoeYaTP|-NbYQk!-1*`STTj|GZF`1H_c2!)%lmsLq5=VQD^8{yAxMrac1NbIOP<_H5cNk5xM;K#~ z7^L9|`zFH6b}^Xkf0^b2YEXIYNaA=uYfxX>kiQOx5S0;=W{V^boD;5l8E*qN;W4QR z#G7{m-zcfY8_1e{@nF4hgmJ~|UG0r2b`cBCiVMMJX-J!scs|>)hg<1(M@mDgLot7+ zu0wc%VI6P|apFm!3;0<(Qv40OHWCBIu(gMAQq;pgGiyCJe8@xiZ6&XSB_L&2)u-IH zM|iC?+LZjB7%d>Wu?%-f=quYrIVYBsL%WO29|E-n&6JqC!)pDlX5EXRN<-Y#fM*k5u_Y-@0J0e_?{ zafMFJNa%@wLEu;i_U#E%3t9B7%d;n$7_c;5O&bNOS<{r*e|L%+h?T>L70VDIf^MVx z6c|-Km}<+k95?foeZ9O*JiT85ZS-Vz>P%51N$f9JUWun2&>vCjdI%09pd(hURv4v4=2du528Run4`86jF#@M12=x4FjqI`FjPfD@1El_cBlXKi zn<{_)RP#d_VQ^F!CPL?<_VQdQD-?(ZIHsqObmJW!LhI$i?N%^a-MT={lzXI#@nH_s z4R{a$Je;#1yZct7h`NjW(kyfe1%Gp9JZZ2mv77tS?l!k&Wq`wsm`&n3nSEQav`ujA z!UAEzQxez{@y%?K_H7uanlmCyrzXXJ(C}Fq(N1NqRUfY2s)3I)gMXT&4O?U@wDH(d zLrKlSjH8~%#E4I}W^4GiSuY>2uLnE%&tEm7i++}klY`=fCvy7W*I_Gd-zVXjYSkES(^UK3ZAM=aOT-_%PKPHHn*H^QECW2 zOJo*9CW~3LA~C+zBY4|Hppm)7n4`6{RAltz#9j&_o%;Mc#b*t(ir=#7g4;9uJ5mJ` zwl|YbAz4BkwLU&RMc1}?Rm`7MuP4=``hty%BI&%C*m1ro3m9$M#^Uc&4s)HZlFVtj z=a>W9xtoeMiUy-U*6B5$XgSuBu+I8uI?xPjOFA01EG(6(K`lO3IipEchx}nVx9-K{ zio>OcRGR+=_lm6u{i0Kp&O|LFVT4+p9z_40r1Tk2)Bj}S0weD-#s?FcP!Fkn4 zu8$tGh03TYEkS5cC6AQ}HAS68$O|8=5LXhOsS``t{aVeC>A=`Df#q1U569X}~92olcQ&9)SAYvMvBel+Hl$?Qg!RvAl9!UsZ3%8nFBXFG)^D}gvaL1cwW zrG%>A;xN=%J0zgKqA67`uWRZOWiQ@9_{klq-YV2CIs8oTi7aD7X6zgjVXGEZhWa~q zLoTl%rat$8#(U7ZSPFu(H9$3+fAUa$oLO$OPJ_GbwIQ|}9bqm1V~bFm3E zu>;O@_;9WpC|xoH6{WJ!MQc&KP%0P0&%)ekD$a3fP$WmU{1&ug3|WVQup*RkPRrH> z0SRaepihf04gJHa2t^Hq{82m5 z@}oA@hgHL3Mb-m)dEPIFlVMkrfrKt9SSwP3G&{9odPiOu3tv<_4s#f{N$%S@2UjF`$y zhzkzb#OZNSj$Ap*g(;M2#6!}CFe})`$7lhLSkhq2;-ELM(IUu58a&`6l#Fg1!3Yiu zG1dkD@_E+SG^Tsf)Iuuv0ln`xD6QZ*|I~|?CzW)q-M*4^+>i*jmjK9?@ zDx4SW2mKQTf(%ZoTv~(LFe5lK*q9WLreJKgZRYPxTis3`eRFzyR$wo>ZaZRDsHjy| z^>6gkK=-g>_coO)bOInLpE2F$UB@Zy?`NdZnV7p%0?@EKxNLXK#~rmsUzRmLNV}07 z5#IKa9nc;ZUc}_{@TBXlq*tynrD9BmiI46+fe5Hx>icHhqf^j~vM`few+Y_M6i;r- z`-k?)1;vcAGLu{{)7;!NPcOBjQzq>G?#TstUmHEbv=zYdBebXP%sJ})TXDBu?h3{6 zBbDdB?ryz=6`K3Evb(p%?$6Wi&y4QRwGU(!w&z_RUoD1G^{2f+Jsr+fXd0%7<(l@q z#_k}euBiI3u_8W?ct6~$0l&L55P|}@Yav2U7p(UiupE9lH9iRe$hf(wsDpl*6M(hL zJA@|(*6DLRdV<;uuAsP=z;JkBLHT^GXbgW4Z4rd(`z_bNLwFnfb5UlSqP=#frrwIj zdsJ+Z3yRrQ>6~bm-q68g^*~Fdej~0jBruO|#@n$16KuV1?wHnGZeIWx0RMx@Jzwk> z;e{Xzktv`)QHbFv)XgX&Y+9SC2{Wyx!1R;Xzr&x$YWyGSh;no3!W_`)OI{`QJ+87P zAOk%>Pp9_?$|{TIhp$Gx561_M)x%8ES(Juv%xH)(lMf-GVZo#n{zwG10@Xm9vPs(x zm}0k7f-mgM4qEKO%GAb#eQqIo>xHv zu|FP5g|SDspRCG?vil1srjuI*%=6Ne$s7hSv`(?iK^+)bhnW@bN{R5R5Gx*% zDTf6RZZCXx%&g#4`wRTP)0W&W1BF80-gMJ%>QeUqY(x5gy~U*eX*L$~FgA5?v9$j$ z=X#Bz-0vSuNWMQ$U5JkQh-mvKW)x%M42+a?<4Y&$ zSRBe2C-e6_AO3 z{7zo)?5iCs2wm{89~-PA1K+}cTcDfI`>(IScAO*#8`3&>1s_Xi)dZ2xZhKHNpU1*UT}yMW0ku|TV1{U0&*hVnmTYGw7j zSM)}Hz~9m8oaX--BhYBvH?{jcqVVr=G5`M@)&JgVQxy882L+LQzb_qJ>&xLFcSPi& zVduEvz+aRYUivEqQt)f-Ssn_->du( z^zKE1KF!l%P*Z46+ttyOgJLN=MYP?0_9pGZRue^ZR9>#QQ20L2xtcWQI%)M*jjtj7 zD%Fe~$?St1Luzn&3wlw&@5;M1J5y1PttMD)uEBd&#^d&2(B&`zW zoVvw-3z;z&JYR8xul<7$(7_02-(FIF;R;A4@Ic*HY~o5TyvuE5uRz5qDF`gTpZ^`( z3O+$G+TTA*>>JzvQ&qR}|DC$7#Zr~ zeuK0!UK>CulT9Ww(kQf`(O|bwwvL}HjMV<&M525b-w-`3CM)7cx7%jFu2=BIpuD22 zN$l)9kai%dE5Ve)_nIPA;|A*4LLc}#%l}9bwB){UpP}SWFh^$D53sx^8awv*B)ylC z&}6?SF+NSNeKSew5z6v{E<<*})l$BYnAbAWuCwb(c@?h#XDe{r`h z+YpZTh5w5&j29+aEZ2$BkzzhvM>@1|^_{{id&pV5ignfx|Wc5yN_wEfS? zDNWcd2%_=rle$^yloZE-?9K^ONl7=XqLhObND2ke|47h~PwL07H>cgGp!WNN{`DhE z&E<7ps+0KXrE11=9ehQnGvIwr>Q#46)wWfXI5 zhQ&gEY7j1*Oob#VA|$a?iP}sTBKE3r=y0r><%lZ#&aOuCme_Vtoqdx%+>>sFN$W)Y z`bp281Q4+U=EE;l61_R=XQ+`x9GFN6MT!S^EQ07=G+}VCino{KcaL3>4+KV&NCtED zMaA5wj1e;LIuAi4CkYdro;p>6i*RmfVCB`L3(5=c(V)og-?_IIqy|Z!%$;x@0e5`Y zYWCxX7Hk+r94!1Y7nWoez8EQ)BCfiD%X3F&w7B8~-Pg^F)QU%ieiV_*YiGm}X1s`@ z4?D){sRdW_O%YT&#Eo+A_CK9LS#XzM>B)v%D?}~Zn_oZaSSR0Cd1FIxm@X#0`;=#G zemH{$jOK>39)jxjE&n|ogrEcdpd&3B)5#{fRx~qA;5Va>xzH4LqiP%7sJTR~NA#3S zsm?0GAsf;twXoF+L0|mC%N=$gW-LkAc!c(g9F+=l%C1fHMhk&;=0$#D(a)asB@oNB?zNeg|-(tt8YW zif1mDBO4(Fam5H4=stn4LpXB@Ly>6yie|(@4$8_%WwkK#0&bFky-4B_u1aMOR@)*N z8ylk%8W_+1so@N3I=?tF;0SBV8XCJgS_e8JP`DvTE?o=o)y(yw1p6($|MUO%_V-ME zX(esC_YE5)JxB!fWI$RV7FrNlEwz6W>%febL)8xt6Xfp#{@wly2pFCNGzaPrPGpMP z?lGY3u9?*zex32DW)QV~mm@Pq^+9LzRG{bdE8yPT(K#cqMfO~J+LM3Dd7)2^gd@$_ z8JxoqgC#2^a}cN_pojg3d#g_s)S}O`duwTO>evj_ze~J71PVX1uc2GY7u4X(~|rQiU+fUA+z0^`Q40a&m%vFi!M zMeB}Oi-B=t+AQ9A&)BNoF)P(Z>kg?X-BBw=1~2kk^o-ktc>=4)GzJBR`T~bKcnS;> zD+&xEPitT@W$D0eOD)0QGwjqI{+uF!yEAW=?AXfMtKT4$x>LV^mJ+Jm(3cV_-Pn~r zQ+hBgyrTi99{j)w4c-g|{76CExkJAw^Hzf31daf4@3*LaAH{@(!sS*{;yj%^w#q9g zbU%^#OaalPf$N#FXJ*biO|+YL`vk#`Q;vi4D`#z`t`kquw9+R~{ZF=9e=%aM$8sG2 z%eG!adcpY?mR)UZYqql{$KLC)!-I0A6_*xqn!Lr;EXzrY7tK=4N9Z_B?VF6=16tBi z4`;hMp>t*DRGV-TY}&AG;+S+~*oRD2Bm}mxfQRCkKe|xHmOGC|24!Z$Jg!ap9LPs# z7^V?3yR`|*lC1(pq+MC4iisO%zhr*-4>j9_MU|pbg)JogW9pB&n!9u@#h6pKRLi@c zlEp>fSjV}YVt${ZO-r#_%RFe6@FJm4bKzv`d*NhdX2tcjk!y$P02*sS2sGRExSvnfDD;rN~WsS(E1jr6=gh@ z-h)=q-fG7eShp5Fzc_cP znOOY4ID4ls%c87JH!^J7HZzc6+qP}nwr$(CZQHhy;fNFUbk*suy7{~MZm-)h=N@Yf zeBb+lZl5Q;Reu^@7cabyO4U;D?FlEwBOxQ)fW_B($Jwt?tcSJNU?aQ=j*$*1)U5hvqjPj{hZDla?3D=fBKsL3^`2{4YG!Qa#JZoc4L|4 zzP=H{=1E8}@$L$_a5Q&Uk-vlC^({V(O=7SqaVVn-f5iGOcz&;&CcrFI7=<$q-wWN& z7o|xKDv+GNJfcsUAp zZk~RXjt(?hS&g__`TcmRaYVMk)C;EakH}d!wbDgV$YECeK8II6X&oO?%GB+Ke|_;{ z>Prm_4~JXh@)>-NpCmBH#(}-TdQ|R&vUe>SVa{JTJEtQ&WrBL_ZPW}h4S*mJKcquc z_H3px?d(yL-InJFaaUAN*@VT3l=xu35(1jOaKU=0dMtooy<-@b=eVY8MwDKDmhKLa zP#V374X{U2K3(~T5?D+1gB^oH_7F^bi~0*-ZR!uMP&juWQT{??Q)!|6h3X!Wek`-) zz12J_zfIkxne&<5E9+OWxC$*8s?lN-5)nRcj{EX*e zxgcfo4&}o)R_%x0W`Cg1k7eYurr+OGX<^*}f!$xiRAHD)V0P*C~&_g)w68vhyP(Ui|X{@!S+=3h}tf zk7i;yaf$lT6*~>GLl|ioQ2~IN@PHs)|04wcg<2EZP41e78&Qpm~M3V0nq`pNnPC{1|n=^e4dhrs@i!p@wt{ zoGD+*tO|>cdreh&%Pr`Ci3q?%`_Y5?eFK-VPxP7WIaDH*aPUzuA_={iyr5y;1ktm5h@}~5k1jZLi72K~l}{oFaeULgG_(5MgeLrYsV=nXdme1_+F`ay(WF$& z@!OSJjI^?G1p=Hj%%yBef=`t7NM3h<$48-z^SA8a}E) z8rJ}m$P0w50-$V^#;8*8&U5nQrX?#Y!Sp_@ZqaGCg2KsDPxiYL6oF6&NrCPke5>b6 zRm98W1Xn0EImefD5M}mBot}cWHg8Q!Q?Q|>5lj8NPf5vo;mq!pWPFHHAS6A8({901Xr2jdCO8GU^ibj5V# zwAoL}W)xTvf&YT@rnG!$@TZKV%=hujhIf8!z%qU-C>w z5`Xa&K*lT%Nt7+)=8?sNoSh2>Y+M}V0vjE`kU@5x&kjJ8Dc^BLGzY}Y;jv>Bm~^kK zjkQMIl_wvv$ivPUMV!D7oH7ji8IEg=B2w*&cfwZpDVq+nm<~Cg4l31Rs6-{kI1|EHt5V9;<>^IJb zYB0fjnTYny(d(|H!BtbLu5{5-lafc>=}rGl5ms;{c`bSv5%EI~aT)4535S}fN~#?> zl)1i>q_rbka2U^uygsCO9X_LCeXRje$xiO6m$SX!ft0SE&ap(v^laWeJbO)OLCf^4 z-aW*4O>jX;jr{mZN_an`y|pUE@Mj-Ce!3Evt*zTbW~wf?p`PwNio$61_Z~6$Wl z4ZA*7WJ<;sCwNW3^mq78-{NOKnuO6>PQV%i?^lt+BDn@8Zp+PfnYd8vI|5u2M}%gq zD<@!ECny1|PC3^UNO}`6gZSS4+8tYCIiu~`MzA`VBcMo2jTE1Lbb18XB-%&_v;v_= zWhC;0F8e7udzci*^%z#+Ez$hN8~l~W;wl<&aOw@B;2i@5EkMo_O|y9ah&O8I!XR!D z&kFOm$-WXNIrzrNmU<@IpJ|u&dnwvog}8I_j-4~8`hw|EaKlpFLYR&S)WYWYXWY^U zjhFC^nB>C6_(tXUXT4Bw2F>(Yd*b5uqv_J&1_`)ANAE`1LhJY^B>HF%Al-s+LYS0N zevoo~?({3%0&&9djHv8dd7^9&5wJ$&49k1MD1AUH@158YcGGJ5`?DprLCBRS_ZyGu z`m*9>uwfP3#oNLnYqBMs3NEyOEl_}|R*PL=36 zfHr6PYWSyvS-}W%l4CA%m2#{e@OAa2f>}ZRb(7^xz0Eb8z0Fm;RfhW=kPNog*9i6m z*5WbCYa0S~1$}*=yW#)ygd<=}cJ&BEwi`hA@U0;|pw)QZv0PTOYiwpK+#zQj)SD?T&W!gP0yTR1VpT z2%Lt0CT+5rn}I8x1lk_>vp4fX6rL-j+P4y${pT48Cca}^G11o)bA(=0K+)F;R8F_M4 zV1itH*Bx4e5AY7XKjK$17kNJhswCF>eFAh&un)m$ZZ_MQ(>pu>!KOi`fv$v&_7=z- ze^SgxK=!IqERXmgyBPF6sa-pDs#c>B4HO6*%9g3SBi9&CEQ1U@Zfi?-wS3O!CgS1g zCbucGEq{|b+6;s#2Gx!5mEq-!xqjxH(%rn8+j&1(Q0IM?EM%HriKRJ4tCe2o8X z*#5casrCciTf+J7GE7*b1|on644e=Gw(koUkr>D$fI+mEO9tel%CaOK3!IW>=a@K> z)3CTY+gg9mZBnd2QH;k|?u2|QUtV5+uT8PKD(lj?xanH6WXX_{{J8h}@mlDqdG^T| z(QbS82A~oW0>HAB9Y6zoA+)C8h3ziflLc^NB}da)wzUB8wz12|^b(r3hTDbpT#DfU zVvQuiyoTJh-Xsbsut%SzFVzzrr~?qUn~m`}dTzzOhSFu}CJlLK&IH%}g$o|L8({A$ z3JWK(9b@|94nSrn4!Z-c%YaLRKQeN7ZwGN37) z_$_T$fDq;!TY0XH7n$3#4O?4Q?7%dkf>*6#FR8tjG0Kzm0vFF6$8UlvDgG2OSa+tY zKOiw$BFwAYQLHFclf}YT3uL*1ozTbBJfP;T4W@S~GGvum0;0XYf&B-1369!}*Uh8u z-p;?L?h|#tfI7G!0Vl`|TNj!$7K6d07yH4T7>TNlMwT;#BS~rQ7A0GinHuLppBvFX z=i&7#IE-5%LjKkd^GjBm$ak%FubxBfor{dsyJt1>2CAwRLT1p)tVC~RH4?|We_>J? zw_TtEq9knqdpl<6-*9cj#uwsjEnnWw2Lm4wxj8ONOl#_Hq zxzQuVT2ZXiPiU0*6;L>Ft~?E=OuW$RC=i9+-#j_nJ}n(0T7gC4nyaTXcqt=KO;mwu zpyRdp+n^d5zkAJ*W!;Wa8x`+)R3(qxOj2P)>JchajiIQwdb4;>hIqkZ;_+_81JSyq zD`kOGC=Qq82AbF}6Q~2CQc@Nzs$NzQd%wx4ga%8oGa6l8cRm$RsLShJIEC6Ty$qQ( zSdZlljWX*w=q)mJ&g$=+K%NxMS)5$vxQLq@lZVTo|6|2;Br*2HR!($bMzjG3O(Ry= z?@13BqVB%A^=qIvgYyDq;Z~Gy^oc0SckYV4jN? zsIT$@kgEBPw3N)~t5t?$Uzg};D7(1ml-Z(#D%~?TwUM1$7e6iWBQ*^=lr4AT8HA%Xizz{_k!KMn|F)~>pOFH6xGQCM(l6!3qhnGk(Fa7A*7Iw z2=EJzc@byv571i3;c4yUh(qn<2o0(lq~plPf%0VN4O_dm`5+>gxKfkLLS?Cp>oSw2 zg$EtUznhY{%4?}>2(yEOvi&E2z82 zMYrfed2xF0Q1nT!APg&OQ75k{GpxlE*p7wj$QMzcLZDsJ6A2SqgAO?D9YY5=spG;P zH9doF;WDC2ZtV@6R^uc_Vn8Yj4p$!yPnoLJ?$kjbZ|PL{zd}n<&kf1fNs3^xGyq1o z)>RdxSnFA^6|LJ1r-qF8H=;%bE!iP{R7MhbmnlK;ez*^wj^dLDm1A6{ouH*Pu8 z&<`EtmN)9Gv?b9%S)XsJ)qL=a+6{&_8-hY(a&x0pT+MQ;I&DaZcCBB)ysZd1W~D)VWME zT^_2MX-nxkxfY!ki+*zX-P;yJYJDHJ*^p|@Fa$wZYlB!G%W%7;Q5a5wbzAWZu&xQe z_xAk7w6b&%f4$H{jOh}KSQTlFhqaG?ld%#ikMw*4BcsJHi9YRtFtL*;n9fVRW*Pzi zx?ZHBi~75XuhPHK_3kp8k5%V}`%-{DP0z`b>&%drqshiKCGXJ2Pg(+&F-NNmYkKtM zKSHXC?kcB&>f&JMZVDxnEb)V}Q$j0kp6n?`2*LPg2l9u1jFtqsOwXhiPAxCZnWv-~ z8+47rawh$i;*Uybg1b9Sk7&df9{94Kp6PZ74}a!pJmYl`QkFs)ZR35=(Uyg5$zu}9 zIysjVFDUh9vKA~8AE*=E=^a;)He|+Q&g@OzSmJCZJTrG!n{DuInZG2;K?%ymV;KWC z3vQKgM6}=W?_>B67H7=*=86TujaWp!=F&AuO| zXg*U>mkD9j*}u?KmUBaPy%B#bDFLNso7HBG77Rh1#$Y`1g&->Dxe->Z{c@N4o0&pR z3Q~VS7HMc7%Tzdjf;NNBucxp?0W41M03al#BtVYKI|ek$w&{u8un|*`NSmRc$>M82 zF30WL7Ld9kufXN|x?!fs-9ZocU?wZmzJOtq3;pV->RU{apK!e331!-=#I((x&L@sV z{H+w&g?&SO3`jkU;)+&wRDgP8+^9b|Dt=Fu5Wj*TbI$h3irT1<81xWqyS|rbcb~NL zw-CdSeS09(KfNo3F^=!$oOq9hm|7|yj$l<@f!PKi!d&>&oxz^sjVhkp#+NNX>#oVC4=y`@W%__en0uY6HFJ-(ZA&yDiI@UqHKzwO*n(yEHAMQ%oOh*8 z+@Y?2DT5wb1-56|hCsN1Nu2&@K7R#|;8#f|?4i?m&{e$JE&l*DhMyT%q<4)uN5ya9 z8Sn^}3pl-?V;*iWIEAETcG)R~vWM5pdlaz_b>piKOFrn z@DBGzx1PrRmmrHGg@W-h&*oDuE1L+*Aac~v^>zAs?bEr(LG3KZ=bIDs7Q-3Y(V!W` z<(63>T~B(rGZF+uZ<56z?#cmV2Y;Y|1cbno%0R8p>p3$l5K(lL#mh)Q44a-}9^Ra|w*ZWC|2kzuNpC z$-eN*N^9DS=)Q+?##3ud$CTveEKh#A^EIism$~MaAaY1DImBt$v`CZq=BMFZa(AQT6`2ImxTN=_Z86}7?qgKM6&`dakhuC++vwRu}ee# zseG{b?`HC;=vfEGXsuX!?2g5>O4@WZ>w8a*&FH6|9s=ZjsjNrAN5ki}$VR2-OeSHf zsTA`rKBc1T&{&)5JU-Qh%~#woWtYiZV3dq1Yddg?uQ1IYxVfk}w)LK>s1)8e>`<6zgyd;p2ys{GIvXe7_G z*uV_564~H_eBcz9WTj1^nDCgnihW#xNM`O%K`A9@ebFKG>{9d*1r(k7RIy;D zNqWwK#K6^WFC0X6>4du4I+FUt*FzHEzsizY$s03ni-|^a-U@U{z)b?Gbf`f|3AQ^o zL0Jw;5lrrcZI_tF`u%o~ct8Hn^;8|ik6CUtE$doEwl~9;vt7Q}f`n;H||tyz_)r zAs*nSeq2lpz&<5O0J!kZh~HQ2n@mfK`E1jxVNO}TvEQk(;ulUx^e397Mkz2I=vIaymnLW@8B zMse8>M)q>%iiZR`0|MvMwmCJ^T(ZWOa^b!C?t{%ZgXK8$nsG*`v-RWJUqGgo%R*X8 zgOIcVvKP(KIp45j$l+m4Lzh`|nd_wtJEGx>0IH{92A?TxfZkiTQ%l^K4E(&CG1BSW z&>U4to1of7HR*76B-gNq!RzomnBzsu2jQyIhjL7VilR&%*I?*K-d}tj`2Ug~1zv?{ zGS#<`gIV(Izr}TV6$f%TV$_Pi#E-WGW4Q{|Yb#__CQxS?S}q>x7m_*+B8|4?%J3>F z_h$eRluM3utfT17P@J<9DPBOoIljaaMICn1xy`3+u8D3xk}Q;rhhV={`B}cimw1h7 zMIF-U+(**2mBm@U#pk@n2Sgc8gkblo;1-{n&MNFyhs!IpeEf$HL`3rN;rpi*;{MY~ z`kyc=|0~HUIO;h%5dZ8tI-42(OPC!M%_luT4=>asVY)7hk3R?zYx)O$xl)#-Fh88I z-oPYY&KAPCE(_d_2^HH{fPj=jUTr_476akbDD$CuE{}y811#`#6;+XX*+pJ<~y!wK_%ean+fy-Ce68hTnqE{W~BaS0h>YDjotF;6~4y$mS7% z!1XD?*)`6k1F5Nr$m#{0&wuEM&CVJX?mz$loPSui|C5gR{|M-sozq?{r zEj$uEsMS=K8e<+FRhm&4ssF47;UyYU%S%W7#m(Ma&&yaYC&QKRzn?NTE3jV_*Z=& zBx&j0a>j&6)qykUr-_I3)58q!fJoEuX@lk>9UvW#k@Vzc?kOwqNBFbD@^X7KQczN< z(NI(;B_PKqYijMq`;<_JQ8W#*ArIKO6>S58GW0h4t+coF?&aWJ0$R}qy?!0Fnk7Cp zGJ!1id1|MJRVq`D=kpfk7LgVyVaK^i@Ipf{b#RawiJu##p@N?@mK7S0d7e{3xo;yW zPSiG5lCcytUw#ckNYzn2$>~^MXg#6&R6cYtD(Jv0Cqfe zT3?~8&ON;yZ1?gzJJ+!JsU*7^>^`yd#+PHA0fCI*>w) zvHWDTNOzeELjjd-ReXQ6aBcgs#5^opChfO~^t2eSMbo&LKt5Zarz10z_eKANzSPMEeFEGv*Ni*deWde_*6>ei5_(>FRUyPj^u4v*wL|<0 zKx`_B5LNR>E_@r9zmgz#Y2h<>Abck&u<&Ia|DIm4QB%p8g*Qr*2!#Bxr4sS%B8hD&;-GKic6#jSZ}Otf@(hD^AucLqT%9{Z2C#F+AkQBdb1_1>>KgsM`VXXkOF zx(9=j`mF|=fPWbVT#~j!|H@}rG$0{V4GL0?VzD>HXlo2%x`IXitBcz*U9{s@%*g&%myBsi!XMlWLxUjyJSpMIs1v$B6&)8gwyDdIne>H>6-=9e7;}M z=7KCpWtcv&5s4s%nk20f?*`{=AQ&bY)iOi0{FPkKOlu!A>gG-Lo%0H-WKij2RPWV0QiiM=pt{Z{lE%1zPy!ZsW5>EoogOc=cVX0* zIPZ%c485ANU5^O#6IpbjA*bavvIm4RN31k^pBvo)VSMzleTZoKW0Q(er>x6S5C-X^ z9Ea zR}!=UWNo|q7f%pNB~vdGhR~uj4MH-FOKy}&cNBSai7iAR6_&)wJQ9DR%E~Q_j5*x^ zkNv!UFtVe_(K z0ptJv4h!)r7eQB&tG%pFaN#q?3cRP4T8wBPDi*hQs1trAa1=>dianGCQn}0)|`K7>=F2Xr9_8;<$71ngc z?VkXZ{RE2V|0z&HuC`|O|N4MQlIfHFN4x@BCGrS&>0ANetRMA5Me-qn0|la5V z!1({z*~H&_Y18jr*1XsowI~~irs96xRfiDrXx2RYBURQ5aPCu}Z1E9CV5Rp|ct ziIN~8v%!lTG{YO35j!g@&hM5avYt+BKBpz&5-JXXAIqz#|EH-2@no;gnDp0EiPIe3 zEMAbi74-ssY%dhv7UMIA!wAuL*Xt)(FD*B#szdGdZdLCl1_o03=`K4gvzebbRk-L4 zTp@!Ywk0^LS_JRX3Ng)*Q=ipUu`s>PX>b)-CQC##VTJG;`b;O^-^yjk)xQx??fz6*wP+smGyWPHvxp{J&Znbwe0ygz1UsEzR zX-2*Lh6|{4a*#YH|Ge{bzJ%_0ikr&u>OzhwP~KV<0M{lnWSE02DW2T^Ucx+9@Jd>O za-I4hTo95jE9l#nzeZ8Lxa{ovt)ieRwo{R+x3#}yrcH+|F?rw9uRcQB?B)-8!IAj& z9|y80XEH(9T)K% zIR;%dqECD%cH|&YSt__l z5jcQWsZ@?Qie6f9v81X6+W3QZKR$1h`;M#|?M-gSV}WCjuYE?gG(xnHj=Zcfz3QAG zU@kN!)ToMv@XMX^(r)d^gFxkl^>#o&2eN58LfD-eG5AQD)|wR(Bhy%rjD??vU<&%l znfg7O=RUyT+AH+n*9j4ASKtJMm%X9NTc59lQ0)U4hFIh4dtB^?BWuab!n6(1qj*wf zZP_Z9LxA`c_57(b8SaEH9yp29!A*ksI0Bf_A~v!fhidSFu8>iMYQrwWpEZhPVj`oXl*Bl8sR@aOfLfxLL@V!H zZcAiDV7uTNFuNT<#2ZWWCtc~MRw%_1p{Y6!{1Ps9M<#J z@DtS%oIeNKgP>ibzl%bR(&h_I)|ksN6uY9UGy_CF$|oFqc;A^gs$)^$ew&{(5|kLX z`dwk9PW8Txu-#4s?J8|*0LLR&*n3;Kk#noI%AzvSqVxw3iq4xh1`Zy{ zlF`em|F%+lQK5wnBlY(E;k(jMORA3PUw1kDFbFuvp5IRnpn6fvF^J{nY*YE5lbD)q z3-?q9AOyydvdOeY49KwzxKG@7B{X^twnve@69}xv^9DY7Az(_N)f~odq)k`^sxMgi zoh+~8pa%}x`6q0bWKS5aqXNIq+mQ&2`OmUt9mJ{XviFw-OJyHEV!RuqZpbv|)KL`eOeS5?u$J@s{G^tp+ASuk1VLP+?rF==S4@ol`X+7I~wmV>vfA5*f7n z)elxdAHglJMa}wQJI9)kSGUCl1SbYF246&F=#mSzv$zDm4d_x+`&+ZPq~+c9Oq9+6 zkeJn?)4ahQF)1-vihz4%+545V>?6pR()FI_gmTfr`-tq*=%Y~tKY()*{6Zorx0m1^ zUyWdWm*d38cEly}Mo|j{(j20d@A>z$G;@?KH~Obegz3M|AOGtByMA^iG?$#XMbhUo zj>MMr$3`zkSNSo`s115q>KU4iFeF5O^VJ8z01p?Km>ted(nnC&8CWI<5^I?%i2n^F zZ!Clv#GH`RkEeC!7t4TeBFK}HNN6_8Xk6PMHOu_od_JCvWIArxv)EhkdfqzPbolzt zbeO^Fd90YpB*>?F#ld}(c%%9=bV^10RvvPlufp_}9+JsK7T`lOwIllAWTu^Tj5Y7M z5OB^_)76;zB~)Oq4g4ThgdM9;TAe*3Y-7+vRsom^MJ-pTfT}qJ`e0 zkQ261qAOHB&d) z)_w?K3yhf9aLq*ud!ifJy7q;C&_@Ved|X*EC5wRsytBz%f(Q$%zPj6A3Wa)9C zqtLBfM)XCXiL8m&8>Zb*pj2d6d^$nrJG;WcCZDy)7hW#4>{au%_R~PhVmcD84xA~fppsb zc}b|74s&HikPKs!MG6S!!hc6P;JcmDl1xvhN5s^bQPc}NXBtVR1*v5Y%E-$~78NPj zJEfOq2XCfEk_s{(n6stCjtu;1cH6Viv!%CZ=mi!Ku>>kbxcAEy&~Z7Bib4a!2|*ua z<>?yw&?6+fSkw|jZ!iZ1J))B6pPkO~Qk#?!F(U>?RQ3pdwTwy^S+6P2q0KSPKceEd zsfwJPTW`b7+EzC(ri)7gR(DpZ)(OMdAWW>#(@bYv`j7P~O76N8_2&_ht`~{*6;;D` zjJcxr{MDbQTL#;C`t}_iI@9kjhASG`UuM|-_)Wz*P}iJ+xMylkj{qvDf!v$A{iYlb zpGoVjIM#rX?gEW{8p@DHO>8Sdh>&>+L8g$}oxl15=+HvJZtz(a2w5^DHRVP%DQ6da zC`Z6n5yECoB_D1oxIvtpLVEPumFg^Z>`zR9AR&$nhCmh)0THd6uyvFoH4bK-!N%I+ z)>5S8uJrNmu^^}HtU9i~ygdG@K^tHG(@-wWmBlQR-EDyaVe+ER;jn_h+w-6Wda|^w zn>s(y@MvN4nzpGXX5>2iuo9kR>A=V>y|6wKs5@;kFw;)06P!+kk^@J=l>90q_D@_F zfNBsGJknE;EK*bX5|1Lc(CD*ASoad?nArpNEg87eN8&Dxk`SYPtw-x9m$S{W4(4vY z6Ryo7yO!xc_A%34n$47@}MN(#taZw)?dXdi~XBxup@+OP4|&BBBo8v}75JzNw5 z7_j@-7y&m`^|}{8Sj;_e>n)BD%5lSeh>S29bGOEc$BTB>O~AoC7|0|;x6TRJ3ttxA z{=1*f@*cmjHUr7e)}!IQ4sSpP$)2v6GQ=fs^_j@J*jVWyZdS~lbn(%rGVMJL@)Caa zYptx)NB{6OI%c{L{5x%>+6l0Ku5Exc273{M{Lpg#wrhd=DlwY&DBHaQgxOc5w8`0K zkmo1hyhmK^gVBn%uIE+}NbGRT=?6K_uwt4+g&%?@V5sKz&_sKoB126e1#5@T@3h~; zu@Cp=Tidq8ghOQ3+M7a6QcyP5ZAzGQ)$skFdZOz}cn(Tyzj>_~ZI15!e^J9}faGnO zP7Of~od*@PjtTpVl}#q?w>IL)J;|%OuRd$L^)30jyRC|DndaI$iP+*?!U@ld#?e9> zcZ?D%qwU|}>_{W>{+8x64^GTEW~| zwu);&sPT6R_1t8|i@6Q)PffcMZggP+k*jzqL_Fr@mv!Kr*ttmaC*pJA-c9*4|ndEHIwvV>Z7(AJY3X#*LllX%Z{&%ei zLS(Yk<*3kVDlqUGu+Q!RXUsN+H+`y-W_~x5qL!*o9(oS8=tNRdP(GYzw79p|w)54N z5%V#rR8bO5?QNr%LhJ?U7!%?Bc0f4~ucyhhSN44b*5drjceFy2O zHn-Qh;1GiRcq7JUIvF+rZ?6!LXs=mj14SN~%(9W!gvG#are%xN=Ppc&-RK=yU5M>q zS6yy5&v`<>EPHrDQJ?D8pKG54o2s%)ljp2c=GC}zWFI0%X_y&_JHc$;N5x<{Ew(s} zL#gO8Djl~P23qRv9}WdLJaZI9K0w#an@YiKx(XHWmS(lzq#_sffHTfo5>F8$zggT# zUcSZNL$y{l!cC4Szr`ro7>*&zO<2jgpsB)OkHsnrck*_WgcavK2oRcpA(u<&!NNLW z5VcZ0`gOG2!?C~A>~UFzj1~%y+G%?{(l@S77m|kQi~CNE||`|d8oOBjusU6 zY8)nK2`eq8WRJ|s`K@G?b)3`7F-)pbv|$!b%sCaKB896ebJ+zUF1_Q6(kXSp{h|Z&TJ4`t{EHyUxWW_4 zu;9cEStaX|L7jK)Oes0js7N<68;)vwI6gZLU1eVO*Qiu4Ss9OhgqBCh!d=6}l!n*h z5=zyS3$^yg-kF7M(VOo%9GclHJ6$H8M}S6Ly&^QHsMn8ughD+fuQ~bri8XgMnCjtF z)nK76Ps^gLTfA`5g6!-ZqaAdkWV(mli95?6FN<<^8{`sEmq1K{90YX!P$czAICsmB zo)KOZBA3CB-UC$cZwPqJ%&$CZq?z6u9%+TsF0jco$;;r(XUf2h-M!zWr8#wLYyvsC zCVpl_A5U=jb%G3e8$>gDg?!w$m8YTxJ)*6i$t?X2f1W1ikAY}PPS>@0e9cF8P(Q3d z99O)?VN;97NumR4k?5}x;6KTSae=rjciqCT6JAn_tz0X5cbds~c$vR0BjWNdN~V1TM&k*DR-9*-7VML&%^AclteVI* zw$hC%XYc?NFxLH~v|Y!i5!2bf)4zQwnhpiw`&ga6R*`AW-Ta)DT$(eBw~C0ju%s8V z5y>Yg=Wt43q5QY#gLAusTOiT9kl9Wl4e@v+w;_X@fvz<21i-iPz+|2S%0haF>`*B1 z`m1Afj|(8HD}MD>p{^9^c8lGq1F~_PEjS{YVeN=>_uh^?!fc;1`Yvu7j0F8bmyUa5CECsPtreh}(@>vk1Pvq`;& z({YJR7?)wJ!w~j}J+d)q)fk;7^JxtF@r^h%Z#V%F0g7wyI-+p40cm?8Xb0Cxqh0|ojahj37qW0*362H=%a+nSZFYx?lXLS zt{1(2yrSfmj<@dKW=U<(ZMx6fXb|3%(*@QfvrS%xbnpW-PXbGV+WGQj<(tkH-ux8Q z;e)wVIt)~~{bD{ZtU17RPqHqs*`IfZ0rr+yI|9pW(JSJm=@qtwB1#U5ICY-j2dz_P z$>XR~C$~&I?OG6QM{cp5E{Ak9Ay@SRM?G|`P-yLp7C+sH{lEc!{L6!g_>Cj87j z6g4S(%DzEAm>&ZZbJ8KxVUb4y3${{yk6TE%gNTDD&S7HrLsR#`m6Z++8tu@ZV%@b* ziwJG=NGHgfHRi+8L6j{=-!X zj&f_CC03@1lG{l6GYCpD%%e#tq*5dSOd@6K-%dH)hv|9pNY?v&SEEDhv#01@CZS@} zI!tg*BC6q9!Z-ct@TGY9HL)a3VJg*vrpsc02_7GhGxy~)(CS*|6ey008(fa4%Q!sG zTpg0i@BI1;iSo_FdQ)iSPD2g5&HDT&!YcTylUG_5z6Enrbs#B=vY?CmMvJB5it zrY4Q-k}Sa)a_Vm=j(p{cCEI<>AAH5fLZgKAap!y+h@AI>{$D5^ zto3XiOl=&6ZS1Y|9RIi3x(b9R@)63n&)D^}F(YujpD2JG8(~I-5N~f_0en6%e<&zX zm^ue>g5jvK%T`v9uho^;PRV0^6N>wqvIP=7N>#J+rP6_WF0mT0$jD`p4f4 zn{AJsj-B^y&c{_P69COZS_oZ==pVU}YX{y9_->Y~iZ9(C3s)BaZtRpJE==pWTWy#d zs9eMrQ21U57&4$zL{E@yyk4+dJ{UYm{7^WHRvO<|*6KZ?UyG1J@OUUJcnL?&YS-{? zc+UsYFgft+V*tgrl)g@^S0h^d3|Av$APz1d)4RBSn-zNszXI(xW8M$ldUUyII%Y=i zd3|egss;mCHNrw}+0?je_MrM7BfLGwV(3)%^*n_JRH*78bQW%(0Mzebc)BTOY#?qo z2=6$4d7|w+9K8Sf0N_C$CA#p6vIEAsIY#OZT3Qgirf!Q)Ld zv-5Vx;r6v1!!!C29n~^t$%e)AgQ0wkcbWjvMpuqT0m6O{z9fA=Qvt9F{xlVx38@(+ zrikeyDV=QLunAu?eW_DXxf_MKBg$?h8Bud$Fdl?UyUGc7;jH`!HfXqbR zn3AMAHkWO@#EJ@Y?Sj6N26lZ;kj3^QLhKJ3Yax}i3v#E+u%$yF-^P0Jo(-hC|5v?B zdb6Gog@b~tb9i<^I&g9R3T7CFGrO2qwU@f3aY^%NxpO0KC_|e=`JYMo;!L>18LCDu z5~G#z_Ek$v}zP$`4gQ2ili*>VLKuh$r+_oj>ZcP z=%Y_rgRvYt6FH8O02X0H#{z#t;6lO;FEDsWe98(?*+h75KI3p2!mhX9Q%EUzYbllb zR#PUSBhTGga)zia_w#5?$tk6$R~$UK21KvyJE`@c6wX95MJ!0xvj`B3b<;Ak=*I*% z2Rbs#C|yvNnj8MnQGrsds-9MWVpb}t5HhbTPCh;NY*JJ~E0z86#WvBRQJ6sKwP00f zbDL0YnM#t(5_B}=w|w3Jg#y9yf4*y`gH$0gcD8zbIvobRkd)&!D~we zUZ8qlsV1!BoX#=&Cah6o` zi3{hidG6s`NZw;bfo2`9=qf6#r93rY$(B%b^l$qe)}LS$ zmL+}Oa=;K?AlF(rmE73I$JwT09sy&P$}RfizL6y;A-Ev#@dtVqbdi^Mj7kglUg+;% z#dIe{8g(+>rvwkLjl>V~b77(!-e97?uae_;?4>qbR37I_`6x;XBdNU1{_xN!P??uz z++K$|j}Yb@1;DN0+40oJP?2+7Gq5RDbFMHhvHG+8;|gi8N>PrbZT(mcPTrD=O8(rJ z;Q?LC6u)+jufKB4le=ZHUAcnyS;G{xoyjYQjd5zHO}X=Oqdu$&bjPG5s|NFqx*fZK z+a9Ja-Dsm8Skm!9V2ZYGJ!b%^9%9WtSlQDoOc(&8a(BoZX3fcDenLosn4sLd%L#PH z>Heg)h&i&R+?#o*A}2Ipe-T#*VTZb>{Q#|Gj6)3!2=Yd_KOr^=7m!oOj)?trAmA{> zM)O*NyjxS>V+r$$b}Um6Npd1*xV?R>TzZ`&R;w?YSG>d;^D;Va9sgKB^{N zDK9#_{9#&)Bdq+xjlkd0q+(GV2GIKwvxplCK^O6bj7r!Sy5X`C%dc#04yEH70wQup zLK_l^nCu+v@`jFjH#9%pvAJ)A$l1Gw?}~;0EGW90(3(@$PdL?oA2u3{T2=7wm@rWo zA(}n@Y|{!R9eCn(Jv{$;Uo5HP0F$?^tTCL+$pMinZy2(xe$P&LPXrIrt`O#)mwBy`kt|9Fq)(?E6oq>-*5PlRWMy2+r?TpWX-bs z*e@pBFVD6wow_0my*N(|;{%m>F>FS`f1SgdNaa?g-o^e!rtTfF@yUdBfz}j*Jj|W6 z3bT0AodD~yc!ptEbP}fcUT`S!N6ZcZ0%RYbQ*1QEHud`>^>-0L*e-VN}uar0+}@keXm1I=T( z14S-2=@xdvf=D{H80h*(p9KT#*-;_NjT#C9#AB;<0_xM5#M$ad3m!Z+h}TCS*w?^H zEQ<~iHleUe2>#+GI@lsvZeK{9_maa_~QTm^|%_{}Yx< z#{9o@UzLq-G9eOQW^k+PCPy$^oC7TqQK}LTxlSR8M=6XDc(BA7n7y`nW-I)5t=qc8 z7hVpnBQ@_Wh&P(iGWa;T5KKxZ4^K|Iv)jMQ9`BDw>|gNel*|#z6f#*Hjwt3bXpYOh z++AD(0ZK5YDsU?BldNO|Kuk3J(4kNj+svod4dj2U)GfiM(563yY!?>2&pQwY6T6}M z-=x|}pbE9}FRgQKLRPTtl85E(ZSHOD+}n1VGxP4>|0#LK5ZBRZ2O1Xdz3l6K-L)%J zcy~$X$OyN0b_^$%tO%P6EtvlZa@}|e(Yn4TLOLvqohs2NfFFVcioHf*3+w##!I91& zB|Tiz4^)G!J`Krvq@K1J?{*rJm0y^M1=X63XdFP_xf=H`-$6GNZ-&#UbpZE-rPjbG zt~KZjHO@6NMD`37CjCLLBa%)0Hj5dQ;~+ejfhy3A-DU&4O+3} zKC8)Au4~q@O3^DqCCe8`-4+#=%r~yWJB_6`}yGjgvl&u0azG`VF z#D?%YABz3y@fC48b2%Y1@`#Ft zBVH62!3cB)>LX!>Fh}`>MaSF&{Qv!VOz4qU;tT!b2PE462iN=m%Pso9Zcue=4?I(J z-yYfdHJf5t91Ce7SZzt^cFA^XG(oLg+jS!gi(Kj<43XlFM3aoENfy?PMp01+G-PxI z7-|m?n7w}zj3$W=4IUsvu>0`nqMd;rJ^B3n%TJSb&6XO*`=4nq3@1D%d^g-X+zm6I zZ*!C&bbSI7b)YNb2Ol{6NP^JSum?f+-TZ-wdyv>Z8U%z9xe$a)4#wcd2(c8STKPFenZEHDHu69)*M-a;b$)RY3`FalM3a-KWrd)r$^ zp7!8RyLz5*;YV-%tv#TKyMpkDA$Jbol*WH5Z)$q2@MaxqL1R2b9^uDHD-5Zk4Y`>~ z!U^CHnA3R~VE!OQP!2p-YT%(og|YFX51$Oq%!J)R%)N-h%ZuN_b(0x%SMF)T>nl=O zS_`P>wNE*A?CX^~pEY}AE-ab0Pqa2wj3`^cG)FhapK=^@;FXN9EfnKE$++|AYvY;g z*3KeNdRh&mW3w7laHe^C@uLr}H)C(acnr5F-31#{vQ%5I`*u#%drcmeCncWNT~@;$ zA<3t@Yv?xbnd>O;^-B`!@DmU7CYhQmf4OXI?&udv+6|}h?vYX1HSE`u2qQ$v6*Xcl&xhhVF54V);KqPaG zgA6IN_4br^o4K$zN)%`d4vyuT+^ad@|HNU)jED9&9;Mj6nA7U02^0mV;q*jtP$xBW zNs9F(@TOg`c4^DH7u2zvLP};ZDP_W~BzXGPE#5SLb7d(S7wQ(|&?1Vi(|t#feWfn6 zhkP|jIF~wr>-1&%BA-%NYX&lXapu&)%5v&@B$o+);pJ#V%ek{%UCg+AK-1JF0M26y z%KE&xB2D#Vnm{La^WTd{qZ#HGWs%q?eTBW)I2 zgu%r#wBGU_QPn}ob9(5XD8HWBJ82DoUY|5f6D~ewJNv;^^sAi8xT^8uMcuq*d(oaM zeh&(YT)LIkBE8}dM#l=1lt<%89qWWSLzGDDsnzOq2d<+`>$|GbX>f#nX@4j4#9X%>g?-};RHMs$ z3i!UgjBQ?5qUW{i#VKmnl=6|*zy;tF=y!UG7Fa&)E)p_a<`SS7i5;^1kNLXtyI8dS z4%QfLNv4)_N}@zIGcNOdV|d3PCF9GfOp_j`SRusYqo@u1VA*6=e%e@S@egNwK<&+8 ziW9^I{O+n_q~NxT$}fQ1@9s5e(`G{++$z1ZTsB&C&F*L|@i!vvY%@>m-wU79KejQ> z8pUF{x82^BZOw$b5v3Lc4>IL?nk%(!pNG{33S25b{gO@s>0IfuvJMY%W4yiF28R*| zo*P(9ZBOpX?}l=2-{4Z`1fIT51%|j8AG`;WXCO(_@i5bQ%|S|Xnxg#20|CS~U5c7> zHQ~EFl93SlDO+bu_Na39HIpT6W;LXGDR$0uUOEJ^8978#$I+DLRq?m@sg?@UtHApv zgUaRHP6pa}Vdg)6qFNfE>ZE~MmcaKYHTn-`j|N>wd;AgNM#pJ4)abRoXFc+4$~fZ{ zno-?BZd|kxWJ{?&elJxf^~z)WlLwY*0&(oGw;$R@v3VvBNKzFO^=Zp-At|CY!XXGB*2etK;8 znMXGVIG^r~Y4}4<%MV#zm@Hm#}rM`RU?6>|%ppd@uJQkH`nh#jZIP{dVZU{?#4MDXiMIIf!Nqy(Y@-`j0q zTr$+kqdN)oF5#$7oGI$ww2TfYAYFChVus=TCOJi#mNV2T%w_oTn~g`^jki2Ld}^4Dz&)=A`}jzU)WqJ5CW0v2mu$o3R=&uBOKnmB^KG$7Xeu zEUGI|j_^yB5v#8s2@)%4#Nqx}!}S9RL-GA&$FLXjL3>g>Uyz|zA5v4?tLy4;T1nakM-iS> zA$L2ddjCMvsKScdW(r~?xdUI$$<8yTBbeSm%)3%+1d^Sa;buNfZJ2~x zh16!bm18CV`J)`6rDCX+R3fPe?p~%-t|FBQQMeqBPh{Ml^gW6yJe|@!FBNOB!h>I0 z_#R%|Q?e9!nk;CA4V*3YnsM-Qs=of`$ek;|dEcA3gBhx~cTZi+*nhl^IWC$3+d1Ys zIJ5(4SWoZLOCiWhQv}kZ?0UdxQ+}VofZ4OuA?EmD65U1Qx<8{oL@SXI+ip*9bX*kA zQ}8%!H*r-*aBX_2dk)wCX9!C726?+V_*^nts?PL2eF7Q#$E=ZN7PmiOENbq&K5Hpt z!d&&w6v%~iT=D)1)U={t=p@A|fNsKkR7g*8*RX$QBpWO(ENGK3e~`$TQo46wGBUn* zZjHCW|N6Rjx;On_XMt3==0mT#qL{xpxLuF|zCKM(?hqyHtBGq;HPp5wjH)WuO@GJ3 z>wbZ%xKT$aIjwCwx`*JD2|``}*~ECSSC}+Tr;_d;QRPn(t}b zd4~JRYlfT0{PSba^2f@7yyIiMEp+d4iC61g{#)lF=B@ zA+wX>Y08Wtv+fCDJ)a{G&*E%mHZO&L*9n%A-0-fleUIpbbvG5=wDeFiNmTnT3xm_S z*}m7?vIPxsHP%=C==+eH?j++*qN-Oc7n7=-3pYQMWZ40eLh+hzTMiZta~6}Nqy9e^rXp5j_yhF_8;0!F5F!(-?V2j7QA@AAbjNY=$?+t zm_@1Uprp(uG>)nfF#QvJ&Y}W^TGxaXrdk7vhSjpY`sh$u+jiufQN9#|kjku5$H2=@ zZ%Cet@E;8SUIWB+vvXdjTJ+g!FfRIOHRh@&kMX!-x7BdVq`&z22>nPTc!k9YSZIly zMxX3lPDU)5vf!3VAG8gPqrJ3WhEO*Nn^TeByxXu(TUWP5vs`cZc5T_E&Fc6G>B;Zm zyL;FLz{C@ueqYf^!Mu7dc@&RUAK&ipu+3ng`fYjbpK}lRnu_OiAm?py;9%8&wjIH# zWESi%lDM;Nx<19b6kyApGk#Am8^)sAuj@yR&TH9pD=)kH#i071Hab z1lm~oU3ppZ9IZKvx^?GFl2tq6RYP0;EHr}su82YQ^*h34g?oo=m)r@YzS z(Ng(uLv7g3wpaaxO0CZ*NB%6tN@{+H1&i)V>@&G?H9gmbd{{cIlw%^+kz{`5E}uaS zj84C35NC|od!BMph-~LP-9nv>ayIPk85cY^>9RW02ML|o2sNEWF5`Xt;US@=2iR#h}cMnh;CPn&V!}=?Xh>M&PkA+XQmsDw{*MBBN6@>pU)TlK46Vhrb#cr zJXJ(0Kou^ZjgbxK3L&$GueZ(nwX8`>^_-_v)~sziB-g&8dMfBkk9gR5wbceP;Ks z7LMfIV%4eWx2`sOrIHY-h_n@@^j;+?J^1OqSC?xIU%RU^0J-0i%b!2Hga9G{q4wBE zqBFz!fxrOGfG-=yj5g*sFV z?g;4^Q*xKZDT5a}2HdM^UCx*ZX^nWhtBk&JZM7Qey~r&SXv6HxvF=7@jJx0Ws<@33 zcHxJ^;TOpER`!6KPDfLD){qJy=~{Km=L0x^bSULtN^E1!GCO`1xlWfC#!9qXqa5kl zf;WmTdgr+@>uJu=`CL(*`*$dL_KdF-YPSuA6S+ zLbN0=zd_IqN7TGPLYBlQL`j?#dneeV)3Mp?(@@7(3{-e>)Sjtko~afe`f_mTnPPcY z+dBc-rngGwRCjW6Yc+JElzDt<MUyoB18#HoG zf9kWa+Ov4lL1&Uu2iJ0$7%C_*Lnq7kXj5W_BBqhXY$|gI(dxP zv(=k{wdVvx{ow;W-n}6Z#_s$BNy40dbysg$VZ(*<7HcZoNb>fevC-j>%s|^vkXq0q9Klk{dx*)ZaPm}z4qLW`NA(v}tyvqqlP z45{FPEmzWTttyqSdE4QsX#!dgPdc|U)1fL6CbDtM5tL6$Dz)aUreT@CWU+ZxD%3BF z$E#))_lsc#dy6Qc6in{fkm|yfQQI-rFyd7ykH-kKdm_Qi7i^>#lOh_+)5Ww)sKo)v zFIAFS5%fV~?3s!Szy@0E?oFk09QM<3MOtc{*mW5%Ne!f&BE$W-tKp<%CS_*ieMyZm z7qTM3g1E5(*$oUrHbyR;V@Kid;&3@*GQ|gUgrE|}CT76mJn4v(c86T)Vjj7=fg!Z>tx|i{e7T&bLw6C{j zXz_bI(f`V`?R%8X?6x$m!i6Eb-d|Rip0h5Dj{CSWxvIIR~^}bd&mDG%C{8ld)JeT zWCNFbY|1^51^XK82{pBG9iaeJp#b8ZH5fY$hYP?_@Bthos4gNRi z{U+kQ>Y2?%I*=CDsU_v?o?cAAx@PpZcrh_NMb3zHqNG{3Yx;1n*ZK*(zemcpxp)!{J2I?lxX~`kjbrt+ zxzzqMVv|*vYH+x0blkmeu;C_8-?F7`VevP_6VGbjA+paX9fr zryz2p?DIV)^Tkp?4dfmJ>9&|-ueIce;x7j1!)~=6;FK?hM4bJwKIhNHKtZm{AdrB+B=-L=Zim+ zd1A)Vhvd1<^FhrG2{34~lQ(N}Nh|6?lMa~hG57HcQM5LIp!3Z}^z?0`b#uk~;;-0H zdqyL}E3GMh?obgSs@PaYvQxw|+lcy^+p)~_=eZO?b$J=*aKq4f9qSa8t}nV_OrjER z%-zdHz;}kczfrZ0d3>7c$9ZW7BpFTw!>q=>X4Vv}?5UpIVVdQ}W+}{!6_(2Yr|?Wz z+XcK-ljTr1^lPN;h`%oLyK5b{cIRAScDnuP(%ih38Mnj*sggs_!c^7`Kz%SD49c7MD-9$mw8E z_#_)gx#9s@7#vYyGLOM*8 zX@gDEXWNFMGu(ic`|>rCW{p|9*)3RW=waZExv1)uI7@8_QQ%-z?ZR_jP)Cv@We8<( z!JYUSsrXtr9o!F4f_j(oktP?MoH{+48%T$aJ-HdBnX&V*XtzgQwe-=RSZ$SF<~>#Y zSIX=|k5XUsGe79|B}%3H6*7m^oiNhc=l#`3e$|QBHC32euO5phl zsrPu{yTYHP=-(Y+{&BqpHw|8FhpB68yfxvDX{WA_`-4_pIoPU9@$)@g*7kXoFpfEI{UB8 z5G0+#?(7)5>eX56pAa%H(0>DK-~`hYx3!7C(shdg;~Kp3ACjO2c=EHLAiJUnP&QQ* zP3w>IiZ(O>Y4Cw;urc`D466TsDVX-dMD!k$rP?{(^$ z)FYxA39fuP+FBue)EZ-jlx7XO=^;YA*YUN@WXYWSu5k3Y8@L0xw8hV^9m%Mg{10_= z?MWJ?)v5!Xl0=wAPC)$qeIe$?sWkaH_D7SPrKEfl_tmfy#J2HU1)s3*oOhuOW_7Jk z!O=sg$2{|?WbLvl`LB!`92rRS@qN0Zg5G%3_>ck~SaEsPvp_fuwHLmh4sMVqmqTs! zbh?A_4wtyCMTnL>fuxUmEGqVAo>HT^|0)ZO*O}<82f*v{mYW0Me83o=;5U66H0jv{n@_0Y`h&on=418WDNU-#P&12|a4(^hn0SV(rbp*Rt#$UQ6}331722}21R z6)j`t$Ajij9-Luz877gy$RkQIY{-Xdbqj~jJD9;_xY6lY1uB`k=B_Ja9h+zr1C!VT?egXx7p5v?07H znSyMC@Ci2fOdit%QAgz@s#)FvAwEzRp9#cZWt335Oknfwt<^R?Wr}Tg>RwS*=d1oK z^YB+FB+dj7a{yZFSf}O=HV04j#SaZ>^R`< z_s_;+{wwx-4x=Q`o!?Y1{O>@m)12nOp#!JQQ&X$_8hrd6eTmpUGygxRAH_q|bRml(A>>Lef!?dPT;gFY0uBD(AAl zMTAdPJAEYL5Eh~U{tWd1s0GJ&>ml9ED3Sm4VVK@O`20rt@gwIOLiRsZ{QaLL@!!&R zlNPK8-hV~5JLWCaNu}?FHHO8S<74PHs5qKIUeKj|B~u6``sF29w(+H{Yur-TbT^y` zu%INNpaXa8niE%6^=R+uvu}vP7Ir($a-$Z7575jPz8RTxn$%dGHWSt3E77VmuVZVMJH}MF@FuvP;QvPhM~~)a#cn zauntMp3zNsluc?@-@y>yjXQOh2i6>8#HdSs(FkheL)uC7u8hiAha|a6*U3eyQLh(j zFb+Xdb4b-mMouX;O3x7wVNe5#R%uGs)%rG$;5zz#;#r0*R>$W{M_w~M-8Ly*Ce7(8 zU8Wetq7ix8r#;=d3O)VB=?SBrZSz50IGiqA+QnUHkRrkq_w{LLJ*p_p1+^_=Ch zVKZl~^Kd3h4>zHokTJ>F*gQ8Q-D+8q03lw)zmUaBuMDwm!DzEd=3}%Y`z`33?`|sB z!57bb#B1$eRfH-Ab7I_XH3^oN0W4xQBQfHzj~<%JDTuK1BL7wXY8=sQ;^#+~$Xoyu z%~SYI=h@+*&d(@qA#aF)D{f*{4gBXaEMek?b61H?v=6IsF>NzNz-6E%^P3E@oFfb| zyKa!!C5RV!4uLjZsyK!#rI8Y)cnO1Z9tlLbSJg^n+=5%Ub-n5lDl*GCgi@|+)V4^ANEq*NWC6w!*)~e@G!*C`mPBb zUQtYL;{f)3xjj~P_Q1X_(1(I#m&X5aKyF5OTzLb9w)*ZZw>!}2ro#$!6Gsnz&=!It zv$lU`a?iIUe%gQubj3{Ui6BN0zI>i^AfHenaJVZUwsR*%r{&;hm$!xV@PQdw5V6g? zih)Iv0BWR_xI$R=h1-!%CQQWSOqU(wZ>Ksr)mX}y(%XD=46yX!a)ILJNY)q-%;P+%K#$n_?d0bTI)6$bXcMpPp@*1e0cy$IBZ1 zwOL~ql@j`wwW~IgG((MrSf*WKK5qVq#c3$UTlEoC+}`b`f7XOe1(*HDNexU^S>6)E zrkV%mz!&=K+SaT1S(epV8+ez$H70}=Y5xfR8DOa!PiuH|~|llXd37Qt8hYGblDH;&3IGcBefQ{6)XRHm+F~*0{naFm{cPE%LD!IA{ZE z@QVC(&B!c8)iK<&**sd%r>>&{gW!j3fMeM4&yPYXz4}CWN_6J!)sAc>k!p-z5h(qj z<4Em6)T9fP0ez*iz1NMfMa+e{+`oGl_k|aqI}@AoyB%sz$HTwA7vrgG*-L6leO)ec zguBr8cA1Rr0RyAM1~o;c^+h8o_lc45Id#Sssm>$ej;YBk2=2y8Bv|;bbhB7Sq^s~Q z;8{YQW3o4k8|=T8rPaNojn=EWv(Ub?6l_kG+u>$xHK@F6;8MX+VWzmEqxtFW;a%xQ zjzJ?^5*!f@ znYSI!WI~_Dfv=lPGzFYPnk{P9KpoStdzXcGofSQvU46_5chUloAQm+aWM;C$>n3}` zNho;tew1PnSuPG9kOT#~t<+%88B4sQ`3+QV;+Rtz$;sQaf2W=@?EI(B;sqz-GBA3e zm9LHzR^C6xtqYaRzDq5UpfHjOjZsL5Db%B>8(5u^U&&RoI%rVbSymm~S*%56pw;8C zqYGnGVV_(9LY#ZePZM%3QVJ-JI~#I7E>fs!=4VX2Qp``SayP_>B@a=sAFjU%18%7d zw?NL9%H$&tToV5)u)i>+6eZ~xhj@lpQsN+Wy~0iWz%Q@=qa9; zPH4m>U-s!_qT$%+hKes@{|Ebtp36o_HVV4 zo7*cu*RP$0v$sdEY`wx%m*45Wg$ve??Q#Mo`lpYyuT9f`cIB8gxo@r0Mc--7! zN`pi!fwk;_^q(EEaOq-NuGZ&BsahN8huhX%3g-k$TdF} z4J{8>Gs$#z>sXBi=X}xc<*4;dD3LfS80q&Jrv9j&LJUSohT>L;D?N9rCqoo4czvxT zwH=fcpCk80@E8Oi4B_$zpf#2(3si&)=i}4x$A$5zeI7U-g++^yhI9+um zlJLxFrLi1kG^|3R@}kld_=M$PH|}-f#(^M}fO4SK5s|Ew7MFx7<)YY#!gP^S($;Ey zc6lk7_=eEmMPXSNdis*=MWK$tUehkCe4uVDv-8P(2k(E5+v2r9=|>Xq|{b4-36-=}G-IQ8;}s zX1Bi7&)s;N_7FRq0S}~ftCzFe{(hVbRp+DOFWwpoccozvoUiq}#x9v1t%JHc58>u_ z;y##uo9>QH?sp|Vm^*TRZi%%Y5`1E3_PHM=+rb(j(L8>9h|o8?Fq}8`EcA#x5XS-( z72=nuGeD?S>3+N5nlwG@NFxPd3ZNzBi3hNyAJ3WoYKz7wuaWJ8oBj0|rZDfte84}t z>l^(^ZStT1l+nT~A17{+56Cf;eucC8voN|K6JKz?*CA+=VaJgGEv9L$@iqR{6x~FKp4pJK$NxA@<_lb8u?t#DuQqz z`PLKu6o)9ZCpMNcaCzP#=>n}DSXJl=A3HDL=%JBbQ~7_f(-+0g)sIbFNPSZSexMA} zJ|)56h}G;XySPf9X=ePKO-ZS5uWz3h%+ObdY-v{tFQvuJSMA6+9}ktR@QkD2`=zYa z__M51j%P)wTOteXUHOOmsAD^X4OPj*C@XXi=F*CpLm-3L3yzqdSc3EN9AodYzR;7O zto0fEa}jJ~_I7a4CnC`Y=5<=j?${@8^cOw8IpqDSQCK5)*kCv0&0R*JSXoYeB}vt!*6zCi!O+&6 z+TNuZ?0j~;PR)A(=A%>7)gjmcHLqmD;AA~Wh3`sd3?i2*Ob-I6Y<>UPnpt)9ypvPv zm8V1Ow+_A*QpSIO`Q#T?@ZNOb??DF+7ZK+Gv3U=XpoOdEjk2GNif@3AWUqdmQNfHe zzCD~Q2V0QUH#E8l;|Q>pwE*RO4Jds3zX_{)zu;!(TJV^8<#*Ix@9|%)Pc{n_HabiB zAQXC}{SImBABz(v_)3ITT?W}wVKs#i?Nt-S?* zQ?aLA)oAFbTU)KdyGEfH`3tCoS3=V*dDSL{?c3C0&i=7lS7P-F{Sk#C!Ty2Plv~~v zs8v=Djc@u`Q@v{PS#kZYzV%XL)IF8r2X<}!2snPG88Ty3wT*p_awGy3J{ zT|T9NTxP8zGY*{+UsyFOJFeGS@{ObE_ z9h7Q#38T|bSJT6RlN^^1etWDKE=BT+9f3(R2=TJcGiEOG`V=Usy0+3SVI6GtsI<@>{f#;4%< zjd%rbc8`{HDGePhXrpXgiF)vu<2%#QN1^Z-sk4FS?T8!Q=TGFBBCiRdNBG5_2z|b_ zuj~`3Ote>l|J$f>@H_%`cv{tpQrEqi-uQ&R_1CuKutoBsx@|L^%;vMQoF zt|-!%?WY4-77BGEl66=CQd8-R&*Nik*R{cdTau z1>2nmu;)DQX}$SIc<8v!)by_Owe+3I>&efzohZK_GI>e;MEuH!DI|Yrop@r1WuA;E zLKqb!4iLH*nKJujH^!A2UgY24Lcl>3+zHupL3(Z4d$+}uTKQwGXmD+F)tLQGe3>8J@idRI_@d;Z;%-0sl(!*ZmPuCQb%N@&g}tFx$YHc_E%5j|2R}s# z&YqD4ET1)hjx;0SrT}S26$#yZFtJ&xD90U+lEkG*5C0w)1N$yfAOlL(&tw(z1~e>O zZpUJfX398b+}wm#!g02?j{hZ!UMK6IyVfr;){I5Ij8A5HZRF&`x%cWfwCIHHBDADw+yC-NoRm0-b!Q9;{S7$U`GIR!4y0IPgqcMmuS6BWq7m zJq%X(!c!~M3ox<7)PcQ}{EG;9*{*)cDf0;`K=^ZdkRZKL;*2%(_)(KZl^@};Y+O^J z;R~a=lmNLe*yp%Pc&#mAILZCB_h%4|&vOu?mk6-I2hs>Iviq6%_rykK9kO*{FMm@V zl5+72I)($|(v`sV`n@~EdrVS2tkbcZ#1@}H?D#?c#-3SiydBzk%nk8Z@>c@>F#g+V z;%$5ZkuPEvzk@C;3%=feBwsX%Dx|+t_Q9-^4givb3T_y7hG490M`$ELEIiW9#-j_{ z$lk$@Ix(GS=Txi>!A^m6Xi|x6QmK=K`;*(u)D*bXa`OU$D2b;{qrNbIMBV{*BJG*a zNr9l2^*ADrZ-oE%;{!4I?G+8`#}72*A3xat|35zddls6k{^pK4g8n(2(p0eo;9`Ls zB*h3=tdFVsF)Vo%EV2p-?n};smRUHhg){Lz3r*fiW-gR{m3WodXq3(5@Zt2C`)Qj8 z5tJuuA@L?Wb0*Js^!WI7%E(<3%R?(YT5r>TcXxTY#j~^d^8HV=@}=j8+pG7k*~ec< z?f|hc3a$`@9RjI4X@)-8rw|u{9UPK{m6K}l6FHc4Ks9cp6Kgu$P;gii`-H4BHy7Cw zn-Fy-cEUrES082{^|>%we(bq1I2GMy#2n)Ogv?v7zk;QksIi->vAd8caD~iUb6DNP zTXpz?r5l4txx0E7-dnhz)3lTNKwFUQV8iGch}BKI=LMGuDGnqHrF;_$9f6dKkwq8@ zgQSQ>jAjjnG;i{xH?uIKNO7^#6EMene(=6QMjeZa>XMCgGi-UrHIDs5~{z;%X zv)wB$kNdpOlAE4=!1#pD8u)1YKE~@N_LkwQHvZDxzgSs~Wk>ni{$hae84js>J-(N&WcL0{A zo)=*>Yil$T(AleeXWS}q z9UlG9n7(FChM@%wkI;*td)1h3Tgu^Kmoe*Q>Eg=$wG3T4V_p9V_2!t5yfdXZ^e>F( zf)d209R5L~4RXyG4|OSRq_N2BN6odYKHXOxT-)s2=LrW+x!aVgKMTQH`F}hjn$;#a z$Sseq&;%-@u(p)QKfk>L=&=W3aCKhMy3qDz&&a-&`va$nj#%OG`?Xw6|K>*WIhQ{JP9(x;;5p* z^+5#V7h+mugTpWa;x3f)`TH;!(18@{d6KQgM$jt}K&w15H3b!lgR#DFDCDWdY#WtH zt)Y@YX!@knr_rVBE;s`P)Z(!#eqm?4Yp8Ym(r2*G(c9kFw(;3}kcOna%jS z?3sB%k(bC@h=j9EnL8JesLF_6zlYY+vFF1*Lc%PHMcwHl53>VLHF$HPlg@?6Ow$?k z9lXFV>?=HeuBdLJ%`It=FJQI9Bqf7D^$eLsBFHH3ZfI^T!lbM;>vML-59|Qiy!uB9?CCxh}vVXvqiIvR%jM^uIP+Bn+lYNxF}S-(GqEL zP@(4)G+nTplc7%0eq=+)jiE054!tkN5>nwWt+B1PDF_?0n&6k_RWhUw$0CSKNk(PV zEBm8j*{i)`_v?@xR0c!Z#MwnvxST3sWBEk}^Cu`Sy|us-CavD~(panx(P9mfLbCg# zb4a_SsjyR3tF5)mY_Wy)(l+@VMu_kCxs3+%mmt)7AxojTCIFfF5h6=~OU~AG$FjZW zCLLg=Whl_<|Do)hf^&@#I|kQwr$&X(&_HMx~l)`i~g$i z)xOvlYt^1>u32L|!>(L-JTVf{Xqi%DPVN+9x-D#N%-(@a3%KM>yAcX!Y zKip#h$fI0mT>5Dva;i!{?#oc3R_IsaQ99wqg0@dOarl#`QWMjLaN%R)-%ZvfUmpZzV!y08m$2gBvmf;a2Yhi|7&AqK@iP`ieoU0b;0Pe(ml3+WC8^fTKA4r=Y2*@thMOT?``O_;`3jKKROAH;^7J;1-)!>lzbIS5vd|bPs^tzHW7{6{8VbrI1??;XVPnDD!h$ z^~T-j>2hd9fMU^2U32F~wtx~5s;u~AT&v2}mLr%^r9LmwYC2wWC+B(evwtfr4EIRhRL7d#ikX@R1s-wLi(ESR?B+TA4c^`~-u0`3pNA(l5cRzny<@s6$L%He^1CI_NJTACnL1mT9oV7!^k{Z2mm| zLY6hyT}smxjjEov>ziV*9&0c;qYhhdl-S!AYYgC?Ce^Q3XV*#S41;TU5)hQ&FU+*v z$0UFb{|Xj>l~;1z=Ydr`cppvr?SzC=m?F7@oJE)_<tjUt|Js@0=@c7Ib?=T zOyz=j2lX-%%tv@^j(m=(n}JgM2jEx1I&8_}Vs8(T|t|YJk>frQATY0*1>gP4djR1_s*<$DnBw;Cp zNJ(;vHU!SPi?PdNuB@`Gpuo?>4Q+E1R*)!8=gRhKmBZ8|>HX>J1A6DzI;jFVrW!+R zXp4#7W)FCP4>1oGE)%ijA$owy{klJaYI0V~pN(}vme$L-xaxO!S52+a61;GYl|&$+ z+%b^TjJp`NM8riur*%OFqiG)5lC<#v{+TL^O;w!m=hk_&rGV>gs9B6L=s;{_8?SFd zbZmRw4~n|Jn7Vutp;DzUmaB09di^d!0@0L_(|+Fo$2$-2fjXu*6Y4jznO^wrK{uVg zS_FKP)|GPwRXbTmOfIA3nUbE3#A-SL0@sY0pc$^27P9agkrLB_y0EC!WmHer-ZZH) z54tUb7|VOBUkcIOeho7#-jF{38(7#mQd~5O&(~x&?mNoJurn6S0Cy#Gg+bPx(_T^5 z>wl9<-+9JyUHF4y9i<|Z?gtq?b4Y@JnhIt z7Wcc#Q!}*VCQkjz@$T@e;|5x#7egI-b|T})9c?q4%g@@6xKlJK_f6wH+k9b^<~7Qc zRIBpm3WKMYzT?{M{87tPibqTg*I1L%Ckmi+ttU9=Q-DJ%WK~v?KqTi^!p}bhwL+1ViR9SE=o(O@fMM3 zR8^W)se9gIf-sq3{fM&?v4AL1=2J7#@FE)!QTigoKPX5uiCC|yBe2q3 z3DWMky2%}Yh~xrxK{jvGKg-FgL4axjd;;@xo{}T9zV;m`!rWe``*Y72rVS}2?zw+B zBn5umIexiBwdZX=29%;B>g8Oili&|(AOa6Fg&02Uq&a4~LUSXD`GuYtfC?J?EXe5Z zQ`$OKq?~Gu8N-C$2sbF@Aw4Ldd?Gc$88fIvL`Kd&l0dvT#b5Su+KhiYg=0DeBRWNC zJOfOg&@(S^+EzXN!KDbPuS=c}1ksV{1<_JhRC8voNT{fM=4X+Oc zlp2C}h~IEl@#)ic$wkTWC{5K0%C-%No}1u@FpvcrXj+=5$J)s_7?vAaVTR^nYK`nH zrBG3X%z=9ir1Y0EKv0|%KAzE)!3jRC1%(yjks*F$VXgXYD28>v+Qy}i@*|<#@GL-1 z`q~MR7tgZd_h=O;P-b!|D4(c)3rUy;JBsZhSUQYVhj{Iqfawd_mNAs!BCiA6%N(=#EuK)KjpGg}tzwl!&y|e%Ii{U@p z#s4}Tw4uC|78m)Bw~{-OM?nMyf&KWwR|U}t$#e9#p#nwx_3Sc#c$#{QC?=#2O^Zzys>y8)jc=Kb+v#k!r16Lk`_JQ^&zr8vj?+Bj z-x;oAz7LCrvcNsUmuu7&D8ISsB|v%Hg3S(x5q0~#%=gDdyM&#P9{H0N@r%M;5K@-< zNwuw>dVbXg;WgbEr_)Y7@MoE^45)kYrPCc_?r9-(ZrFt0zxZ?!bp3#U2Ejh=1cfek zLc(8m_pI#0T9h!78I+S9*QsFb3&pfZT<(65X>CJ@%Oyq413G%>g|UI?+^mAaV7_@_T3TeHcO;~%(ut#$*$Q!?GqPC z7p4k(TSlO>zb6ZK%M|UV-O-nv?HI0mvX>m}){kiGp%>sLiP3$>ulA89_7W-fq0%v# zynVX&j2jxpXZOg}=C+LJdyx7)!2FR~>K;vZeSZkj^$|qpd${ClsQ=nd`?c$LKUnR2 zD~q6o&wDT~*7mXjf)R$$x=N;%m*(rf!+1-f%gZ=mDz$UGx(9R1A^Tv9`@knP?F)BH z^8A)f+ocfhYcxf%UJKfWr@u@^Uh9 zV1wykwk^YK1klDk6f^9xqkOA^&XO?!S8Qia`OFo?P8xmOMkyqd?;SV)%Ge*nWFCD; z?UPbD&Jmn(?$>-sEi^@h6o1d-;HO0n8sAsMYR| zr*d`&itwRWbvL5Xs*z0Qv16zb9DRr_RK;=t(5l!^K8JO$Bame{C~f1O1kq*O%s;?V zpCdbmTh$v86?$_YBNbwGtVZtwO(8jdt@9J}8H^DSYPy>^++Z~tq2FGAsgqAeLVzb$ zB+Pe8woJKapl$r^5KqhIX$Lv~B-r+BVXHc0-owVvmx+NH|n&rT` ztz#=7e$`1Ti4F5k)aF(@x&v^vH$LgrIvCS@cE>8Tb=8~d!1nx+MSD$ukQv-D9<%*C z(rz3=aLsUl;N)ISaJ`Syu^oeOawmX58_)MJ5@N)qyK*qX)8;i0P;LYx`IaBNF&;N> zKH^>R598dSTMRcQ8mn%c2rpR{^A3PuRRou7%w0HM?ED%*n>~LB19{Yeu3NI&J+_S) z#XM)|OnwkO0rQq8?~AH`Q^(JY4au_nc8U&9vog118!iu|Kcl6~JVU>YabFqr^69t^ z(ktpKoD6{kUZ6xcl7<$J=wR6wdo*TNbGq`F$g+E6;%kI0*!@_kY4I5={WYT19UYum zg8jPY1xNEWX1#j@-rYTkR~k`9nt0x~TUbUo8#IK;Z^-UgCNJTMGq{=5hLF*%x0``t zsgm8*=CP&FMy>hx@1pQphaEZgeUO2rSFcMq=*U*I-T*M%ZxslQ&xL9h#Jbl$wB0An z<=3WyrN`(sG$h@Aa1$~2>V-wpMicbGrod2L7;m^*J0q$-`%af)17^g?S0T8su^_)3 z-^V05>yc$qkQuqPCb;`^Bnc1GI|UQDPP(&VTr8UPCQxx&J1BohTmF%1_3QP6a@e>T zCC)v5xOX-%!r7HE@fN=mgn3G`2hLH-?;)4&a-PT>u{9xrIF>Xk96^@`?Mp|YfoZQu zwaffPf_9iu6#L}o;@CGeL8(Lzboa|z{+hbr7yf;dMesDCE;Y}AR{ zxS~f2oKpL6a*2%$x)Cp&T?n7bif5-`FPi=7AKZTg)vx=KQry>GScr4(`yv~DVm^Qg zzVDuvJbaBBt9egUhw^(dwD!Y6UskIY3}}GzD)+=!1~q1K$j_PdAyel;a`~-D)$5Rp z?xwcdpYr?{7Z=4EnkkAN=>22mw2BzHj+%Ozx)wViHM?Kax)&!pkfROF@~Gv+2*X7U zJuQD@-H`?okR;`S^(9gP#tkfq{8D)Qwwly=*pFxm;r!WVQH`pCX;L@25lz?d?vrt1O-RLaD$@(HaWV?t@rv3oFd4PdMnE#EEB}zog7IT zshw$dp_}oL5u|#%buo(rUb<555cRXymJQxS00DtG0tD+DwzZ`${8c$C>+s*!l$~7B*H4 zYd%VzP8V#a;s*nLmvA&4&cZFBL17!A-Koaxbw;azj;a(oWXp!Q~N=aN84Us!X^FmiWtpv>no zmsnzwzoa*aR(vZC*L+wjni-p5;A`>=5vIhKr1x+#(!y~ln>(49v=5hxz_uKcFiwwR`kAa1i&)6fVd zEiD1SWQL+*1^=T7mPl%O&=dLe@D-8*h={IO#H@cd(&{R-SjE#}nmNU5%$h1BleeGl z?6db0&hf*BzVusv#V}bIZ;Khj&$NI5l7MH?Jp(Nqc0fDdxyLwh1x53$dG;Qg#M#lS zh_{gmT`KOx)<@`TD;LhZ^^s!@h7IVKkA(@lDyh~CtuGn++HOfetgn4{GEI!WVv-^$v$# z`KPR+iOKc$-I>-uHCj${>b>;xucdUkuy&Dv(INJ!DW!Y%vb`>mzOlBp;8WdB7&1Sk z@zqkg{8jiW@eKlDxlV+U0ex zXv!iR@fngT^_u2>xw_1U*AuLgED3rhUmSVM;ZMCP9y zOKLMqd$uHUh!Gm=Lh=_G`094+vT7$R-IHr#!|p4r4ldi19-Q%cS}K+a=zKW>w=Jwd zl6D*?ZUcwpR_4Ri~nng}90^4wvx=Rv8l zU1nm`(FWur?ws}b#YVJ0ZU&+~7!?8A4*#BF)8x8_E^}y26LF{QndMlKsRH;7c7JRS zYvaq=D`ftHlBQWounTl~vx;a~phQuBXDV)0W6_Bx>&hu3&FU?UEdsb|Fv!K%h9hi4 zgCR>K#y_$y&%|?)f;UGCOJs-4OKgE$Qkc)xY25T-BR;2B#;_NO<5W8X&zOO#P6&6B zb7jD!mHK5^XJ>k?+XEk`v&K*coRz7E8nrAh9YvSID&6WFCZHCT2T3Z@Fheu_EfQ)1 zwppEn-;ncmWvtEJqW>}$oG;yov5P{^2-vnc%ydtkR3~6L&5dcxzyOvlZDlH1+H3XS zx)k^C>0EIJpmtPJ(+lH05jTb~k>-Vpupic}5z$+UZC>l4tkg!BYI+MU8$PVeBeD#q zP0Qf1Xf?|}S4aG%y+~ipo3uN$S4v$SBLB|KE+#i=25{eEox(SD59PkNq=c4 zf72iG9flNri2GQL$sTk|y;9JJ2^3fmj9seG9B;cqPX=+=N4Ses+ zATozkjCC~AfhO#Wb*T20#7EzO`ljk$IQ*7+O~kYwt2PIZrEppcbnm5yfY9NpdB*J= zT;LZ%$?h+fmfq217J!cKKtn^*QOzs=$9bB@%pyHbZ_hL*R8)GpvE4%!dI6U4C6N^f zYu~yoMfK9|B!C{3Mhk zkHiv;C0f$4A!(DFh1t4jd3c508R*fmG#n?IL^T&SM)dIk*lhSB1+P_J22at5Vx3uV zXyon_^8N%KBZ6gQ2;6O=q>Ad+cs`4&{@>xRN1#c zYL*BJ@BbB3ic=}wKm?6Fxlu9~E8OkJa9lv*Tcd?QZR_ek$Usy-YZF`8I9lIy=}v#@ZywMCty z$VVXccoaHKrn)=^@aJyWYq`vJqQb)5K!D5e-#~ zIE}e@k|OnmbucgTR%#xWUU<&0l_CUX{cW6Maa9(`WU%<9=dwt}tZ*LtHg9@98 zKa;9&O9hQjO5@PhLe`&Ps;A&O*RI0f5d}@ZjwB4L=$YmQXA*MQmT8bg=hggN`jWU( zW_c-fZS;>{#8o1FNJ6g;UM2aFb*|>ZHG=!=d@dtPKnE&3g^7kUI@_&}*RNc5UVcli z&M1*4|dk*kUBBc^Ok{9K&5Jg}IdOMrjWE!waFWNr0$~Aq(p* z(?eZw;(7!g z^1_UGdb7p)2*1Wi&MZ2FCMJiwVD#^gT&T|U^aJ7W(#%pEv6p(*n#pOK{B^Snk||_M z=SwR^E-|j0fS< zth(69tPQa)1c>*p=`^%*mq>h<0}*aHNhZG}6Fjf^@}8xB_et=Yvk=728Y-gYB`NFp z?824Pjc$q7=^GVvWL!ca^5##%4~(3Uw8h#X+WKpf*-Zf4^n|qtt=6kYB49|F8rUE; ze<#<120osx!7H|h60Il>V)2BTwLPSiYFB9c)8{07PagPgiRce`C0isD;KwSsfFfm9 zY1>$+nyDhA*Q#D2SVIf<5j<5Zx49&iUM^IT-gIc~CVQYv4_@BN^*-9;?!FN5P396> zG_`&zQh7x8Aiq}6+B=Gy5Xy63rQ@0ir##WX^B@7UJK&zIvGDl)U9j>(j*pvhj<%f( zS8ikG>pQSb;4k#?ehp0M&|Gq5!rLvC%jfKxyj}U zeyiy0xERZQ{vh(Y?znjhE;>Geru2KYPw9&v2oQnfCo(H zbr6Y(tI&Kfo&S`jp(|t!ysS_%!2mBNy95{*=s5@+$whvOu)opAK!GS3eySw#w>p~k zG(;fMfp?C(5&Ty%kY1f%8Kt2xAz_+HJSVXLl}r6^C~Xk|R2(G@C34QIV%nwiDF2W5 zdnl7MLA3P3e&80%`ss|SPT9RSW#PDH%n_nTUNGO9%~6lKvqNk8TBW?;w=m&x;A?*x z3)uA~3hkff^Qex&^n4II=Nd8d&>uXscsAD-zczb<$H{K4b`jH z8L@!3%+a`?JXszI@pJqIFj{Qe9bFGb6}FoStr1M!7#vhZy z3ht#_Bt(^Bc{T%z6Ov{7s~^T zsR|FZU`)jP*0mrU7iLOQQl+asaWz%<mx3w^TRw88(1UgaI+Zar z6w2wt(}%T$S6)#hIp;|#ij#YMS+x2wRxBq~3s8r0QS!iV8{!0vD**adw`Cx>#A-(f zCZCs^*|^}tnz!v4u_8aF7&EAp?XcI=fLT2Pr{?#_;4)x415vYhV?DaI=%4h;cSb%A$;^}n*ye6;CWDCW1-+67c3CT z4+U6f%eZGLcRe1(J__7JJa0E*HE~g26}6BRjR^UVX`iaYGkf|vb?veP$K!Jnm(A>( ztj7b3;TTh;RqFKH8eGl7DQ|FM1#WqR7#6T^@w(;pmfl|gVTAM3-M~z>+JwKLxvC+O z7qez0WTlEnK<{L|AMKGA=WMW~nhkEErN{fPo3cbt4U;@8Ch6%*SkD^;Eu&wb@p`%9 zzlY?$d;R2uh%fzivwRw6=MlhOm@B;3BKgt_v-;2P1;ACK|9nvZxCH)!;xVmm*L>fC z=H2&IS_|-j1Zt(j0E%ITA8~tIQpIC+{KJSm)!aUIye)bRhZ)2QFB6ae@7K&*jkzH%s>U)eAc4O6XS5!!{a$4fw_!hVu9Z zi@&T}IMSJdM>jZ`Yg%F2J9(2T-d29%=NhG2BsbMfgZK!xT> z5I~n&*n;seSJA1L;3A;1$$N7VWJQ^#bkU6QrfAQP^Ej~z1A^j|fK2jBb#6pD2972M zr6bsPc}Uc77b#;O#~RGgi|ou~X?P zFcxBm90&?`qEH#+f)f_Oh7@-L7-mM#6>T!*?<<@Rd7X98j# zWo-+zvVmV>*#KdA>72D2mr)6FxMk7axlLY_>nHPwLZyS`_`krC6MuPU<9EFWUd(t( zX-_?T?lN+UMi2Ic1J3{Tl&O>WL@)YnRpzO??%_05DA!%g@&pL~pkt&*)qAo62W?%AIp$i{TKBMPVT$1Q|kcfj&@y#Ue~wP z=CP)<6$S$QunUY2-*YV7Fr3T(FN#80;DtabgET!6L$_~bTddpt{tmBQE< z-QR77-Y-&@V`ge%;3#Q`q*3!aFlyZ)?slRrbgCDJQ(a?-SOXGjLJL-}h9Tx4VTz!~ zb?2eS173vM@1@qoP3qPRDebcnVZILy+=_&>issNvUWAMY7V8+IHR^FQRmT#X`X*#j z7QyPnb{#D$(pn+(aQ)WrK#bmYNin5^z{JWlv83?#nc-YK(QvRiafiq|QK*IrZdV*EO{^8bjzc2L74$VZYUH z411*y9I9hACy#_(!Mn3ZgzW`7&~?;fw(fVEaAmEG={Pc6DSS&>dx#nszJ(I=2qfYe z0vVkP5HT+p4wvQ3ES~YX=c;dr^6>O3zPjWMUS}BFm7P$Nj6QW*C*XhtJL;GjIwJz6fC|3BSgF z*w?%TDoLh&wi;cr2MvoN>n!m%v?Ep8rJft$pe=9gA+Yw7#!k+X#}0TQ{8}NsGcbVC zTuvokhJ$p}$}lIk=s=fm^#O;4$bs_96|PTL-y$|Syfb6RbJF@Xyi1tk7p|=|=~R*i zSbJxbFYH$m!LvP*g{07UdXrhVo223bUF$cp)Hs?Up%*a>cDru`snH<{WNdAs<;g}xE#AV0Tl{Y#< z?AJ9e*u(sWQ}UT)AhK1&86UzrIOv1jU3=;q`a~ocV~e8+Q*f@BVx@b}@ust?)3MnD zf8ez-YeH;qme9*-p^)Aj3NcUWrAI3pK?9)P?q7l7c?t}eIXV(_lGkoI# zk3?+PJa!b%O_i_QIA+~GlHt;q8Wu^u(>dr87b|_x+#Cb6E&=USH2x=|mQ=EKkJ_x;R5M1 z|9kiMZpC>pjDeo*LgbuvI~4xZ6#HSo8!GsJE+Q?xZ>SBnHFZ{!D@W8?zoZ_ z!0yT-J$$uam|LY}D(Pyy=`8$r#)&bRYD9NV9R5F_Q@MuF3vxwG@p~N=Qz1VQr+M!8 zox9E*NtQCUnCrJH%haGL#%()ro~_~6YwY|{1E9Jbp^W;yfs zPYU?Grv|xG)v8}J4z!NyY*h^vu?NzsL-PmgKrPUI>Q+3u4uAJpeTD*dYBB9u!bVsq zfbh2f1>4esc66i1-UAB|depq)>z{~H1mdg#Geds_Q8q#NBYnH|6rbZF$&%_E;%S-} zJ5Z`F{{l&5R@X14qHeo)GfaA_xEe#?s(`=~=X9kS=@I@(&%2L~{c!2!_xksz9|cq} zi@4Q0nD|&dE+fE>ninDd7A!@&tnHUB@yKHwk@RZ=S5lFzi-d4n0KCusnwz;TH$lXC zBhypgW9jd2M*C|rb5GjRtPAWBYuGa&qE?_HsGN9@p{uPmDJL0@V*Z^FdYGWch=Z2*N}1 z7*?ZSKXD&(ocH3f-=V$+vg6%^wWhj;o?8Ti>dTv;?r4Rm3ELfbHmy)jl_1Qib=U-2 zWqbpv)}i~GAv^EP5JHJRvyG=e5P(OxwrNvsOrKviX!Oeai|A%g4&{t6OcWg(h1VhL zWe$TyedxQZuw&>+EK)R+|9^fE^Mp!$8ad3X~@ zpW0A=U*Rk7txd;Bij+&{bd`a7h#PvSW=Hs=RlUXo?ddbLF1O8454KNNWK3Hp*$TG@ z(I9p_u?s?@Fo9R|W(MilsJ#Y{lEc0S5Kxeae1zNY8GH<;9rz|?MKP0FkcJZ?K>ev? zMeST$W`;AJMyF2#+pVZohY+@9Z+#8eA25h=b z`vkHHvr{T9ud4!GGNx1aZqp03~-%SVd5d@}#_*NFP(qoQNgi5XmsqKf* zn@Z%4%V2yZ@ef=5q0wl)w9RrH;bWhYuYOL2AWln*-geh_v@OvtyI+)*F~E@!WsJ~F z!dC&AH~s(mA~r%ecX#W=^rvZ0bW`NtV7cSo&B!i{gPrb-w|!QoCFD|8p&*?4I+|Vk z3@yiBYRk^jUFfkLfVwf24<9tZi9#;k zRkLS|LMh%Mwx^UqDyGV14`O8;enO42+nc`SGr}gx0OcE^8)df$gv%xad1Zl`mT5`) zc-SWA{Ub&?dnQ1*gLzPD@7Grf49EZ*wh~++wJODlG+ABup;ul$??r^8)xOSRE{cos z*XOvCO~3RTw|>B{Qvg<+J-GW#h?{a(?PA^87LJ}R@|>$}R}%F|u1oYvF!OGZD6e7W z)F&C{6#|VrC^}aHdFr7Q*&(rdkPDwFgu@xaiOrJWOJBrnQ8Ho>sX#I)s(gotzc^Zd zLYQ7rwR^y^z*X1VmKcxmX+D{1@HaDoS9sw@{F)J+Q`jcgX0<+|!$rP?1JY*!iq6&R z(_RZtw=}dL#bT`*MyGPz+ZylVq0|9cS~gMTODp$DiNbtonse8=?bD0z!vOsBnEr}? zE_KeLQ?VW}V zw{c=|&s8OSTa#u?IrvCl!;BSGfFTHel^*5jm+xP-oU8C%?3S!O)-FCtr1ZvDX*uyf z>%!EVQdfpl2mh?)h^PDv?9tr@SLH~jf&d0wQW(em2KEFMs9fUJwEl_%gEPdmek%49 z0|RJwku5+~7+kyH_AFkqS|GBs_w{?6ZZcX3*P*{N8~OBT75gR%a{OSbvQ)& z>Mz}Iz(B(-8 z3D?u)bwy*O`r8*#dIOovi<``g2qTNJN@1Sja97U!^S>Ngnpl3o4VCoq8b4vs`R*e@ zwn`FQO0bg2IGG6+(71uHxBU;T6!L+p3@WJoqo*#zQSl0M4FHzDQoW>s@GD*&*ENdP5h45`6ULJeUaCNq(cK#4dK;o zYPPe8*A^p6o)k}S&o6MR&-_*qb#?Q z4obE$ah<1mZUsWO;)(0WlfU<8IM$DJfo4+)T&S2!j1fh57OX`nce`DXbaoJVn^gPw z#D(1c=L@1fU)jKK0-jxi3~9G8p_f1DH@(89q)1a7yfmgU((WddhrUkoyeqk0UeyWT zByfkRr3u}#3BLRZ--QrklGbD6f5uE#@?wYI3`oexocna4lEReAzOV8)r*qr1lnN9L zd6ZrF@^C&|AXu_;x_WX-P{oR4)p(R6`J}08QJ^T)GpR177@uuRwZAXVC^M^u*>xWk zx=p@$oUy)%^zM`qbn7pp4L{b6hI4WZ7TT zK|DBe3Wr}GAGXVg-(k;0%~{#CFh%rdT~L(!*5B!%Zj1CmsS*n9JL7GA6W%x7#i&qA zY%|rkLDkjma7(M3_v{cj*V!Q{Qnt9zkCVSCrqt1^=cxTXGN71>jP7Xa|1pIHDMoxu zc zcX7p@{oK|wA7+B3tvql^vEcsUgG1TlBzl%zWShF^)C2gyDT~^X?0JQEhDeAJH}a$4 ziJ`04Z(+_AehOe=#5g;^zZvy1ixWYN0jm)}?@M%GzlbB!i+BxR9KxX*S0o*2m$v!+I=y+_^ ztve&c^U&)e4(29DE0k_j*lJT;mYFXrilZoSSie+y3_54%$C+DgEY%@npLAE4ibrS%DkKh#bcfL$ciW%N-du z*4ZxMCs=TFUN4UuM`esjyx-fdt%3~B_OHq+B+_ZfWF3~v2%KG*mEXA0FWlI3{cfwE zVq-|182uM<(c12f!2APr>iBiLIBt~Uk@_1qcUJQcj+6M~T^Dy=9Xh%m%|rMOX`P7Y z5y=|@cZx22y8h*Z!0zfCa}dKE)=%0D>1+d;giZJ7s_0K3j_X9S~I1 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 HIGX Date: Sun, 21 Feb 2016 14:26:19 -0800 Subject: [PATCH 421/826] Add HBase appends and iddle connections closed stats. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 64cf75c779..c38bc229d9 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -548,12 +548,14 @@ public void collectStats(final StatsCollector collector) { collector.record("hbase.rpcs", stats.deletes(), "type=delete"); collector.record("hbase.rpcs", stats.gets(), "type=get"); collector.record("hbase.rpcs", stats.puts(), "type=put"); + collector.record("hbase.rpcs", stats.appends(), "type=append"); collector.record("hbase.rpcs", stats.rowLocks(), "type=rowLock"); collector.record("hbase.rpcs", stats.scannersOpened(), "type=openScanner"); collector.record("hbase.rpcs", stats.scans(), "type=scan"); collector.record("hbase.rpcs.batched", stats.numBatchedRpcSent()); collector.record("hbase.flushes", stats.flushes()); collector.record("hbase.connections.created", stats.connectionsCreated()); + collector.record("hbase.connections.idle_closed", stats.idleConnectionsClosed()); collector.record("hbase.nsre", stats.noSuchRegionExceptions()); collector.record("hbase.nsre.rpcs_delayed", stats.numRpcDelayedDueToNSRE()); From 58215c611690ca3cd6a3c0b44f58cc735953324e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 21 Feb 2016 14:26:19 -0800 Subject: [PATCH 422/826] Add HBase appends and iddle connections closed stats. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 900f038128..5e3f9657f7 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -553,12 +553,14 @@ public void collectStats(final StatsCollector collector) { collector.record("hbase.rpcs", stats.deletes(), "type=delete"); collector.record("hbase.rpcs", stats.gets(), "type=get"); collector.record("hbase.rpcs", stats.puts(), "type=put"); + collector.record("hbase.rpcs", stats.appends(), "type=append"); collector.record("hbase.rpcs", stats.rowLocks(), "type=rowLock"); collector.record("hbase.rpcs", stats.scannersOpened(), "type=openScanner"); collector.record("hbase.rpcs", stats.scans(), "type=scan"); collector.record("hbase.rpcs.batched", stats.numBatchedRpcSent()); collector.record("hbase.flushes", stats.flushes()); collector.record("hbase.connections.created", stats.connectionsCreated()); + collector.record("hbase.connections.idle_closed", stats.idleConnectionsClosed()); collector.record("hbase.nsre", stats.noSuchRegionExceptions()); collector.record("hbase.nsre.rpcs_delayed", stats.numRpcDelayedDueToNSRE()); From 01feeaf1c9024af4c94b29af80b1ff51bf8d8fba Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 22:39:28 -0800 Subject: [PATCH 423/826] Comment out the QueryExecutor class lines where the calls to QueryStats object have changed. Need to fix that up long term Signed-off-by: Chris Larsen --- src/tsd/QueryExecutor.java | 61 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java index 9a5683f01b..90cdca0165 100644 --- a/src/tsd/QueryExecutor.java +++ b/src/tsd/QueryExecutor.java @@ -206,7 +206,7 @@ public QueryExecutor(final TSDB tsdb, final Query query) { public void execute(final HttpQuery query) { http_query = query; final QueryStats query_stats = - new QueryStats(query.getRemoteAddress(), ts_query); + new QueryStats(query.getRemoteAddress(), ts_query, query.getHeaders()); ts_query.setQueryStats(query_stats); final long start = DateTime.currentTimeMillis(); @@ -241,7 +241,6 @@ class QueriesCB implements Callback> { public Object call(final ArrayList query_results) throws Exception { - query_stats.setTimeStorage(DateTime.currentTimeMillis() - start); for (int i = 0; i < query_results.size(); i++) { final TSSubQuery sub = ts_query.getQueries().get(i); @@ -436,27 +435,27 @@ public ChannelBuffer call(final Object obj) throws Exception { json.writeEndArray(); - ts_query.getQueryStats().setTimeSerialization( - DateTime.currentTimeMillis() - start); - ts_query.getQueryStats().markComplete(); +// ts_query.getQueryStats().setTimeSerialization( +// DateTime.currentTimeMillis() - start); + ts_query.getQueryStats().markSerializationSuccessful(); // dump overall stats as an extra object in the array - if (true) { - final QueryStats stats = ts_query.getQueryStats(); - json.writeFieldName("statsSummary"); - json.writeStartObject(); - //json.writeStringField("hostname", TSDB.getHostname()); - //json.writeNumberField("runningQueries", stats.getNumRunningQueries()); - json.writeNumberField("datapoints", stats.getAggregatedSize()); - json.writeNumberField("rawDatapoints", stats.getSize()); - //json.writeNumberField("rowsFetched", stats.getRowsFetched()); - json.writeNumberField("aggregationTime", stats.getTimeAggregation()); - json.writeNumberField("serializationTime", stats.getTimeSerialization()); - json.writeNumberField("storageTime", stats.getTimeStorage()); - json.writeNumberField("timeTotal", - ((double)stats.getTimeTotal() / (double)1000000)); - json.writeEndObject(); - } +// if (true) { +// final QueryStats stats = ts_query.getQueryStats(); +// json.writeFieldName("statsSummary"); +// json.writeStartObject(); +// //json.writeStringField("hostname", TSDB.getHostname()); +// //json.writeNumberField("runningQueries", stats.getNumRunningQueries()); +// json.writeNumberField("datapoints", stats.getAggregatedSize()); +// json.writeNumberField("rawDatapoints", stats.getSize()); +// //json.writeNumberField("rowsFetched", stats.getRowsFetched()); +// json.writeNumberField("aggregationTime", stats.getTimeAggregation()); +// json.writeNumberField("serializationTime", stats.getTimeSerialization()); +// json.writeNumberField("storageTime", stats.getTimeStorage()); +// json.writeNumberField("timeTotal", +// ((double)stats.getTimeTotal() / (double)1000000)); +// json.writeEndObject(); +// } // dump the original query if (true) { @@ -488,29 +487,29 @@ public Object call(final Exception e) throws Exception { if (ex != null) { LOG.error("Unexpected exception: ", ex); // TODO - find a better way to determine the real error - QueryExecutor.this.ts_query.getQueryStats() - .markComplete(HttpResponseStatus.BAD_REQUEST, ex); +// QueryExecutor.this.ts_query.getQueryStats() +// .markComplete(HttpResponseStatus.BAD_REQUEST, ex); QueryExecutor.this.http_query.badRequest(new BadRequestException(ex)); } else { LOG.error("The deferred group exception didn't have a cause???"); - QueryExecutor.this.ts_query.getQueryStats() - .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); +// QueryExecutor.this.ts_query.getQueryStats() +// .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); } } else if (e.getClass() == QueryException.class) { - QueryExecutor.this.ts_query.getQueryStats() - .markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); +// QueryExecutor.this.ts_query.getQueryStats() +// .markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); QueryExecutor.this.http_query.badRequest(new BadRequestException((QueryException)e)); } else { - QueryExecutor.this.ts_query.getQueryStats() - .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); +// QueryExecutor.this.ts_query.getQueryStats() +// .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); } return null; } catch (RuntimeException ex) { LOG.error("Exception thrown during exception handling", ex); - QueryExecutor.this.ts_query.getQueryStats() - .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); +// QueryExecutor.this.ts_query.getQueryStats() +// .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); QueryExecutor.this.http_query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex.getMessage().getBytes()); return null; From 629fc558d15d357805059ee12e3a8077eef56d92 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 22 Feb 2016 19:15:52 -0800 Subject: [PATCH 424/826] Fix up the expression UTs now that the response expects a callback from the Netty channel. Signed-off-by: Chris Larsen --- src/core/TSSubQuery.java | 4 +++- test/tsd/TestQueryExecutor.java | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 8ed1c23bce..f7a3e35177 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -288,7 +288,9 @@ public ByteSet getFilterTagKs() { } final ByteSet tagks = new ByteSet(); for (final TagVFilter filter : filters) { - tagks.add(filter.getTagkBytes()); + if (filter != null && filter.getTagkBytes() != null) { + tagks.add(filter.getTagkBytes()); + } } return tagks; } diff --git a/test/tsd/TestQueryExecutor.java b/test/tsd/TestQueryExecutor.java index a1a021156b..eeabd47b3b 100644 --- a/test/tsd/TestQueryExecutor.java +++ b/test/tsd/TestQueryExecutor.java @@ -87,8 +87,9 @@ public void oneExpressionWithOutputAlias() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); - + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); + final String response = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"alias\":\"A plus B\"")); @@ -111,6 +112,7 @@ public void oneExpressionDefaultOutput() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -140,6 +142,7 @@ public void oneExpressionOutputAndBAlso() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -169,6 +172,7 @@ public void oneExpressionDefaultFill() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -200,6 +204,7 @@ public void twoExpressionsDefaultOutput() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -245,6 +250,7 @@ public void twoExpressionsOneWithoutResultsDefaultOutput() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -277,6 +283,7 @@ public void multiExpressionsOneOutput() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -305,6 +312,7 @@ public void nestedExpressionsOneLevelDefaultOutput() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -339,6 +347,7 @@ public void nestedExpressionsTwoLevelsDefaultOutput() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -379,6 +388,7 @@ public void nestedExpressionsTwoLevelsDefaultOutputOrdering() throws Exception { final QueryRpc rpc = new QueryRpc(); final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -410,6 +420,7 @@ public void emptyResultSet() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -431,6 +442,7 @@ public void scannerException() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -453,6 +465,7 @@ public void nsunMetric() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -479,6 +492,7 @@ public void selfReferencingExpression() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -504,6 +518,7 @@ public void circularReferenceExpression() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -521,6 +536,7 @@ public void noIntersectionsFound() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -553,6 +569,7 @@ public void noIntersectionsFoundNestedExpression() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -574,6 +591,7 @@ public void noIntersectionsFoundOneMetricEmpty() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = @@ -592,6 +610,7 @@ public void notEnoughMetrics() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query/exp", json); query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); final String response = From 320ee4468fcf33be33d51533151e3aa0b0f66a6a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 23 Feb 2016 13:17:25 -0800 Subject: [PATCH 425/826] Fix gexp query unit test where the channel future wasn't mocked. Signed-off-by: Chris Larsen --- test/tsd/TestQueryRpc.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index dfd885fcb0..666e3ac399 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -549,6 +549,7 @@ public void gexp() throws Exception { final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query/gexp?start=1h-ago&exp=scale(sum:sys.cpu.user,1)"); + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); assertEquals(query.response().getStatus(), HttpResponseStatus.OK); final String json = From b2c987a0c9e3fdfcf6ca454a7d589b79f347ffa2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 23 Feb 2016 18:16:21 -0800 Subject: [PATCH 426/826] Add ignore unknown flags for Jackson to the TSQuery and TSSubQuery classes so we can parse forward and backward compatible queries. Signed-off-by: Chris Larsen --- src/core/TSQuery.java | 2 ++ src/core/TSSubQuery.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index ccdd48abf2..6ae15fc789 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -18,6 +18,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Objects; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -38,6 +39,7 @@ * {@code start_time} and {@code end_time} fields. * @since 2.0 */ +@JsonIgnoreProperties(ignoreUnknown = true) public final class TSQuery { /** User given start date/time, could be relative or absolute */ diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index f7a3e35177..8985d6f9cb 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -22,6 +22,7 @@ import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.utils.ByteSet; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; @@ -42,6 +43,7 @@ * {@code agg} and {@code downsample_specifier} fields. * @since 2.0 */ +@JsonIgnoreProperties(ignoreUnknown = true) public final class TSSubQuery { /** User given name of an aggregation function to use */ private String aggregator; From e2efe9a4875f8cddd3404e9c6b64f5aab4b72c0a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 23 Feb 2016 18:16:21 -0800 Subject: [PATCH 427/826] Add ignore unknown flags for Jackson to the TSQuery and TSSubQuery classes so we can parse forward and backward compatible queries. Signed-off-by: Chris Larsen --- src/core/TSQuery.java | 2 ++ src/core/TSSubQuery.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 5310d9a4cf..c4c3ba918b 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -18,6 +18,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Objects; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -38,6 +39,7 @@ * {@code start_time} and {@code end_time} fields. * @since 2.0 */ +@JsonIgnoreProperties(ignoreUnknown = true) public final class TSQuery { /** User given start date/time, could be relative or absolute */ diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 462ca6950c..8e9833f2f7 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -21,6 +21,7 @@ import net.opentsdb.query.filter.TagVFilter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; @@ -41,6 +42,7 @@ * {@code agg} and {@code downsample_specifier} fields. * @since 2.0 */ +@JsonIgnoreProperties(ignoreUnknown = true) public final class TSSubQuery { /** User given name of an aggregation function to use */ private String aggregator; From 3449a53933b753e7525889a089f809e5eb834f86 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 14:05:29 -0800 Subject: [PATCH 428/826] Add an explicit tags flag to queries so series that only have the tags listed in the filters will be returned. This helps to avoid picking up series with more or fewer tags on accident. Also implemnt the Fuzzy Row Filter for any explicit tag query based on the work of @junegunn in pr #588. This allows HBase to perform skip-scan operations and help improve queries TREMENDOUSLY when looking for a small subset of time series in a high-cardinality set. E.g. if you have hundreds of hosts for a metric but only want a few of them. Signed-off-by: Chris Larsen --- src/core/Const.java | 17 +++- src/core/Internal.java | 11 +++ src/core/TSSubQuery.java | 28 +++++- src/core/TsdbQuery.java | 35 +++++-- src/query/QueryUtil.java | 143 +++++++++++++++++++++++++++- src/tsd/QueryRpc.java | 2 + src/utils/Config.java | 1 + test/core/TestTSSubQuery.java | 21 +++- test/core/TestTsdbQueryQueries.java | 95 ++++++++++++++++++ test/storage/MockBase.java | 87 +++++++++++++++-- test/tsd/TestQueryRpc.java | 49 ++++++++++ 11 files changed, 461 insertions(+), 28 deletions(-) diff --git a/src/core/Const.java b/src/core/Const.java index d9f97c2aab..6369589354 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -12,6 +12,9 @@ // see . package net.opentsdb.core; +import java.nio.charset.Charset; +import java.util.TimeZone; + /** Constants used in various places. */ public final class Const { @@ -38,7 +41,19 @@ static void setMaxNumTags(final short tags) { } MAX_NUM_TAGS = tags; } - + + /** The default ASCII character set for encoding tables and qualifiers that + * don't depend on user input that may be encoded with UTF. + * Charset to use with our server-side row-filter. + * We use this one because it preserves every possible byte unchanged. + */ + public static final Charset ASCII_CHARSET = Charset.forName("ISO-8859-1"); + + /** Used for metrics, tags names and tag values */ + public static final Charset UTF8_CHARSET = Charset.forName("UTF8"); + + /** The UTC timezone used for rollup and calendar conversions */ + public static final TimeZone UTC_TZ = TimeZone.getTimeZone("UTC"); /** Number of LSBs in time_deltas reserved for flags. */ public static final short FLAG_BITS = 4; diff --git a/src/core/Internal.java b/src/core/Internal.java index 733dc04859..5a40e073bf 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -123,6 +123,17 @@ public static long baseTime(final long timestamp) { } } + /** + * Sets the time in a raw data table row key + * @param row The row to modify + * @param base_time The base time to store + * @since 2.3 + */ + public static void setBaseTime(final byte[] row, int base_time) { + Bytes.setInt(row, base_time, Const.SALT_WIDTH() + + TSDB.metrics_width()); + } + /** @see Tags#getTags */ public static Map getTags(final TSDB tsdb, final byte[] row) { return Tags.getTags(tsdb, row); diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 8ed1c23bce..91bade8580 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -71,6 +71,9 @@ public final class TSSubQuery { * tags map. In the future we'll have special JSON objects for them. */ private List filters; + /** Whether or not to match series with ONLY the given tags */ + private boolean explicit_tags; + /** Index of the sub query */ private int index; @@ -87,7 +90,7 @@ public int hashCode() { // NOTE: Do not add any non-user submitted variables to the hash. We don't // want the hash to change after validation. return Objects.hashCode(aggregator, metric, tsuids, downsample, rate, - rate_options, filters); + rate_options, filters, explicit_tags); } @Override @@ -111,7 +114,8 @@ public boolean equals(final Object obj) { && Objects.equal(downsample, query.downsample) && Objects.equal(rate, query.rate) && Objects.equal(rate_options, query.rate_options) - && Objects.equal(filters, query.filters); + && Objects.equal(filters, query.filters) + && Objects.equal(explicit_tags, query.explicit_tags); } public String toString() { @@ -149,8 +153,12 @@ public String toString() { .append(", rate=") .append(rate) .append(", rate_options=") - .append(rate_options); - buf.append(")"); + .append(rate_options) + .append(", explicit_tags=") + .append("explicit_tags") + .append(", index=") + .append(index) + .append(")"); return buf.toString(); } @@ -293,6 +301,12 @@ public ByteSet getFilterTagKs() { return tagks; } + /** @return whether or not to match series with ONLY the given tags + * @since 2.3 */ + public boolean getExplicitTags() { + return explicit_tags; + } + /** @return the index of the sub query * @since 2.3 */ public int getIndex() { @@ -347,6 +361,12 @@ public void setFilters(List filters) { this.filters = filters; } + /** @param whether or not to match series with ONLY the given tags + * @since 2.3 */ + public void setExplicitTags(final boolean explicit_tags) { + this.explicit_tags = explicit_tags; + } + /** @param index the index of the sub query * @since 2.3 */ public void setIndex(final int index) { diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 5d96c4e1c3..8ea2f48509 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -95,6 +95,9 @@ final class TsdbQuery implements Query { /** 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; + /** * Tags by which we must group the results. * Each element is a tag ID. @@ -140,10 +143,14 @@ final class TsdbQuery implements Query { /** 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; + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; - + enable_fuzzy_filter = tsdb.getConfig() + .getBoolean("tsd.query.enable_fuzzy_filter"); // By default, we should interpolate. fill_policy = DownsamplingSpecification.DEFAULT_FILL_POLICY; } @@ -307,6 +314,14 @@ public void setTimeSeries(final List tsuids, this.rate_options = rate_options; } + /** + * @param explicit_tags Whether or not to match only on the given tags + * @since 2.3 + */ + public void setExplicitTags(final boolean explicit_tags) { + this.explicit_tags = explicit_tags; + } + public Deferred configureFromQuery(final TSQuery query, final int index) { if (query.getQueries() == null || query.getQueries().isEmpty()) { @@ -334,6 +349,7 @@ public Deferred configureFromQuery(final TSQuery query, sample_interval_ms = sub_query.downsampleInterval(); fill_policy = sub_query.fillPolicy(); filters = sub_query.getFilters(); + explicit_tags = sub_query.getExplicitTags(); // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { @@ -555,8 +571,11 @@ private Deferred> findSpans() throws HBaseException { delete, query_stats, query_index).scan(); } - scan_start_time = DateTime.nanoTime(); + scan_start_time = DateTime.nanoTime(); final Scanner scanner = getScanner(); + if (query_stats != null) { + query_stats.addScannerId(query_index, 0, scanner.toString()); + } final Deferred> results = new Deferred>(); @@ -1042,13 +1061,11 @@ private long getScanEndTimeSeconds() { * @param scanner The scanner on which to add the filter. */ private void createAndSetFilter(final Scanner scanner) { - if (regex == null) { - regex = QueryUtil.getRowKeyUIDRegex(group_bys, row_key_literals); - } - scanner.setKeyRegexp(regex, CHARSET); - if (LOG.isDebugEnabled()) { - LOG.debug("Scanner regex: " + QueryUtil.byteRegexToString(regex)); - } + QueryUtil.setDataTableScanFilter(scanner, group_bys, row_key_literals, + explicit_tags, enable_fuzzy_filter, + (end_time == UNSET + ? -1 // Will scan until the end (0xFFF...). + : (int) getScanEndTimeSeconds())); } /** diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 1fc1363632..994ce86214 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -20,12 +20,19 @@ import java.util.Map.Entry; import net.opentsdb.core.Const; +import net.opentsdb.core.Internal; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes; +import org.hbase.async.FilterList; +import org.hbase.async.FuzzyRowFilter; +import org.hbase.async.KeyRegexpFilter; import org.hbase.async.Bytes.ByteMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; /** @@ -34,6 +41,7 @@ * @since 2.2 */ public class QueryUtil { + private static final Logger LOG = LoggerFactory.getLogger(QueryUtil.class); /** * Crafts a regular expression for scanning over data table rows and filtering @@ -48,9 +56,35 @@ public class QueryUtil { */ public static String getRowKeyUIDRegex(final List group_bys, final ByteMap row_key_literals) { + return getRowKeyUIDRegex(group_bys, row_key_literals, false, null, null); + } + + /** + * Crafts a regular expression for scanning over data table rows and filtering + * time series that the user doesn't want. Also fills in an optional fuzzy + * mask and key as it builds the regex if configured to do so. + * @param group_bys An optional list of tag keys that we want to group on. May + * be null. + * @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 + * @param fuzzy_key An optional fuzzy filter row key + * @param fuzzy_mask An optional fuzzy filter mask + * @return A regular expression string to pass to the storage layer. + * @since 2.3 + */ + public static String getRowKeyUIDRegex( + final List group_bys, + final ByteMap row_key_literals, + final boolean explicit_tags, + final byte[] fuzzy_key, + final byte[] fuzzy_mask) { 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 short tagsize = (short) (name_width + value_width); @@ -73,7 +107,14 @@ public static String getRowKeyUIDRegex(final List group_bys, final Iterator> it = row_key_literals == null ? new ByteMap().iterator() : row_key_literals.iterator(); - + int fuzzy_offset = Const.SALT_WIDTH() + TSDB.metrics_width(); + if (fuzzy_mask != null) { + // make sure to skip the timestamp when scanning + while (fuzzy_offset < prefix_width) { + fuzzy_mask[fuzzy_offset++] = 1; + } + } + while(it.hasNext()) { Entry entry = it.hasNext() ? it.next() : null; // TODO - This look ahead may be expensive. We need to get some data around @@ -83,7 +124,19 @@ public static String getRowKeyUIDRegex(final List group_bys, entry.getValue() != null && entry.getValue().length == 0; // Skip any number of tags. - buf.append("(?:.{").append(tagsize).append("})*"); + if (!explicit_tags) { + buf.append("(?:.{").append(tagsize).append("})*"); + } else if (fuzzy_mask != null) { + // TODO - see if we can figure out how to improve the fuzzy filter by + // setting explicit tag values whenever we can. In testing there was + // a conflict between the row key regex and fuzzy filter that prevented + // results from returning properly. + System.arraycopy(entry.getKey(), 0, fuzzy_key, fuzzy_offset, name_width); + fuzzy_offset += name_width; + for (int i = 0; i < value_width; i++) { + fuzzy_mask[fuzzy_offset++] = 1; + } + } if (not_key) { // start the lookahead as we have a key we explicitly do not want in the // results @@ -115,10 +168,94 @@ public static String getRowKeyUIDRegex(final List group_bys, } } // Skip any number of tags before the end. - buf.append("(?:.{").append(tagsize).append("})*$"); + if (!explicit_tags) { + buf.append("(?:.{").append(tagsize).append("})*"); + } + buf.append("$"); return buf.toString(); } + /** + * Sets a filter or filter list on the scanner based on whether or not the + * query had tags it needed to match. + * @param scanner The scanner to modify. + * @param group_bys An optional list of tag keys that we want to group on. May + * be null. + * @param row_key_literals An optional list of key value pairs to filter on. + * May be null. + * @param explicit_tag sWhether or not explicit tags are enabled so that the + * regex only picks out series with the specified tags + * @param enable_fuzzy_filter Whether or not a fuzzy filter should be used + * in combination with the explicit tags param. If explicit tags is disabled + * then this param is ignored. + * @param end_time The end of the query time so the fuzzy filter knows when + * to stop scanning. + */ + public static void setDataTableScanFilter( + final Scanner scanner, + final List group_bys, + final ByteMap row_key_literals, + final boolean explicit_tags, + final boolean enable_fuzzy_filter, + final int end_time) { + + // no-op + if (group_bys.isEmpty() && row_key_literals.isEmpty()) { + return; + } + + 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))]; + System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, + scanner.getCurrentKey().length); + } else { + fuzzy_key = fuzzy_mask = 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)); + } + + if (!explicit_tags || !enable_fuzzy_filter) { + 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)); + } + /** * Creates a regular expression with a list of or'd TUIDs to compare * against the rows in storage. diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index a9e87566c2..9a321025d7 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -630,6 +630,8 @@ private static void parseMTypeSubQuery(final String query_string, } } else if (Character.isDigit(parts[x].charAt(0))) { sub_query.setDownsample(parts[x]); + } else if (parts[x].toLowerCase().startsWith("explicit_tags")) { + sub_query.setExplicitTags(true); } } diff --git a/src/utils/Config.java b/src/utils/Config.java index f1ff0e03f1..4e8cb9ed02 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -501,6 +501,7 @@ protected void setDefaults() { default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); + default_map.put("tsd.query.enable_fuzzy_filter", "true"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 675dc1ceb1..92cd5661a8 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.core; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -30,7 +29,6 @@ import net.opentsdb.query.filter.TagVWildcardFilter; import org.junit.Test; -import org.powermock.reflect.Whitebox; public final class TestTSSubQuery { @@ -555,6 +553,25 @@ public void testHashCodeandEqualsRateOptionsNull() { assertFalse(sub1 == sub2); } + @Test + public void testHashCodeandEqualsExplicitTags() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setExplicitTags(true); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setExplicitTags(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + @Test public void testEqualsNull() { final TSSubQuery sub1 = getBaseQuery(); diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 0f8c84551d..051b5157e9 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -14,6 +14,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.never; @@ -28,10 +30,12 @@ import java.util.Map; import net.opentsdb.storage.MockBase; +import net.opentsdb.storage.MockBase.MockScanner; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.utils.Config; import org.hbase.async.Bytes; +import org.hbase.async.FilterList; import org.hbase.async.Scanner; import org.junit.Before; import org.junit.Test; @@ -39,6 +43,8 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.stumbleupon.async.Deferred; + /** * An integration test class that makes sure our query path is up to snuff. * This class should have tests for different data point types, rates, @@ -1446,4 +1452,93 @@ public void runRegexpNoMatch() throws Exception { verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); assertEquals(0, dps.length); } + + @Test + public void filterExplicitTagsOK() throws Exception { + tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setExplicitTags(true); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + // assert fuzzy + for (final MockScanner scanner : storage.getScanners()) { + assertTrue(scanner.getFilter() instanceof FilterList); + } + } + + @Test + public void filterExplicitTagsGroupByOK() throws Exception { + tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "*"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setExplicitTags(true); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + // assert fuzzy + for (final MockScanner scanner : storage.getScanners()) { + assertTrue(scanner.getFilter() instanceof FilterList); + } + } + + @Test + public void filterExplicitTagsMissing() throws Exception { + tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); + when(tag_names.getIdAsync("colo")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 0, 4 })); + when(tag_values.getIdAsync("lga")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 0, 4 })); + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + tags.put("colo", "lga"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setExplicitTags(true); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals(0, dps.length); + // assert fuzzy + for (final MockScanner scanner : storage.getScanners()) { + assertTrue(scanner.getFilter() instanceof FilterList); + } + } + } diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 73099e38b5..0a99c24197 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -33,6 +33,7 @@ import javax.xml.bind.DatatypeConverter; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.utils.Pair; @@ -41,10 +42,13 @@ import org.hbase.async.Bytes.ByteMap; import org.hbase.async.AppendRequest; import org.hbase.async.DeleteRequest; +import org.hbase.async.FilterList; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; +import org.hbase.async.KeyRegexpFilter; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; +import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; import org.junit.Ignore; import org.mockito.invocation.InvocationOnMock; @@ -632,6 +636,11 @@ public Set getKeys(final byte[] table) { return unique_rows.keySet(); } + /** @return The set of scanners configured by the caller */ + public HashSet getScanners() { + return scanners; + } + /** * Return the mocked TSDB object to use for HBaseClient access * @return @@ -1332,20 +1341,22 @@ public Deferred answer(InvocationOnMock invocation) * The KeyRegexp can be set and it will run against the hex value of the * row key. In testing it seems to work nicely even with byte patterns. */ - private class MockScanner implements + public class MockScanner implements Answer>>> { + private final Scanner mock_scanner; private final byte[] table; private byte[] start = null; private byte[] stop = null; private HashSet scnr_qualifiers = null; private byte[] family = null; - private String regex = null; + private ScanFilter filter = null; private int max_num_rows = Scanner.DEFAULT_MAX_NUM_ROWS; private ByteMap>>>> cursors; private ByteMap>>> cf_rows; private byte[] last_row; + private String rex; // TEMP /** * Default ctor @@ -1353,6 +1364,7 @@ private class MockScanner implements * @param table The table (confirmed to exist) */ public MockScanner(final Scanner mock_scanner, final byte[] table) { + this.mock_scanner = mock_scanner; this.table = table; // capture the scanner fields when set @@ -1360,7 +1372,8 @@ public MockScanner(final Scanner mock_scanner, final byte[] table) { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); - regex = (String)args[0]; + filter = new KeyRegexpFilter((String)args[0], Const.ASCII_CHARSET); + rex = (String)args[0]; return null; } }).when(mock_scanner).setKeyRegexp(anyString()); @@ -1369,11 +1382,21 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); - regex = (String)args[0]; + filter = new KeyRegexpFilter((String)args[0], (Charset)args[1]); + rex = (String)args[0]; return null; } }).when(mock_scanner).setKeyRegexp(anyString(), (Charset)any()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + final Object[] args = invocation.getArguments(); + filter = (ScanFilter)args[0]; + return null; + } + }).when(mock_scanner).setFilter(any(ScanFilter.class)); + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1424,6 +1447,13 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }).when(mock_scanner).setQualifiers((byte[][])any()); + doAnswer(new Answer() { + @Override + public byte[] answer(InvocationOnMock invocation) throws Throwable { + return start; + } + }).when(mock_scanner).getCurrentKey(); + when(mock_scanner.nextRows()).thenAnswer(this); } @@ -1470,12 +1500,41 @@ public Deferred>> answer( return Deferred.fromResult(null); } + // TODO - fuzzy filter support + // TODO - fix the regex comparator Pattern pattern = null; - if (regex != null && !regex.isEmpty()) { - try { - pattern = Pattern.compile(regex); - } catch (PatternSyntaxException e) { - e.printStackTrace(); + if (rex != null) { + if (!rex.isEmpty()) { + pattern = Pattern.compile(rex); + } + } else if (filter != null) { + KeyRegexpFilter regex_filter = null; + + if (filter instanceof KeyRegexpFilter) { + regex_filter = (KeyRegexpFilter)filter; + } else if (filter instanceof FilterList) { + final List filters = + Whitebox.getInternalState(filter, "filters"); + for (final ScanFilter f : filters) { + if (f instanceof KeyRegexpFilter) { + regex_filter = (KeyRegexpFilter)f; + } + } + } + + if (regex_filter != null) { + try { + final String regexp = new String( + (byte[])Whitebox.getInternalState(regex_filter, "regexp"), + Charset.forName(new String( + (byte[])Whitebox.getInternalState(regex_filter, "charset")))); + if (!regexp.isEmpty()) { + pattern = Pattern.compile(regexp); + } + } catch (PatternSyntaxException e) { + e.printStackTrace(); + return Deferred.fromError(e); + } } } @@ -1624,6 +1683,16 @@ private void advance() { } } } + + /** @return The scanner for this mock */ + public Scanner getScanner() { + return mock_scanner; + } + + /** @return The filter for this mock */ + public ScanFilter getFilter() { + return filter; + } } /** diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index dfd885fcb0..1a04d909d6 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -307,6 +307,55 @@ public void parseQueryMTypeWEmptyFilterBrackets() throws Exception { assertEquals(0, sub.getFilters().size()); } + @Test + public void parseQueryMTypeWExplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getExplicitTags()); + } + + @Test + public void parseQueryMTypeWExplicitAndRate() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:rate:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getRate()); + assertTrue(sub.getExplicitTags()); + } + + @Test + public void parseQueryMTypeWExplicitAndRateAndDS() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:rate:1m-sum:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getRate()); + assertTrue(sub.getExplicitTags()); + assertEquals("1m-sum", sub.getDownsample()); + } + + @Test + public void parseQueryMTypeWExplicitAndDSAndRate() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:1m-sum:rate:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getRate()); + assertTrue(sub.getExplicitTags()); + assertEquals("1m-sum", sub.getDownsample()); + } + @Test public void parseQueryTSUIDType() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, From dc387ab27c3a5c62f520e8b0e02ed685de0ee425 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 13 Feb 2016 14:05:29 -0800 Subject: [PATCH 429/826] Add an explicit tags flag to queries so series that only have the tags listed in the filters will be returned. This helps to avoid picking up series with more or fewer tags on accident. Also implemnt the Fuzzy Row Filter for any explicit tag query based on the work of @junegunn in pr #588. This allows HBase to perform skip-scan operations and help improve queries TREMENDOUSLY when looking for a small subset of time series in a high-cardinality set. E.g. if you have hundreds of hosts for a metric but only want a few of them. Signed-off-by: Chris Larsen --- src/core/Const.java | 17 +++- src/core/Internal.java | 11 +++ src/core/TSSubQuery.java | 28 +++++- src/core/TsdbQuery.java | 35 +++++-- src/query/QueryUtil.java | 143 +++++++++++++++++++++++++++- src/tsd/QueryRpc.java | 2 + src/utils/Config.java | 1 + test/core/TestTSSubQuery.java | 21 +++- test/core/TestTsdbQueryQueries.java | 95 ++++++++++++++++++ test/storage/MockBase.java | 87 +++++++++++++++-- test/tsd/TestQueryRpc.java | 49 ++++++++++ 11 files changed, 461 insertions(+), 28 deletions(-) diff --git a/src/core/Const.java b/src/core/Const.java index d9f97c2aab..6369589354 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -12,6 +12,9 @@ // see . package net.opentsdb.core; +import java.nio.charset.Charset; +import java.util.TimeZone; + /** Constants used in various places. */ public final class Const { @@ -38,7 +41,19 @@ static void setMaxNumTags(final short tags) { } MAX_NUM_TAGS = tags; } - + + /** The default ASCII character set for encoding tables and qualifiers that + * don't depend on user input that may be encoded with UTF. + * Charset to use with our server-side row-filter. + * We use this one because it preserves every possible byte unchanged. + */ + public static final Charset ASCII_CHARSET = Charset.forName("ISO-8859-1"); + + /** Used for metrics, tags names and tag values */ + public static final Charset UTF8_CHARSET = Charset.forName("UTF8"); + + /** The UTC timezone used for rollup and calendar conversions */ + public static final TimeZone UTC_TZ = TimeZone.getTimeZone("UTC"); /** Number of LSBs in time_deltas reserved for flags. */ public static final short FLAG_BITS = 4; diff --git a/src/core/Internal.java b/src/core/Internal.java index 733dc04859..5a40e073bf 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -123,6 +123,17 @@ public static long baseTime(final long timestamp) { } } + /** + * Sets the time in a raw data table row key + * @param row The row to modify + * @param base_time The base time to store + * @since 2.3 + */ + public static void setBaseTime(final byte[] row, int base_time) { + Bytes.setInt(row, base_time, Const.SALT_WIDTH() + + TSDB.metrics_width()); + } + /** @see Tags#getTags */ public static Map getTags(final TSDB tsdb, final byte[] row) { return Tags.getTags(tsdb, row); diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 8985d6f9cb..bff500a75f 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -73,6 +73,9 @@ public final class TSSubQuery { * tags map. In the future we'll have special JSON objects for them. */ private List filters; + /** Whether or not to match series with ONLY the given tags */ + private boolean explicit_tags; + /** Index of the sub query */ private int index; @@ -89,7 +92,7 @@ public int hashCode() { // NOTE: Do not add any non-user submitted variables to the hash. We don't // want the hash to change after validation. return Objects.hashCode(aggregator, metric, tsuids, downsample, rate, - rate_options, filters); + rate_options, filters, explicit_tags); } @Override @@ -113,7 +116,8 @@ public boolean equals(final Object obj) { && Objects.equal(downsample, query.downsample) && Objects.equal(rate, query.rate) && Objects.equal(rate_options, query.rate_options) - && Objects.equal(filters, query.filters); + && Objects.equal(filters, query.filters) + && Objects.equal(explicit_tags, query.explicit_tags); } public String toString() { @@ -151,8 +155,12 @@ public String toString() { .append(", rate=") .append(rate) .append(", rate_options=") - .append(rate_options); - buf.append(")"); + .append(rate_options) + .append(", explicit_tags=") + .append("explicit_tags") + .append(", index=") + .append(index) + .append(")"); return buf.toString(); } @@ -297,6 +305,12 @@ public ByteSet getFilterTagKs() { return tagks; } + /** @return whether or not to match series with ONLY the given tags + * @since 2.3 */ + public boolean getExplicitTags() { + return explicit_tags; + } + /** @return the index of the sub query * @since 2.3 */ public int getIndex() { @@ -351,6 +365,12 @@ public void setFilters(List filters) { this.filters = filters; } + /** @param whether or not to match series with ONLY the given tags + * @since 2.3 */ + public void setExplicitTags(final boolean explicit_tags) { + this.explicit_tags = explicit_tags; + } + /** @param index the index of the sub query * @since 2.3 */ public void setIndex(final int index) { diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 5d96c4e1c3..8ea2f48509 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -95,6 +95,9 @@ final class TsdbQuery implements Query { /** 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; + /** * Tags by which we must group the results. * Each element is a tag ID. @@ -140,10 +143,14 @@ final class TsdbQuery implements Query { /** 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; + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; - + enable_fuzzy_filter = tsdb.getConfig() + .getBoolean("tsd.query.enable_fuzzy_filter"); // By default, we should interpolate. fill_policy = DownsamplingSpecification.DEFAULT_FILL_POLICY; } @@ -307,6 +314,14 @@ public void setTimeSeries(final List tsuids, this.rate_options = rate_options; } + /** + * @param explicit_tags Whether or not to match only on the given tags + * @since 2.3 + */ + public void setExplicitTags(final boolean explicit_tags) { + this.explicit_tags = explicit_tags; + } + public Deferred configureFromQuery(final TSQuery query, final int index) { if (query.getQueries() == null || query.getQueries().isEmpty()) { @@ -334,6 +349,7 @@ public Deferred configureFromQuery(final TSQuery query, sample_interval_ms = sub_query.downsampleInterval(); fill_policy = sub_query.fillPolicy(); filters = sub_query.getFilters(); + explicit_tags = sub_query.getExplicitTags(); // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { @@ -555,8 +571,11 @@ private Deferred> findSpans() throws HBaseException { delete, query_stats, query_index).scan(); } - scan_start_time = DateTime.nanoTime(); + scan_start_time = DateTime.nanoTime(); final Scanner scanner = getScanner(); + if (query_stats != null) { + query_stats.addScannerId(query_index, 0, scanner.toString()); + } final Deferred> results = new Deferred>(); @@ -1042,13 +1061,11 @@ private long getScanEndTimeSeconds() { * @param scanner The scanner on which to add the filter. */ private void createAndSetFilter(final Scanner scanner) { - if (regex == null) { - regex = QueryUtil.getRowKeyUIDRegex(group_bys, row_key_literals); - } - scanner.setKeyRegexp(regex, CHARSET); - if (LOG.isDebugEnabled()) { - LOG.debug("Scanner regex: " + QueryUtil.byteRegexToString(regex)); - } + QueryUtil.setDataTableScanFilter(scanner, group_bys, row_key_literals, + explicit_tags, enable_fuzzy_filter, + (end_time == UNSET + ? -1 // Will scan until the end (0xFFF...). + : (int) getScanEndTimeSeconds())); } /** diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 1fc1363632..994ce86214 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -20,12 +20,19 @@ import java.util.Map.Entry; import net.opentsdb.core.Const; +import net.opentsdb.core.Internal; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes; +import org.hbase.async.FilterList; +import org.hbase.async.FuzzyRowFilter; +import org.hbase.async.KeyRegexpFilter; import org.hbase.async.Bytes.ByteMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; /** @@ -34,6 +41,7 @@ * @since 2.2 */ public class QueryUtil { + private static final Logger LOG = LoggerFactory.getLogger(QueryUtil.class); /** * Crafts a regular expression for scanning over data table rows and filtering @@ -48,9 +56,35 @@ public class QueryUtil { */ public static String getRowKeyUIDRegex(final List group_bys, final ByteMap row_key_literals) { + return getRowKeyUIDRegex(group_bys, row_key_literals, false, null, null); + } + + /** + * Crafts a regular expression for scanning over data table rows and filtering + * time series that the user doesn't want. Also fills in an optional fuzzy + * mask and key as it builds the regex if configured to do so. + * @param group_bys An optional list of tag keys that we want to group on. May + * be null. + * @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 + * @param fuzzy_key An optional fuzzy filter row key + * @param fuzzy_mask An optional fuzzy filter mask + * @return A regular expression string to pass to the storage layer. + * @since 2.3 + */ + public static String getRowKeyUIDRegex( + final List group_bys, + final ByteMap row_key_literals, + final boolean explicit_tags, + final byte[] fuzzy_key, + final byte[] fuzzy_mask) { 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 short tagsize = (short) (name_width + value_width); @@ -73,7 +107,14 @@ public static String getRowKeyUIDRegex(final List group_bys, final Iterator> it = row_key_literals == null ? new ByteMap().iterator() : row_key_literals.iterator(); - + int fuzzy_offset = Const.SALT_WIDTH() + TSDB.metrics_width(); + if (fuzzy_mask != null) { + // make sure to skip the timestamp when scanning + while (fuzzy_offset < prefix_width) { + fuzzy_mask[fuzzy_offset++] = 1; + } + } + while(it.hasNext()) { Entry entry = it.hasNext() ? it.next() : null; // TODO - This look ahead may be expensive. We need to get some data around @@ -83,7 +124,19 @@ public static String getRowKeyUIDRegex(final List group_bys, entry.getValue() != null && entry.getValue().length == 0; // Skip any number of tags. - buf.append("(?:.{").append(tagsize).append("})*"); + if (!explicit_tags) { + buf.append("(?:.{").append(tagsize).append("})*"); + } else if (fuzzy_mask != null) { + // TODO - see if we can figure out how to improve the fuzzy filter by + // setting explicit tag values whenever we can. In testing there was + // a conflict between the row key regex and fuzzy filter that prevented + // results from returning properly. + System.arraycopy(entry.getKey(), 0, fuzzy_key, fuzzy_offset, name_width); + fuzzy_offset += name_width; + for (int i = 0; i < value_width; i++) { + fuzzy_mask[fuzzy_offset++] = 1; + } + } if (not_key) { // start the lookahead as we have a key we explicitly do not want in the // results @@ -115,10 +168,94 @@ public static String getRowKeyUIDRegex(final List group_bys, } } // Skip any number of tags before the end. - buf.append("(?:.{").append(tagsize).append("})*$"); + if (!explicit_tags) { + buf.append("(?:.{").append(tagsize).append("})*"); + } + buf.append("$"); return buf.toString(); } + /** + * Sets a filter or filter list on the scanner based on whether or not the + * query had tags it needed to match. + * @param scanner The scanner to modify. + * @param group_bys An optional list of tag keys that we want to group on. May + * be null. + * @param row_key_literals An optional list of key value pairs to filter on. + * May be null. + * @param explicit_tag sWhether or not explicit tags are enabled so that the + * regex only picks out series with the specified tags + * @param enable_fuzzy_filter Whether or not a fuzzy filter should be used + * in combination with the explicit tags param. If explicit tags is disabled + * then this param is ignored. + * @param end_time The end of the query time so the fuzzy filter knows when + * to stop scanning. + */ + public static void setDataTableScanFilter( + final Scanner scanner, + final List group_bys, + final ByteMap row_key_literals, + final boolean explicit_tags, + final boolean enable_fuzzy_filter, + final int end_time) { + + // no-op + if (group_bys.isEmpty() && row_key_literals.isEmpty()) { + return; + } + + 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))]; + System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, + scanner.getCurrentKey().length); + } else { + fuzzy_key = fuzzy_mask = 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)); + } + + if (!explicit_tags || !enable_fuzzy_filter) { + 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)); + } + /** * Creates a regular expression with a list of or'd TUIDs to compare * against the rows in storage. diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index a9e87566c2..9a321025d7 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -630,6 +630,8 @@ private static void parseMTypeSubQuery(final String query_string, } } else if (Character.isDigit(parts[x].charAt(0))) { sub_query.setDownsample(parts[x]); + } else if (parts[x].toLowerCase().startsWith("explicit_tags")) { + sub_query.setExplicitTags(true); } } diff --git a/src/utils/Config.java b/src/utils/Config.java index f1ff0e03f1..4e8cb9ed02 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -501,6 +501,7 @@ protected void setDefaults() { default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); + default_map.put("tsd.query.enable_fuzzy_filter", "true"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 675dc1ceb1..92cd5661a8 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.core; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -30,7 +29,6 @@ import net.opentsdb.query.filter.TagVWildcardFilter; import org.junit.Test; -import org.powermock.reflect.Whitebox; public final class TestTSSubQuery { @@ -555,6 +553,25 @@ public void testHashCodeandEqualsRateOptionsNull() { assertFalse(sub1 == sub2); } + @Test + public void testHashCodeandEqualsExplicitTags() { + final TSSubQuery sub1 = getBaseQuery(); + final int hash_a = sub1.hashCode(); + + sub1.setExplicitTags(true); + final int hash_b = sub1.hashCode(); + assertFalse(hash_a == sub1.hashCode()); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSSubQuery sub2 = getBaseQuery(); + sub2.setExplicitTags(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + @Test public void testEqualsNull() { final TSSubQuery sub1 = getBaseQuery(); diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 0f8c84551d..051b5157e9 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -14,6 +14,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.never; @@ -28,10 +30,12 @@ import java.util.Map; import net.opentsdb.storage.MockBase; +import net.opentsdb.storage.MockBase.MockScanner; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.utils.Config; import org.hbase.async.Bytes; +import org.hbase.async.FilterList; import org.hbase.async.Scanner; import org.junit.Before; import org.junit.Test; @@ -39,6 +43,8 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.stumbleupon.async.Deferred; + /** * An integration test class that makes sure our query path is up to snuff. * This class should have tests for different data point types, rates, @@ -1446,4 +1452,93 @@ public void runRegexpNoMatch() throws Exception { verify(tag_values, atLeast(1)).getNameAsync(TAGV_B_BYTES); assertEquals(0, dps.length); } + + @Test + public void filterExplicitTagsOK() throws Exception { + tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setExplicitTags(true); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + // assert fuzzy + for (final MockScanner scanner : storage.getScanners()) { + assertTrue(scanner.getFilter() instanceof FilterList); + } + } + + @Test + public void filterExplicitTagsGroupByOK() throws Exception { + tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "*"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setExplicitTags(true); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals("sys.cpu.user", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.longValue()); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + // assert fuzzy + for (final MockScanner scanner : storage.getScanners()) { + assertTrue(scanner.getFilter() instanceof FilterList); + } + } + + @Test + public void filterExplicitTagsMissing() throws Exception { + tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); + when(tag_names.getIdAsync("colo")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 0, 4 })); + when(tag_values.getIdAsync("lga")) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 0, 4 })); + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + tags.put("colo", "lga"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setExplicitTags(true); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + assertEquals(0, dps.length); + // assert fuzzy + for (final MockScanner scanner : storage.getScanners()) { + assertTrue(scanner.getFilter() instanceof FilterList); + } + } + } diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 73099e38b5..0a99c24197 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -33,6 +33,7 @@ import javax.xml.bind.DatatypeConverter; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.utils.Pair; @@ -41,10 +42,13 @@ import org.hbase.async.Bytes.ByteMap; import org.hbase.async.AppendRequest; import org.hbase.async.DeleteRequest; +import org.hbase.async.FilterList; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; +import org.hbase.async.KeyRegexpFilter; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; +import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; import org.junit.Ignore; import org.mockito.invocation.InvocationOnMock; @@ -632,6 +636,11 @@ public Set getKeys(final byte[] table) { return unique_rows.keySet(); } + /** @return The set of scanners configured by the caller */ + public HashSet getScanners() { + return scanners; + } + /** * Return the mocked TSDB object to use for HBaseClient access * @return @@ -1332,20 +1341,22 @@ public Deferred answer(InvocationOnMock invocation) * The KeyRegexp can be set and it will run against the hex value of the * row key. In testing it seems to work nicely even with byte patterns. */ - private class MockScanner implements + public class MockScanner implements Answer>>> { + private final Scanner mock_scanner; private final byte[] table; private byte[] start = null; private byte[] stop = null; private HashSet scnr_qualifiers = null; private byte[] family = null; - private String regex = null; + private ScanFilter filter = null; private int max_num_rows = Scanner.DEFAULT_MAX_NUM_ROWS; private ByteMap>>>> cursors; private ByteMap>>> cf_rows; private byte[] last_row; + private String rex; // TEMP /** * Default ctor @@ -1353,6 +1364,7 @@ private class MockScanner implements * @param table The table (confirmed to exist) */ public MockScanner(final Scanner mock_scanner, final byte[] table) { + this.mock_scanner = mock_scanner; this.table = table; // capture the scanner fields when set @@ -1360,7 +1372,8 @@ public MockScanner(final Scanner mock_scanner, final byte[] table) { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); - regex = (String)args[0]; + filter = new KeyRegexpFilter((String)args[0], Const.ASCII_CHARSET); + rex = (String)args[0]; return null; } }).when(mock_scanner).setKeyRegexp(anyString()); @@ -1369,11 +1382,21 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); - regex = (String)args[0]; + filter = new KeyRegexpFilter((String)args[0], (Charset)args[1]); + rex = (String)args[0]; return null; } }).when(mock_scanner).setKeyRegexp(anyString(), (Charset)any()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + final Object[] args = invocation.getArguments(); + filter = (ScanFilter)args[0]; + return null; + } + }).when(mock_scanner).setFilter(any(ScanFilter.class)); + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1424,6 +1447,13 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }).when(mock_scanner).setQualifiers((byte[][])any()); + doAnswer(new Answer() { + @Override + public byte[] answer(InvocationOnMock invocation) throws Throwable { + return start; + } + }).when(mock_scanner).getCurrentKey(); + when(mock_scanner.nextRows()).thenAnswer(this); } @@ -1470,12 +1500,41 @@ public Deferred>> answer( return Deferred.fromResult(null); } + // TODO - fuzzy filter support + // TODO - fix the regex comparator Pattern pattern = null; - if (regex != null && !regex.isEmpty()) { - try { - pattern = Pattern.compile(regex); - } catch (PatternSyntaxException e) { - e.printStackTrace(); + if (rex != null) { + if (!rex.isEmpty()) { + pattern = Pattern.compile(rex); + } + } else if (filter != null) { + KeyRegexpFilter regex_filter = null; + + if (filter instanceof KeyRegexpFilter) { + regex_filter = (KeyRegexpFilter)filter; + } else if (filter instanceof FilterList) { + final List filters = + Whitebox.getInternalState(filter, "filters"); + for (final ScanFilter f : filters) { + if (f instanceof KeyRegexpFilter) { + regex_filter = (KeyRegexpFilter)f; + } + } + } + + if (regex_filter != null) { + try { + final String regexp = new String( + (byte[])Whitebox.getInternalState(regex_filter, "regexp"), + Charset.forName(new String( + (byte[])Whitebox.getInternalState(regex_filter, "charset")))); + if (!regexp.isEmpty()) { + pattern = Pattern.compile(regexp); + } + } catch (PatternSyntaxException e) { + e.printStackTrace(); + return Deferred.fromError(e); + } } } @@ -1624,6 +1683,16 @@ private void advance() { } } } + + /** @return The scanner for this mock */ + public Scanner getScanner() { + return mock_scanner; + } + + /** @return The filter for this mock */ + public ScanFilter getFilter() { + return filter; + } } /** diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index 666e3ac399..0741efb487 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -307,6 +307,55 @@ public void parseQueryMTypeWEmptyFilterBrackets() throws Exception { assertEquals(0, sub.getFilters().size()); } + @Test + public void parseQueryMTypeWExplicit() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getExplicitTags()); + } + + @Test + public void parseQueryMTypeWExplicitAndRate() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:rate:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getRate()); + assertTrue(sub.getExplicitTags()); + } + + @Test + public void parseQueryMTypeWExplicitAndRateAndDS() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:rate:1m-sum:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getRate()); + assertTrue(sub.getExplicitTags()); + assertEquals("1m-sum", sub.getDownsample()); + } + + @Test + public void parseQueryMTypeWExplicitAndDSAndRate() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:explicit_tags:1m-sum:rate:sys.cpu.0{host=web01}"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub.getTags()); + assertEquals("literal_or(web01)", sub.getTags().get("host")); + assertTrue(sub.getRate()); + assertTrue(sub.getExplicitTags()); + assertEquals("1m-sum", sub.getDownsample()); + } + @Test public void parseQueryTSUIDType() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, From 47d94b8d2528ad7cf405b75faeadc993e81cb292 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 28 Feb 2016 11:18:20 -0800 Subject: [PATCH 430/826] Add the NONE aggregator that prevents aggregation without having to specify a group by for every tag combination. Also helps with #713 Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 34 +++++++++++++++++ src/core/Downsampler.java | 4 ++ src/core/DownsamplingSpecification.java | 8 ++++ src/core/TsdbQuery.java | 21 +++++++++++ test/core/TestDownsamplingSpecification.java | 5 +++ test/core/TestTSSubQuery.java | 7 ++++ test/core/TestTsdbQueryDownsample.java | 39 ++++++++++++++++++++ test/core/TestTsdbQueryQueries.java | 35 ++++++++++++++++++ 8 files changed, 153 insertions(+) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index ce18e6e560..05eabf4e45 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -53,6 +53,9 @@ public enum Interpolation { public static final Aggregator AVG = new Avg( Interpolation.LERP, "avg"); + /** Aggregator that skips aggregation/interpolation and/or downsampling. */ + public static final Aggregator NONE = new None(Interpolation.ZIM, "raw"); + /** Return the product of two time series * @since 2.3 */ public static final Aggregator MULTIPLY = new Multiply( @@ -144,6 +147,7 @@ public enum Interpolation { aggregators.put("min", MIN); aggregators.put("max", MAX); aggregators.put("avg", AVG); + aggregators.put("none", NONE); aggregators.put("mult", MULTIPLY); aggregators.put("dev", DEV); aggregators.put("count", COUNT); @@ -319,6 +323,36 @@ public double runDouble(final Doubles values) { } + /** + * An aggregator that isn't meant for aggregation. Paradoxical!! + * Really it's used as a flag to indicate that, during sorting and iteration, + * that the pipeline should not perform any aggregation and should emit + * raw time series. + */ + private static final class None extends Aggregator { + public None(final Interpolation method, final String name) { + super(method, name); + } + + @Override + public long runLong(final Longs values) { + final long v = values.nextLongValue(); + if (values.hasNextValue()) { + throw new IllegalDataException("More than one value in aggregator " + values); + } + return v; + } + + @Override + public double runDouble(final Doubles values) { + final double v = values.nextDoubleValue(); + if (values.hasNextValue()) { + throw new IllegalDataException("More than one value in aggregator " + values); + } + return v; + } + } + private static final class Multiply extends Aggregator { public Multiply(final Interpolation method, final String name) { diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index d4d56ff51d..2b57a92c96 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -40,6 +40,10 @@ public class Downsampler implements SeekableView, DataPoint { final Aggregator downsampler) { this.values_in_interval = new ValuesInInterval(source, interval_ms); this.downsampler = downsampler; + if (downsampler == Aggregators.NONE) { + throw new IllegalArgumentException("cannot use the NONE " + + "aggregator for downsampling"); + } } // ------------------ // diff --git a/src/core/DownsamplingSpecification.java b/src/core/DownsamplingSpecification.java index 3f590d20f0..020638b7c4 100644 --- a/src/core/DownsamplingSpecification.java +++ b/src/core/DownsamplingSpecification.java @@ -71,6 +71,10 @@ public DownsamplingSpecification(final long interval, if (null == fill_policy) { throw new IllegalArgumentException("fill policy cannot be null"); } + if (function == Aggregators.NONE) { + throw new IllegalArgumentException("cannot use the NONE " + + "aggregator for downsampling"); + } this.interval = interval; this.function = function; @@ -115,6 +119,10 @@ public DownsamplingSpecification(final String specification) { throw new IllegalArgumentException("No such downsampling function: " + parts[1]); } + if (function == Aggregators.NONE) { + throw new IllegalArgumentException("cannot use the NONE " + + "aggregator for downsampling"); + } // FILL POLICY. if (3 == parts.length) { diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 8ea2f48509..a5a1450506 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -427,6 +427,10 @@ public void downsample(final long interval, final Aggregator downsampler, */ @Override public void downsample(final long interval, final Aggregator downsampler) { + if (downsampler == Aggregators.NONE) { + throw new IllegalArgumentException("cannot use the NONE " + + "aggregator for downsampling"); + } downsample(interval, downsampler, FillPolicy.NONE); } @@ -850,6 +854,23 @@ public DataPoints[] call(final TreeMap spans) throws Exception { } return NO_RESULT; } + + // The raw aggregator skips group bys and ignores downsampling + if (aggregator == Aggregators.NONE) { + final SpanGroup[] groups = new SpanGroup[spans.size()]; + int i = 0; + for (final Span span : spans.values()) { + final SpanGroup group = new SpanGroup(tsdb, getScanStartTimeSeconds(), + getScanEndTimeSeconds(), + null, rate, rate_options, aggregator, + sample_interval_ms, downsampler, query_index, + fill_policy); + group.add(span); + groups[i++] = group; + } + return groups; + } + if (group_bys == null) { // We haven't been asked to find groups, so let's put all the spans // together in the same group. diff --git a/test/core/TestDownsamplingSpecification.java b/test/core/TestDownsamplingSpecification.java index a149a08c34..a85cb90f1a 100644 --- a/test/core/TestDownsamplingSpecification.java +++ b/test/core/TestDownsamplingSpecification.java @@ -65,5 +65,10 @@ public void testBadFunction() { public void testBadFillPolicy() { new DownsamplingSpecification("10m-avg-max"); } + + @Test (expected = IllegalArgumentException.class) + public void testNoneAgg() { + new DownsamplingSpecification("1m-none-lerp"); + } } diff --git a/test/core/TestTSSubQuery.java b/test/core/TestTSSubQuery.java index 92cd5661a8..a86ed8bb8f 100644 --- a/test/core/TestTSSubQuery.java +++ b/test/core/TestTSSubQuery.java @@ -225,6 +225,13 @@ public void validateWithFilterAndGroupByFilterSameTag() { assertEquals(300000, sub.downsampleInterval()); } + @Test (expected = IllegalArgumentException.class) + public void validateWithDownsampleNone() { + TSSubQuery sub = getMetricForValidate(); + sub.setDownsample("1m-none"); + sub.validateAndSetQuery(); + } + // NOTE: Each of the hash and equals tests should make sure that we the code // doesn't change after validation. diff --git a/test/core/TestTsdbQueryDownsample.java b/test/core/TestTsdbQueryDownsample.java index b213a73466..4e3b881232 100644 --- a/test/core/TestTsdbQueryDownsample.java +++ b/test/core/TestTsdbQueryDownsample.java @@ -28,7 +28,9 @@ import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; +import com.google.common.collect.Lists; import com.google.common.math.DoubleMath; /** @@ -497,6 +499,43 @@ public void runFloatSingleTSDownsampleAndRateAndCount() throws Exception { assertEquals(150, dps[0].size()); } + @Test (expected = IllegalArgumentException.class) + public void runLongSingleTSDownsampleNone() throws Exception { + storeLongTimeSeriesSeconds(true, false); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.downsample(60000, Aggregators.NONE); + query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); + } + + @Test (expected = RuntimeException.class) + public void runLongSingleTSDownsampleNoneSnuckIn() throws Exception { + storeLongTimeSeriesSeconds(true, false); + final TSQuery ts_query = new TSQuery(); + ts_query.setStart("1356998400"); + ts_query.setEnd("1357041600"); + + final HashMap tags = new HashMap(1); + tags.put("host", "web01"); + final TSSubQuery sub = new TSSubQuery(); + sub.setTags(tags); + sub.setMetric("sys.cpu.user"); + sub.setAggregator("sum"); + sub.setDownsample("1m-sum"); + + ts_query.setQueries(Lists.newArrayList(sub)); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + Whitebox.setInternalState(query, "downsampler", Aggregators.NONE); + + final DataPoints[] dps = query.run(); + for (DataPoint dp : dps[0]) { + dp.timestamp(); + } + } + /** * A helper interface to be used by the filling-test code. */ diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 051b5157e9..4deaea7daf 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -302,6 +302,41 @@ public void runFloatTwoAggSum() throws Exception { assertEquals(300, dps[0].size()); } + @Test + public void runFloatTwoAggNoneAgg() throws Exception { + storeFloatTimeSeriesSeconds(true, false); + + tags.clear(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.NONE, false); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + assertMeta(dps, 1, false); + assertEquals(2, dps.length); + + double value = 1.25D; + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(timestamp, dp.timestamp()); + value += 0.25D; + timestamp += 30000; + } + assertEquals(300, dps[0].size()); + + value = 75D; + timestamp = 1356998430000L; + for (DataPoint dp : dps[1]) { + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(timestamp, dp.timestamp()); + value -= 0.25d; + timestamp += 30000; + } + assertEquals(300, dps[1].size()); + } + @Test public void runFloatTwoAggSumMs() throws Exception { storeFloatTimeSeriesMs(); From 6e6e45399f2d896be7551696c7b56c664c0d8362 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 28 Feb 2016 13:40:05 -0800 Subject: [PATCH 431/826] Allow for downsampling all values between the query start and end times into a single value Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 49 +++++ src/core/Downsampler.java | 132 +++++++++--- src/core/DownsamplingSpecification.java | 21 +- src/core/FillingDownsampler.java | 62 ++++-- src/core/Span.java | 25 +++ src/core/SpanGroup.java | 67 +++++-- src/core/TSSubQuery.java | 12 +- src/core/TsdbQuery.java | 70 +++---- test/core/TestDownsampler.java | 193 +++++++++++++++++- test/core/TestDownsamplingSpecification.java | 2 +- test/core/TestFillingDownsampler.java | 201 +++++++++++++++---- test/core/TestTsdbQueryDownsample.java | 100 ++++++++- 12 files changed, 789 insertions(+), 145 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index a53636b751..03bc93b8cf 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -277,6 +277,55 @@ public static AggregationIterator create(final List spans, method, rate); } + /** + * Creates a new iterator for a {@link SpanGroup}. + * @param spans Spans in a group. + * @param start_time Any data point strictly before this timestamp will be + * ignored. + * @param end_time Any data point strictly after this timestamp will be + * ignored. + * @param aggregator The aggregation function to use. + * @param method Interpolation method to use when aggregating time series + * @param downsampler The downsampling specifier to use (cannot be null) + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @param rate If {@code true}, the rate of the series will be used instead + * of the actual values. + * @param rate_options Specifies the optional additional rate calculation + * options. + * @return an AggregationIterator + * @since 2.3 + */ + public static AggregationIterator create(final List spans, + final long start_time, + final long end_time, + final Aggregator aggregator, + final Interpolation method, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final boolean rate, + final RateOptions rate_options) { + final int size = spans.size(); + final SeekableView[] iterators = new SeekableView[size]; + for (int i = 0; i < size; i++) { + SeekableView it; + if (downsampler == null || + downsampler == DownsamplingSpecification.NO_DOWNSAMPLER) { + it = spans.get(i).spanIterator(); + } else { + it = spans.get(i).downsampler(start_time, end_time, downsampler, + query_start, query_end); + } + if (rate) { + it = new RateSpan(it, rate_options); + } + iterators[i] = it; + } + return new AggregationIterator(iterators, start_time, end_time, aggregator, + method, rate); + } + /** * Creates an aggregation iterator for a group of data point iterators. * @param iterators An array of Seekable views of spans in a group. Ignored diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 2b57a92c96..06bd421582 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -19,31 +19,79 @@ */ public class Downsampler implements SeekableView, DataPoint { - /** Function to use for downsampling. */ - protected final Aggregator downsampler; + /** The downsampling specification when provided */ + protected final DownsamplingSpecification specification; + + /** The start timestamp of the actual query for use with "all" */ + protected final long query_start; + + /** The end timestamp of the actual query for use with "all" */ + protected final long query_end; + + /** The data source */ + protected final SeekableView source; + /** Iterator to iterate the values of the current interval. */ protected final ValuesInInterval values_in_interval; + /** Last normalized timestamp */ protected long timestamp; + /** Last value as a double */ protected double value; + /** Whether or not to merge all DPs in the source into one vaalue */ + protected final boolean run_all; + /** * Ctor. * @param source The iterator to access the underlying data. * @param interval_ms The interval in milli seconds wanted between each data * point. * @param downsampler The downsampling function to use. + * @deprecated as of 2.3 */ Downsampler(final SeekableView source, final long interval_ms, final Aggregator downsampler) { - this.values_in_interval = new ValuesInInterval(source, interval_ms); - this.downsampler = downsampler; + this.source = source; + values_in_interval = new ValuesInInterval(); if (downsampler == Aggregators.NONE) { throw new IllegalArgumentException("cannot use the NONE " + "aggregator for downsampling"); } + specification = new DownsamplingSpecification(interval_ms, downsampler, + DownsamplingSpecification.DEFAULT_FILL_POLICY); + query_start = 0; + query_end = 0; + run_all = false; + } + + /** + * Ctor. + * @param source The iterator to access the underlying data. + * @param specification The downsampling spec to use + * @param query_start The start timestamp of the actual query for use with "all" + * @param query_end The end timestamp of the actual query for use with "all" + * @since 2.3 + */ + Downsampler(final SeekableView source, + final DownsamplingSpecification specification, + final long query_start, + final long query_end + ) { + this.source = source; + this.specification = specification; + values_in_interval = new ValuesInInterval(); + this.query_start = query_start; + this.query_end = query_end; + + final String s = specification.getStringInterval(); + if (s != null && s.toLowerCase().contains("all")) { + run_all = true; + } else { + run_all = false; + } } // ------------------ // @@ -61,7 +109,7 @@ public boolean hasNext() { @Override public DataPoint next() { if (hasNext()) { - value = downsampler.runDouble(values_in_interval); + value = specification.getFunction().runDouble(values_in_interval); timestamp = values_in_interval.getIntervalTimestamp(); values_in_interval.moveToNextInterval(); return this; @@ -89,6 +137,9 @@ public void seek(final long timestamp) { @Override public long timestamp() { + if (run_all) { + return query_start; + } return timestamp; } @@ -116,8 +167,10 @@ public double toDouble() { public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("Downsampler: ") - .append("interval_ms=").append(values_in_interval.interval_ms) - .append(", downsampler=").append(downsampler) + .append(", downsampler=").append(specification) + .append(", queryStart=").append(query_start) + .append(", queryEnd=").append(query_end) + .append(", runAll=").append(run_all) .append(", current data=(timestamp=").append(timestamp) .append(", value=").append(value) .append("), values_in_interval=").append(values_in_interval); @@ -125,16 +178,14 @@ public String toString() { } /** Iterates source values for an interval. */ - protected static class ValuesInInterval implements Aggregator.Doubles { + protected class ValuesInInterval implements Aggregator.Doubles { - /** The iterator of original source values. */ - private final SeekableView source; - /** The sampling interval in milliseconds. */ - protected final long interval_ms; /** The end of the current interval. */ private long timestamp_end_interval = Long.MIN_VALUE; + /** True if the last value was successfully extracted from the source. */ private boolean has_next_value_from_source = false; + /** The last data point extracted from the source. */ private DataPoint next_dp = null; @@ -143,13 +194,11 @@ protected static class ValuesInInterval implements Aggregator.Doubles { /** * Constructor. - * @param source The iterator to access the underlying data. - * @param interval_ms Downsampling interval. */ - ValuesInInterval(final SeekableView source, final long interval_ms) { - this.source = source; - this.interval_ms = interval_ms; - this.timestamp_end_interval = interval_ms; + protected ValuesInInterval() { + if (run_all) { + timestamp_end_interval = query_end; + } } /** Initializes to iterate intervals. */ @@ -160,7 +209,9 @@ protected void initializeIfNotDone() { if (!initialized) { initialized = true; moveToNextValue(); - resetEndOfInterval(); + if (!run_all) { + resetEndOfInterval(); + } } } @@ -168,7 +219,25 @@ protected void initializeIfNotDone() { private void moveToNextValue() { if (source.hasNext()) { has_next_value_from_source = true; - next_dp = source.next(); + // filter out dps that don't match start and end for run_alls + if (run_all) { + while (source.hasNext()) { + next_dp = source.next(); + if (next_dp.timestamp() < query_start) { + next_dp = null; + continue; + } + if (next_dp.timestamp() >= query_end) { + has_next_value_from_source = false; + } + break; + } + if (next_dp == null) { + has_next_value_from_source = false; + } + } else { + next_dp = source.next(); + } } else { has_next_value_from_source = false; } @@ -179,10 +248,10 @@ private void moveToNextValue() { * the next value read from source. It is the first value of the next * interval. */ private void resetEndOfInterval() { - if (has_next_value_from_source) { + if (has_next_value_from_source && !run_all) { // Sets the end of the interval of the timestamp. timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + - interval_ms; + specification.getInterval(); } } @@ -198,7 +267,11 @@ void seekInterval(final long timestamp) { // rounds up the seeking timestamp to the smallest timestamp that is // a multiple of the interval and is greater than or equal to the given // timestamp.. - source.seek(alignTimestamp(timestamp + interval_ms - 1)); + if (run_all) { + source.seek(timestamp); + } else { + source.seek(alignTimestamp(timestamp + specification.getInterval() - 1)); + } initialized = false; } @@ -207,12 +280,17 @@ protected long getIntervalTimestamp() { // NOTE: It is well-known practice taking the start time of // a downsample interval as a representative timestamp of it. It also // provides the correct context for seek. - return alignTimestamp(timestamp_end_interval - interval_ms); + if (run_all) { + return timestamp_end_interval; + } else { + return alignTimestamp(timestamp_end_interval - + specification.getInterval()); + } } /** Returns timestamp aligned by interval. */ protected long alignTimestamp(final long timestamp) { - return timestamp - (timestamp % interval_ms); + return timestamp - (timestamp % specification.getInterval()); } // ---------------------- // @@ -222,6 +300,9 @@ protected long alignTimestamp(final long timestamp) { @Override public boolean hasNextValue() { initializeIfNotDone(); + if (run_all) { + return has_next_value_from_source; + } return has_next_value_from_source && next_dp.timestamp() < timestamp_end_interval; } @@ -241,7 +322,6 @@ public double nextDoubleValue() { public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("ValuesInInterval: ") - .append("interval_ms=").append(interval_ms) .append(", timestamp_end_interval=").append(timestamp_end_interval) .append(", has_next_value_from_source=") .append(has_next_value_from_source); diff --git a/src/core/DownsamplingSpecification.java b/src/core/DownsamplingSpecification.java index 020638b7c4..5abe810029 100644 --- a/src/core/DownsamplingSpecification.java +++ b/src/core/DownsamplingSpecification.java @@ -37,6 +37,9 @@ public final class DownsamplingSpecification { // Parsed downsample interval. private final long interval; + + //The string interval, e.g. 1h, 30d, etc + private final String string_interval; // Parsed downsampler function. private final Aggregator function; @@ -51,6 +54,7 @@ private DownsamplingSpecification() { interval = NO_INTERVAL; function = NO_FUNCTION; fill_policy = DEFAULT_FILL_POLICY; + string_interval = null; } /** @@ -59,6 +63,7 @@ private DownsamplingSpecification() { * @param function The downsampling function. * @param fill_policy The policy specifying how to deal with missing data. * @throws IllegalArgumentException if any argument is invalid. + * @deprecated since 2.3 */ public DownsamplingSpecification(final long interval, final Aggregator function, final FillPolicy fill_policy) { @@ -79,6 +84,7 @@ public DownsamplingSpecification(final long interval, this.interval = interval; this.function = function; this.fill_policy = fill_policy; + string_interval = null; } /** @@ -110,7 +116,13 @@ public DownsamplingSpecification(final String specification) { // INTERVAL. // This will throw if interval is invalid. - interval = DateTime.parseDuration(parts[0]); + if (parts[0].contains("all")) { + interval = NO_INTERVAL; + string_interval = parts[0]; + } else { + interval = DateTime.parseDuration(parts[0]); + string_interval = parts[0]; + } // FUNCTION. try { @@ -153,6 +165,12 @@ public long getInterval() { return interval; } + /** @return The string interval from the user (without the 'c' if given) + * @since 2.3 */ + public String getStringInterval() { + return string_interval; + } + /** * Get the downsampling function. * @return the downsampling function. @@ -175,6 +193,7 @@ public String toString() { .add("interval", getInterval()) .add("function", getFunction()) .add("fillPolicy", getFillPolicy()) + .add("stringInterval", string_interval) .toString(); } } diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index 0e4d77ad10..bcba99f61a 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -26,9 +26,6 @@ public class FillingDownsampler extends Downsampler { /** Track when the downsampled data should end. */ protected long end_timestamp; - /** Downsampling fill policy. */ - protected final FillPolicy fill_policy; - /** * Create a new nulling downsampler. * @param source The iterator to access the underlying data. @@ -40,24 +37,50 @@ public class FillingDownsampler extends Downsampler { * @param fill_policy Policy specifying whether to interpolate or to fill * missing intervals with special values. * @throws IllegalArgumentException if fill_policy is interpolation. + * @deprecated as of 2.3 */ FillingDownsampler(final SeekableView source, final long start_time, final long end_time, final long interval_ms, final Aggregator downsampler, final FillPolicy fill_policy) { + this(source, start_time, end_time, + new DownsamplingSpecification(interval_ms, downsampler, fill_policy) + , 0, 0); + } + + /** + * Create a new filling downsampler. + * @param source The iterator to access the underlying data. + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. + * @param specification The downsampling spec to use + * @param query_start The start timestamp of the actual query for use with "all" + * @param query_end The end timestamp of the actual query for use with "all" + * @throws IllegalArgumentException if fill_policy is interpolation. + * @since 2.3 + */ + FillingDownsampler(final SeekableView source, final long start_time, + final long end_time, final DownsamplingSpecification specification, + final long query_start, final long end_start) { // Lean on the superclass implementation. - super(source, interval_ms, downsampler); + super(source, specification, query_start, end_start); // Ensure we aren't given a bogus fill policy. - if (FillPolicy.NONE == fill_policy) { + if (FillPolicy.NONE == specification.getFillPolicy()) { throw new IllegalArgumentException("Cannot instantiate this class with" + " linear-interpolation fill policy"); } - this.fill_policy = fill_policy; - + // Use the values-in-interval object to align the timestamps at which we // expect data to arrive for the first and last intervals. - this.timestamp = values_in_interval.alignTimestamp(start_time); - this.end_timestamp = values_in_interval.alignTimestamp(end_time); + if (run_all) { + timestamp = start_time; + end_timestamp = end_time; + } else { + // Use the values-in-interval object to align the timestamps at which we + // expect data to arrive for the first and last intervals. + timestamp = values_in_interval.alignTimestamp(start_time); + end_timestamp = values_in_interval.alignTimestamp(end_time); + } } /** @@ -72,6 +95,9 @@ public boolean hasNext() { // No matter the state of the values-in-interval object, if our current // timestamp hasn't reached the end of the requested overall interval, then // we still have iterating to do. + if (run_all) { + return values_in_interval.hasNextValue(); + } return timestamp < end_timestamp; } @@ -92,26 +118,27 @@ public DataPoint next() { // Skip any leading data outside the query bounds. long actual = values_in_interval.getIntervalTimestamp(); - while (values_in_interval.hasNextValue() && actual < timestamp) { + while (!run_all && values_in_interval.hasNextValue() + && actual < timestamp) { // The actual timestamp precedes our expected, so there's data in the // values-in-interval object that we wish to ignore. - downsampler.runDouble(values_in_interval); + specification.getFunction().runDouble(values_in_interval); values_in_interval.moveToNextInterval(); actual = values_in_interval.getIntervalTimestamp(); } // Check whether the timestamp of the calculation interval matches what // we expect. - if (actual == timestamp) { + if (run_all || actual == timestamp) { // The calculated interval timestamp matches what we expect, so we can // do normal processing. - value = downsampler.runDouble(values_in_interval); + value = specification.getFunction().runDouble(values_in_interval); values_in_interval.moveToNextInterval(); } else { // Our expected timestamp precedes the actual, so the interval is // missing. We will use a special value, based on the fill policy, to // represent this case. - switch (fill_policy) { + switch (specification.getFillPolicy()) { case NOT_A_NUMBER: case NULL: value = Double.NaN; @@ -127,7 +154,7 @@ public DataPoint next() { } // Advance the expected timestamp to the next interval. - timestamp += values_in_interval.interval_ms; + timestamp += specification.getInterval(); // This object also represents the data. return this; @@ -140,7 +167,10 @@ public DataPoint next() { @Override public long timestamp() { - return timestamp - values_in_interval.interval_ms; + if (run_all) { + return query_start; + } + return timestamp - specification.getInterval(); } } diff --git a/src/core/Span.java b/src/core/Span.java index c9f31ba77f..15338956ae 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -485,6 +485,31 @@ Downsampler downsampler(final long start_time, interval_ms, downsampler, fill_policy); } } + + /** + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. + * @param downsampler The downsampling specification to use + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @return A new downsampler. + * @since 2.3 + */ + Downsampler downsampler(final long start_time, + final long end_time, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end) { + if (downsampler == null) { + return null; + } + if (FillPolicy.NONE == downsampler.getFillPolicy()) { + return new Downsampler(spanIterator(), downsampler, + query_start, query_end); + } + return new FillingDownsampler(spanIterator(), start_time, end_time, + downsampler, query_start, query_end); + } public int getQueryIndex() { throw new UnsupportedOperationException("Not mapped to a query"); diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 17625699ef..90133dcf51 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -91,21 +91,18 @@ final class SpanGroup implements DataPoints { /** Aggregator to use to aggregate data points from different Spans. */ private final Aggregator aggregator; - /** - * Downsampling function to use, if any (can be {@code null}). - * If this is non-null, {@code sample_interval} must be strictly positive. - */ - private final Aggregator downsampler; - - /** Minimum time interval (in seconds) wanted between each data point. */ - private final long sample_interval; + /** Downsampling specification to use, if any (can be {@code null}). */ + private DownsamplingSpecification downsampler; + + /** Start timestamp of the query for filtering */ + private final long query_start; + + /** End timestamp of the query for filtering */ + private final long query_end; /** Index of the query in the TSQuery class */ private final int query_index; - /** Downsampling fill policy. */ - private final FillPolicy fill_policy; - /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; @@ -192,6 +189,43 @@ final class SpanGroup implements DataPoints { final Aggregator aggregator, final long interval, final Aggregator downsampler, final int query_index, final FillPolicy fill_policy) { + this(tsdb, start_time, end_time, spans, rate, rate_options, aggregator, + downsampler != null ? + new DownsamplingSpecification(interval, downsampler, fill_policy) : + null, + query_index, 0, 0); + } + + /** + * Ctor. + * @param tsdb The TSDB we belong to. + * @param start_time Any data point strictly before this timestamp will be + * ignored. + * @param end_time Any data point strictly after this timestamp will be + * ignored. + * @param spans A sequence of initial {@link Spans} to add to this group. + * Ignored if {@code null}. Additional spans can be added with {@link #add}. + * @param rate If {@code true}, the rate of the series will be used instead + * of the actual values. + * @param rate_options Specifies the optional additional rate calculation options. + * @param aggregator The aggregation function to use. + * @param downsampler The specification to use for downsampling, may be null. + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @param query_index index of the original query + * @since 2.3 + */ + SpanGroup(final TSDB tsdb, + final long start_time, + final long end_time, + final Iterable spans, + final boolean rate, + final RateOptions rate_options, + final Aggregator aggregator, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final int query_index) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; @@ -206,9 +240,9 @@ final class SpanGroup implements DataPoints { this.rate_options = rate_options; this.aggregator = aggregator; this.downsampler = downsampler; - this.sample_interval = interval; + this.query_start = query_start; + this.query_end = query_end; this.query_index = query_index; - this.fill_policy = fill_policy; this.tsdb = tsdb; } @@ -452,8 +486,8 @@ public int aggregatedSize() { public SeekableView iterator() { return AggregationIterator.create(spans, start_time, end_time, aggregator, aggregator.interpolationMethod(), - downsampler, sample_interval, - rate, rate_options, fill_policy); + downsampler, query_start, query_end, + rate, rate_options); } /** @@ -509,7 +543,8 @@ private String toStringSharedAttributes() { + ", rate=" + rate + ", aggregator=" + aggregator + ", downsampler=" + downsampler - + ", sample_interval=" + sample_interval + + ", query_start=" + query_start + + ", query_end" + query_end + ')'; } diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index bff500a75f..578a32b89f 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -211,16 +211,24 @@ public Aggregator aggregator() { return this.agg; } - /** @return the parsed downsampler aggregation function */ + /** @return the parsed downsampler aggregation function + * @deprecated use {@link #downsamplingSpecification()} instead */ public Aggregator downsampler() { return downsample_specifier.getFunction(); } - /** @return the parsed downsample interval in seconds */ + /** @return the parsed downsample interval in seconds + * @deprecated use {@link #downsamplingSpecification()} instead */ public long downsampleInterval() { return downsample_specifier.getInterval(); } + /** @return The downsampling specification for more options + * @since 2.3 */ + public DownsamplingSpecification downsamplingSpecification() { + return downsample_specifier; + } + /** * @return the downsampling fill policy * @since 2.2 diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index a5a1450506..105cecf8c2 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -119,17 +119,8 @@ final class TsdbQuery implements Query { /** Aggregator function to use. */ private Aggregator aggregator; - /** - * Downsampling function to use, if any (can be {@code null}). - * If this is non-null, {@code sample_interval_ms} must be strictly positive. - */ - private Aggregator downsampler; - - /** Minimum time interval (in milliseconds) wanted between each data point. */ - private long sample_interval_ms; - - /** Downsampling fill policy. */ - private FillPolicy fill_policy; + /** Downsampling specification to use, if any (can be {@code null}). */ + private DownsamplingSpecification downsampler; /** Optional list of TSUIDs to fetch and aggregate instead of a metric */ private List tsuids; @@ -151,8 +142,6 @@ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; enable_fuzzy_filter = tsdb.getConfig() .getBoolean("tsd.query.enable_fuzzy_filter"); - // By default, we should interpolate. - fill_policy = DownsamplingSpecification.DEFAULT_FILL_POLICY; } /** @@ -322,6 +311,7 @@ public void setExplicitTags(final boolean explicit_tags) { this.explicit_tags = explicit_tags; } + @Override public Deferred configureFromQuery(final TSQuery query, final int index) { if (query.getQueries() == null || query.getQueries().isEmpty()) { @@ -345,9 +335,7 @@ public Deferred configureFromQuery(final TSQuery query, if (rate_options == null) { rate_options = new RateOptions(); } - downsampler = sub_query.downsampler(); - sample_interval_ms = sub_query.downsampleInterval(); - fill_policy = sub_query.fillPolicy(); + downsampler = sub_query.downsamplingSpecification(); filters = sub_query.getFilters(); explicit_tags = sub_query.getExplicitTags(); @@ -408,14 +396,8 @@ public Object call(final byte[] uid) throws Exception { @Override public void downsample(final long interval, final Aggregator downsampler, final FillPolicy fill_policy) { - if (downsampler == null) { - throw new NullPointerException("downsampler"); - } else if (interval <= 0) { - throw new IllegalArgumentException("interval not > 0: " + interval); - } - this.downsampler = downsampler; - this.sample_interval_ms = interval; - this.fill_policy = fill_policy; + this.downsampler = new DownsamplingSpecification( + interval, downsampler,fill_policy); } /** @@ -860,11 +842,18 @@ public DataPoints[] call(final TreeMap spans) throws Exception { final SpanGroup[] groups = new SpanGroup[spans.size()]; int i = 0; for (final Span span : spans.values()) { - final SpanGroup group = new SpanGroup(tsdb, getScanStartTimeSeconds(), + final SpanGroup group = new SpanGroup( + tsdb, + getScanStartTimeSeconds(), getScanEndTimeSeconds(), - null, rate, rate_options, aggregator, - sample_interval_ms, downsampler, query_index, - fill_policy); + null, + rate, + rate_options, + aggregator, + downsampler, + getStartTime(), + getEndTime(), + query_index); group.add(span); groups[i++] = group; } @@ -880,8 +869,10 @@ public DataPoints[] call(final TreeMap spans) throws Exception { spans.values(), rate, rate_options, aggregator, - sample_interval_ms, downsampler, - query_index, fill_policy); + downsampler, + getStartTime(), + getEndTime(), + query_index); if (query_stats != null) { query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); } @@ -928,8 +919,10 @@ public DataPoints[] call(final TreeMap spans) throws Exception { thegroup = new SpanGroup(tsdb, getScanStartTimeSeconds(), getScanEndTimeSeconds(), null, rate, rate_options, aggregator, - sample_interval_ms, downsampler, query_index, - fill_policy); + downsampler, + getStartTime(), + getEndTime(), + query_index); // 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]; @@ -1010,9 +1003,10 @@ private long getScanStartTimeSeconds() { // First, we align the start timestamp to its representative value for the // interval in which it appears, if downsampling. long interval_aligned_ts = start; - if (0L != sample_interval_ms) { + if (downsampler != null && downsampler.getInterval() > 0) { // Downsampling enabled. - final long interval_offset = (1000L * start) % sample_interval_ms; + // TODO - calendar interval + final long interval_offset = (1000L * start) % downsampler.getInterval(); interval_aligned_ts -= interval_offset / 1000L; } @@ -1036,7 +1030,7 @@ private long getScanEndTimeSeconds() { } // The calculation depends on whether we're downsampling. - if (0L != sample_interval_ms) { + if (downsampler != null && downsampler.getInterval() > 0) { // Downsampling enabled. // // First, we align the end timestamp to its representative value for the @@ -1049,9 +1043,9 @@ private long getScanEndTimeSeconds() { // skip forward an entire extra interval. // // This can be accomplished by simply not testing for zero offset. - final long interval_offset = (1000L * end) % sample_interval_ms; + final long interval_offset = (1000L * end) % downsampler.getInterval(); final long interval_aligned_ts = end + - (sample_interval_ms - interval_offset) / 1000L; + (downsampler.getInterval() - interval_offset) / 1000L; // Then, if we're now aligned on a timespan boundary, then we need no // further adjustment: we are guaranteed to have always moved the end time @@ -1217,7 +1211,7 @@ static long getScanEndTimeSeconds(final TsdbQuery query) { /** @return the downsampling interval for unit tests. */ static long getDownsampleIntervalMs(final TsdbQuery query) { - return query.sample_interval_ms; + return query.downsampler.getInterval(); } static byte[] getMetric(final TsdbQuery query) { diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 32e94c8d2a..97b54b02de 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.core; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -29,7 +28,6 @@ import org.junit.Before; import org.junit.Test; - /** Tests {@link Downsampler}. */ public class TestDownsampler { @@ -57,6 +55,7 @@ public class TestDownsampler { private SeekableView source; private Downsampler downsampler; + private DownsamplingSpecification specification; @Before public void before() { @@ -65,7 +64,8 @@ public void before() { @Test public void testDownsampler() { - downsampler = new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG); + specification = new DownsamplingSpecification("1000s-avg"); + downsampler = new Downsampler(source, specification, 0, 0); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -90,7 +90,7 @@ public void testDownsampler() { } @Test - public void testDownsampler_10seconds() { + public void testDownsamplerDeprecated_10seconds() { source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), @@ -131,7 +131,49 @@ public void testDownsampler_10seconds() { } @Test - public void testDownsampler_15seconds() { + public void testDownsampler_10seconds() { + 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), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 4, 16), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 5, 32), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 6, 64), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 7, 128), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 8, 256), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 9, 512), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 10, 1024) + })); + specification = new DownsamplingSpecification("10s-sum"); + downsampler = new Downsampler(source, specification, 0, 0); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(6, values.size()); + assertEquals(3, values.get(0), 0.0000001); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(12, values.get(1), 0.0000001); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + assertEquals(48, values.get(2), 0.0000001); + assertEquals(BASE_TIME + 20000L, timestamps_in_millis.get(2).longValue()); + assertEquals(192, values.get(3), 0.0000001); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(3).longValue()); + assertEquals(768, values.get(4), 0.0000001); + assertEquals(BASE_TIME + 40000L, timestamps_in_millis.get(4).longValue()); + assertEquals(1024, values.get(5), 0.0000001); + assertEquals(BASE_TIME + 50000L, timestamps_in_millis.get(5).longValue()); + } + + @Test + public void testDownsamplerDeprecated_15seconds() { source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), @@ -162,6 +204,147 @@ public void testDownsampler_15seconds() { assertEquals(BASE_TIME + 45000L, timestamps_in_millis.get(3).longValue()); } + @Test + public void testDownsampler_15seconds() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + })); + specification = new DownsamplingSpecification("15s-sum"); + downsampler = new Downsampler(source, specification, 0, 0); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(4, values.size()); + assertEquals(1, values.get(0), 0.0000001); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(6, values.get(1), 0.0000001); + assertEquals(BASE_TIME + 15000L, timestamps_in_millis.get(1).longValue()); + assertEquals(8, values.get(2), 0.0000001); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(2).longValue()); + assertEquals(48, values.get(3), 0.0000001); + assertEquals(BASE_TIME + 45000L, timestamps_in_millis.get(3).longValue()); + } + + @Test + public void testDownsampler_allFullRange() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + System.out.println(downsampler); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(63, values.get(0), 0.0000001); + assertEquals(0L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testDownsampler_allFilterOnQuery() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new Downsampler(source, specification, + BASE_TIME + 15000L, BASE_TIME + 45000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(14, values.get(0), 0.0000001); + assertEquals(BASE_TIME + 15000L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new Downsampler(source, specification, + BASE_TIME + 65000L, BASE_TIME + 75000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(0, values.size()); + } + + @Test + public void testDownsampler_allFilterOnQueryOutOfRangeLate() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new Downsampler(source, specification, + BASE_TIME - 15000L, BASE_TIME - 5000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(0, values.size()); + } + @Test(expected = UnsupportedOperationException.class) public void testRemove() { new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG).remove(); diff --git a/test/core/TestDownsamplingSpecification.java b/test/core/TestDownsamplingSpecification.java index a85cb90f1a..0fc51f0368 100644 --- a/test/core/TestDownsamplingSpecification.java +++ b/test/core/TestDownsamplingSpecification.java @@ -44,7 +44,7 @@ public void testStringCtor() { @Test public void testToString() { assertEquals("DownsamplingSpecification{interval=4532019, function=zimsum, " - + "fillPolicy=NOT_A_NUMBER}", + + "fillPolicy=NOT_A_NUMBER, stringInterval=null}", new DownsamplingSpecification( 4532019L, Aggregators.ZIMSUM, diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index 70dd698aca..e762a8d33b 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -16,16 +16,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** Tests {@link FillingDownsampler}. */ public class TestFillingDownsampler { - private static final Aggregator SUM = Aggregators.get("sum"); - - private static final FillPolicy NAN = FillPolicy.fromString("nan"); - private static final FillPolicy ZERO = FillPolicy.fromString("zero"); - + private static final long BASE_TIME = 1356998400000L; + + private DownsamplingSpecification specification; + /** Data with gaps: before, during, and after. */ @Test public void testNaNMissingInterval() { @@ -43,18 +41,20 @@ public void testNaNMissingInterval() { MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), }); + specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, 100L, SUM, NAN); - - step(downsampler, Double.NaN); - step(downsampler, 3.); - step(downsampler, Double.NaN); - step(downsampler, 2.); - step(downsampler, Double.NaN); - step(downsampler, Double.NaN); - step(downsampler, 4.); - step(downsampler, Double.NaN); - step(downsampler, Double.NaN); + baseTime + 36 * 25L, specification, 0, 0); + + long timestamp = baseTime; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 100, 3.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 2.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 4.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); assertFalse(downsampler.hasNext()); } @@ -73,19 +73,21 @@ public void testZeroMissingInterval() { MutableDataPoint.ofDoubleValue(baseTime + 25L * 26L, 1.), MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), }); - + + specification = new DownsamplingSpecification("100ms-sum-zero"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, 100L, SUM, ZERO); - - step(downsampler, 0.); - step(downsampler, 3.); - step(downsampler, 0.); - step(downsampler, 2.); - step(downsampler, 0.); - step(downsampler, 0.); - step(downsampler, 4.); - step(downsampler, 0.); - step(downsampler, 0.); + baseTime + 36 * 25L, specification, 0, 0); + + long timestamp = baseTime; + step(downsampler, timestamp, 0.); + step(downsampler, timestamp += 100, 3.); + step(downsampler, timestamp += 100, 0.); + step(downsampler, timestamp += 100, 2.); + step(downsampler, timestamp += 100, 0.); + step(downsampler, timestamp += 100, 0.); + step(downsampler, timestamp += 100, 4.); + step(downsampler, timestamp += 100, 0.); + step(downsampler, timestamp += 100, 0.); assertFalse(downsampler.hasNext()); } @@ -109,12 +111,14 @@ public void testWithoutMissingIntervals() { MutableDataPoint.ofDoubleValue(baseTime + 25L * 11L, 1.), }); + specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, 100L, SUM, NAN); + baseTime + 12L * 25L, specification, 0, 0); - step(downsampler, 42.); - step(downsampler, 26.); - step(downsampler, 10.); + long timestamp = baseTime; + step(downsampler, timestamp, 42.); + step(downsampler, timestamp += 100, 26.); + step(downsampler, timestamp += 100, 10.); assertFalse(downsampler.hasNext()); } @@ -139,20 +143,139 @@ public void testWithOutOfBoundsData() { MutableDataPoint.ofDoubleValue(baseTime + 60000L * 2L + 30384L, 37.), MutableDataPoint.ofDoubleValue(baseTime + 60000L * 4L + 1530L, 86.) }); + + specification = new DownsamplingSpecification("1m-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 60000L * 2L, specification, 0, 0); + + long timestamp = 1425335880000L; + step(downsampler, timestamp, 30.); + step(downsampler, timestamp += 60000, 9.); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testWithOutOfBoundsDataEarly() { + final long baseTime = 1425335895000L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime - 60000L * 5L + 320L, 53.), + MutableDataPoint.ofDoubleValue(baseTime - 60000L * 2L + 8839L, 16.) + }); + specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, 60000L, SUM, NAN); + baseTime + 60000L * 2L, specification, 0, 0); + + long timestamp = 1425335880000L; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 60000, Double.NaN); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testWithOutOfBoundsDataLate() { + final long baseTime = 1425335895000L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 2L + 30384L, 37.), + MutableDataPoint.ofDoubleValue(baseTime + 60000L * 4L + 1530L, 86.) + }); - step(downsampler, 30.); - step(downsampler, 9.); + specification = new DownsamplingSpecification("1m-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 60000L * 2L, specification, 0, 0); + + long timestamp = 1425335880000L; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 60000, Double.NaN); assertFalse(downsampler.hasNext()); } - private void step(final Downsampler downsampler, final double expected) { + @Test + public void testDownsampler_allFullRange() { + final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + }); + + specification = new DownsamplingSpecification("0all-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, + BASE_TIME + 5000L,BASE_TIME + 55000L, specification, 0, + Long.MAX_VALUE); + + step(downsampler, 0, 63); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_allFilterOnQuery() { + final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + }); + + specification = new DownsamplingSpecification("0all-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, + BASE_TIME + 5000L,BASE_TIME + 55000L, specification, + BASE_TIME + 15000L, BASE_TIME + 45000L); + + step(downsampler, BASE_TIME + 15000L, 14); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { + final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + }); + + specification = new DownsamplingSpecification("0all-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, + BASE_TIME + 5000L,BASE_TIME + 55000L, specification, + BASE_TIME + 65000L, BASE_TIME + 75000L); + + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_allFilterOnQueryOutOfRangeLate() { + final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + }); + + specification = new DownsamplingSpecification("0all-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, + BASE_TIME + 5000L,BASE_TIME + 55000L, specification, + BASE_TIME - 15000L, BASE_TIME - 5000L); + + assertFalse(downsampler.hasNext()); + } + + private void step(final Downsampler downsampler, final long expected_timestamp, + final double expected_value) { assertTrue(downsampler.hasNext()); final DataPoint point = downsampler.next(); - assertNotNull(point); - assertEquals(expected, point.doubleValue(), 0.01); + assertEquals(expected_timestamp, point.timestamp()); + assertEquals(expected_value, point.doubleValue(), 0.01); } } diff --git a/test/core/TestTsdbQueryDownsample.java b/test/core/TestTsdbQueryDownsample.java index 4e3b881232..02bdce9469 100644 --- a/test/core/TestTsdbQueryDownsample.java +++ b/test/core/TestTsdbQueryDownsample.java @@ -124,7 +124,7 @@ public void downsampleMilliseconds() throws Exception { TsdbQuery.ForTesting.getScanEndTimeSeconds(query)); } - @Test (expected = NullPointerException.class) + @Test (expected = IllegalArgumentException.class) public void downsampleNullAgg() throws Exception { query.downsample(60, null); } @@ -461,6 +461,104 @@ public void runLongSingleTSDownsampleCount() throws Exception { assertEquals(151, dps[0].size()); } + @Test + public void runLongSingleTSDownsampleAll() throws Exception { + storeLongTimeSeriesSeconds(true, false); + final TSQuery ts_query = new TSQuery(); + ts_query.setStart("1356998400"); + ts_query.setEnd("1357041600"); + + final HashMap tags = new HashMap(1); + tags.put("host", "web01"); + final TSSubQuery sub = new TSSubQuery(); + sub.setTags(tags); + sub.setMetric("sys.cpu.user"); + sub.setAggregator("sum"); + sub.setDownsample("0all-sum"); + + ts_query.setQueries(Lists.newArrayList(sub)); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + for (DataPoint dp : dps[0]) { + // Downsampler outputs just doubles. + assertFalse(dp.isInteger()); + assertEquals(45150, dp.doubleValue(), 0.00001); + assertEquals(1356998400000L, dp.timestamp()); + } + // Out of 300 values, the first and the last intervals have one value each, + // and the 149 intervals in the middle have two values for each. + assertEquals(1, dps[0].size()); + } + + @Test + public void runLongSingleTSDownsampleAllSubSet() throws Exception { + storeLongTimeSeriesSeconds(true, false); + final TSQuery ts_query = new TSQuery(); + ts_query.setStart("1356998500"); + ts_query.setEnd("1356998600"); + + final HashMap tags = new HashMap(1); + tags.put("host", "web01"); + final TSSubQuery sub = new TSSubQuery(); + sub.setTags(tags); + sub.setMetric("sys.cpu.user"); + sub.setAggregator("sum"); + sub.setDownsample("0all-sum"); + + ts_query.setQueries(Lists.newArrayList(sub)); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + for (DataPoint dp : dps[0]) { + // Downsampler outputs just doubles. + assertFalse(dp.isInteger()); + assertEquals(15, dp.doubleValue(), 0.00001); + assertEquals(1356998500000L, dp.timestamp()); + } + // Out of 300 values, the first and the last intervals have one value each, + // and the 149 intervals in the middle have two values for each. + assertEquals(1, dps[0].size()); + } + + @Test + public void runLongSingleTSDownsampleAllNoEnd() throws Exception { + storeLongTimeSeriesSeconds(true, false); + final TSQuery ts_query = new TSQuery(); + ts_query.setStart("1356998400"); + + final HashMap tags = new HashMap(1); + tags.put("host", "web01"); + final TSSubQuery sub = new TSSubQuery(); + sub.setTags(tags); + sub.setMetric("sys.cpu.user"); + sub.setAggregator("sum"); + sub.setDownsample("0all-sum"); + + ts_query.setQueries(Lists.newArrayList(sub)); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + for (DataPoint dp : dps[0]) { + // Downsampler outputs just doubles. + assertFalse(dp.isInteger()); + assertEquals(45150, dp.doubleValue(), 0.00001); + assertEquals(1356998400000L, dp.timestamp()); + } + // Out of 300 values, the first and the last intervals have one value each, + // and the 149 intervals in the middle have two values for each. + assertEquals(1, dps[0].size()); + } + // this could happen. @Test public void runFloatSingleTSDownsampleAndRateAndCount() throws Exception { From 43eb419aa381f651eb1f16928d6c916d3680c297 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 28 Feb 2016 16:06:49 -0800 Subject: [PATCH 432/826] Bump the asyncbigtable version to include filters that TSD needs Signed-off-by: Chris Larsen --- ...gtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..78c394f1d3 --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +512cc4c7ba345a11aa8d6662d03bb3ed diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index 9e903a49f7..8549f1451c 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCBIGTABLE_VERSION := 0.2.1-20151029.214823-2 +ASYNCBIGTABLE_VERSION := 0.2.1-20160228.235952-3 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.2.1-SNAPSHOT/ From e5b8be4d6eecf0eda949ffb895a2aba1225cc9d6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 28 Feb 2016 16:46:33 -0800 Subject: [PATCH 433/826] Bump the asynccassandra version with filters needed for the TSD Signed-off-by: Chris Larsen --- ...sandra-0.0.1-20160229.001338-4-jar-with-dependencies.jar.md5 | 1 + third_party/asynccassandra/include.mk | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/asynccassandra/asynccassandra-0.0.1-20160229.001338-4-jar-with-dependencies.jar.md5 diff --git a/third_party/asynccassandra/asynccassandra-0.0.1-20160229.001338-4-jar-with-dependencies.jar.md5 b/third_party/asynccassandra/asynccassandra-0.0.1-20160229.001338-4-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..cbd7d36bb3 --- /dev/null +++ b/third_party/asynccassandra/asynccassandra-0.0.1-20160229.001338-4-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +cb857f54223905d744fafa475d82623f \ No newline at end of file diff --git a/third_party/asynccassandra/include.mk b/third_party/asynccassandra/include.mk index 7eb8f99e96..f98aab5050 100644 --- a/third_party/asynccassandra/include.mk +++ b/third_party/asynccassandra/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCCASSANDRA_VERSION := 0.0.1-20151104.191228-3 +ASYNCCASSANDRA_VERSION := 0.0.1-20160229.001338-4 ASYNCCASSANDRA := third_party/asynccassandra/asynccassandra-$(ASYNCCASSANDRA_VERSION)-jar-with-dependencies.jar ASYNCCASSANDRA_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/net/opentsdb/asynccassandra/0.0.1-SNAPSHOT/ From 07ab5f29cd79297bbde8fb73d8298a84f8820913 Mon Sep 17 00:00:00 2001 From: dominosly Date: Fri, 4 Mar 2016 16:54:05 -0800 Subject: [PATCH 434/826] Adding support latest java versions 1.8.0u65 to 1.8.0u74 and their corresponding ALPN versions for BigTable build. --- third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 | 1 + third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 | 1 + third_party/alpn-boot/include.mk | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 create mode 100644 third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 diff --git a/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 new file mode 100644 index 0000000000..209c34b148 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 @@ -0,0 +1 @@ +0f7bbc8e3da3948082c4d3a510d6fe43 \ No newline at end of file diff --git a/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 new file mode 100644 index 0000000000..a70c6c1356 --- /dev/null +++ b/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 @@ -0,0 +1 @@ +4af7a18a9b4549a1796b182dabca9062 \ No newline at end of file diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk index c4f94eb448..13cd19dfa9 100644 --- a/third_party/alpn-boot/include.mk +++ b/third_party/alpn-boot/include.mk @@ -14,6 +14,7 @@ # along with this library. If not, see . # ALPN_BOOT_VERSION := 7.1.3.v20150130 + ALPN_BOOT_VERSION = $(shell version= ;\ if [[ "@JAVA@" ]]; then \ version=$$("@JAVA@" -version 2>&1 | awk -F '"' '/version/ {print $$2}'); \ @@ -42,8 +43,12 @@ ALPN_BOOT_VERSION = $(shell version= ;\ echo "8.1.3.v20150130"; \ elif [[ $$sub < 60 ]]; then \ echo "8.1.4.v20150727"; \ - else \ + elif [[ $$sub < 65 ]]; then \ echo "8.1.5.v20150921"; \ + elif [[ $$sub < 71 ]]; then \ + echo "8.1.6.v20151105"; \ + else \ + echo "8.1.7.v20160121"; \ fi \ else \ echo "Unsupported major Java version: $$major"; \ From 507f80a007f1aa8a40b1c6de9dbaeb03d451e368 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 6 Mar 2016 11:30:33 -0800 Subject: [PATCH 435/826] Add the explicitTags to the expressions endpoint Signed-off-by: Chris Larsen --- src/query/pojo/Filter.java | 25 ++++++++++++++++++++++--- src/tsd/QueryExecutor.java | 1 + test/query/pojo/TestFilter.java | 8 +++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/query/pojo/Filter.java b/src/query/pojo/Filter.java index 902422da1f..c1eeea2818 100644 --- a/src/query/pojo/Filter.java +++ b/src/query/pojo/Filter.java @@ -34,6 +34,10 @@ public class Filter extends Validatable { /** The list of filters in the filter set */ private List tags; + /** Whether or not to only fetch series with exactly the same tag keys as + * in the filter list. */ + private boolean explicit_tags; + /** * Default ctor * @param builder The builder to pull values from @@ -41,6 +45,7 @@ public class Filter extends Validatable { private Filter(Builder builder) { this.id = builder.id; this.tags = builder.tags; + this.explicit_tags = builder.explicitTags; } /** @return the id of the filter set to use in a metric query */ @@ -53,6 +58,12 @@ public List getTags() { return tags; } + /** @return Whether or not to only fetch series with exactly the same tag keys as + * in the filter list. */ + public boolean getExplicitTags() { + return explicit_tags; + } + /** @return A new builder for the filter */ public static Builder Builder() { return new Builder(); @@ -78,12 +89,13 @@ public boolean equals(final Object o) { final Filter filter = (Filter) o; return Objects.equal(id, filter.id) - && Objects.equal(tags, filter.tags); + && Objects.equal(tags, filter.tags) + && Objects.equal(explicit_tags, filter.explicit_tags); } @Override public int hashCode() { - return Objects.hashCode(id, tags); + return Objects.hashCode(id, tags, explicit_tags); } /** @@ -96,7 +108,9 @@ public static final class Builder { private String id; @JsonProperty private List tags; - + @JsonProperty + private boolean explicitTags; + public Builder setId(String id) { Query.validateId(id); this.id = id; @@ -108,6 +122,11 @@ public Builder setTags(List tags) { return this; } + public Builder setExplicitTags(boolean explicit_tags) { + this.explicitTags = explicit_tags; + return this; + } + public Filter build() { return new Filter(this); } diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java index 90cdca0165..20e81bd76b 100644 --- a/src/tsd/QueryExecutor.java +++ b/src/tsd/QueryExecutor.java @@ -170,6 +170,7 @@ public QueryExecutor(final TSDB tsdb, final Query query) { sub.setFilters(filters.getTags()); sub.setAggregator( mq.getAggregator() != null ? mq.getAggregator() : timespan.getAggregator()); + sub.setExplicitTags(filters.getExplicitTags()); } } diff --git a/test/query/pojo/TestFilter.java b/test/query/pojo/TestFilter.java index e38088c09b..1d1f9ea0a5 100644 --- a/test/query/pojo/TestFilter.java +++ b/test/query/pojo/TestFilter.java @@ -41,14 +41,15 @@ public void validationBadId() throws Exception { @Test public void deserialize() throws Exception { String json = "{\"id\":\"f1\",\"tags\":[{\"tagk\":\"host\"," - + "\"filter\":\"*\",\"type\":\"iwildcard\",\"groupBy\":false}]}"; + + "\"filter\":\"*\",\"type\":\"iwildcard\",\"groupBy\":false}]," + + "\"explicitTags\":\"true\"}"; TagVFilter tag = new TagVFilter.Builder().setFilter("*").setGroupBy( false) .setTagk("host").setType("iwildcard").build(); Filter expectedFilter = Filter.Builder().setId("f1") - .setTags(Arrays.asList(tag)).build(); + .setTags(Arrays.asList(tag)).setExplicitTags(true).build(); Filter filter = JSON.parseToObject(json, Filter.class); filter.validate(); @@ -61,12 +62,13 @@ public void serialize() throws Exception { .setTagk("host").setType("iwildcard").build(); Filter filter = Filter.Builder().setId("f1") - .setTags(Arrays.asList(tag)).build(); + .setTags(Arrays.asList(tag)).setExplicitTags(true).build(); String actual = JSON.serializeToString(filter); assertTrue(actual.contains("\"id\":\"f1\"")); assertTrue(actual.contains("\"tags\":[")); assertTrue(actual.contains("\"tagk\":\"host\"")); + assertTrue(actual.contains("\"explicitTags\":true")); } @Test From 5012f7da15bb872bae3c4fd6129b10d38c7837c4 Mon Sep 17 00:00:00 2001 From: Carlos Devoto Date: Sat, 12 Mar 2016 15:18:01 -0800 Subject: [PATCH 436/826] Align downsampling intervals to the Gregorian calendar. This feature supports the alignment of downsampling intervals to the Gregorian calendar based on four different time categories: - DAILY: The start time of each interval is computed as the start of the day in which the first data point occurs, based on a specified time zone (or the default JVM time zone, if no time zone has been specified). The end time of each interval is computed as the end of the day in which the first data point occurs. For instance, if the specified time zone is UTC, and the timestamp of the first data point is 2016-01-05T05:32:00Z, then start of the interval will be computed as 2016-01-05T00:00:00.000Z, while the end of the interval will be computed as 2016-01-05T23:59:59.999Z. - WEEKLY: The start time of each interval is computed as the start of the week in which the first data point occurs, based on a specified time zone (or the default JVM time zone, if no time zone has been specified). The end time of each interval is computed as the end of the week in which the first data point occurs. Weeks are considered to begin on Sundays (in the future, it might be a good idea to allow for variations based on a configuration setting). For instance, if the specified time zone is UTC, and the timestamp of the first data point is 2016-01-05T05:32:00Z, then start of the interval will be computed as 2016-01-03T00:00:00.000Z, while the end of the interval will be computed as 2016-01-09T23:59:59.999Z. - MONTHLY: The start time of each interval is computed as the start of the month in which the first data point occurs, based on a specified time zone (or the default JVM time zone, if no time zone has been specified). The end time of each interval is computed as the end of the month in which the first data point occurs. For instance, if the specified time zone is UTC, and the timestamp of the first data point is 2016-01-05T05:32:00Z, then start of the interval will be computed as 2016-01-01T00:00:00.000Z, while the end of the interval will be computed as 2016-01-31T23:59:59.999Z. - YEARLY: The start time of each interval is computed as the start of the year in which the first data point occurs, based on a specified time zone (or the default JVM time zone, if no time zone has been specified). The end time of each interval is computed as the end of the year in which the first data point occurs. For instance, if the specified time zone is UTC, and the timestamp of the first data point is 2016-01-05T05:32:00Z, then start of the interval will be computed as 2016-01-01T00:00:00.000Z, while the end of the interval will be computed as 2016-12-31T23:59:59.999Z. This feature also allows for the alignment of intervals that are multiples of one year, one month, one week, or one day. In cases where a given interval is a multiple of more than one time category, the larger time category will be used. For instance, an interval of 24 months will be interpreted as a interval of two years, and will be aligned to the calendar accordingly. As such, if the specified time zone is UTC, and the timestamp of the first data point is 2016-03-05T05:32:00Z, then start of the interval will be computed as 2016-01-01T00:00:00.000Z, while the end of the interval will be computed as 2017-12-31T23:59:59.999Z. This is in keeping with the principle of least astonishment. To specify the time zone for a given HTTP query, include a query string parameter named "tz" with a value equal to a JVM time zone id (e.g. "UTC"). If a time zone is not included in the query string, the default JVM time zone will be used. To specify that a given HTTP query should use the calendar alignment feature for downsampling, include a query string parameter named "use_calendar" with a value of "true". You can stipulate that all HTTP queries should use the calendar alignment feature by including a "tsd.query.downsample.use_calendar" configuration setting within the opentsdb.conf file and by setting its value to "true" (the default value is "false"). This config file setting can be overridden on a per-query basis by including the "use_calendar" parameter in the query string as specified above. (NOTE: Modified by @manolama to fit in with the new 2.3 method params) Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 5 +- src/core/Downsampler.java | 135 ++++++- src/core/FillingDownsampler.java | 9 +- src/core/Query.java | 18 + src/core/Span.java | 9 +- src/core/SpanGroup.java | 12 +- src/core/TSQuery.java | 13 + src/core/TsdbQuery.java | 44 +++ src/utils/DateTime.java | 172 ++++++++ test/core/TestDownsampler.java | 546 +++++++++++++++++++++++++- test/core/TestFillingDownsampler.java | 22 +- 11 files changed, 950 insertions(+), 35 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 03bc93b8cf..64e9179f18 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; +import java.util.TimeZone; import com.google.common.annotations.VisibleForTesting; @@ -304,6 +305,8 @@ public static AggregationIterator create(final List spans, final DownsamplingSpecification downsampler, final long query_start, final long query_end, + final TimeZone timezone, + final boolean use_calendar, final boolean rate, final RateOptions rate_options) { final int size = spans.size(); @@ -315,7 +318,7 @@ public static AggregationIterator create(final List spans, it = spans.get(i).spanIterator(); } else { it = spans.get(i).downsampler(start_time, end_time, downsampler, - query_start, query_end); + query_start, query_end, timezone, use_calendar); } if (rate) { it = new RateSpan(it, rate_options); diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 06bd421582..8123cb5579 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -13,12 +13,20 @@ package net.opentsdb.core; import java.util.NoSuchElementException; +import java.util.TimeZone; + +import net.opentsdb.utils.DateTime; /** * Iterator that downsamples data points using an {@link Aggregator}. */ public class Downsampler implements SeekableView, DataPoint { + static final long ONE_WEEK_INTERVAL = 604800000L; + static final long ONE_MONTH_INTERVAL = 2592000000L; + static final long ONE_YEAR_INTERVAL = 31536000000L; + static final long ONE_DAY_INTERVAL = 86400000L; + /** The downsampling specification when provided */ protected final DownsamplingSpecification specification; @@ -43,6 +51,9 @@ public class Downsampler implements SeekableView, DataPoint { /** Whether or not to merge all DPs in the source into one vaalue */ protected final boolean run_all; + protected final TimeZone timezone; + protected final boolean use_calendar; + /** * Ctor. * @param source The iterator to access the underlying data. @@ -65,6 +76,8 @@ public class Downsampler implements SeekableView, DataPoint { query_start = 0; query_end = 0; run_all = false; + timezone = TimeZone.getDefault(); + use_calendar = false; } /** @@ -78,13 +91,17 @@ public class Downsampler implements SeekableView, DataPoint { Downsampler(final SeekableView source, final DownsamplingSpecification specification, final long query_start, - final long query_end + final long query_end, + final TimeZone timezone, + final boolean use_calendar ) { this.source = source; this.specification = specification; values_in_interval = new ValuesInInterval(); this.query_start = query_start; this.query_end = query_end; + this.timezone = timezone; + this.use_calendar = use_calendar; final String s = specification.getStringInterval(); if (s != null && s.toLowerCase().contains("all")) { @@ -249,9 +266,13 @@ private void moveToNextValue() { * interval. */ private void resetEndOfInterval() { if (has_next_value_from_source && !run_all) { - // Sets the end of the interval of the timestamp. - timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + - specification.getInterval(); + if (use_calendar && isCalendarInterval()) { + timestamp_end_interval = toEndOfInterval(next_dp.timestamp()); + } else { + // Sets the end of the interval of the timestamp. + timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + + specification.getInterval(); + } } } @@ -269,7 +290,10 @@ void seekInterval(final long timestamp) { // timestamp.. if (run_all) { source.seek(timestamp); - } else { + } else if (use_calendar && isCalendarInterval()) { + source.seek(alignTimestamp(timestamp + toEndOfInterval(timestamp) + - toStartOfInterval(timestamp))); + } else { source.seek(alignTimestamp(timestamp + specification.getInterval() - 1)); } initialized = false; @@ -282,7 +306,9 @@ protected long getIntervalTimestamp() { // provides the correct context for seek. if (run_all) { return timestamp_end_interval; - } else { + } else if (use_calendar && isCalendarInterval()) { + return toStartOfInterval(timestamp_end_interval); + } else { return alignTimestamp(timestamp_end_interval - specification.getInterval()); } @@ -290,9 +316,104 @@ protected long getIntervalTimestamp() { /** Returns timestamp aligned by interval. */ protected long alignTimestamp(final long timestamp) { - return timestamp - (timestamp % specification.getInterval()); + if (use_calendar && isCalendarInterval()) { + return toStartOfInterval(timestamp); + } else { + return timestamp - (timestamp % specification.getInterval()); + } + } + + /** Returns a flag denoting whether the interval can + * be aligned to the calendar */ + private boolean isCalendarInterval () { + if (specification.getInterval() != 0 && + (specification.getInterval() % ONE_YEAR_INTERVAL == 0 || + specification.getInterval() % ONE_MONTH_INTERVAL == 0 || + specification.getInterval() % ONE_WEEK_INTERVAL == 0 || + specification.getInterval() % ONE_DAY_INTERVAL == 0)) { + return true; + } + return false; + } + + /** Returns a timestamp corresponding to the start of the interval + * in which the specified timestamp occurs, aligned to the calendar + * based on the timezone. */ + private long toStartOfInterval(long timestamp) { + if (specification.getInterval() % ONE_YEAR_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_YEAR_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toStartOfYear(result, timezone) - 1; + } + return result + 1; + } else if (specification.getInterval() % ONE_MONTH_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_MONTH_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toStartOfMonth(result, timezone) - 1; + } + return result + 1; + } else if (specification.getInterval() % ONE_WEEK_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_WEEK_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toStartOfWeek(result, timezone) - 1; + } + return result + 1; + } else if (specification.getInterval() % ONE_DAY_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_DAY_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toStartOfDay(result, timezone) - 1; + } + return result + 1; + } else { + throw new IllegalArgumentException(specification.getInterval() + + " does not correspond to a " + + "an interval that can be aligned to the calendar."); + } } + /** Returns a timestamp corresponding to the end of the interval + * in which the specified timestamp occurs, aligned to the calendar + * based on the timezone. */ + private long toEndOfInterval(long timestamp) { + if (specification.getInterval() % ONE_YEAR_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_YEAR_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toEndOfYear(result, timezone) + 1; + } + return result - 1; + } else if (specification.getInterval() % ONE_MONTH_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_MONTH_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toEndOfMonth(result, timezone) + 1; + } + return result - 1; + } else if (specification.getInterval() % ONE_WEEK_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_WEEK_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toEndOfWeek(result, timezone) + 1; + } + return result - 1; + } else if (specification.getInterval() % ONE_DAY_INTERVAL == 0) { + final long multiplier = specification.getInterval() / ONE_DAY_INTERVAL; + long result = timestamp; + for (long i = 0; i < multiplier; i++) { + result = DateTime.toEndOfDay(result, timezone) + 1; + } + return result - 1; + } else { + throw new IllegalArgumentException(specification.getInterval() + + " does not correspond to a " + + "an interval that can be aligned to the calendar."); + } + } + // ---------------------- // // Doubles interface // // ---------------------- // diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index bcba99f61a..2de42bb734 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -13,6 +13,7 @@ package net.opentsdb.core; import java.util.NoSuchElementException; +import java.util.TimeZone; /** * A specialized downsampler that returns special values, based on the fill @@ -44,7 +45,7 @@ public class FillingDownsampler extends Downsampler { final Aggregator downsampler, final FillPolicy fill_policy) { this(source, start_time, end_time, new DownsamplingSpecification(interval_ms, downsampler, fill_policy) - , 0, 0); + , 0, 0, TimeZone.getDefault(), false); } /** @@ -60,9 +61,11 @@ public class FillingDownsampler extends Downsampler { */ FillingDownsampler(final SeekableView source, final long start_time, final long end_time, final DownsamplingSpecification specification, - final long query_start, final long end_start) { + final long query_start, final long end_start, + final TimeZone timezone, + final boolean use_calendar) { // Lean on the superclass implementation. - super(source, specification, query_start, end_start); + super(source, specification, query_start, end_start, timezone, use_calendar); // Ensure we aren't given a bogus fill policy. if (FillPolicy.NONE == specification.getFillPolicy()) { diff --git a/src/core/Query.java b/src/core/Query.java index 34553c591e..2d1db08f83 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -67,6 +67,24 @@ public interface Query { */ long getEndTime(); + /** + * Sets the timezone to use for aligning intervals based on the calendar. + * @param timezone the timezone to use + */ + void setTimezone(String timezone); + + /** @return the timezone to use for aligning intervals based on the calendar. */ + String getTimezone(); + + /** + * Sets a flag denoting whether or not to align intervals based on the calendar. + * @param use_calendar true, if the intervals should be aligned based on the calendar; false, otherwise + */ + void setUseCalendar(boolean use_calendar); + + /** @return A flag denoting whether or not to align intervals based on the calendar. */ + boolean getUseCalendar(); + /** * Sets whether or not the data queried will be deleted. * @param delete True if data should be deleted, false otherwise. diff --git a/src/core/Span.java b/src/core/Span.java index 15338956ae..a3d9d4894e 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.TimeZone; import net.opentsdb.meta.Annotation; import net.opentsdb.uid.UniqueId; @@ -499,16 +500,18 @@ Downsampler downsampler(final long start_time, final long end_time, final DownsamplingSpecification downsampler, final long query_start, - final long query_end) { + final long query_end, + final TimeZone timezone, + final boolean use_calendar) { if (downsampler == null) { return null; } if (FillPolicy.NONE == downsampler.getFillPolicy()) { return new Downsampler(spanIterator(), downsampler, - query_start, query_end); + query_start, query_end, timezone, use_calendar); } return new FillingDownsampler(spanIterator(), start_time, end_time, - downsampler, query_start, query_end); + downsampler, query_start, query_end, timezone, use_calendar); } public int getQueryIndex() { diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 90133dcf51..a24eb1e369 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; @@ -106,6 +107,9 @@ final class SpanGroup implements DataPoints { /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; + private final TimeZone timezone; + private final boolean use_calendar; + /** * Ctor. * @param tsdb The TSDB we belong to. @@ -193,7 +197,7 @@ final class SpanGroup implements DataPoints { downsampler != null ? new DownsamplingSpecification(interval, downsampler, fill_policy) : null, - query_index, 0, 0); + 0, 0, TimeZone.getDefault(), false, query_index); } /** @@ -225,6 +229,8 @@ final class SpanGroup implements DataPoints { final DownsamplingSpecification downsampler, final long query_start, final long query_end, + final TimeZone timezone, + final boolean use_calendar, final int query_index) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? @@ -244,6 +250,8 @@ final class SpanGroup implements DataPoints { this.query_end = query_end; this.query_index = query_index; this.tsdb = tsdb; + this.timezone = timezone; + this.use_calendar = use_calendar; } /** @@ -487,7 +495,7 @@ public SeekableView iterator() { return AggregationIterator.create(spans, start_time, end_time, aggregator, aggregator.interpolationMethod(), downsampler, query_start, query_end, - rate, rate_options); + timezone, use_calendar, rate, rate_options); } /** diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 6ae15fc789..80a4a11fc0 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -95,6 +95,9 @@ public final class TSQuery { /** Whether or not to delete the queried data */ private boolean delete = false; + /** A flag denoting whether or not to align intervals based on the calendar */ + private boolean use_calendar; + /** The query status for tracking over all performance of this query */ private QueryStats query_stats; @@ -362,6 +365,11 @@ public boolean getDelete() { return this.delete; } + /** @return the flag denoting whether intervals should be aligned based on the calendar */ + public boolean getUseCalendar() { + return use_calendar; + } + /** @return the query stats object. Ignored during JSON serialization */ @JsonIgnore public QueryStats getQueryStats() { @@ -447,6 +455,11 @@ public void setDelete(boolean delete) { this.delete = delete; } + /** @param use_calendar a flag denoting whether or not to align intervals based on the calendar */ + public void setUseCalendar(boolean use_calendar) { + this.use_calendar = use_calendar; + } + /** @param query_stats the query stats object to associate with this query */ public void setQueryStats(final QueryStats query_stats) { this.query_stats = query_stats; diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 105cecf8c2..5282dd4f8c 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import java.util.TreeMap; import org.slf4j.Logger; @@ -131,6 +132,12 @@ final class TsdbQuery implements Query { /** Tag value filters to apply post scan */ private List filters; + /** The timezone to use for aligning intervals based on the calendar */ + private String timezone; + + /** A flag denoting whether or not to align intervals based on the calendar */ + private boolean use_calendar; + /** An object for storing stats in regarding the query. May be null */ private QueryStats query_stats; @@ -303,6 +310,35 @@ public void setTimeSeries(final List tsuids, this.rate_options = rate_options; } + /** + * Sets the timezone to use for aligning intervals based on the calendar. + * @param timezone the timezone to use + */ + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + /** @return the timezone to use for aligning intervals based on the calendar. */ + @Override + public String getTimezone() { + return this.timezone; + } + + /** + * Sets a flag denoting whether or not to align intervals based on the calendar. + * @param use_calendar true, if the intervals should be aligned based on the calendar; false, otherwise + */ + @Override + public void setUseCalendar(boolean use_calendar) { + this.use_calendar = use_calendar; + } + + /** @return A flag denoting whether or not to align intervals based on the calendar. */ + @Override + public boolean getUseCalendar() { + return this.use_calendar; + } + /** * @param explicit_tags Whether or not to match only on the given tags * @since 2.3 @@ -325,6 +361,8 @@ public Deferred configureFromQuery(final TSQuery query, setStartTime(query.startTime()); setEndTime(query.endTime()); setDelete(query.getDelete()); + setTimezone(query.getTimezone()); + setUseCalendar(query.getUseCalendar()); query_index = index; query_stats = query.getQueryStats(); @@ -853,6 +891,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), + timezone != null ? DateTime.timezones.get(timezone) : TimeZone.getDefault(), + use_calendar, query_index); group.add(span); groups[i++] = group; @@ -872,6 +912,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), + timezone != null ? DateTime.timezones.get(timezone) : TimeZone.getDefault(), + use_calendar, query_index); if (query_stats != null) { query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); @@ -922,6 +964,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), + timezone != null ? DateTime.timezones.get(timezone) : TimeZone.getDefault(), + use_calendar, query_index); // Copy the array because we're going to keep `group' and overwrite // its contents. So we want the collection to have an immutable copy. diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index d3d9a08c44..d1386a45fa 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -14,6 +14,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.HashMap; import java.util.TimeZone; @@ -305,4 +306,175 @@ public static double msFromNanoDiff(final long end, final long start) { } return ((double) end - (double) start) / 1000000; } + + /** + * Returns a timestamp corresponding the beginning of the year in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the year + * @return the epoch time corresponding to the beginning of the year + */ + public static long toStartOfYear(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfYear(timestamp, time_zone); + return c.getTimeInMillis(); + } + + /** + * Returns a timestamp corresponding the end of the year in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the end of the year + * @return the epoch time corresponding to the end of the year + */ + public static long toEndOfYear(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfYear(timestamp, time_zone); + c.add(Calendar.YEAR, 1); + return c.getTimeInMillis() - 1; + } + + /** + * Returns a timestamp corresponding the beginning of the month in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the month + * @return the epoch time corresponding to the beginning of the month + */ + public static long toStartOfMonth(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfMonth(timestamp, time_zone); + return c.getTimeInMillis(); + } + + /** + * Returns a timestamp corresponding the end of the month in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the end of the month + * @return the epoch time corresponding to the end of the month + */ + public static long toEndOfMonth(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfMonth(timestamp, time_zone); + c.add(Calendar.MONTH, 1); + return c.getTimeInMillis() - 1; + } + + /** + * Returns a timestamp corresponding the beginning of the week in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the week + * @return the epoch time corresponding to the beginning of the week + */ + public static long toStartOfWeek(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfWeek(timestamp, time_zone); + return c.getTimeInMillis(); + } + + /** + * Returns a timestamp corresponding the end of the week in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the end of the week + * @return the epoch time corresponding to the end of the week + */ + public static long toEndOfWeek(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfWeek(timestamp, time_zone); + c.add(Calendar.DATE, 7); + return c.getTimeInMillis() - 1; + } + + /** + * Returns a timestamp corresponding the beginning of the day in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the day + * @return the epoch time corresponding to the beginning of the day + */ + public static long toStartOfDay(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfDay(timestamp, time_zone); + return c.getTimeInMillis(); + } + + /** + * Returns a timestamp corresponding the end of the day in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the end of the day + * @return the epoch time corresponding to the end of the day + */ + public static long toEndOfDay(final long timestamp, final TimeZone time_zone) { + final Calendar c = getStartOfDay(timestamp, time_zone); + c.add(Calendar.DATE, 1); + return c.getTimeInMillis() - 1; + } + + /** + * Returns a Calendar object corresponding the beginning of the year in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the year + * @return a Calendar object corresponding to the beginning of the year + */ + private static Calendar getStartOfYear(final long timestamp, final TimeZone time_zone) { + final Calendar c = Calendar.getInstance(time_zone); + c.setTimeInMillis(timestamp); + c.set(Calendar.DAY_OF_YEAR, 1); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c; + } + + /** + * Returns a Calendar object corresponding the beginning of the month in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the month + * @return a Calendar object corresponding to the beginning of the month + */ + private static Calendar getStartOfMonth(final long timestamp, final TimeZone time_zone) { + final Calendar c = Calendar.getInstance(time_zone); + c.setTimeInMillis(timestamp); + c.set(Calendar.DAY_OF_MONTH, 1); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c; + } + + /** + * Returns a Calendar object corresponding the beginning of the week in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the week + * @return a Calendar object corresponding to the beginning of the week + */ + private static Calendar getStartOfWeek(final long timestamp, final TimeZone time_zone) { + final Calendar c = Calendar.getInstance(time_zone); + c.setTimeInMillis(timestamp); + c.set(Calendar.DAY_OF_WEEK, 1); // 1-sun, 2-mon. + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c; + } + + /** + * Returns a Calendar object corresponding the beginning of the day in which the specified + * timestamp occurs. This operation is performed based on the specified time zone. + * @param timestamp the epoch time + * @param time_zone the time zone used to determine the beginning of the day + * @return a Calendar object corresponding to the beginning of the day + */ + private static Calendar getStartOfDay(final long timestamp, final TimeZone time_zone) { + final Calendar c = Calendar.getInstance(time_zone); + c.setTimeInMillis(timestamp); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c; + } } diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 97b54b02de..ccaa1eba5a 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -19,7 +19,10 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import java.util.Arrays; +import java.util.Calendar; import java.util.List; +import java.util.TimeZone; import com.google.common.collect.Lists; @@ -52,7 +55,9 @@ public class TestDownsampler { (int)DateTime.parseDuration("10s"); private static final Aggregator AVG = Aggregators.get("avg"); private static final Aggregator SUM = Aggregators.get("sum"); - + private static final TimeZone UTC_TIME_ZONE = DateTime.timezones.get("UTC"); + private static final TimeZone EST_TIME_ZONE = DateTime.timezones.get("EST"); + private SeekableView source; private Downsampler downsampler; private DownsamplingSpecification specification; @@ -65,7 +70,7 @@ public void before() { @Test public void testDownsampler() { specification = new DownsamplingSpecification("1000s-avg"); - downsampler = new Downsampler(source, specification, 0, 0); + downsampler = new Downsampler(source, specification, 0, 0, TimeZone.getDefault(), false); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -146,7 +151,7 @@ public void testDownsampler_10seconds() { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 10, 1024) })); specification = new DownsamplingSpecification("10s-sum"); - downsampler = new Downsampler(source, specification, 0, 0); + downsampler = new Downsampler(source, specification, 0, 0, TimeZone.getDefault(), false); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -215,7 +220,7 @@ public void testDownsampler_15seconds() { MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) })); specification = new DownsamplingSpecification("15s-sum"); - downsampler = new Downsampler(source, specification, 0, 0); + downsampler = new Downsampler(source, specification, 0, 0, TimeZone.getDefault(), false); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -248,8 +253,8 @@ public void testDownsampler_allFullRange() { MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) })); specification = new DownsamplingSpecification("0all-sum"); - downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); - System.out.println(downsampler); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, TimeZone.getDefault(), false); + verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -277,7 +282,7 @@ public void testDownsampler_allFilterOnQuery() { })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new Downsampler(source, specification, - BASE_TIME + 15000L, BASE_TIME + 45000L); + BASE_TIME + 15000L, BASE_TIME + 45000L, TimeZone.getDefault(), false); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -305,7 +310,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new Downsampler(source, specification, - BASE_TIME + 65000L, BASE_TIME + 75000L); + BASE_TIME + 65000L, BASE_TIME + 75000L, TimeZone.getDefault(), false); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -331,7 +336,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeLate() { })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new Downsampler(source, specification, - BASE_TIME - 15000L, BASE_TIME - 5000L); + BASE_TIME - 15000L, BASE_TIME - 5000L, TimeZone.getDefault(), false); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -345,6 +350,465 @@ public void testDownsampler_allFilterOnQueryOutOfRangeLate() { assertEquals(0, values.size()); } + @Test + public void testDownsampler_calendar() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME + 5000L, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 15000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 25000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 35000L, 8), + MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), + MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) + })); + specification = new DownsamplingSpecification("1d-sum"); + //specification.setTimezone(DateTime.timezones.get("America/Denver")); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, DateTime.timezones.get("America/Denver"), true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(63, values.get(0), 0.0000001); + assertEquals(1356937200000L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testDownsampler_noData() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { })); + specification = new DownsamplingSpecification("1d-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, TimeZone.getDefault(), false); + verify(source, never()).next(); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_noDataCalendar() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { })); + specification = new DownsamplingSpecification("1m-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, + UTC_TIME_ZONE, true); + verify(source, never()).next(); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_1day() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfDay(timestamp, UTC_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + System.out.println(Arrays.toString(data_points)); + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + downsampler = new Downsampler(source, Downsampler.ONE_DAY_INTERVAL, SUM); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfDay(timestamp, UTC_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1day_timezone() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfDay(timestamp, EST_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + specification = new DownsamplingSpecification("1d-sum"); + downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfDay(timestamp, EST_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1week() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfWeek(timestamp, UTC_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1w-sum"); + downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfWeek(timestamp, UTC_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1week_timezone() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfWeek(timestamp, EST_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1w-sum"); + downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfWeek(timestamp, EST_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1month() { + final DataPoint [] data_points = new DataPoint[24]; + long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1n-sum"); + downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(12, values.size()); + timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1month_alt() { + /* + 1380600000 -> 2013-10-01T04:00:00Z + 1383278400 -> 2013-11-01T04:00:00Z + 1385874000 -> 2013-12-01T05:00:00Z + 1388552400 -> 2014-01-01T05:00:00Z + 1391230800 -> 2014-02-01T05:00:00Z + 1393650000 -> 2014-03-01T05:00:00Z + 1396324800 -> 2014-04-01T04:00:00Z + 1398916800 -> 2014-05-01T04:00:00Z + 1401595200 -> 2014-06-01T04:00:00Z + 1404187200 -> 2014-07-01T04:00:00Z + 1406865600 -> 2014-08-01T04:00:00Z + 1409544000 -> 2014-09-01T04:00:00Z + */ + + int value = 1; + final DataPoint [] data_points = new DataPoint[] { + MutableDataPoint.ofLongValue(1380600000000L, value), + MutableDataPoint.ofLongValue(1383278400000L, value), + MutableDataPoint.ofLongValue(1385874000000L, value), + MutableDataPoint.ofLongValue(1388552400000L, value), + MutableDataPoint.ofLongValue(1391230800000L, value), + MutableDataPoint.ofLongValue(1393650000000L, value), + MutableDataPoint.ofLongValue(1396324800000L, value), + MutableDataPoint.ofLongValue(1398916800000L, value), + MutableDataPoint.ofLongValue(1401595200000L, value), + MutableDataPoint.ofLongValue(1404187200000L, value), + MutableDataPoint.ofLongValue(1406865600000L, value), + MutableDataPoint.ofLongValue(1409544000000L, value), + }; + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1d-sum"); + downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(12, values.size()); + long timestamp = DateTime.toStartOfMonth(data_points[0].timestamp(), UTC_TIME_ZONE); + for (int i = 0; i < values.size(); i++) { + assertEquals(1, values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_2months() { + final DataPoint [] data_points = new DataPoint[24]; + long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("2n-sum"); + downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(6, values.size()); + timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0, j = 0; i < values.size(); i++) { + long value = 0; + for (int k = 0; k < 4; k++) { + value += (1 << j++); + } + assertEquals(value, values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1month_timezone() { + final DataPoint [] data_points = new DataPoint[24]; + long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfMonth(timestamp, EST_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1n-sum"); + downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(12, values.size()); + timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfMonth(timestamp, EST_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1year() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1y-sum"); + downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + } + } + + @Test + public void testDownsampler_1year_timezone() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + + i += 1; + long startOfNextInterval = DateTime.toEndOfYear(timestamp, EST_TIME_ZONE) + 1; + timestamp = timestamp + (startOfNextInterval - timestamp) / 2; + value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = startOfNextInterval; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + specification = new DownsamplingSpecification("1y-sum"); + downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + for (int i = 0, j = 0; i < values.size(); i++) { + assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfYear(timestamp, EST_TIME_ZONE) + 1; + } + } + @Test(expected = UnsupportedOperationException.class) public void testRemove() { new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG).remove(); @@ -373,6 +837,70 @@ public void testSeek() { assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(2).longValue()); } + @Test + public void testSeek_useCalendar() { + final DataPoint [] data_points = new DataPoint[4]; + long timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE); + final Calendar c = Calendar.getInstance(UTC_TIME_ZONE); + c.setTimeInMillis(timestamp); + for (int i = 0; i < data_points.length; i++) { + long value = 1 << i; + data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); + timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + c.add(Calendar.YEAR, 2); + specification = new DownsamplingSpecification("1y-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, + UTC_TIME_ZONE, true); + System.out.println("SEEK: " + c.getTimeInMillis()); + downsampler.seek(c.getTimeInMillis()); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + timestamp = DateTime.toStartOfYear(c.getTimeInMillis(), UTC_TIME_ZONE); + for (int i = 2; i < values.size(); i++) { + assertEquals(1 << i, values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + } + + source = spy(SeekableViewsForTest.fromArray(data_points)); + + c.add(Calendar.MILLISECOND, 1); + specification = new DownsamplingSpecification("1y-sum"); + downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + downsampler.seek(c.getTimeInMillis()); + verify(source, never()).next(); + values = Lists.newArrayList(); + timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(1, values.size()); + timestamp = DateTime.toStartOfYear(c.getTimeInMillis(), UTC_TIME_ZONE); + for (int i = 3; i < values.size(); i++) { + assertEquals(1 << i, values.get(i), 0.0000001); + assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); + timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + } + + } + @Test public void testSeek_skipPartialInterval() { downsampler = new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG); diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index e762a8d33b..b33dc68fd9 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -18,6 +18,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.TimeZone; + /** Tests {@link FillingDownsampler}. */ public class TestFillingDownsampler { private static final long BASE_TIME = 1356998400000L; @@ -43,7 +45,7 @@ public void testNaNMissingInterval() { specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0); + baseTime + 36 * 25L, specification, 0, 0, TimeZone.getDefault(), false); long timestamp = baseTime; step(downsampler, timestamp, Double.NaN); @@ -76,7 +78,7 @@ public void testZeroMissingInterval() { specification = new DownsamplingSpecification("100ms-sum-zero"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0); + baseTime + 36 * 25L, specification, 0, 0, TimeZone.getDefault(), false); long timestamp = baseTime; step(downsampler, timestamp, 0.); @@ -113,7 +115,7 @@ public void testWithoutMissingIntervals() { specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, specification, 0, 0); + baseTime + 12L * 25L, specification, 0, 0, TimeZone.getDefault(), false); long timestamp = baseTime; step(downsampler, timestamp, 42.); @@ -146,7 +148,7 @@ public void testWithOutOfBoundsData() { specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, specification, 0, 0); + baseTime + 60000L * 2L, specification, 0, 0, TimeZone.getDefault(), false); long timestamp = 1425335880000L; step(downsampler, timestamp, 30.); @@ -165,7 +167,7 @@ public void testWithOutOfBoundsDataEarly() { specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, specification, 0, 0); + baseTime + 60000L * 2L, specification, 0, 0, TimeZone.getDefault(), false); long timestamp = 1425335880000L; step(downsampler, timestamp, Double.NaN); @@ -184,7 +186,7 @@ public void testWithOutOfBoundsDataLate() { specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, specification, 0, 0); + baseTime + 60000L * 2L, specification, 0, 0, TimeZone.getDefault(), false); long timestamp = 1425335880000L; step(downsampler, timestamp, Double.NaN); @@ -206,7 +208,7 @@ public void testDownsampler_allFullRange() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, 0, - Long.MAX_VALUE); + Long.MAX_VALUE, TimeZone.getDefault(), false); step(downsampler, 0, 63); assertFalse(downsampler.hasNext()); @@ -226,7 +228,7 @@ public void testDownsampler_allFilterOnQuery() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, - BASE_TIME + 15000L, BASE_TIME + 45000L); + BASE_TIME + 15000L, BASE_TIME + 45000L, TimeZone.getDefault(), false); step(downsampler, BASE_TIME + 15000L, 14); assertFalse(downsampler.hasNext()); @@ -246,7 +248,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, - BASE_TIME + 65000L, BASE_TIME + 75000L); + BASE_TIME + 65000L, BASE_TIME + 75000L, TimeZone.getDefault(), false); assertFalse(downsampler.hasNext()); } @@ -265,7 +267,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeLate() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, - BASE_TIME - 15000L, BASE_TIME - 5000L); + BASE_TIME - 15000L, BASE_TIME - 5000L, TimeZone.getDefault(), false); assertFalse(downsampler.hasNext()); } From 9ae709bded4160cea232383ce56c36ab84a392eb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 12 Mar 2016 18:06:50 -0800 Subject: [PATCH 437/826] Modify @cpdevoto's downsampling to allow for hourly downsample alignment as well as align to useful boundaries based on the start of calendar events such as the start of the year or start of the month instead of basing it off the first data point timestamp. Also modify the API calls so they use the downsample specification instead of adding params to the functions. And squash a bunch of the code to improve Calendar performance a bit and make the UTs a little simpler. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 5 +- src/core/Downsampler.java | 214 ++--- src/core/DownsamplingSpecification.java | 53 +- src/core/FillingDownsampler.java | 64 +- src/core/Query.java | 18 - src/core/Span.java | 9 +- src/core/SpanGroup.java | 12 +- src/core/TSQuery.java | 26 +- src/core/TsdbQuery.java | 44 - src/utils/DateTime.java | 403 +++++---- test/core/SeekableViewsForTest.java | 6 +- test/core/TestDownsampler.java | 867 +++++++++++++------ test/core/TestDownsamplingSpecification.java | 130 ++- test/core/TestFillingDownsampler.java | 564 +++++++++++- test/core/TestTSQuery.java | 65 ++ test/utils/TestDateTime.java | 488 +++++++++++ 16 files changed, 2288 insertions(+), 680 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 64e9179f18..03bc93b8cf 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -15,7 +15,6 @@ import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; -import java.util.TimeZone; import com.google.common.annotations.VisibleForTesting; @@ -305,8 +304,6 @@ public static AggregationIterator create(final List spans, final DownsamplingSpecification downsampler, final long query_start, final long query_end, - final TimeZone timezone, - final boolean use_calendar, final boolean rate, final RateOptions rate_options) { final int size = spans.size(); @@ -318,7 +315,7 @@ public static AggregationIterator create(final List spans, it = spans.get(i).spanIterator(); } else { it = spans.get(i).downsampler(start_time, end_time, downsampler, - query_start, query_end, timezone, use_calendar); + query_start, query_end); } if (rate) { it = new RateSpan(it, rate_options); diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 8123cb5579..004e768e33 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -12,8 +12,8 @@ // see . package net.opentsdb.core; +import java.util.Calendar; import java.util.NoSuchElementException; -import java.util.TimeZone; import net.opentsdb.utils.DateTime; @@ -21,11 +21,11 @@ * Iterator that downsamples data points using an {@link Aggregator}. */ public class Downsampler implements SeekableView, DataPoint { - - static final long ONE_WEEK_INTERVAL = 604800000L; - static final long ONE_MONTH_INTERVAL = 2592000000L; - static final long ONE_YEAR_INTERVAL = 31536000000L; - static final long ONE_DAY_INTERVAL = 86400000L; + + /** Matches the weekly downsampler as it requires special handling. */ + protected final static int WEEK_UNIT = DateTime.unitsToCalendarType("w"); + protected final static int DAY_UNIT = DateTime.unitsToCalendarType("d"); + protected final static int WEEK_LENGTH = 7; /** The downsampling specification when provided */ protected final DownsamplingSpecification specification; @@ -51,8 +51,11 @@ public class Downsampler implements SeekableView, DataPoint { /** Whether or not to merge all DPs in the source into one vaalue */ protected final boolean run_all; - protected final TimeZone timezone; - protected final boolean use_calendar; + /** The interval to use with a calendar */ + protected final int interval; + + /** The unit to use with a calendar as a Calendar integer */ + protected final int unit; /** * Ctor. @@ -66,18 +69,17 @@ public class Downsampler implements SeekableView, DataPoint { final long interval_ms, final Aggregator downsampler) { this.source = source; - values_in_interval = new ValuesInInterval(); if (downsampler == Aggregators.NONE) { throw new IllegalArgumentException("cannot use the NONE " + "aggregator for downsampling"); } specification = new DownsamplingSpecification(interval_ms, downsampler, DownsamplingSpecification.DEFAULT_FILL_POLICY); + values_in_interval = new ValuesInInterval(); query_start = 0; query_end = 0; + interval = unit = 0; run_all = false; - timezone = TimeZone.getDefault(); - use_calendar = false; } /** @@ -91,23 +93,30 @@ public class Downsampler implements SeekableView, DataPoint { Downsampler(final SeekableView source, final DownsamplingSpecification specification, final long query_start, - final long query_end, - final TimeZone timezone, - final boolean use_calendar + final long query_end ) { this.source = source; this.specification = specification; values_in_interval = new ValuesInInterval(); this.query_start = query_start; this.query_end = query_end; - this.timezone = timezone; - this.use_calendar = use_calendar; final String s = specification.getStringInterval(); if (s != null && s.toLowerCase().contains("all")) { run_all = true; - } else { + interval = unit = 0; + } else if (s != null && specification.useCalendar()) { + if (s.toLowerCase().contains("ms")) { + interval = Integer.parseInt(s.substring(0, s.length() - 2)); + unit = DateTime.unitsToCalendarType(s.substring(s.length() - 2)); + } else { + interval = Integer.parseInt(s.substring(0, s.length() - 1)); + unit = DateTime.unitsToCalendarType(s.substring(s.length() - 1)); + } + run_all = false; + } else { run_all = false; + interval = unit = 0; } } @@ -197,6 +206,12 @@ public String toString() { /** Iterates source values for an interval. */ protected class ValuesInInterval implements Aggregator.Doubles { + /** An optional calendar set to the current timestamp for the data point */ + private Calendar previous_calendar; + + /** An optional calendar set to the end of the interval timestamp */ + private Calendar next_calendar; + /** The end of the current interval. */ private long timestamp_end_interval = Long.MIN_VALUE; @@ -215,6 +230,8 @@ protected class ValuesInInterval implements Aggregator.Doubles { protected ValuesInInterval() { if (run_all) { timestamp_end_interval = query_end; + } else if (!specification.useCalendar()) { + timestamp_end_interval = specification.getInterval(); } } @@ -225,9 +242,25 @@ protected void initializeIfNotDone() { // performance penalty by accessing the unnecessary first data of a span. if (!initialized) { initialized = true; - moveToNextValue(); - if (!run_all) { - resetEndOfInterval(); + if (source.hasNext()) { + moveToNextValue(); + if (!run_all) { + if (specification.useCalendar()) { + previous_calendar = DateTime.previousInterval(next_dp.timestamp(), + interval, unit, specification.getTimezone()); + next_calendar = DateTime.previousInterval(next_dp.timestamp(), + interval, unit, specification.getTimezone()); + if (unit == WEEK_UNIT) { + next_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + next_calendar.add(unit, interval); + } + timestamp_end_interval = next_calendar.getTimeInMillis(); + } else { + timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + + specification.getInterval(); + } + } } } } @@ -266,10 +299,18 @@ private void moveToNextValue() { * interval. */ private void resetEndOfInterval() { if (has_next_value_from_source && !run_all) { - if (use_calendar && isCalendarInterval()) { - timestamp_end_interval = toEndOfInterval(next_dp.timestamp()); - } else { - // Sets the end of the interval of the timestamp. + if (specification.useCalendar()) { + while (next_dp.timestamp() >= timestamp_end_interval) { + if (unit == WEEK_UNIT) { + previous_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + next_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + previous_calendar.add(unit, interval); + next_calendar.add(unit, interval); + } + timestamp_end_interval = next_calendar.getTimeInMillis(); + } + } else { timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + specification.getInterval(); } @@ -290,10 +331,18 @@ void seekInterval(final long timestamp) { // timestamp.. if (run_all) { source.seek(timestamp); - } else if (use_calendar && isCalendarInterval()) { - source.seek(alignTimestamp(timestamp + toEndOfInterval(timestamp) - - toStartOfInterval(timestamp))); - } else { + } else if (specification.useCalendar()) { + final Calendar seek_calendar = DateTime.previousInterval( + timestamp, interval, unit, specification.getTimezone()); + if (timestamp > seek_calendar.getTimeInMillis()) { + if (unit == WEEK_UNIT) { + seek_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + seek_calendar.add(unit, interval); + } + } + source.seek(seek_calendar.getTimeInMillis()); + } else { source.seek(alignTimestamp(timestamp + specification.getInterval() - 1)); } initialized = false; @@ -306,9 +355,9 @@ protected long getIntervalTimestamp() { // provides the correct context for seek. if (run_all) { return timestamp_end_interval; - } else if (use_calendar && isCalendarInterval()) { - return toStartOfInterval(timestamp_end_interval); - } else { + } else if (specification.useCalendar()) { + return previous_calendar.getTimeInMillis(); + } else { return alignTimestamp(timestamp_end_interval - specification.getInterval()); } @@ -316,102 +365,7 @@ protected long getIntervalTimestamp() { /** Returns timestamp aligned by interval. */ protected long alignTimestamp(final long timestamp) { - if (use_calendar && isCalendarInterval()) { - return toStartOfInterval(timestamp); - } else { - return timestamp - (timestamp % specification.getInterval()); - } - } - - /** Returns a flag denoting whether the interval can - * be aligned to the calendar */ - private boolean isCalendarInterval () { - if (specification.getInterval() != 0 && - (specification.getInterval() % ONE_YEAR_INTERVAL == 0 || - specification.getInterval() % ONE_MONTH_INTERVAL == 0 || - specification.getInterval() % ONE_WEEK_INTERVAL == 0 || - specification.getInterval() % ONE_DAY_INTERVAL == 0)) { - return true; - } - return false; - } - - /** Returns a timestamp corresponding to the start of the interval - * in which the specified timestamp occurs, aligned to the calendar - * based on the timezone. */ - private long toStartOfInterval(long timestamp) { - if (specification.getInterval() % ONE_YEAR_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_YEAR_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toStartOfYear(result, timezone) - 1; - } - return result + 1; - } else if (specification.getInterval() % ONE_MONTH_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_MONTH_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toStartOfMonth(result, timezone) - 1; - } - return result + 1; - } else if (specification.getInterval() % ONE_WEEK_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_WEEK_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toStartOfWeek(result, timezone) - 1; - } - return result + 1; - } else if (specification.getInterval() % ONE_DAY_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_DAY_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toStartOfDay(result, timezone) - 1; - } - return result + 1; - } else { - throw new IllegalArgumentException(specification.getInterval() + - " does not correspond to a " + - "an interval that can be aligned to the calendar."); - } - } - - /** Returns a timestamp corresponding to the end of the interval - * in which the specified timestamp occurs, aligned to the calendar - * based on the timezone. */ - private long toEndOfInterval(long timestamp) { - if (specification.getInterval() % ONE_YEAR_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_YEAR_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toEndOfYear(result, timezone) + 1; - } - return result - 1; - } else if (specification.getInterval() % ONE_MONTH_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_MONTH_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toEndOfMonth(result, timezone) + 1; - } - return result - 1; - } else if (specification.getInterval() % ONE_WEEK_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_WEEK_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toEndOfWeek(result, timezone) + 1; - } - return result - 1; - } else if (specification.getInterval() % ONE_DAY_INTERVAL == 0) { - final long multiplier = specification.getInterval() / ONE_DAY_INTERVAL; - long result = timestamp; - for (long i = 0; i < multiplier; i++) { - result = DateTime.toEndOfDay(result, timezone) + 1; - } - return result - 1; - } else { - throw new IllegalArgumentException(specification.getInterval() + - " does not correspond to a " + - "an interval that can be aligned to the calendar."); - } + return timestamp - (timestamp % specification.getInterval()); } // ---------------------- // @@ -445,7 +399,11 @@ public String toString() { buf.append("ValuesInInterval: ") .append(", timestamp_end_interval=").append(timestamp_end_interval) .append(", has_next_value_from_source=") - .append(has_next_value_from_source); + .append(has_next_value_from_source) + .append(", previousCalendar=") + .append(previous_calendar == null ? "null" : previous_calendar) + .append(", nextCalendar=") + .append(next_calendar == null ? "null" : next_calendar); if (has_next_value_from_source) { buf.append(", nextValue=(").append(next_dp).append(')'); } diff --git a/src/core/DownsamplingSpecification.java b/src/core/DownsamplingSpecification.java index 5abe810029..b64cceb157 100644 --- a/src/core/DownsamplingSpecification.java +++ b/src/core/DownsamplingSpecification.java @@ -13,6 +13,7 @@ package net.opentsdb.core; import java.util.NoSuchElementException; +import java.util.TimeZone; import com.google.common.base.MoreObjects; import net.opentsdb.utils.DateTime; @@ -40,12 +41,18 @@ public final class DownsamplingSpecification { //The string interval, e.g. 1h, 30d, etc private final String string_interval; - + // Parsed downsampler function. private final Aggregator function; // Parsed fill policy: whether to interpolate or to fill. private final FillPolicy fill_policy; + + // Whether or not to use the calendar for intervals + private boolean use_calendar; + + // The user provided timezone for calendar alignment (defaults to UTC) + private TimeZone timezone; /** * A specification indicating no downsampling is requested. @@ -55,6 +62,8 @@ private DownsamplingSpecification() { function = NO_FUNCTION; fill_policy = DEFAULT_FILL_POLICY; string_interval = null; + use_calendar = false; + timezone = DateTime.timezones.get(DateTime.UTC_ID); } /** @@ -85,12 +94,16 @@ public DownsamplingSpecification(final long interval, this.function = function; this.fill_policy = fill_policy; string_interval = null; + use_calendar = false; + timezone = DateTime.timezones.get(DateTime.UTC_ID); } /** * C-tor for string representations. * The argument to this c-tor should have the following format: * {@code interval-function[-fill_policy]}. + * This ctor supports the "all" flag to downsample to a single value as well + * as units suffixed with 'c' to use the calendar for downsample alignment. * @param specification String representation of a downsample specifier. * @throws IllegalArgumentException if the specification is null or invalid. */ @@ -118,9 +131,16 @@ public DownsamplingSpecification(final String specification) { // This will throw if interval is invalid. if (parts[0].contains("all")) { interval = NO_INTERVAL; + use_calendar = false; string_interval = parts[0]; + } else if (parts[0].charAt(parts[0].length() - 1) == 'c') { + final String duration = parts[0].substring(0, parts[0].length() - 1); + interval = DateTime.parseDuration(duration); + string_interval = duration; + use_calendar = true; } else { interval = DateTime.parseDuration(parts[0]); + use_calendar = false; string_interval = parts[0]; } @@ -155,8 +175,25 @@ public DownsamplingSpecification(final String specification) { // Default to linear interpolation. fill_policy = FillPolicy.NONE; } + timezone = DateTime.timezones.get(DateTime.UTC_ID); } + /** @param use_calendar Whether or not to use the calendar when downsampling + * @since 2.3 */ + public void setUseCalendar(final boolean use_calendar) { + this.use_calendar = use_calendar; + } + + /** @param timezone The timezone to use when downsampling on calendar + * boundaries. + * @since 2.3 */ + public void setTimezone(final TimeZone timezone) { + if (timezone == null) { + throw new IllegalArgumentException("Timezone cannot be null"); + } + this.timezone = timezone; + } + /** * Get the downsampling interval, in milliseconds. * @return the downsampling interval, in milliseconds. @@ -187,6 +224,18 @@ public FillPolicy getFillPolicy() { return fill_policy; } + /** @return Whether or not to use the calendar when downsampling + * @since 2.3 */ + public boolean useCalendar() { + return use_calendar; + } + + /** @return The timezone to use when downsampling on calendar boundaries. + * @since 2.3 */ + public TimeZone getTimezone() { + return timezone; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -194,6 +243,8 @@ public String toString() { .add("function", getFunction()) .add("fillPolicy", getFillPolicy()) .add("stringInterval", string_interval) + .add("useCalendar", useCalendar()) + .add("timeZone", getTimezone() != null ? getTimezone().getID() : null) .toString(); } } diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index 2de42bb734..bd47205bf8 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -12,8 +12,10 @@ // see . package net.opentsdb.core; +import java.util.Calendar; import java.util.NoSuchElementException; -import java.util.TimeZone; + +import net.opentsdb.utils.DateTime; /** * A specialized downsampler that returns special values, based on the fill @@ -26,6 +28,12 @@ public class FillingDownsampler extends Downsampler { /** Track when the downsampled data should end. */ protected long end_timestamp; + + /** An optional calendar set to the current timestamp for the data point */ + private final Calendar previous_calendar; + + /** An optional calendar set to the end of the interval timestamp */ + private final Calendar next_calendar; /** * Create a new nulling downsampler. @@ -45,7 +53,7 @@ public class FillingDownsampler extends Downsampler { final Aggregator downsampler, final FillPolicy fill_policy) { this(source, start_time, end_time, new DownsamplingSpecification(interval_ms, downsampler, fill_policy) - , 0, 0, TimeZone.getDefault(), false); + , 0, 0); } /** @@ -61,11 +69,9 @@ public class FillingDownsampler extends Downsampler { */ FillingDownsampler(final SeekableView source, final long start_time, final long end_time, final DownsamplingSpecification specification, - final long query_start, final long end_start, - final TimeZone timezone, - final boolean use_calendar) { + final long query_start, final long end_start) { // Lean on the superclass implementation. - super(source, specification, query_start, end_start, timezone, use_calendar); + super(source, specification, query_start, end_start); // Ensure we aren't given a bogus fill policy. if (FillPolicy.NONE == specification.getFillPolicy()) { @@ -78,11 +84,36 @@ public class FillingDownsampler extends Downsampler { if (run_all) { timestamp = start_time; end_timestamp = end_time; + previous_calendar = next_calendar = null; + } else if (specification.useCalendar()) { + previous_calendar = DateTime.previousInterval(start_time, interval, unit, + specification.getTimezone()); + if (unit == WEEK_UNIT) { + previous_calendar.add(DAY_UNIT, -(interval * WEEK_LENGTH)); + } else { + previous_calendar.add(unit, -interval); + } + next_calendar = DateTime.previousInterval(start_time, interval, unit, + specification.getTimezone()); + + final Calendar end_calendar = DateTime.previousInterval( + end_time, interval, unit, specification.getTimezone()); + if (end_calendar.getTimeInMillis() == next_calendar.getTimeInMillis()) { + // advance once + if (unit == WEEK_UNIT) { + end_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + end_calendar.add(unit, interval); + } + } + timestamp = next_calendar.getTimeInMillis(); + end_timestamp = end_calendar.getTimeInMillis(); } else { // Use the values-in-interval object to align the timestamps at which we // expect data to arrive for the first and last intervals. timestamp = values_in_interval.alignTimestamp(start_time); end_timestamp = values_in_interval.alignTimestamp(end_time); + previous_calendar = next_calendar = null; } } @@ -120,7 +151,9 @@ public DataPoint next() { values_in_interval.initializeIfNotDone(); // Skip any leading data outside the query bounds. - long actual = values_in_interval.getIntervalTimestamp(); + long actual = values_in_interval.hasNextValue() ? + values_in_interval.getIntervalTimestamp() : Long.MAX_VALUE; + while (!run_all && values_in_interval.hasNextValue() && actual < timestamp) { // The actual timestamp precedes our expected, so there's data in the @@ -157,7 +190,20 @@ public DataPoint next() { } // Advance the expected timestamp to the next interval. - timestamp += specification.getInterval(); + if (!run_all) { + if (specification.useCalendar()) { + if (unit == WEEK_UNIT) { + previous_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + next_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + previous_calendar.add(unit, interval); + next_calendar.add(unit, interval); + } + timestamp = next_calendar.getTimeInMillis(); + } else { + timestamp += specification.getInterval(); + } + } // This object also represents the data. return this; @@ -172,6 +218,8 @@ public DataPoint next() { public long timestamp() { if (run_all) { return query_start; + } else if (specification.useCalendar()) { + return previous_calendar.getTimeInMillis(); } return timestamp - specification.getInterval(); } diff --git a/src/core/Query.java b/src/core/Query.java index 2d1db08f83..f4ea1bce4f 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -66,24 +66,6 @@ public interface Query { * @return A strictly positive integer. */ long getEndTime(); - - /** - * Sets the timezone to use for aligning intervals based on the calendar. - * @param timezone the timezone to use - */ - void setTimezone(String timezone); - - /** @return the timezone to use for aligning intervals based on the calendar. */ - String getTimezone(); - - /** - * Sets a flag denoting whether or not to align intervals based on the calendar. - * @param use_calendar true, if the intervals should be aligned based on the calendar; false, otherwise - */ - void setUseCalendar(boolean use_calendar); - - /** @return A flag denoting whether or not to align intervals based on the calendar. */ - boolean getUseCalendar(); /** * Sets whether or not the data queried will be deleted. diff --git a/src/core/Span.java b/src/core/Span.java index a3d9d4894e..15338956ae 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.TimeZone; import net.opentsdb.meta.Annotation; import net.opentsdb.uid.UniqueId; @@ -500,18 +499,16 @@ Downsampler downsampler(final long start_time, final long end_time, final DownsamplingSpecification downsampler, final long query_start, - final long query_end, - final TimeZone timezone, - final boolean use_calendar) { + final long query_end) { if (downsampler == null) { return null; } if (FillPolicy.NONE == downsampler.getFillPolicy()) { return new Downsampler(spanIterator(), downsampler, - query_start, query_end, timezone, use_calendar); + query_start, query_end); } return new FillingDownsampler(spanIterator(), start_time, end_time, - downsampler, query_start, query_end, timezone, use_calendar); + downsampler, query_start, query_end); } public int getQueryIndex() { diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index a24eb1e369..7e50bb8ef6 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; @@ -107,9 +106,6 @@ final class SpanGroup implements DataPoints { /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; - private final TimeZone timezone; - private final boolean use_calendar; - /** * Ctor. * @param tsdb The TSDB we belong to. @@ -197,7 +193,7 @@ final class SpanGroup implements DataPoints { downsampler != null ? new DownsamplingSpecification(interval, downsampler, fill_policy) : null, - 0, 0, TimeZone.getDefault(), false, query_index); + 0, 0, query_index); } /** @@ -229,8 +225,6 @@ final class SpanGroup implements DataPoints { final DownsamplingSpecification downsampler, final long query_start, final long query_end, - final TimeZone timezone, - final boolean use_calendar, final int query_index) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? @@ -250,8 +244,6 @@ final class SpanGroup implements DataPoints { this.query_end = query_end; this.query_index = query_index; this.tsdb = tsdb; - this.timezone = timezone; - this.use_calendar = use_calendar; } /** @@ -495,7 +487,7 @@ public SeekableView iterator() { return AggregationIterator.create(spans, start_time, end_time, aggregator, aggregator.interpolationMethod(), downsampler, query_start, query_end, - timezone, use_calendar, rate, rate_options); + rate, rate_options); } /** diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 80a4a11fc0..071b401812 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -16,6 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -113,7 +114,7 @@ public int hashCode() { // NOTE: Do not add any non-user submitted variables to the hash. We don't // want the hash to change after validation. // We also don't care about stats or summary - return Objects.hashCode(start, end, timezone, options, padding, + return Objects.hashCode(start, end, timezone, use_calendar, options, padding, no_annotations, with_global_annotations, show_tsuids, queries, ms_resolution); } @@ -137,6 +138,7 @@ public boolean equals(final Object obj) { return Objects.equal(start, query.start) && Objects.equal(end, query.end) && Objects.equal(timezone, query.timezone) + && Objects.equal(use_calendar,query.use_calendar) && Objects.equal(options, query.options) && Objects.equal(padding, query.padding) && Objects.equal(no_annotations, query.no_annotations) @@ -182,6 +184,21 @@ public void validateAndSetQuery() { int i = 0; for (TSSubQuery sub : queries) { sub.validateAndSetQuery(); + final DownsamplingSpecification ds = sub.downsamplingSpecification(); + if (ds != null && timezone != null && !timezone.isEmpty() && + ds != DownsamplingSpecification.NO_DOWNSAMPLER) { + final TimeZone tz = DateTime.timezones.get(timezone); + if (tz == null) { + throw new IllegalArgumentException( + "The timezone specification could not be found"); + } + ds.setTimezone(tz); + } + if (ds != null && use_calendar && + ds != DownsamplingSpecification.NO_DOWNSAMPLER) { + ds.setUseCalendar(true); + } + sub.setIndex(i++); } } @@ -365,7 +382,9 @@ public boolean getDelete() { return this.delete; } - /** @return the flag denoting whether intervals should be aligned based on the calendar */ + /** @return the flag denoting whether intervals should be aligned based on + * the calendar + * @since 2.3 */ public boolean getUseCalendar() { return use_calendar; } @@ -455,7 +474,8 @@ public void setDelete(boolean delete) { this.delete = delete; } - /** @param use_calendar a flag denoting whether or not to align intervals based on the calendar */ + /** @param use_calendar a flag denoting whether or not to align intervals + * based on the calendar @since 2.3 */ public void setUseCalendar(boolean use_calendar) { this.use_calendar = use_calendar; } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 5282dd4f8c..105cecf8c2 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.TreeMap; import org.slf4j.Logger; @@ -132,12 +131,6 @@ final class TsdbQuery implements Query { /** Tag value filters to apply post scan */ private List filters; - /** The timezone to use for aligning intervals based on the calendar */ - private String timezone; - - /** A flag denoting whether or not to align intervals based on the calendar */ - private boolean use_calendar; - /** An object for storing stats in regarding the query. May be null */ private QueryStats query_stats; @@ -310,35 +303,6 @@ public void setTimeSeries(final List tsuids, this.rate_options = rate_options; } - /** - * Sets the timezone to use for aligning intervals based on the calendar. - * @param timezone the timezone to use - */ - public void setTimezone(String timezone) { - this.timezone = timezone; - } - - /** @return the timezone to use for aligning intervals based on the calendar. */ - @Override - public String getTimezone() { - return this.timezone; - } - - /** - * Sets a flag denoting whether or not to align intervals based on the calendar. - * @param use_calendar true, if the intervals should be aligned based on the calendar; false, otherwise - */ - @Override - public void setUseCalendar(boolean use_calendar) { - this.use_calendar = use_calendar; - } - - /** @return A flag denoting whether or not to align intervals based on the calendar. */ - @Override - public boolean getUseCalendar() { - return this.use_calendar; - } - /** * @param explicit_tags Whether or not to match only on the given tags * @since 2.3 @@ -361,8 +325,6 @@ public Deferred configureFromQuery(final TSQuery query, setStartTime(query.startTime()); setEndTime(query.endTime()); setDelete(query.getDelete()); - setTimezone(query.getTimezone()); - setUseCalendar(query.getUseCalendar()); query_index = index; query_stats = query.getQueryStats(); @@ -891,8 +853,6 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), - timezone != null ? DateTime.timezones.get(timezone) : TimeZone.getDefault(), - use_calendar, query_index); group.add(span); groups[i++] = group; @@ -912,8 +872,6 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), - timezone != null ? DateTime.timezones.get(timezone) : TimeZone.getDefault(), - use_calendar, query_index); if (query_stats != null) { query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); @@ -964,8 +922,6 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), - timezone != null ? DateTime.timezones.get(timezone) : TimeZone.getDefault(), - use_calendar, query_index); // Copy the array because we're going to keep `group' and overwrite // its contents. So we want the collection to have an immutable copy. diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index d1386a45fa..6690573ddc 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -27,7 +27,9 @@ * @since 2.0 */ public class DateTime { - + /** ID of the UTC timezone */ + public static final String UTC_ID = "UTC"; + /** * Immutable cache mapping a timezone name to its object. * We do this because the JDK's TimeZone class was implemented by retards, @@ -171,6 +173,10 @@ public static final long parseDuration(final String duration) { int unit = 0; while (Character.isDigit(duration.charAt(unit))) { unit++; + if (unit >= duration.length()) { + throw new IllegalArgumentException("Invalid duration, must have an " + + "integer and unit: " + duration); + } } try { interval = Long.parseLong(duration.substring(0, unit)); @@ -306,175 +312,254 @@ public static double msFromNanoDiff(final long end, final long start) { } return ((double) end - (double) start) / 1000000; } - - /** - * Returns a timestamp corresponding the beginning of the year in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the year - * @return the epoch time corresponding to the beginning of the year - */ - public static long toStartOfYear(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfYear(timestamp, time_zone); - return c.getTimeInMillis(); - } - - /** - * Returns a timestamp corresponding the end of the year in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the end of the year - * @return the epoch time corresponding to the end of the year - */ - public static long toEndOfYear(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfYear(timestamp, time_zone); - c.add(Calendar.YEAR, 1); - return c.getTimeInMillis() - 1; - } - - /** - * Returns a timestamp corresponding the beginning of the month in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the month - * @return the epoch time corresponding to the beginning of the month - */ - public static long toStartOfMonth(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfMonth(timestamp, time_zone); - return c.getTimeInMillis(); - } /** - * Returns a timestamp corresponding the end of the month in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the end of the month - * @return the epoch time corresponding to the end of the month + * Returns a calendar set to the previous interval time based on the + * units and UTC the timezone. This allows for snapping to day, week, + * monthly, etc. boundaries. + * NOTE: It uses a calendar for snapping so isn't as efficient as a simple + * modulo calculation. + * NOTE: For intervals that don't nicely divide into their given unit (e.g. + * a 23s interval where 60 seconds is not divisible by 23) the base time may + * start at the top of the day (for ms and s) or from Unix epoch 0. In the + * latter case, setting up the base timestamp may be slow if the caller does + * something silly like "23m" where we iterate 23 minutes at a time from 0 + * till we find the proper timestamp. + * TODO - There is likely a better way to do all of this + * @param ts The timestamp to find an interval for, in milliseconds as + * a Unix epoch. + * @param interval The interval as a measure of units. + * @param unit The unit. This must cast to a Calendar time unit. + * @return A calendar set to the timestamp aligned to the proper interval + * before the given ts + * @throws IllegalArgumentException if the timestamp is negative, if the + * interval is less than 1 or the unit is unrecognized. + * @since 2.3 */ - public static long toEndOfMonth(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfMonth(timestamp, time_zone); - c.add(Calendar.MONTH, 1); - return c.getTimeInMillis() - 1; - } - - /** - * Returns a timestamp corresponding the beginning of the week in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the week - * @return the epoch time corresponding to the beginning of the week - */ - public static long toStartOfWeek(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfWeek(timestamp, time_zone); - return c.getTimeInMillis(); - } - - /** - * Returns a timestamp corresponding the end of the week in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the end of the week - * @return the epoch time corresponding to the end of the week - */ - public static long toEndOfWeek(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfWeek(timestamp, time_zone); - c.add(Calendar.DATE, 7); - return c.getTimeInMillis() - 1; - } - - /** - * Returns a timestamp corresponding the beginning of the day in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the day - * @return the epoch time corresponding to the beginning of the day - */ - public static long toStartOfDay(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfDay(timestamp, time_zone); - return c.getTimeInMillis(); + public static Calendar previousInterval(final long ts, final int interval, + final int unit) { + return previousInterval(ts, interval, unit, null); } /** - * Returns a timestamp corresponding the end of the day in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the end of the day - * @return the epoch time corresponding to the end of the day - */ - public static long toEndOfDay(final long timestamp, final TimeZone time_zone) { - final Calendar c = getStartOfDay(timestamp, time_zone); - c.add(Calendar.DATE, 1); - return c.getTimeInMillis() - 1; - } - - /** - * Returns a Calendar object corresponding the beginning of the year in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the year - * @return a Calendar object corresponding to the beginning of the year + * Returns a calendar set to the previous interval time based on the + * units and timezone. This allows for snapping to day, week, monthly, etc. + * boundaries. + * NOTE: It uses a calendar for snapping so isn't as efficient as a simple + * modulo calculation. + * NOTE: For intervals that don't nicely divide into their given unit (e.g. + * a 23s interval where 60 seconds is not divisible by 23) the base time may + * start at the top of the day (for ms and s) or from Unix epoch 0. In the + * latter case, setting up the base timestamp may be slow if the caller does + * something silly like "23m" where we iterate 23 minutes at a time from 0 + * till we find the proper timestamp. + * TODO - There is likely a better way to do all of this + * @param ts The timestamp to find an interval for, in milliseconds as + * a Unix epoch. + * @param interval The interval as a measure of units. + * @param unit The unit. This must cast to a Calendar time unit. + * @param tz An optional timezone. + * @return A calendar set to the timestamp aligned to the proper interval + * before the given ts + * @throws IllegalArgumentException if the timestamp is negative, if the + * interval is less than 1 or the unit is unrecognized. + * @since 2.3 */ - private static Calendar getStartOfYear(final long timestamp, final TimeZone time_zone) { - final Calendar c = Calendar.getInstance(time_zone); - c.setTimeInMillis(timestamp); - c.set(Calendar.DAY_OF_YEAR, 1); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - return c; - } - - /** - * Returns a Calendar object corresponding the beginning of the month in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the month - * @return a Calendar object corresponding to the beginning of the month - */ - private static Calendar getStartOfMonth(final long timestamp, final TimeZone time_zone) { - final Calendar c = Calendar.getInstance(time_zone); - c.setTimeInMillis(timestamp); - c.set(Calendar.DAY_OF_MONTH, 1); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - return c; + public static Calendar previousInterval(final long ts, final int interval, + final int unit, final TimeZone tz) { + if (ts < 0) { + throw new IllegalArgumentException("Timestamp cannot be less than zero"); + } + if (interval < 1) { + throw new IllegalArgumentException("Interval must be greater than zero"); + } + + int unit_override = unit; + int interval_override = interval; + final Calendar calendar; + if (tz == null) { + calendar = Calendar.getInstance(timezones.get(UTC_ID)); + } else { + calendar = Calendar.getInstance(tz); + } + + switch (unit_override) { + case Calendar.MILLISECOND: + if (1000 % interval_override == 0) { + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + if (interval_override > 1000) { + calendar.add(Calendar.MILLISECOND, -interval_override); + } + } else { + // from top of minute + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + } + break; + case Calendar.SECOND: + if (60 % interval_override == 0) { + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + if (interval_override > 60) { + calendar.add(Calendar.SECOND, -interval_override); + } + } else { + // from top of hour + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + } + break; + case Calendar.MINUTE: + if (60 % interval_override == 0) { + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + if (interval_override > 60) { + calendar.add(Calendar.MINUTE, -interval_override); + } + } else { + // from top of day + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + } + break; + case Calendar.HOUR_OF_DAY: + if (24 % interval_override == 0) { + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + if (interval_override > 24) { + calendar.add(Calendar.HOUR_OF_DAY, -interval_override); + } + } else { + // from top of month + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.DAY_OF_MONTH, 1); + } + break; + case Calendar.DAY_OF_MONTH: + if (interval_override == 1) { + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.DAY_OF_MONTH, 1); + } else { + // from top of year + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.MONTH, 0); + } + break; + case Calendar.DAY_OF_WEEK: + if (2 % interval_override == 0) { + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + } else { + // from top of year + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MONTH, 0); + calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + } + unit_override = Calendar.DAY_OF_MONTH; + interval_override = 7; + break; + case Calendar.WEEK_OF_YEAR: + // from top of year + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.MONTH, 0); + break; + case Calendar.MONTH: + case Calendar.YEAR: + calendar.setTimeInMillis(ts); + calendar.set(Calendar.MILLISECOND, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.MONTH, 0); + break; + default: + throw new IllegalArgumentException("Unexpected unit_overrides of type: " + + unit_override); + } + + if (calendar.getTimeInMillis() == ts) { + return calendar; + } + // TODO optimize a bit. We probably don't need to go past then back. + while (calendar.getTimeInMillis() <= ts) { + calendar.add(unit_override, interval_override); + } + calendar.add(unit_override, -interval_override); + return calendar; } /** - * Returns a Calendar object corresponding the beginning of the week in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the week - * @return a Calendar object corresponding to the beginning of the week + * Return the proper Calendar time unit as an integer given the string + * @param units The unit to parse + * @return An integer matching a Calendar. enum + * @throws IllegalArgumentException if the unit is null, empty or doesn't + * match one of the configured units. + * @since 2.3 */ - private static Calendar getStartOfWeek(final long timestamp, final TimeZone time_zone) { - final Calendar c = Calendar.getInstance(time_zone); - c.setTimeInMillis(timestamp); - c.set(Calendar.DAY_OF_WEEK, 1); // 1-sun, 2-mon. - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - return c; + public static int unitsToCalendarType(final String units) { + if (units == null || units.isEmpty()) { + throw new IllegalArgumentException("Units cannot be null or empty"); + } + + final String lc = units.toLowerCase(); + if (lc.equals("ms")) { + return Calendar.MILLISECOND; + } else if (lc.equals("s")) { + return Calendar.SECOND; + } else if (lc.equals("m")) { + return Calendar.MINUTE; + } else if (lc.equals("h")) { + return Calendar.HOUR_OF_DAY; + } else if (lc.equals("d")) { + return Calendar.DAY_OF_MONTH; + } else if (lc.equals("w")) { + return Calendar.DAY_OF_WEEK; + } else if (lc.equals("n")) { + return Calendar.MONTH; + } else if (lc.equals("y")) { + return Calendar.YEAR; + } + throw new IllegalArgumentException("Unrecognized unit type: " + units); } - /** - * Returns a Calendar object corresponding the beginning of the day in which the specified - * timestamp occurs. This operation is performed based on the specified time zone. - * @param timestamp the epoch time - * @param time_zone the time zone used to determine the beginning of the day - * @return a Calendar object corresponding to the beginning of the day - */ - private static Calendar getStartOfDay(final long timestamp, final TimeZone time_zone) { - final Calendar c = Calendar.getInstance(time_zone); - c.setTimeInMillis(timestamp); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - return c; - } } diff --git a/test/core/SeekableViewsForTest.java b/test/core/SeekableViewsForTest.java index 4b638e5ac6..bc6463da1c 100644 --- a/test/core/SeekableViewsForTest.java +++ b/test/core/SeekableViewsForTest.java @@ -93,7 +93,7 @@ public static SeekableView generator(final long start_time, } /** Iterates an array of data points. */ - private static class MockSeekableView implements SeekableView { + public static class MockSeekableView implements SeekableView { private final DataPoint[] data_points; private int index = 0; @@ -128,6 +128,10 @@ public void seek(long timestamp) { } } } + + public void resetIndex() { + index = 0; + } } /** Generates a sequence of data points. */ diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index ccaa1eba5a..07f7296cdd 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -19,13 +19,13 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.TimeZone; import com.google.common.collect.Lists; +import net.opentsdb.core.SeekableViewsForTest.MockSeekableView; import net.opentsdb.utils.DateTime; import org.junit.Before; @@ -55,8 +55,15 @@ public class TestDownsampler { (int)DateTime.parseDuration("10s"); private static final Aggregator AVG = Aggregators.get("avg"); private static final Aggregator SUM = Aggregators.get("sum"); - private static final TimeZone UTC_TIME_ZONE = DateTime.timezones.get("UTC"); private static final TimeZone EST_TIME_ZONE = DateTime.timezones.get("EST"); + //30 minute offset + final static TimeZone AF = DateTime.timezones.get("Asia/Kabul"); + // 12h offset w/o DST + final static TimeZone TV = DateTime.timezones.get("Pacific/Funafuti"); + // 12h offset w DST + final static TimeZone FJ = DateTime.timezones.get("Pacific/Fiji"); + // Tue, 15 Dec 2015 04:02:25.123 UTC + final static long DST_TS = 1450137600000L; private SeekableView source; private Downsampler downsampler; @@ -70,7 +77,7 @@ public void before() { @Test public void testDownsampler() { specification = new DownsamplingSpecification("1000s-avg"); - downsampler = new Downsampler(source, specification, 0, 0, TimeZone.getDefault(), false); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -151,7 +158,7 @@ public void testDownsampler_10seconds() { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 10, 1024) })); specification = new DownsamplingSpecification("10s-sum"); - downsampler = new Downsampler(source, specification, 0, 0, TimeZone.getDefault(), false); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -220,7 +227,7 @@ public void testDownsampler_15seconds() { MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) })); specification = new DownsamplingSpecification("15s-sum"); - downsampler = new Downsampler(source, specification, 0, 0, TimeZone.getDefault(), false); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -253,7 +260,7 @@ public void testDownsampler_allFullRange() { MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) })); specification = new DownsamplingSpecification("0all-sum"); - downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, TimeZone.getDefault(), false); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); List values = Lists.newArrayList(); @@ -282,7 +289,7 @@ public void testDownsampler_allFilterOnQuery() { })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new Downsampler(source, specification, - BASE_TIME + 15000L, BASE_TIME + 45000L, TimeZone.getDefault(), false); + BASE_TIME + 15000L, BASE_TIME + 45000L); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -310,7 +317,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new Downsampler(source, specification, - BASE_TIME + 65000L, BASE_TIME + 75000L, TimeZone.getDefault(), false); + BASE_TIME + 65000L, BASE_TIME + 75000L); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -336,7 +343,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeLate() { })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new Downsampler(source, specification, - BASE_TIME - 15000L, BASE_TIME - 5000L, TimeZone.getDefault(), false); + BASE_TIME - 15000L, BASE_TIME - 5000L); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -360,9 +367,9 @@ public void testDownsampler_calendar() { MutableDataPoint.ofLongValue(BASE_TIME + 45000L, 16), MutableDataPoint.ofLongValue(BASE_TIME + 55000L, 32) })); - specification = new DownsamplingSpecification("1d-sum"); - //specification.setTimezone(DateTime.timezones.get("America/Denver")); - downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, DateTime.timezones.get("America/Denver"), true); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(DateTime.timezones.get("America/Denver")); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -378,11 +385,423 @@ public void testDownsampler_calendar() { assertEquals(1356937200000L, timestamps_in_millis.get(0).longValue()); } + @Test + public void testDownsampler_calendarHour() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 1800000, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 3599000L, 3), + MutableDataPoint.ofLongValue(BASE_TIME + 3600000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 5400000L, 5), + MutableDataPoint.ofLongValue(BASE_TIME + 7199000L, 6) + })); + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(TV); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = BASE_TIME; + double value = 6; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 3600000; + value = 15; + } + + // hour offset by 30m + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1356996600000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 3600000; + if (value == 1) { + value = 9; + } else { + value = 11; + } + } + + // multiple hours + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("4hc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1356996600000L; + value = 21; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + } + } + + @Test + public void testDownsampler_calendarDay() { + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(DST_TS, 1), + MutableDataPoint.ofLongValue(DST_TS + 86399000, 2), + MutableDataPoint.ofLongValue(DST_TS + 126001000L, 3), // falls to the next in FJ + MutableDataPoint.ofLongValue(DST_TS + 172799000L, 4), + MutableDataPoint.ofLongValue(DST_TS + 172800000L, 5), + MutableDataPoint.ofLongValue(DST_TS + 242999000L, 6) // falls within 30m offset + })); + + // control + specification = new DownsamplingSpecification("1dc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = DST_TS; + double value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 3) { + value = 7; + } else if (value == 7) { + value = 11; + } + } + + // 12 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(TV); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450094400000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 9; + } else { + value = 6; + } + } + + // 11 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(FJ); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450090800000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 1) { + value = 2; + } else if (value == 2) { + value = 12; + } else { + value = 6; + } + } + + // 30m offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450121400000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 15; + } + } + + // multiple days + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("3dc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450121400000L; + value = 21; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + } + } + + @Test + public void testDownsampler_calendarWeek() { + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(DST_TS, 1), // a Tuesday in UTC land + MutableDataPoint.ofLongValue(DST_TS + (86400000L * 7), 2), + MutableDataPoint.ofLongValue(1451129400000L, 3), // falls to the next in FJ + MutableDataPoint.ofLongValue(DST_TS + (86400000L * 21), 4), + MutableDataPoint.ofLongValue(1452367799000L, 5) // falls within 30m offset + }); + // control + specification = new DownsamplingSpecification("1wc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = 1449964800000L; + double value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1450569600000L) { + ts = 1451779200000L; // skips a week + } else { + ts += 86400000L * 7; + } + if (value == 1) { + value = 5; + } else { + value = 9; + } + } + + // 12 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(TV); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449921600000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1450526400000L) { + ts = 1451736000000L; // skip a week + } else { + ts += 86400000L * 7; + } + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 4; + } else { + value = 5; + } + } + + // 11 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(FJ); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449918000000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 7; + value++; + } + + // 30m offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449948600000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1449948600000L) { + ts = 1450553400000L; + } else { + ts = 1451763000000L; + } + if (value == 1) { + value = 5; + } else { + value = 9; + } + } + + // multiple weeks + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("2wc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449948600000L; + value = 6; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts = 1451158200000L; + value = 9; + } + } + + @Test + public void testDownsampler_calendarMonth() { + final long dec_1st = 1448928000000L; + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(dec_1st, 1), + MutableDataPoint.ofLongValue(1451559600000L, 2), // falls to the next in FJ + MutableDataPoint.ofLongValue(1451606400000L, 3), // jan 1st + MutableDataPoint.ofLongValue(1454284800000L, 4), // feb 1st + MutableDataPoint.ofLongValue(1456704000000L, 5), // feb 29th (leap year) + MutableDataPoint.ofLongValue(1456772400000L, 6) // falls within 30m offset AF + })); + + // control + specification = new DownsamplingSpecification("1nc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = dec_1st; + double value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1448928000000L) { + ts = 1451606400000L; + } else { + ts = 1454284800000L; + value = 15; + } + } + + // 12h offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(TV); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1448884800000L; + value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1448884800000L) { + ts = 1451563200000L; + } else if (ts == 1451563200000L) { + value = 9; + ts = 1454241600000L; + } else { + ts = 1456747200000L; + value = 6; + } + } + + // 11h offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(FJ); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1448881200000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + if (ts == 1448881200000L) { + ts = 1451559600000L; + value = 5; + } else if (ts == 1451559600000L) { + ts = 1454241600000L; + value = 9; + } else { + ts = 1456747200000L; + value = 6; + } + } + + // 30m offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(AF); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1448911800000L; + value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1448911800000L) { + ts = 1451590200000L; + } else { + ts = 1454268600000L; + value = 15; + } + } + + // multiple months + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("3nc-sum"); + specification.setTimezone(TV); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1443614400000L; + value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts = 1451563200000L; + value = 18; + } + } + @Test public void testDownsampler_noData() { source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { })); specification = new DownsamplingSpecification("1d-sum"); - downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, TimeZone.getDefault(), false); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); assertFalse(downsampler.hasNext()); } @@ -390,179 +809,124 @@ public void testDownsampler_noData() { @Test public void testDownsampler_noDataCalendar() { source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { })); - specification = new DownsamplingSpecification("1m-sum"); - downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, - UTC_TIME_ZONE, true); + specification = new DownsamplingSpecification("1mc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); assertFalse(downsampler.hasNext()); } @Test public void testDownsampler_1day() { - final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0; i < data_points.length; i++) { - long value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - - i += 1; - long startOfNextInterval = DateTime.toEndOfDay(timestamp, UTC_TIME_ZONE) + 1; - timestamp = timestamp + (startOfNextInterval - timestamp) / 2; - value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - timestamp = startOfNextInterval; - } - - System.out.println(Arrays.toString(data_points)); - - source = spy(SeekableViewsForTest.fromArray(data_points)); + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 43200000L, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 86400000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 129600000L, 8) + })); - downsampler = new Downsampler(source, Downsampler.ONE_DAY_INTERVAL, SUM); + downsampler = new Downsampler(source, 86400000, SUM); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + long timestamp = BASE_TIME; + double value = 3; while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfDay(timestamp, UTC_TIME_ZONE) + 1; + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.000001); + timestamp = 1357084800000L; + value = 12; } } @Test public void testDownsampler_1day_timezone() { - final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); - for (int i = 0; i < data_points.length; i++) { - long value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - - i += 1; - long startOfNextInterval = DateTime.toEndOfDay(timestamp, EST_TIME_ZONE) + 1; - timestamp = timestamp + (startOfNextInterval - timestamp) / 2; - value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - timestamp = startOfNextInterval; - } + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(1357016400000L, 1), + MutableDataPoint.ofLongValue(1357059600000L, 2), + MutableDataPoint.ofLongValue(1357102800000L, 4), + MutableDataPoint.ofLongValue(1357146000000L, 8) + })); - source = spy(SeekableViewsForTest.fromArray(data_points)); - specification = new DownsamplingSpecification("1d-sum"); - downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(EST_TIME_ZONE); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + + long timestamp = 1357016400000L; + double value = 3; while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfDay(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfDay(timestamp, EST_TIME_ZONE) + 1; + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.000001); + timestamp = 1357102800000L; + value = 12; } } @Test public void testDownsampler_1week() { - final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0; i < data_points.length; i++) { - long value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - - i += 1; - long startOfNextInterval = DateTime.toEndOfWeek(timestamp, UTC_TIME_ZONE) + 1; - timestamp = timestamp + (startOfNextInterval - timestamp) / 2; - value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - timestamp = startOfNextInterval; - } - - source = spy(SeekableViewsForTest.fromArray(data_points)); + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(1356825600000L, 1), + MutableDataPoint.ofLongValue(1357128000000L, 2), + MutableDataPoint.ofLongValue(1357430400000L, 4), + MutableDataPoint.ofLongValue(1357732800000L, 8) + })); - specification = new DownsamplingSpecification("1w-sum"); - downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + specification = new DownsamplingSpecification("1wc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + long timestamp = 1356825600000L; + double value = 3; while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfWeek(timestamp, UTC_TIME_ZONE) + 1; + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.000001); + timestamp = 1357430400000L; + value = 12; } } @Test public void testDownsampler_1week_timezone() { - final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); - for (int i = 0; i < data_points.length; i++) { - long value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - - i += 1; - long startOfNextInterval = DateTime.toEndOfWeek(timestamp, EST_TIME_ZONE) + 1; - timestamp = timestamp + (startOfNextInterval - timestamp) / 2; - value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - timestamp = startOfNextInterval; - } - - source = spy(SeekableViewsForTest.fromArray(data_points)); + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(1356843600000L, 1), + MutableDataPoint.ofLongValue(1357146000000L, 2), + MutableDataPoint.ofLongValue(1357448400000L, 4), + MutableDataPoint.ofLongValue(1357750800000L, 8) + })); - specification = new DownsamplingSpecification("1w-sum"); - downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(EST_TIME_ZONE); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + long timestamp = 1356843600000L; + double value = 3; while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfWeek(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfWeek(timestamp, EST_TIME_ZONE) + 1; + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.000001); + timestamp = 1357448400000L; + value = 12; } } @Test public void testDownsampler_1month() { + final int field = DateTime.unitsToCalendarType("n"); final DataPoint [] data_points = new DataPoint[24]; - long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + Calendar c = DateTime.previousInterval(BASE_TIME, 1, field); + //long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + long timestamp = c.getTimeInMillis(); for (int i = 0; i < data_points.length; i++) { long value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); i += 1; - long startOfNextInterval = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + c.add(field, 1); + long startOfNextInterval = c.getTimeInMillis() + 1; timestamp = timestamp + (startOfNextInterval - timestamp) / 2; value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); @@ -570,25 +934,18 @@ public void testDownsampler_1month() { } source = spy(SeekableViewsForTest.fromArray(data_points)); - - specification = new DownsamplingSpecification("1n-sum"); - downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + + specification = new DownsamplingSpecification("1nc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + c = DateTime.previousInterval(BASE_TIME, 1, field); + int j = 0; while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(12, values.size()); - timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + assertEquals((1 << j++) + (1 << j++), dp.doubleValue(), 0.0000001); + assertEquals(c.getTimeInMillis(), dp.timestamp()); + c.add(field, 1); } } @@ -627,37 +984,35 @@ public void testDownsampler_1month_alt() { source = spy(SeekableViewsForTest.fromArray(data_points)); - specification = new DownsamplingSpecification("1d-sum"); - downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + specification = new DownsamplingSpecification("1dc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + final int field = DateTime.unitsToCalendarType("n"); + final Calendar c = DateTime.previousInterval(1380585600000L, 1, field); + long timestamp = c.getTimeInMillis(); while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(12, values.size()); - long timestamp = DateTime.toStartOfMonth(data_points[0].timestamp(), UTC_TIME_ZONE); - for (int i = 0; i < values.size(); i++) { - assertEquals(1, values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + assertEquals(1, dp.doubleValue(), 0.0000001); + assertEquals(timestamp, dp.timestamp()); + c.add(field, 1); + timestamp = c.getTimeInMillis(); } } @Test public void testDownsampler_2months() { + final int field = DateTime.unitsToCalendarType("n"); final DataPoint [] data_points = new DataPoint[24]; - long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); + Calendar c = DateTime.previousInterval(BASE_TIME, 1, field); + long timestamp = c.getTimeInMillis(); for (int i = 0; i < data_points.length; i++) { long value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); i += 1; - long startOfNextInterval = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + c.add(field, 1); + long startOfNextInterval = c.getTimeInMillis(); timestamp = timestamp + (startOfNextInterval - timestamp) / 2; value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); @@ -666,42 +1021,37 @@ public void testDownsampler_2months() { source = spy(SeekableViewsForTest.fromArray(data_points)); - specification = new DownsamplingSpecification("2n-sum"); - downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + specification = new DownsamplingSpecification("2nc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + int j = 0; + c = DateTime.previousInterval(BASE_TIME, 1, field); while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(6, values.size()); - timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0, j = 0; i < values.size(); i++) { long value = 0; for (int k = 0; k < 4; k++) { value += (1 << j++); } - assertEquals(value, values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; - timestamp = DateTime.toEndOfMonth(timestamp, UTC_TIME_ZONE) + 1; + assertEquals(value, dp.doubleValue(), 0.0000001); + assertEquals(c.getTimeInMillis(), dp.timestamp()); + c.add(field, 2); } } - + @Test public void testDownsampler_1month_timezone() { + final int field = DateTime.unitsToCalendarType("n"); final DataPoint [] data_points = new DataPoint[24]; - long timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + Calendar c = DateTime.previousInterval(1357016400000L, 1, field, EST_TIME_ZONE); + long timestamp = c.getTimeInMillis(); for (int i = 0; i < data_points.length; i++) { long value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); i += 1; - long startOfNextInterval = DateTime.toEndOfMonth(timestamp, EST_TIME_ZONE) + 1; + c.add(field, 1); + long startOfNextInterval = c.getTimeInMillis(); timestamp = timestamp + (startOfNextInterval - timestamp) / 2; value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); @@ -710,37 +1060,34 @@ public void testDownsampler_1month_timezone() { source = spy(SeekableViewsForTest.fromArray(data_points)); - specification = new DownsamplingSpecification("1n-sum"); - downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(EST_TIME_ZONE); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + int j = 0; + c = DateTime.previousInterval(1357016400000L, 1, field, EST_TIME_ZONE); while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(12, values.size()); - timestamp = DateTime.toStartOfMonth(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfMonth(timestamp, EST_TIME_ZONE) + 1; + assertEquals((1 << j++) + (1 << j++), dp.doubleValue(), 0.0000001); + assertEquals(c.getTimeInMillis(), dp.timestamp()); + c.add(field, 1); } } @Test public void testDownsampler_1year() { + final int field = DateTime.unitsToCalendarType("y"); final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE); + Calendar c = DateTime.previousInterval(BASE_TIME, 1, field); + long timestamp = c.getTimeInMillis(); for (int i = 0; i < data_points.length; i++) { long value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); i += 1; - long startOfNextInterval = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + c.add(field, 1); + long startOfNextInterval = c.getTimeInMillis(); timestamp = timestamp + (startOfNextInterval - timestamp) / 2; value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); @@ -749,37 +1096,34 @@ public void testDownsampler_1year() { source = spy(SeekableViewsForTest.fromArray(data_points)); - specification = new DownsamplingSpecification("1y-sum"); - downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); + specification = new DownsamplingSpecification("1yc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + int j = 0; + c = DateTime.previousInterval(BASE_TIME, 1, field); while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; + assertEquals((1 << j++) + (1 << j++), dp.doubleValue(), 0.0000001); + assertEquals(c.getTimeInMillis(), dp.timestamp()); + c.add(field, 1); } } @Test public void testDownsampler_1year_timezone() { + final int field = DateTime.unitsToCalendarType("y"); final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); + Calendar c = DateTime.previousInterval(1357016400000L, 1, field, + EST_TIME_ZONE); + long timestamp = c.getTimeInMillis(); for (int i = 0; i < data_points.length; i++) { long value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); i += 1; - long startOfNextInterval = DateTime.toEndOfYear(timestamp, EST_TIME_ZONE) + 1; + c.add(field, 1); + long startOfNextInterval = c.getTimeInMillis(); timestamp = timestamp + (startOfNextInterval - timestamp) / 2; value = 1 << i; data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); @@ -788,24 +1132,18 @@ public void testDownsampler_1year_timezone() { source = spy(SeekableViewsForTest.fromArray(data_points)); - specification = new DownsamplingSpecification("1y-sum"); - downsampler = new Downsampler(source, specification, 0, 0, EST_TIME_ZONE, true); + specification = new DownsamplingSpecification("1yc-sum"); + specification.setTimezone(EST_TIME_ZONE); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + int j = 0; + c = DateTime.previousInterval(1357016400000L, 1, field, EST_TIME_ZONE); while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); - } - - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE) - EST_TIME_ZONE.getOffset(BASE_TIME); - for (int i = 0, j = 0; i < values.size(); i++) { - assertEquals((1 << j++) + (1 << j++), values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfYear(timestamp, EST_TIME_ZONE) + 1; + assertEquals((1 << j++) + (1 << j++), dp.doubleValue(), 0.0000001); + assertEquals(c.getTimeInMillis(), dp.timestamp()); + c.add(field, 1); } } @@ -839,66 +1177,42 @@ public void testSeek() { @Test public void testSeek_useCalendar() { - final DataPoint [] data_points = new DataPoint[4]; - long timestamp = DateTime.toStartOfYear(BASE_TIME, UTC_TIME_ZONE); - final Calendar c = Calendar.getInstance(UTC_TIME_ZONE); - c.setTimeInMillis(timestamp); - for (int i = 0; i < data_points.length; i++) { - long value = 1 << i; - data_points[i] = MutableDataPoint.ofLongValue(timestamp, value); - timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; - } - - source = spy(SeekableViewsForTest.fromArray(data_points)); + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(1356998400000L, 1), + MutableDataPoint.ofLongValue(1388534400000L, 2), + MutableDataPoint.ofLongValue(1420070400000L, 4), + MutableDataPoint.ofLongValue(1451606400000L, 8) + })); - c.add(Calendar.YEAR, 2); specification = new DownsamplingSpecification("1y-sum"); - downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE, - UTC_TIME_ZONE, true); - System.out.println("SEEK: " + c.getTimeInMillis()); - downsampler.seek(c.getTimeInMillis()); + specification.setUseCalendar(true); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + + downsampler.seek(1420070400000L); verify(source, never()).next(); - List values = Lists.newArrayList(); - List timestamps_in_millis = Lists.newArrayList(); + + long timestamp = 1420070400000L; + double value = 4; while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.0000001); + timestamp = 1451606400000L; + value = 8; } - assertEquals(2, values.size()); - timestamp = DateTime.toStartOfYear(c.getTimeInMillis(), UTC_TIME_ZONE); - for (int i = 2; i < values.size(); i++) { - assertEquals(1 << i, values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; - } + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1yc-sum"); + downsampler = new Downsampler(source, specification, 0, Long.MAX_VALUE); + downsampler.seek(1420070400001L); - source = spy(SeekableViewsForTest.fromArray(data_points)); - - c.add(Calendar.MILLISECOND, 1); - specification = new DownsamplingSpecification("1y-sum"); - downsampler = new Downsampler(source, specification, 0, 0, UTC_TIME_ZONE, true); - downsampler.seek(c.getTimeInMillis()); - verify(source, never()).next(); - values = Lists.newArrayList(); - timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { DataPoint dp = downsampler.next(); assertFalse(dp.isInteger()); - values.add(dp.doubleValue()); - timestamps_in_millis.add(dp.timestamp()); + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.0000001); } - - assertEquals(1, values.size()); - timestamp = DateTime.toStartOfYear(c.getTimeInMillis(), UTC_TIME_ZONE); - for (int i = 3; i < values.size(); i++) { - assertEquals(1 << i, values.get(i), 0.0000001); - assertEquals(timestamp, timestamps_in_millis.get(i).longValue()); - timestamp = DateTime.toEndOfYear(timestamp, UTC_TIME_ZONE) + 1; - } - } @Test @@ -995,7 +1309,6 @@ public void testSeek_abandoningIncompleteInterval() { public void testToString() { downsampler = new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG); DataPoint dp = downsampler.next(); - System.out.println(downsampler.toString()); assertTrue(downsampler.toString().contains(dp.toString())); } } diff --git a/test/core/TestDownsamplingSpecification.java b/test/core/TestDownsamplingSpecification.java index 0fc51f0368..9e541f55ec 100644 --- a/test/core/TestDownsamplingSpecification.java +++ b/test/core/TestDownsamplingSpecification.java @@ -14,9 +14,20 @@ import org.junit.Test; +import net.opentsdb.utils.DateTime; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.TimeZone; public class TestDownsamplingSpecification { + final long interval = 60000L; + final FillPolicy fill_policy = FillPolicy.ZERO; + final Aggregator function = Aggregators.SUM; + final TimeZone timezone = DateTime.timezones.get(DateTime.UTC_ID); + @Test public void testCtor() { final long interval = 1234567L; @@ -41,16 +52,6 @@ public void testStringCtor() { assertEquals(FillPolicy.NOT_A_NUMBER, ds.getFillPolicy()); } - @Test - public void testToString() { - assertEquals("DownsamplingSpecification{interval=4532019, function=zimsum, " - + "fillPolicy=NOT_A_NUMBER, stringInterval=null}", - new DownsamplingSpecification( - 4532019L, - Aggregators.ZIMSUM, - FillPolicy.NOT_A_NUMBER).toString()); - } - @Test(expected = RuntimeException.class) public void testBadInterval() { new DownsamplingSpecification("blah-avg-lerp"); @@ -70,5 +71,114 @@ public void testBadFillPolicy() { public void testNoneAgg() { new DownsamplingSpecification("1m-none-lerp"); } + + @Test (expected = IllegalArgumentException.class) + public void testCtorNegativeInterval() { + new DownsamplingSpecification(-1, function, fill_policy); + } + + @Test (expected = IllegalArgumentException.class) + public void testCtorZeroInterval() { + new DownsamplingSpecification(DownsamplingSpecification.NO_INTERVAL, + function, fill_policy); + } + + @Test (expected = IllegalArgumentException.class) + public void testCtorNullFunction() { + new DownsamplingSpecification(interval, null, fill_policy); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorNull() { + new DownsamplingSpecification(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorEmpty() { + new DownsamplingSpecification(""); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorNoIntervalString() { + new DownsamplingSpecification("blah-avg-lerp"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorZeroInterval() { + new DownsamplingSpecification("0m-avg-lerp"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorNegativeInterval() { + new DownsamplingSpecification("-60m-avg-lerp"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorUnknownUnits() { + new DownsamplingSpecification("1j-avg-lerp"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorMissingUnits() { + new DownsamplingSpecification("1-avg-lerp"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorBadFunction() { + new DownsamplingSpecification("1m-hurp-lerp"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorMissingFunction() { + new DownsamplingSpecification("1m"); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringCtorBadFillPolicy() { + new DownsamplingSpecification("10m-avg-max"); + } + + @Test + public void testSetCalendar() { + DownsamplingSpecification ds = new DownsamplingSpecification("15m-avg"); + assertFalse(ds.useCalendar()); + + ds.setUseCalendar(true); + assertTrue(ds.useCalendar()); + + ds.setUseCalendar(false); + assertFalse(ds.useCalendar()); + } + + @Test + public void setTimezone() { + DownsamplingSpecification ds = new DownsamplingSpecification("15m-avg"); + assertEquals(timezone, ds.getTimezone()); + + final TimeZone tz = DateTime.timezones.get("America/Denver"); + ds.setTimezone(tz); + assertEquals(tz, ds.getTimezone()); + } + + @Test (expected = IllegalArgumentException.class) + public void setTimezoneNull() { + DownsamplingSpecification ds = new DownsamplingSpecification("15m-avg"); + ds.setTimezone(null); + } + + @Test + public void testToString() { + final String string = new DownsamplingSpecification( + 4532019L, + Aggregators.ZIMSUM, + FillPolicy.NOT_A_NUMBER).toString(); + + assertTrue(string.contains("interval=4532019")); + assertTrue(string.contains("function=zimsum")); + assertTrue(string.contains("fillPolicy=NOT_A_NUMBER")); + assertTrue(string.contains("useCalendar=false")); + assertTrue(string.contains("timeZone=UTC")); + } + } diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index b33dc68fd9..ef42ebc963 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -14,16 +14,30 @@ import org.junit.Test; +import net.opentsdb.core.SeekableViewsForTest.MockSeekableView; +import net.opentsdb.utils.DateTime; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; import java.util.TimeZone; /** Tests {@link FillingDownsampler}. */ public class TestFillingDownsampler { private static final long BASE_TIME = 1356998400000L; - + //30 minute offset + final static TimeZone AF = DateTime.timezones.get("Asia/Kabul"); + // 12h offset w/o DST + final static TimeZone TV = DateTime.timezones.get("Pacific/Funafuti"); + // 12h offset w DST + final static TimeZone FJ = DateTime.timezones.get("Pacific/Fiji"); + // Tue, 15 Dec 2015 04:02:25.123 UTC + final static long DST_TS = 1450137600000L; + + private SeekableView source; + private Downsampler downsampler; private DownsamplingSpecification specification; /** Data with gaps: before, during, and after. */ @@ -45,7 +59,7 @@ public void testNaNMissingInterval() { specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0, TimeZone.getDefault(), false); + baseTime + 36 * 25L, specification, 0, 0); long timestamp = baseTime; step(downsampler, timestamp, Double.NaN); @@ -78,7 +92,7 @@ public void testZeroMissingInterval() { specification = new DownsamplingSpecification("100ms-sum-zero"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0, TimeZone.getDefault(), false); + baseTime + 36 * 25L, specification, 0, 0); long timestamp = baseTime; step(downsampler, timestamp, 0.); @@ -115,7 +129,7 @@ public void testWithoutMissingIntervals() { specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, specification, 0, 0, TimeZone.getDefault(), false); + baseTime + 12L * 25L, specification, 0, 0); long timestamp = baseTime; step(downsampler, timestamp, 42.); @@ -148,7 +162,7 @@ public void testWithOutOfBoundsData() { specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, specification, 0, 0, TimeZone.getDefault(), false); + baseTime + 60000L * 2L, specification, 0, 0); long timestamp = 1425335880000L; step(downsampler, timestamp, 30.); @@ -167,7 +181,7 @@ public void testWithOutOfBoundsDataEarly() { specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, specification, 0, 0, TimeZone.getDefault(), false); + baseTime + 60000L * 2L, specification, 0, 0); long timestamp = 1425335880000L; step(downsampler, timestamp, Double.NaN); @@ -186,7 +200,7 @@ public void testWithOutOfBoundsDataLate() { specification = new DownsamplingSpecification("1m-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 60000L * 2L, specification, 0, 0, TimeZone.getDefault(), false); + baseTime + 60000L * 2L, specification, 0, 0); long timestamp = 1425335880000L; step(downsampler, timestamp, Double.NaN); @@ -208,7 +222,7 @@ public void testDownsampler_allFullRange() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, 0, - Long.MAX_VALUE, TimeZone.getDefault(), false); + Long.MAX_VALUE); step(downsampler, 0, 63); assertFalse(downsampler.hasNext()); @@ -228,7 +242,7 @@ public void testDownsampler_allFilterOnQuery() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, - BASE_TIME + 15000L, BASE_TIME + 45000L, TimeZone.getDefault(), false); + BASE_TIME + 15000L, BASE_TIME + 45000L); step(downsampler, BASE_TIME + 15000L, 14); assertFalse(downsampler.hasNext()); @@ -248,7 +262,7 @@ public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, - BASE_TIME + 65000L, BASE_TIME + 75000L, TimeZone.getDefault(), false); + BASE_TIME + 65000L, BASE_TIME + 75000L); assertFalse(downsampler.hasNext()); } @@ -267,8 +281,536 @@ public void testDownsampler_allFilterOnQueryOutOfRangeLate() { specification = new DownsamplingSpecification("0all-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME + 5000L,BASE_TIME + 55000L, specification, - BASE_TIME - 15000L, BASE_TIME - 5000L, TimeZone.getDefault(), false); + BASE_TIME - 15000L, BASE_TIME - 5000L); + + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_calendarHour() { + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 1800000, 2), + MutableDataPoint.ofLongValue(BASE_TIME + 3599000L, 3), + MutableDataPoint.ofLongValue(BASE_TIME + 3600000L, 4), + MutableDataPoint.ofLongValue(BASE_TIME + 5400000L, 5), + MutableDataPoint.ofLongValue(BASE_TIME + 7199000L, 6) + }); + specification = new DownsamplingSpecification("1hc-sum-nan"); + specification.setTimezone(TV); + downsampler = new FillingDownsampler(source, + BASE_TIME, BASE_TIME + (3600000 * 3), specification, 0, Long.MAX_VALUE); + + long ts = BASE_TIME; + double value = 6; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 3600000; + if (value == 6) { + value = 15; + } else { + value = Double.NaN; + } + } + + // hour offset by 30m + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1hc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, 1356996600000L, + 1356996600000L + (3600000 * 4), specification, 0, Long.MAX_VALUE); + + ts = 1356996600000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 3600000; + if (value == 1) { + value = 9; + } else if (value == 9) { + value = 11; + } else { + value = Double.NaN; + } + } + + // multiple hours + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("4hc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, 1356996600000L, + 1356996600000L + (3600000 * 8), specification, 0, Long.MAX_VALUE); + + ts = 1356996600000L; + value = 21; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts = 1357011000000L; + value = Double.NaN; + } + } + + @Test + public void testDownsampler_calendarDay() { + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(DST_TS, 1), + MutableDataPoint.ofLongValue(DST_TS + 86399000, 2), + MutableDataPoint.ofLongValue(DST_TS + 126001000L, 3), // falls to the next in FJ + MutableDataPoint.ofLongValue(DST_TS + 172799000L, 4), + MutableDataPoint.ofLongValue(DST_TS + 172800000L, 5), + MutableDataPoint.ofLongValue(DST_TS + 242999000L, 6) // falls within 30m offset + }); + + // control + specification = new DownsamplingSpecification("1d-sum-nan"); + downsampler = new FillingDownsampler(source, DST_TS, + DST_TS + (86400000 * 4), specification, 0, Long.MAX_VALUE); + + long ts = DST_TS; + double value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 3) { + value = 7; + } else if (value == 7) { + value = 11; + } else { + value = Double.NaN; + } + } + + // 12 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum-nan"); + specification.setTimezone(TV); + downsampler = new FillingDownsampler(source, 1450094400000L - 86400000, + DST_TS + (86400000 * 5), specification, 0, Long.MAX_VALUE); + + ts = 1450094400000L - 86400000; // make sure we front-fill too + value = Double.NaN; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (Double.isNaN(value)) { + value = 1; + } else if (value == 1) { + value = 5; + } else if (value == 5) { + value = 9; + } else if (value == 9) { + value = 6; + } else { + value = Double.NaN; + } + } + + // 11 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum-nan"); + specification.setTimezone(FJ); + downsampler = new FillingDownsampler(source, 1450094400000L, + DST_TS + (86400000 * 5), specification, 0, Long.MAX_VALUE); + + ts = 1450090800000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 1) { + value = 2; + } else if (value == 2) { + value = 12; + } else if (value == 12) { + value = 6; + } else { + value = Double.NaN; + } + } + + // 30m offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, 1450121400000L, + DST_TS + (86400000 * 4), specification, 0, Long.MAX_VALUE); + + ts = 1450121400000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 15; + } else { + value = Double.NaN; + } + } + + // multiple days + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("3dc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, 1450121400000L, + DST_TS + (86400000 * 6), specification, 0, Long.MAX_VALUE); + + ts = 1450121400000L; + value = 21; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 3; + value = Double.NaN; + } + } + + @Test + public void testDownsampler_calendarWeek() { + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(DST_TS, 1), // a Tuesday in UTC land + MutableDataPoint.ofLongValue(DST_TS + (86400000L * 7), 2), + MutableDataPoint.ofLongValue(1451129400000L, 3), // falls to the next in FJ + MutableDataPoint.ofLongValue(DST_TS + (86400000L * 21), 4), + MutableDataPoint.ofLongValue(1452367799000L, 5) // falls within 30m offset + }); + // control + specification = new DownsamplingSpecification("1wc-sum-nan"); + downsampler = new FillingDownsampler(source, 1449964800000L, + DST_TS + (86400000L * 35), specification, 0, Long.MAX_VALUE); + + long ts = 1449964800000L; + double value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 7; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = Double.NaN; + } else if (Double.isNaN(value)) { + value = 9; + } else { + value = Double.NaN; + } + } + + // 12 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum-nan"); + specification.setTimezone(TV); + downsampler = new FillingDownsampler(source, 1449964800000L, + DST_TS + (86400000L * 35), specification, 0, Long.MAX_VALUE); + + ts = 1449921600000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 7; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = Double.NaN; + } else if (Double.isNaN(value)) { + value = 4; + } else { + value = 5; + } + } + + // 11 hour offset from UTC + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum-nan"); + specification.setTimezone(FJ); + downsampler = new FillingDownsampler(source, 1449964800000L, + DST_TS + (86400000L * 35), specification, 0, Long.MAX_VALUE); + + ts = 1449918000000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 7; + value++; + } + // 30m offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, 1449964800000L, + DST_TS + (86400000L * 35), specification, 0, Long.MAX_VALUE); + + ts = 1449948600000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 7; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = Double.NaN; + } else if (Double.isNaN(value)) { + value = 9; + } else { + value = Double.NaN; + } + } + + // multiple weeks + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("2wc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, 1449964800000L, + DST_TS + (86400000L * 35), specification, 0, Long.MAX_VALUE); + + ts = 1449948600000L; + value = 6; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 86400000L * 14; + if (value == 6) { + value = 9; + } else { + value = Double.NaN; + } + } + } + + @Test + public void testDownsampler_calendarMonth() { + final long dec_1st = 1448928000000L; + source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(dec_1st, 1), + MutableDataPoint.ofLongValue(1451559600000L, 2), // falls to the next in FJ + MutableDataPoint.ofLongValue(1451606400000L, 3), // jan 1st + MutableDataPoint.ofLongValue(1454284800000L, 4), // feb 1st + MutableDataPoint.ofLongValue(1456704000000L, 5), // feb 29th (leap year) + MutableDataPoint.ofLongValue(1456772400000L, 6) // falls within 30m offset AF + })); + + // control + specification = new DownsamplingSpecification("1n-sum-nan"); + downsampler = new FillingDownsampler(source, dec_1st, + dec_1st + (2592000000L * 5), specification, 0, Long.MAX_VALUE); + + long ts = dec_1st; + double value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 2592000000L; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 4; + } else if (value == 4) { + value = 11; + } else { + value = Double.NaN; + } + } + + // 12h offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum-nan"); + specification.setTimezone(TV); + downsampler = new FillingDownsampler(source, dec_1st, + dec_1st + (2592000000L * 6), specification, 0, Long.MAX_VALUE); + + ts = 1448884800000L; + value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1448884800000L) { + ts = 1451563200000L; + } else if (ts == 1451563200000L) { + ts = 1454241600000L; + value = 9; + } else if (ts == 1454241600000L) { + ts = 1456747200000L; + value = 6; + } else { + ts = 1459425600000L; + value = Double.NaN; + } + } + + // 11h offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum-nan"); + specification.setTimezone(FJ); + downsampler = new FillingDownsampler(source, dec_1st, + dec_1st + (2592000000L * 6), specification, 0, Long.MAX_VALUE); + + ts = 1448881200000L; + value = 1; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1448881200000L) { + ts = 1451559600000L; + value = 5; + } else if (ts == 1451559600000L) { + ts = 1454241600000L; + value = 9; + } else if (ts == 1454241600000L) { + ts = 1456747200000L; + value = 6; + } else { + ts = 1459425600000L; + value = Double.NaN; + } + } + + // 30m offset + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum-nan"); + specification.setTimezone(AF); + downsampler = new FillingDownsampler(source, dec_1st, + dec_1st + (2592000000L * 5), specification, 0, Long.MAX_VALUE); + + ts = 1448911800000L; + value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1448911800000L) { + ts = 1451590200000L; + } else if (ts == 1451590200000L) { + ts = 1454268600000L; + value = 15; + } else { + ts = 1456774200000L; + value = Double.NaN; + } + } + + // multiple months + ((MockSeekableView)source).resetIndex(); + specification = new DownsamplingSpecification("3nc-sum-nan"); + specification.setTimezone(TV); + downsampler = new FillingDownsampler(source, dec_1st, + dec_1st + (2592000000L * 9), specification, 0, Long.MAX_VALUE); + + ts = 1443614400000L; + value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + if (ts == 1443614400000L) { + ts = 1451563200000L; + value = 18; + } else { + ts = 1459425600000L; + value = Double.NaN; + } + } + } + + @Test + public void testDownsampler_calendarSkipSomePoints() { + source = SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME, 1), + MutableDataPoint.ofLongValue(BASE_TIME + 1800000, 2), + // skip an hour + MutableDataPoint.ofLongValue(BASE_TIME + 7200000, 6) + }); + specification = new DownsamplingSpecification("1hc-sum-nan"); + specification.setTimezone(TV); + downsampler = new FillingDownsampler(source, 1356998400000L, 1357009200000L, + specification, 0, Long.MAX_VALUE); + + long ts = BASE_TIME; + double value = 3; + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.001); + ts += 3600000; + if (value == 3) { + value = Double.NaN; + } else { + value = 6; + } + } + } + + @Test + public void testDownsampler_noData() { + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { }); + specification = new DownsamplingSpecification("1m-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME, + BASE_TIME + 60000L * 2L, specification, 0, 0); + + long timestamp = 1356998400000L; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 60000, Double.NaN); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_noDataCalendar() { + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { }); + specification = new DownsamplingSpecification("1mc-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, BASE_TIME, + BASE_TIME + 60000L * 2L, specification, 0, 0); + + long timestamp = 1356998400000L; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 60000, Double.NaN); assertFalse(downsampler.hasNext()); } diff --git a/test/core/TestTSQuery.java b/test/core/TestTSQuery.java index 9c1fdf90dd..d528eaaf56 100644 --- a/test/core/TestTSQuery.java +++ b/test/core/TestTSQuery.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; 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; @@ -51,6 +52,51 @@ public void validate() { assertEquals(Aggregators.SUM, q.getQueries().get(0).aggregator()); assertEquals(Aggregators.AVG, q.getQueries().get(0).downsampler()); assertEquals(300000, q.getQueries().get(0).downsampleInterval()); + assertNull(q.getTimezone()); + assertEquals("UTC", q.getQueries().get(0).downsamplingSpecification() + .getTimezone().getID()); + assertFalse(q.getQueries().get(0).downsamplingSpecification().useCalendar()); + } + + @Test + public void validateWithTimezone() { + TSQuery q = this.getMetricForValidate(); + q.setUseCalendar(true); + q.setTimezone("Pacific/Funafuti"); + q.validateAndSetQuery(); + assertEquals(1356998400000L, q.startTime()); + assertEquals(1356998460000L, q.endTime()); + assertEquals("sys.cpu.0", q.getQueries().get(0).getMetric()); + assertEquals("wildcard(*)", q.getQueries().get(0).getTags().get("host")); + assertEquals("literal_or(lga)", q.getQueries().get(0).getTags().get("dc")); + assertEquals(Aggregators.SUM, q.getQueries().get(0).aggregator()); + assertEquals(Aggregators.AVG, q.getQueries().get(0).downsampler()); + assertEquals(300000, q.getQueries().get(0).downsampleInterval()); + assertEquals("Pacific/Funafuti", q.getTimezone()); + assertEquals("Pacific/Funafuti", q.getQueries().get(0).downsamplingSpecification() + .getTimezone().getID()); + assertTrue(q.getQueries().get(0).downsamplingSpecification().useCalendar()); + } + + @Test + public void validateVerifyNoDSOverrideWithCalendar() { + TSQuery q = this.getMetricForValidate(); + q.setUseCalendar(true); + q.setTimezone("Pacific/Funafuti"); + q.getQueries().get(0).setDownsample(null); + q.validateAndSetQuery(); + assertEquals(1356998400000L, q.startTime()); + assertEquals(1356998460000L, q.endTime()); + assertEquals("sys.cpu.0", q.getQueries().get(0).getMetric()); + assertEquals("wildcard(*)", q.getQueries().get(0).getTags().get("host")); + assertEquals("literal_or(lga)", q.getQueries().get(0).getTags().get("dc")); + assertEquals(Aggregators.SUM, q.getQueries().get(0).aggregator()); + assertNull(q.getQueries().get(0).downsampler()); + assertEquals(0, q.getQueries().get(0).downsampleInterval()); + assertEquals("Pacific/Funafuti", q.getTimezone()); + assertEquals("UTC", q.getQueries().get(0).downsamplingSpecification() + .getTimezone().getID()); + assertFalse(q.getQueries().get(0).downsamplingSpecification().useCalendar()); } @Test (expected = IllegalArgumentException.class) @@ -241,6 +287,25 @@ public void testHashCodeandEqualsTimezoneInvalid() throws Exception { assertFalse(sub1 == sub2); } + @Test + public void testHashCodeandEqualsUseCalendar() { + TSQuery sub1 = getMetricForValidate(); + + final int hash_a = sub1.hashCode(); + sub1.setUseCalendar(true); + final int hash_b = sub1.hashCode(); + assertTrue(hash_a != hash_b); + sub1.validateAndSetQuery(); + assertEquals(hash_b, sub1.hashCode()); + + TSQuery sub2 = getMetricForValidate(); + sub2.setUseCalendar(true); + + assertEquals(hash_b, sub2.hashCode()); + assertEquals(sub1, sub2); + assertFalse(sub1 == sub2); + } + @Test public void testHashCodeandEqualsOptions() { TSQuery sub1 = getMetricForValidate(); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 63ae2ffab8..07facd6567 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -21,19 +21,39 @@ import static org.mockito.Mockito.when; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.TimeZone; 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; +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) @RunWith(PowerMockRunner.class) @PrepareForTest({ DateTime.class, System.class }) public final class TestDateTime { + //30 minute offset + final static TimeZone AF = DateTime.timezones.get("Asia/Kabul"); + // 45 minute offset w DST + final static TimeZone NZ = DateTime.timezones.get("Pacific/Chatham"); + // 12h offset w/o DST + final static TimeZone TV = DateTime.timezones.get("Pacific/Funafuti"); + // 12h offset w DST + final static TimeZone FJ = DateTime.timezones.get("Pacific/Fiji"); + // Fri, 15 May 2015 14:21:13.432 UTC + final static long NON_DST_TS = 1431699673432L; + // Tue, 15 Dec 2015 04:02:25.123 UTC + final static long DST_TS = 1450152145123L; + @Before public void before() { PowerMockito.mockStatic(System.class); @@ -380,6 +400,474 @@ public void nanoTime() { assertEquals(1388534400000000000L, DateTime.nanoTime()); } + @Test + public void previousIntervalMilliseconds() { + // interval 1 + assertEquals(DST_TS, DateTime.previousInterval(DST_TS, + 1, Calendar.MILLISECOND).getTimeInMillis()); + assertEquals(NON_DST_TS, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MILLISECOND).getTimeInMillis()); + + // interval 100 + assertEquals(1450152145100L, DateTime.previousInterval(DST_TS, + 100, Calendar.MILLISECOND).getTimeInMillis()); + assertEquals(1450152145000L, DateTime.previousInterval(1450152145000L, + 100, Calendar.MILLISECOND).getTimeInMillis()); + + // odd interval + assertEquals(1450152144769L, DateTime.previousInterval(DST_TS, + 799, Calendar.MILLISECOND).getTimeInMillis()); + + // TZs - all the same for ms + assertEquals(1450152145100L, DateTime.previousInterval(DST_TS, + 100, Calendar.MILLISECOND, AF).getTimeInMillis()); + assertEquals(1431699673400L, DateTime.previousInterval(NON_DST_TS, + 100, Calendar.MILLISECOND, AF).getTimeInMillis()); + assertEquals(1450152145100L, DateTime.previousInterval(DST_TS, + 100, Calendar.MILLISECOND, NZ).getTimeInMillis()); + assertEquals(1431699673400L, DateTime.previousInterval(NON_DST_TS, + 100, Calendar.MILLISECOND, NZ).getTimeInMillis()); + assertEquals(1450152145100L, DateTime.previousInterval(DST_TS, + 100, Calendar.MILLISECOND, TV).getTimeInMillis()); + assertEquals(1431699673400L, DateTime.previousInterval(NON_DST_TS, + 100, Calendar.MILLISECOND, TV).getTimeInMillis()); + assertEquals(1450152145100L, DateTime.previousInterval(DST_TS, + 100, Calendar.MILLISECOND, FJ).getTimeInMillis()); + assertEquals(1431699673400L, DateTime.previousInterval(NON_DST_TS, + 100, Calendar.MILLISECOND, FJ).getTimeInMillis()); + + // multiples + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 60000, Calendar.MILLISECOND).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 60000, Calendar.MILLISECOND).getTimeInMillis()); + } + + @Test + public void previousIntervalSeconds() { + // interval 1 + assertEquals(1450152145000L, DateTime.previousInterval(DST_TS, + 1, Calendar.SECOND).getTimeInMillis()); + assertEquals(1431699673000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.SECOND).getTimeInMillis()); + + // interval 30 + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 30, Calendar.SECOND).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.SECOND).getTimeInMillis()); + assertEquals(1450152120000L, DateTime.previousInterval(1450152120000L, + 30, Calendar.SECOND).getTimeInMillis()); + + // odd interval + assertEquals(1431699647000L, DateTime.previousInterval(NON_DST_TS, + 29, Calendar.SECOND).getTimeInMillis()); + assertEquals(1450152145000L, DateTime.previousInterval(DST_TS, + 29, Calendar.SECOND).getTimeInMillis()); + + // TZs - all the same for seconds + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 30, Calendar.SECOND, AF).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.SECOND, AF).getTimeInMillis()); + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 30, Calendar.SECOND, NZ).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.SECOND, NZ).getTimeInMillis()); + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 30, Calendar.SECOND, TV).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.SECOND, TV).getTimeInMillis()); + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 30, Calendar.SECOND, FJ).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.SECOND, FJ).getTimeInMillis()); + + // multiples + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 60000, Calendar.SECOND).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 60000, Calendar.SECOND).getTimeInMillis()); + } + + @Test + public void previousIntervalMinutes() { + // interval 1 + assertEquals(1450152120000L, DateTime.previousInterval(DST_TS, + 1, Calendar.MINUTE).getTimeInMillis()); + assertEquals(1431699660000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MINUTE).getTimeInMillis()); + + // interval 30 + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 30, Calendar.MINUTE).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.MINUTE).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(1431698400000L, + 30, Calendar.MINUTE).getTimeInMillis()); + + // odd interval + assertEquals(1431698460000L, DateTime.previousInterval(NON_DST_TS, + 29, Calendar.MINUTE).getTimeInMillis()); + assertEquals(1450151520000L, DateTime.previousInterval(DST_TS, + 29, Calendar.MINUTE).getTimeInMillis()); + + // TZs + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 30, Calendar.MINUTE, AF).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.MINUTE, AF).getTimeInMillis()); + // 15 min diff + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 15, Calendar.MINUTE, AF).getTimeInMillis()); + assertEquals(1431699300000L, DateTime.previousInterval(NON_DST_TS, + 15, Calendar.MINUTE, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1450151100000L, DateTime.previousInterval(DST_TS, + 30, Calendar.MINUTE, NZ).getTimeInMillis()); + assertEquals(1431699300000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.MINUTE, NZ).getTimeInMillis()); + // back to normal + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 30, Calendar.MINUTE, TV).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.MINUTE, TV).getTimeInMillis()); + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 30, Calendar.MINUTE, FJ).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 30, Calendar.MINUTE, FJ).getTimeInMillis()); + + // multiples + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 120, Calendar.MINUTE).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 120, Calendar.MINUTE).getTimeInMillis()); + } + + @Test + public void previousIntervalHours() { + // interval 1 + assertEquals(1450152000000L, DateTime.previousInterval(DST_TS, + 1, Calendar.HOUR_OF_DAY).getTimeInMillis()); + assertEquals(1431698400000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.HOUR_OF_DAY).getTimeInMillis()); + + // interval 12 + assertEquals(1450137600000L, DateTime.previousInterval(DST_TS, + 12, Calendar.HOUR_OF_DAY).getTimeInMillis()); + assertEquals(1431691200000L, DateTime.previousInterval(NON_DST_TS, + 12, Calendar.HOUR_OF_DAY).getTimeInMillis()); + assertEquals(1450137600000L, DateTime.previousInterval(1450137600000L, + 12, Calendar.HOUR_OF_DAY).getTimeInMillis()); + + // odd interval + assertEquals(1431680400000L, DateTime.previousInterval(NON_DST_TS, + 15, Calendar.HOUR_OF_DAY).getTimeInMillis()); + assertEquals(1450116000000L, DateTime.previousInterval(DST_TS, + 15, Calendar.HOUR_OF_DAY).getTimeInMillis()); + + // TZs - 30m offset here + assertEquals(1450121400000L, DateTime.previousInterval(DST_TS, + 12, Calendar.HOUR_OF_DAY, AF).getTimeInMillis()); + assertEquals(1431675000000L, DateTime.previousInterval(NON_DST_TS, + 12, Calendar.HOUR_OF_DAY, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1450131300000L, DateTime.previousInterval(DST_TS, + 12, Calendar.HOUR_OF_DAY, NZ).getTimeInMillis()); + assertEquals(1431688500000L, DateTime.previousInterval(NON_DST_TS, + 12, Calendar.HOUR_OF_DAY, NZ).getTimeInMillis()); + // back to normal + assertEquals(1450137600000L, DateTime.previousInterval(DST_TS, + 12, Calendar.HOUR_OF_DAY, TV).getTimeInMillis()); + assertEquals(1431691200000L, DateTime.previousInterval(NON_DST_TS, + 12, Calendar.HOUR_OF_DAY, TV).getTimeInMillis()); + assertEquals(1450134000000L, DateTime.previousInterval(DST_TS, + 12, Calendar.HOUR_OF_DAY, FJ).getTimeInMillis()); + assertEquals(1431691200000L, DateTime.previousInterval(NON_DST_TS, + 12, Calendar.HOUR_OF_DAY, FJ).getTimeInMillis()); + + // multiples + assertEquals(1450094400000L, DateTime.previousInterval(DST_TS, + 36, Calendar.HOUR_OF_DAY).getTimeInMillis()); + assertEquals(1431604800000L, DateTime.previousInterval(NON_DST_TS, + 36, Calendar.HOUR_OF_DAY).getTimeInMillis()); + } + + @Test + public void previousIntervalDays() { + // interval 1 + assertEquals(1450137600000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_MONTH).getTimeInMillis()); + assertEquals(1431648000000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_MONTH).getTimeInMillis()); + + // interval 7 - since days aren't consistent, the only thing we can + // do is pick a starting day, i.e. start of the year + assertEquals(1449705600000L, DateTime.previousInterval(DST_TS, + 7, Calendar.DAY_OF_MONTH).getTimeInMillis()); + assertEquals(1431561600000L, DateTime.previousInterval(NON_DST_TS, + 7, Calendar.DAY_OF_MONTH).getTimeInMillis()); + assertEquals(1449705600000L, DateTime.previousInterval(1449705600000L, + 7, Calendar.DAY_OF_MONTH).getTimeInMillis()); + + // leap year + assertEquals(1330473600000L, DateTime.previousInterval(1330516800000L, + 1, Calendar.DAY_OF_MONTH).getTimeInMillis()); + + // TZs - 30m offset here + assertEquals(1450121400000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_MONTH, AF).getTimeInMillis()); + assertEquals(1431631800000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_MONTH, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1450088100000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_MONTH, NZ).getTimeInMillis()); + assertEquals(1431688500000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_MONTH, NZ).getTimeInMillis()); + // back to normal + assertEquals(1450094400000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_MONTH, TV).getTimeInMillis()); + assertEquals(1431691200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_MONTH, TV).getTimeInMillis()); + assertEquals(1450090800000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_MONTH, FJ).getTimeInMillis()); + assertEquals(1431691200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_MONTH, FJ).getTimeInMillis()); + + // multiples + assertEquals(1445990400000L, DateTime.previousInterval(DST_TS, + 60, Calendar.DAY_OF_MONTH).getTimeInMillis()); + assertEquals(1430438400000L, DateTime.previousInterval(NON_DST_TS, + 60, Calendar.DAY_OF_MONTH).getTimeInMillis()); + } + + @Test + public void previousIntervalWeeks() { + // 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()); + assertEquals(1431216000000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_WEEK).getTimeInMillis()); + + // interval 2 + assertEquals(1449964800000L, DateTime.previousInterval(DST_TS, + 2, Calendar.DAY_OF_WEEK).getTimeInMillis()); + assertEquals(1431216000000L, DateTime.previousInterval(NON_DST_TS, + 2, Calendar.DAY_OF_WEEK).getTimeInMillis()); + assertEquals(1435449600000L, DateTime.previousInterval(1435795200000L, + 2, Calendar.DAY_OF_WEEK).getTimeInMillis()); + + // TZs - 30m offset here + assertEquals(1449948600000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_WEEK, AF).getTimeInMillis()); + assertEquals(1431199800000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_WEEK, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1449915300000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_WEEK, NZ).getTimeInMillis()); + assertEquals(1431170100000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_WEEK, NZ).getTimeInMillis()); + // back to normal + assertEquals(1449921600000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_WEEK, TV).getTimeInMillis()); + assertEquals(1431172800000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_WEEK, TV).getTimeInMillis()); + assertEquals(1449918000000L, DateTime.previousInterval(DST_TS, + 1, Calendar.DAY_OF_WEEK, FJ).getTimeInMillis()); + assertEquals(1431172800000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.DAY_OF_WEEK, FJ).getTimeInMillis()); + + // multiples - still from start of the week + assertEquals(1449964800000L, DateTime.previousInterval(DST_TS, + 104, Calendar.DAY_OF_WEEK).getTimeInMillis()); + assertEquals(1431216000000L, DateTime.previousInterval(NON_DST_TS, + 104, Calendar.DAY_OF_WEEK).getTimeInMillis()); + } + + @Test + public void previousIntervalWeekOfYear() { + // interval 1 DST_TS starts on 10th of Dec, NON starts on the 14th of May + assertEquals(1449705600000L, DateTime.previousInterval(DST_TS, + 1, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + assertEquals(1431561600000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + + // interval 26 + assertEquals(1435795200000L, DateTime.previousInterval(DST_TS, + 26, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(NON_DST_TS, + 26, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + assertEquals(1435795200000L, DateTime.previousInterval(1435795200000L, + 26, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + + // TZs - 30m offset here + assertEquals(1449689400000L, DateTime.previousInterval(DST_TS, + 1, Calendar.WEEK_OF_YEAR, AF).getTimeInMillis()); + assertEquals(1431545400000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.WEEK_OF_YEAR, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1449656100000L, DateTime.previousInterval(DST_TS, + 1, Calendar.WEEK_OF_YEAR, NZ).getTimeInMillis()); + assertEquals(1431515700000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.WEEK_OF_YEAR, NZ).getTimeInMillis()); + // back to normal + assertEquals(1449662400000L, DateTime.previousInterval(DST_TS, + 1, Calendar.WEEK_OF_YEAR, TV).getTimeInMillis()); + assertEquals(1431518400000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.WEEK_OF_YEAR, TV).getTimeInMillis()); + assertEquals(1449658800000L, DateTime.previousInterval(DST_TS, + 1, Calendar.WEEK_OF_YEAR, FJ).getTimeInMillis()); + assertEquals(1431518400000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.WEEK_OF_YEAR, FJ).getTimeInMillis()); + + // multiples + assertEquals(1420070400000L, DateTime.previousInterval(DST_TS, + 104, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(NON_DST_TS, + 104, Calendar.WEEK_OF_YEAR).getTimeInMillis()); + } + + @Test + public void previousIntervalMonths() { + // interval 1 + assertEquals(1448928000000L, DateTime.previousInterval(DST_TS, + 1, Calendar.MONTH).getTimeInMillis()); + assertEquals(1430438400000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MONTH).getTimeInMillis()); + + // interval 3 (quarters) + assertEquals(1443657600000L, DateTime.previousInterval(DST_TS, + 3, Calendar.MONTH).getTimeInMillis()); + assertEquals(1427846400000L, DateTime.previousInterval(NON_DST_TS, + 3, Calendar.MONTH).getTimeInMillis()); + assertEquals(1443657600000L, DateTime.previousInterval(1443657600000L, + 3, Calendar.MONTH).getTimeInMillis()); + + // odd intervals + assertEquals(1446336000000L, DateTime.previousInterval(DST_TS, + 5, Calendar.MONTH).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(NON_DST_TS, + 5, Calendar.MONTH).getTimeInMillis()); + + // TZs - 30m offset here + assertEquals(1448911800000L, DateTime.previousInterval(DST_TS, + 1, Calendar.MONTH, AF).getTimeInMillis()); + assertEquals(1430422200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MONTH, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1448878500000L, DateTime.previousInterval(DST_TS, + 1, Calendar.MONTH, NZ).getTimeInMillis()); + assertEquals(1430392500000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MONTH, NZ).getTimeInMillis()); + // back to normal + assertEquals(1448884800000L, DateTime.previousInterval(DST_TS, + 1, Calendar.MONTH, TV).getTimeInMillis()); + assertEquals(1430395200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MONTH, TV).getTimeInMillis()); + assertEquals(1448881200000L, DateTime.previousInterval(DST_TS, + 1, Calendar.MONTH, FJ).getTimeInMillis()); + assertEquals(1430395200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.MONTH, FJ).getTimeInMillis()); + + // multiples + assertEquals(1420070400000L, DateTime.previousInterval(DST_TS, + 24, Calendar.MONTH).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(NON_DST_TS, + 24, Calendar.MONTH).getTimeInMillis()); + } + + @Test + public void previousIntervalYears() { + // interval 1 + assertEquals(1420070400000L, DateTime.previousInterval(DST_TS, + 1, Calendar.YEAR).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.YEAR).getTimeInMillis()); + + // interval 5 + assertEquals(1420070400000L, DateTime.previousInterval(DST_TS, + 5, Calendar.YEAR).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(NON_DST_TS, + 5, Calendar.YEAR).getTimeInMillis()); + assertEquals(1420070400000L, DateTime.previousInterval(1420070400000L, + 5, Calendar.YEAR).getTimeInMillis()); + + // TZs - 30m offset here + assertEquals(1420054200000L, DateTime.previousInterval(DST_TS, + 1, Calendar.YEAR, AF).getTimeInMillis()); + assertEquals(1420054200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.YEAR, AF).getTimeInMillis()); + // outliers @ 45 minutes + assertEquals(1420020900000L, DateTime.previousInterval(DST_TS, + 1, Calendar.YEAR, NZ).getTimeInMillis()); + assertEquals(1420020900000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.YEAR, NZ).getTimeInMillis()); + // back to normal + assertEquals(1420027200000L, DateTime.previousInterval(DST_TS, + 1, Calendar.YEAR, TV).getTimeInMillis()); + assertEquals(1420027200000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.YEAR, TV).getTimeInMillis()); + assertEquals(1420023600000L, DateTime.previousInterval(DST_TS, + 1, Calendar.YEAR, FJ).getTimeInMillis()); + assertEquals(1420023600000L, DateTime.previousInterval(NON_DST_TS, + 1, Calendar.YEAR, FJ).getTimeInMillis()); + } + + @Test (expected = IllegalArgumentException.class) + public void previousIntervalNegativeTs() { + DateTime.previousInterval(-42, 1, Calendar.MINUTE); + } + + @Test (expected = IllegalArgumentException.class) + public void previousIntervalNegativeInterval() { + DateTime.previousInterval(1355961600000L, -1, Calendar.MINUTE); + } + + @Test (expected = IllegalArgumentException.class) + public void previousIntervalZeroInterval() { + DateTime.previousInterval(1355961600000L, 0, Calendar.MINUTE); + } + + @Test (expected = IllegalArgumentException.class) + public void previousIntervalNegativeUnit() { + DateTime.previousInterval(1355961600000L, 1, -1); + } + + @Test (expected = IllegalArgumentException.class) + public void previousIntervalUnsupportedUnit() { + DateTime.previousInterval(1355961600000L, 1, Calendar.HOUR); + } + + @Test (expected = IllegalArgumentException.class) + public void previousIntervalMassiveUnit() { + DateTime.previousInterval(1355961600000L, 1, 6048); + } + + @Test + public void unitsToCalendarType() { + assertEquals(Calendar.MILLISECOND, DateTime.unitsToCalendarType("ms")); + assertEquals(Calendar.SECOND, DateTime.unitsToCalendarType("s")); + assertEquals(Calendar.MINUTE, DateTime.unitsToCalendarType("m")); + assertEquals(Calendar.HOUR_OF_DAY, DateTime.unitsToCalendarType("h")); + assertEquals(Calendar.DAY_OF_MONTH, DateTime.unitsToCalendarType("d")); + assertEquals(Calendar.DAY_OF_WEEK, DateTime.unitsToCalendarType("w")); + assertEquals(Calendar.MONTH, DateTime.unitsToCalendarType("n")); + assertEquals(Calendar.YEAR, DateTime.unitsToCalendarType("y")); + try { + DateTime.unitsToCalendarType("j"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + DateTime.unitsToCalendarType(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + DateTime.unitsToCalendarType(""); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } + + @Test public void msFromNano() { assertEquals(0, DateTime.msFromNano(0), 0.0001); From c7dee0184bbc89aa90b7688c6224ca6cf3a2430d Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 4 Mar 2016 17:53:03 -0600 Subject: [PATCH 438/826] Added timeShift function Fixes #175 Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/query/expression/ExpressionFactory.java | 2 + src/query/expression/TimeShift.java | 135 ++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 src/query/expression/TimeShift.java diff --git a/Makefile.am b/Makefile.am index 78abae4d5d..b569d7913b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -96,6 +96,7 @@ tsdb_SRC := \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ src/query/expression/SumSeries.java \ + src/query/expression/TimeShift.java \ src/query/expression/TimeSyncedIterator.java \ src/query/expression/UnionIterator.java \ src/query/expression/VariableIterator.java \ diff --git a/src/query/expression/ExpressionFactory.java b/src/query/expression/ExpressionFactory.java index 23bd39d1dd..e0fbdd44e8 100644 --- a/src/query/expression/ExpressionFactory.java +++ b/src/query/expression/ExpressionFactory.java @@ -35,6 +35,8 @@ public final class ExpressionFactory { available_functions.put("movingAverage", new MovingAverage()); available_functions.put("highestCurrent", new HighestCurrent()); available_functions.put("highestMax", new HighestMax()); + available_functions.put("shift", new TimeShift()); + available_functions.put("timeShift", new TimeShift()); } /** Don't instantiate me! */ diff --git a/src/query/expression/TimeShift.java b/src/query/expression/TimeShift.java new file mode 100644 index 0000000000..876175d64a --- /dev/null +++ b/src/query/expression/TimeShift.java @@ -0,0 +1,135 @@ +package net.opentsdb.query.expression; +/** + * Copyright 2015 The opentsdb Authors + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net.opentsdb.core.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class TimeShift implements Expression { + /** + * in place modify of TsdbResult array to increase timestamps by timeshift + * @param data_query + * @param results + * @param params + * @return + */ + @Override + public DataPoints[] evaluate(TSQuery data_query, List results, List params) { + //not 100% sure what to do here -> do I need to think of the case where I have no data points + if(results == null || results.isEmpty()) { + return new DataPoints[]{}; + } + if(params == null || results.isEmpty()) { + throw new IllegalArgumentException("Need amount of timeshift to perform timeshift"); + } + + String param = params.get(0); + if (param == null || param.length() == 0) { + throw new IllegalArgumentException("Invalid timeshift='" + param + "'"); + } + + param = param.trim(); + + long timeshift = -1; + if (param.startsWith("'") && param.endsWith("'")) { + timeshift = parseParam(param) / 1000; + } else { + throw new RuntimeException("Invalid timeshift parameter: eg '10min'"); + } + + if (timeshift <= 0) { + throw new RuntimeException("timeshift <= 0"); + } + + DataPoints[] inputPoints = results.get(0); + DataPoints[] outputPoints = new DataPoints[inputPoints.length]; + for(int n = 0; n < inputPoints.length; n++) { + outputPoints[n] = shift(inputPoints[n], timeshift); + } + return outputPoints; + } + + public static long parseParam(String param) { + char[] chars = param.toCharArray(); + int tuIndex = 0; + for (int c = 1; c < chars.length; c++) { + if (Character.isDigit(chars[c])) { + tuIndex++; + } else { + break; + } + } + + if (tuIndex == 0) { + throw new RuntimeException("Invalid Parameter: " + param); + } + + int time = Integer.parseInt(param.substring(1, tuIndex + 1)); + String unit = param.substring(tuIndex + 1, param.length() - 1); + if ("sec".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.SECONDS); + } else if ("min".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.MINUTES); + } else if ("hr".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.HOURS); + } else if ("day".equals(unit) || "days".equals(unit)) { + return TimeUnit.MILLISECONDS.convert(time, TimeUnit.DAYS); + } else if ("week".equals(unit) || "weeks".equals(unit)) { + //didn't have week so small cheat here + return TimeUnit.MILLISECONDS.convert(time*7, TimeUnit.DAYS); + } + else { + throw new RuntimeException("unknown time unit=" + unit); + } + } + + /** + * Adjusts the timestamp of each datapoint by timeshift + * @param points The data points to factor + * @param timeshift The factor to multiply by + * @return The resulting data points + */ + private DataPoints shift(final DataPoints points, final long timeshift) { + // TODO(cl) - Using an array as the size function may not return the exact + // results and we should figure a way to avoid copying data anyway. + final List dps = new ArrayList(); + final boolean shift_is_int = (timeshift == Math.floor(timeshift)) && + !Double.isInfinite(timeshift); + final SeekableView view = points.iterator(); + while (view.hasNext()) { + DataPoint pt = view.next(); + if (shift_is_int) { + dps.add(MutableDataPoint.ofLongValue(pt.timestamp() + timeshift, + pt.longValue())); + } else { + // NaNs are fine here, they'll just be re-computed as NaN + dps.add(MutableDataPoint.ofDoubleValue(pt.timestamp() + timeshift, + timeshift * pt.toDouble())); + } + } + final DataPoint[] results = new DataPoint[dps.size()]; + dps.toArray(results); + return new PostAggregatedDataPoints(points, results); + } + + @Override + public String writeStringField(List params, String inner_expression) { + return "timeshift(" + inner_expression + ")"; + } +} From cdf1a116fb9df61cbb23344b1db9b8f0aaa76c05 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 13 Mar 2016 15:43:17 -0700 Subject: [PATCH 439/826] Add the MetaDataCache plugin API that will let us bypass the built-in TSUID incrementing or "putting" calls to cut down on the amount of traffic sent to the server. Implementations can handle queuing and aggregation however they like. Signed-off-by: Chris Larsen --- Makefile.am | 1 + src/core/TSDB.java | 52 +++++++++++++++++++++----- src/meta/MetaDataCache.java | 73 +++++++++++++++++++++++++++++++++++++ src/utils/Config.java | 1 + 4 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/meta/MetaDataCache.java diff --git a/Makefile.am b/Makefile.am index b569d7913b..db49f0589e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -70,6 +70,7 @@ tsdb_SRC := \ src/core/WritableDataPoints.java \ src/graph/Plot.java \ src/meta/Annotation.java \ + src/meta/MetaDataCache.java \ src/meta/TSMeta.java \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 5e3f9657f7..72930603d1 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -52,6 +52,7 @@ import net.opentsdb.utils.PluginLoader; import net.opentsdb.utils.Threads; import net.opentsdb.meta.Annotation; +import net.opentsdb.meta.MetaDataCache; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; import net.opentsdb.query.expression.ExpressionFactory; @@ -121,6 +122,9 @@ public final class TSDB { /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; + /** Optional plugin for handling meta data caching and updating */ + private MetaDataCache meta_cache = null; + /** Plugin for dealing with data points that can't be stored */ private StorageExceptionHandler storage_exception_handler = null; @@ -313,6 +317,26 @@ public void initializePlugins(final boolean init_rpcs) { rt_publisher = null; } + // load the meta cache plugin if enabled + if (config.getBoolean("tsd.core.meta.cache.enable")) { + meta_cache = PluginLoader.loadSpecificPlugin( + config.getString("tsd.core.meta.cache.plugin"), MetaDataCache.class); + if (meta_cache == null) { + throw new IllegalArgumentException( + "Unable to locate meta cache plugin: " + + config.getString("tsd.core.meta.cache.plugin")); + } + try { + meta_cache.initialize(this); + } catch (Exception e) { + throw new RuntimeException( + "Failed to initialize meta cache plugin", e); + } + LOG.info("Successfully initialized meta cache plugin [" + + meta_cache.getClass().getCanonicalName() + "] version: " + + meta_cache.version()); + } + // load the storage exception plugin if enabled if (config.getBoolean("tsd.core.storage_exception_handler.enable")) { storage_exception_handler = PluginLoader.loadSpecificPlugin( @@ -827,15 +851,20 @@ private Deferred addPointInternal(final String metric, 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 the meta cache plugin is instantiated then tracking goes through it + if (meta_cache != null) { + meta_cache.increment(tsuid); + } else { + // 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) { @@ -974,6 +1003,11 @@ public Object call(ArrayList compactions) throws Exception { rt_publisher.getClass().getCanonicalName()); deferreds.add(rt_publisher.shutdown()); } + if (meta_cache != null) { + LOG.info("Shutting down meta cache plugin: " + + meta_cache.getClass().getCanonicalName()); + deferreds.add(meta_cache.shutdown()); + } if (storage_exception_handler != null) { LOG.info("Shutting down storage exception handler plugin: " + storage_exception_handler.getClass().getCanonicalName()); diff --git a/src/meta/MetaDataCache.java b/src/meta/MetaDataCache.java new file mode 100644 index 0000000000..96504b1ce7 --- /dev/null +++ b/src/meta/MetaDataCache.java @@ -0,0 +1,73 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.meta; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; + +/** + * This is a first stab at a meta data cache. Initially it only handles + * incrementing TSUID counters in a local database. The class will then + * periodically sync the local counter cache with HBase and generate TSMeta + * objects if necessary. This keeps us from having to maintain thousands or + * millions of callback objects in memory while we wait for individual atomic + * increments per data point. + * @since 2.3 + */ +public abstract class MetaDataCache { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); + + /** + * Called when the TSD is shutting down to gracefully flush any buffers or + * close open connections. + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. 2.0.1. The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + + /** + * Increments the given TSUID in the cache by 1 + * @param tsuid The tsuid to increment + */ + public abstract void increment(final byte[] tsuid); + +} diff --git a/src/utils/Config.java b/src/utils/Config.java index 4e8cb9ed02..3191b1602a 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -491,6 +491,7 @@ protected void setDefaults() { default_map.put("tsd.core.meta.enable_realtime_uid", "false"); default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); default_map.put("tsd.core.meta.enable_tsuid_tracking", "false"); + default_map.put("tsd.core.meta.cache.enable", "false"); default_map.put("tsd.core.plugin_path", ""); default_map.put("tsd.core.socket.timeout", "0"); default_map.put("tsd.core.tree.enable_processing", "false"); From d60a7ec40c6b9f6924a72d8cd9492b79b8367bb7 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 2 Mar 2016 22:22:32 -0600 Subject: [PATCH 440/826] Attempting to build a framework for startup plugins. Testing with this [skeleton plugin](https://github.com/johann8384/opentsdb-discoveryplugins/blob/master/src/main/java/io/tsdb/opentsdb/discoveryplugins/CuratorPlugin.java) that I intend to turn into an [Apache Curator](https://curator.apache.org/curator-x-discovery/index.html) plugin. Fixes #716 --- Makefile.am | 1 + src/core/TSDB.java | 59 +++++++++++++++++++++------ src/tools/StartupPlugin.java | 78 ++++++++++++++++++++++++++++++++++++ src/tools/TSDMain.java | 64 ++++++++++++++++++++++++++++- src/utils/PluginLoader.java | 2 +- 5 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 src/tools/StartupPlugin.java diff --git a/Makefile.am b/Makefile.am index db49f0589e..edaaf6e37a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -132,6 +132,7 @@ tsdb_SRC := \ src/tools/MetaPurge.java \ src/tools/MetaSync.java \ src/tools/Search.java \ + src/tools/StartupPlugin.java \ src/tools/TSDMain.java \ src/tools/TextImporter.java \ src/tools/TreeSync.java \ diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 72930603d1..a0c6152007 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -59,6 +59,7 @@ import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.search.SearchPlugin; import net.opentsdb.search.SearchQuery; +import net.opentsdb.tools.StartupPlugin; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.QueryStats; import net.opentsdb.stats.StatsCollector; @@ -118,7 +119,10 @@ public final class TSDB { /** Search indexer to use if configure */ private SearchPlugin search = null; - + + /** Optional Startup Plugin to use if configured */ + private StartupPlugin startup = null; + /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; @@ -233,7 +237,22 @@ public TSDB(final Config config) { public static byte[] FAMILY() { return FAMILY; } - + + /** + * Called by initializePlugins, also used to load startup plugins. + * @since 2.3 + */ + public static void loadPluginPath(final String plugin_path) throws RuntimeException { + if (plugin_path != null && !plugin_path.isEmpty()) { + try { + PluginLoader.loadJARs(plugin_path); + } catch (Exception e) { + throw new RuntimeException("Error loading plugins from plugin path: " + + plugin_path, e); + } + } + } + /** * Should be called immediately after construction to initialize plugins and * objects that rely on such. It also moves most of the potential exception @@ -246,16 +265,12 @@ public static byte[] FAMILY() { */ public void initializePlugins(final boolean init_rpcs) { final String plugin_path = config.getString("tsd.core.plugin_path"); - if (plugin_path != null && !plugin_path.isEmpty()) { - try { - PluginLoader.loadJARs(plugin_path); - } catch (Exception e) { - LOG.error("Error loading plugins from plugin path: " + plugin_path, e); - throw new RuntimeException("Error loading plugins from plugin path: " + - plugin_path, e); - } + try { + loadPluginPath(plugin_path); + } catch (Exception e) { + LOG.error("Error loading plugins from plugin path: " + plugin_path, e); } - + try { TagVFilter.initializeFilterMap(this); // @#$@%$%#$ing typed exceptions @@ -367,8 +382,21 @@ public void initializePlugins(final boolean init_rpcs) { public final HBaseClient getClient() { return this.client; } - - /** + + /** + * Sets the startup plugin so that it can be shutdown properly. + * @param startup + * @since 2.3 + */ + public final void setStartup(StartupPlugin startup) { this.startup = startup; } + /** + * Getter that returns the startup plugin object + * @return The StartupPlugin object + * @since 2.3 + */ + public final StartupPlugin getStartup() { return this.startup; } + + /** * Getter that returns the configuration object * @return The configuration object * @since 2.0 @@ -993,6 +1021,11 @@ public Object call(ArrayList compactions) throws Exception { LOG.info("Flushing compaction queue"); deferreds.add(compactionq.flush().addCallback(new CompactCB())); } + if (startup != null) { + LOG.info("Shutting down startup plugin: " + + startup.getClass().getCanonicalName()); + deferreds.add(startup.shutdown()); + } if (search != null) { LOG.info("Shutting down search plugin: " + search.getClass().getCanonicalName()); diff --git a/src/tools/StartupPlugin.java b/src/tools/StartupPlugin.java new file mode 100644 index 0000000000..580caa7ed3 --- /dev/null +++ b/src/tools/StartupPlugin.java @@ -0,0 +1,78 @@ +// 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.tools; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.utils.Config; +import net.opentsdb.stats.StatsCollector; + +/** + * The StartupPlugin allows users to interact with the OpenTSDB configuration + * as soon as it is completely parsed, just before OpenTSDB begins to use it. + *

    + * Note: Implementations must have a parameterless constructor. The + * {@link #initialize(TSDB)} method will be called immediately after the plugin is + * instantiated and before any other methods are called. + * @since 2.3 + */ +public abstract class StartupPlugin { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception if something else goes wrong + */ + public abstract void initialize(final Config config) throws IllegalArgumentException, Exception; + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. "2.0.1". The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. "2.0.1". The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String getType(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + +} \ No newline at end of file diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index da02826a5f..ce23661335 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -13,10 +13,17 @@ package net.opentsdb.tools; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.socket.ServerSocketChannelFactory; @@ -34,6 +41,8 @@ import net.opentsdb.tsd.RpcManager; import net.opentsdb.utils.Config; import net.opentsdb.utils.FileSystem; +import net.opentsdb.utils.Pair; +import net.opentsdb.utils.PluginLoader; import net.opentsdb.utils.Threads; /** @@ -53,6 +62,11 @@ static void usage(final ArgP argp, final String errmsg, final int retval) { System.exit(retval); } + /** A map of configured filters for use in querying */ + private static Map, Constructor>> + startupPlugin_filter_map = new HashMap, Constructor>>(); + private static final short DEFAULT_FLUSH_INTERVAL = 1000; private static TSDB tsdb = null; @@ -145,9 +159,21 @@ public static void main(String[] args) throws IOException { Executors.newCachedThreadPool(), Executors.newCachedThreadPool(), new Threads.PrependThreadNamer()); } - + + StartupPlugin startup = null; + try { + startup = loadStartupPlugins(config); + } catch (IllegalArgumentException e) { + usage(argp, e.getMessage(), 3); + } catch (Exception e) { + throw new RuntimeException("Initialization failed", e); + } + try { tsdb = new TSDB(config); + if (startup != null) { + tsdb.setStartup(startup); + } tsdb.initializePlugins(true); if (config.getBoolean("tsd.storage.hbase.prefetch_meta")) { tsdb.preFetchHBaseMeta(); @@ -198,6 +224,42 @@ public static void main(String[] args) throws IOException { // The server is now running in separate threads, we can exit main. } + private static StartupPlugin loadStartupPlugins(Config config) { + Logger log = LoggerFactory.getLogger(TSDMain.class); + + // load the startup plugin if enabled + StartupPlugin startup = null; + + if (config.getBoolean("tsd.startup.enable")) { + final String plugin_path = config.getString("tsd.core.plugin_path"); + + try { + TSDB.loadPluginPath(plugin_path); + } catch (Exception e) { + log.error("Error loading plugins from plugin path: " + plugin_path, e); + } + + startup = PluginLoader.loadSpecificPlugin( + config.getString("tsd.startup.plugin"), StartupPlugin.class); + if (startup == null) { + throw new IllegalArgumentException("Unable to locate startup plugin: " + + config.getString("tsd.startup.plugin")); + } + try { + startup.initialize(config); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize startup plugin", e); + } + log.info("Successfully initialized startup plugin [" + + startup.getClass().getCanonicalName() + "] version: " + + startup.version()); + } else { + startup = null; + } + + return startup; + } + private static void registerShutdownHook() { final class TSDBShutdown extends Thread { public TSDBShutdown() { diff --git a/src/utils/PluginLoader.java b/src/utils/PluginLoader.java index 8dadfc5ec3..d66c75f0c9 100644 --- a/src/utils/PluginLoader.java +++ b/src/utils/PluginLoader.java @@ -104,7 +104,7 @@ public static T loadSpecificPlugin(final String name, while(it.hasNext()) { T plugin = it.next(); - if (plugin.getClass().getName().equals(name)) { + if (plugin.getClass().getName().equals(name) || plugin.getClass().getSuperclass().getName().equals(name)) { return plugin; } } From 5d53abbcdbfd12097918c1c9366be279a1bfc41b Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 9 Mar 2016 20:28:35 -0600 Subject: [PATCH 441/826] Fixed formatting From 5cdbf14dbbc50d0ebc06551f343305a447913261 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Thu, 10 Mar 2016 12:53:31 -0600 Subject: [PATCH 442/826] Removed extra imports, fixed exception handling in initializePlugins --- src/core/TSDB.java | 17 +++++------------ src/tools/TSDMain.java | 6 +----- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index a0c6152007..48440481a4 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -25,18 +25,11 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.uid.NoSuchUniqueId; +import org.hbase.async.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.hbase.async.AppendRequest; -import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; -import org.hbase.async.ClientStats; -import org.hbase.async.DeleteRequest; -import org.hbase.async.GetRequest; -import org.hbase.async.HBaseClient; -import org.hbase.async.HBaseException; -import org.hbase.async.KeyValue; -import org.hbase.async.PutRequest; import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.Timeout; import org.jboss.netty.util.Timer; @@ -263,12 +256,12 @@ public static void loadPluginPath(final String plugin_path) throws RuntimeExcept * @throws IllegalArgumentException if a plugin could not be initialized * @since 2.0 */ - public void initializePlugins(final boolean init_rpcs) { + public void initializePlugins(final boolean init_rpcs) throws RuntimeException { final String plugin_path = config.getString("tsd.core.plugin_path"); try { loadPluginPath(plugin_path); - } catch (Exception e) { - LOG.error("Error loading plugins from plugin path: " + plugin_path, e); + } catch (RuntimeException e) { + throw e; } try { diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index ce23661335..08586aeb7f 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -14,15 +14,12 @@ import java.io.IOException; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; + import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.jboss.netty.bootstrap.ServerBootstrap; @@ -34,7 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.tools.BuildData; import net.opentsdb.core.TSDB; import net.opentsdb.core.Const; import net.opentsdb.tsd.PipelineFactory; From cb57dcc5cb72b9fc19f4cfa274d0defedf77fba2 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Tue, 15 Mar 2016 13:25:18 -0500 Subject: [PATCH 443/826] Added read-only CLI flag --- src/tools/CliOptions.java | 2 ++ src/tools/TSDMain.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index aeccb1bb36..e82c9825fa 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -140,6 +140,8 @@ static void overloadConfig(final ArgP argp, final Config config) { config.overrideConfig("tsd.core.flushinterval", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--backlog")) { config.overrideConfig("tsd.network.backlog", entry.getValue()); + } else if (entry.getKey().toLowerCase().equals("--read-only")) { + config.overrideConfig("tsd.mode", "ro"); } else if (entry.getKey().toLowerCase().equals("--bind")) { config.overrideConfig("tsd.network.bind", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--async-io")) { diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index da02826a5f..e1da594e5f 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -80,6 +80,8 @@ public static void main(String[] args) throws IOException { "Number for async io workers (default: cpu * 2)."); argp.addOption("--async-io", "true|false", "Use async NIO (default true) or traditional blocking io"); + argp.addOption("--read-only", "true|false", + "Set tsd.mode to ro (default false)"); argp.addOption("--backlog", "NUM", "Size of connection attempt queue (default: 3072 or kernel" + " somaxconn."); From 344d80871dccd8a6f7e7a5e0ed35beb261adeab3 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Mon, 14 Mar 2016 15:34:36 -0500 Subject: [PATCH 444/826] Added ready function Fixes #716 Fixes #688 --- src/core/TSDB.java | 12 ++++++++++-- src/tools/StartupPlugin.java | 8 +++++++- src/tools/TSDMain.java | 13 ++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 48440481a4..3a7a7cedeb 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -25,11 +25,18 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; -import net.opentsdb.uid.NoSuchUniqueId; -import org.hbase.async.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.hbase.async.AppendRequest; +import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.ClientStats; +import org.hbase.async.DeleteRequest; +import org.hbase.async.GetRequest; +import org.hbase.async.HBaseClient; +import org.hbase.async.HBaseException; +import org.hbase.async.KeyValue; +import org.hbase.async.PutRequest; import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.Timeout; import org.jboss.netty.util.Timer; @@ -37,6 +44,7 @@ import net.opentsdb.tree.TreeBuilder; import net.opentsdb.tsd.RTPublisher; import net.opentsdb.tsd.StorageExceptionHandler; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; diff --git a/src/tools/StartupPlugin.java b/src/tools/StartupPlugin.java index 580caa7ed3..3c56464a68 100644 --- a/src/tools/StartupPlugin.java +++ b/src/tools/StartupPlugin.java @@ -15,6 +15,7 @@ import com.stumbleupon.async.Deferred; import net.opentsdb.utils.Config; +import net.opentsdb.core.TSDB; import net.opentsdb.stats.StatsCollector; /** @@ -40,7 +41,12 @@ public abstract class StartupPlugin { * missing * @throws Exception if something else goes wrong */ - public abstract void initialize(final Config config) throws IllegalArgumentException, Exception; + public abstract Config initialize(Config config) throws IllegalArgumentException, Exception; + + /** + * Called when the TSD is fully initialized and ready to handle traffic. + */ + public abstract void setReady(final TSDB tsdb) throws Exception; /** * Called to gracefully shutdown the plugin. Implementations should close diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 08586aeb7f..c804c26adc 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.opentsdb.tools.BuildData; import net.opentsdb.core.TSDB; import net.opentsdb.core.Const; import net.opentsdb.tsd.PipelineFactory; @@ -206,6 +207,9 @@ public static void main(String[] args) throws IOException { final InetSocketAddress addr = new InetSocketAddress(bindAddress, config.getInt("tsd.network.port")); server.bind(addr); + if (startup != null) { + startup.setReady(tsdb); + } log.info("Ready to serve on " + addr); } catch (Throwable e) { factory.releaseExternalResources(); @@ -226,17 +230,20 @@ private static StartupPlugin loadStartupPlugins(Config config) { // load the startup plugin if enabled StartupPlugin startup = null; - if (config.getBoolean("tsd.startup.enable")) { + if (config.getBoolean("tsd.startup.enabled")) { + log.debug("Startup Plugin is Enabled"); final String plugin_path = config.getString("tsd.core.plugin_path"); + final String plugin_class = config.getString("tsd.startup.plugin"); + log.debug("Plugin Path: " + plugin_path); try { TSDB.loadPluginPath(plugin_path); } catch (Exception e) { log.error("Error loading plugins from plugin path: " + plugin_path, e); } - startup = PluginLoader.loadSpecificPlugin( - config.getString("tsd.startup.plugin"), StartupPlugin.class); + log.debug("Attempt to Load: " + plugin_class); + startup = PluginLoader.loadSpecificPlugin(plugin_class, StartupPlugin.class); if (startup == null) { throw new IllegalArgumentException("Unable to locate startup plugin: " + config.getString("tsd.startup.plugin")); From 0f3a1476cda843c0579e2276d9bc3c2d0515acfe Mon Sep 17 00:00:00 2001 From: Nathan Owens Date: Fri, 11 Jul 2014 10:15:25 -0400 Subject: [PATCH 445/826] add datapoint counter --- src/core/TSDB.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 72930603d1..4389d40708 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -128,6 +128,9 @@ public final class TSDB { /** Plugin for dealing with data points that can't be stored */ private StorageExceptionHandler storage_exception_handler = null; + /** Datapoint Counter */ + private static final AtomicLong datapoints_received = new AtomicLong(); + /** * Constructor * @param client An initialized HBase client object @@ -722,6 +725,8 @@ public Deferred addPoint(final String metric, } else { v = Bytes.fromLong(value); } + collector.record("datapoints.received", datapoints_received, "type=all"); + datapoints_received.incrementAndGet(); final short flags = (short) (v.length - 1); // Just the length. return addPointInternal(metric, timestamp, v, tags, flags); } From 640ab2e357848f1fe19c26e5485b9a7cf280af8a Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Tue, 15 Mar 2016 15:29:51 -0500 Subject: [PATCH 446/826] Only record stat when collectStats() is called. Fixes #369 --- src/core/TSDB.java | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 4389d40708..500a1bbb64 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -127,9 +128,9 @@ public final class TSDB { /** Plugin for dealing with data points that can't be stored */ private StorageExceptionHandler storage_exception_handler = null; - - /** Datapoint Counter */ - private static final AtomicLong datapoints_received = new AtomicLong(); + + /** Datapoints Added */ + private static final AtomicLong datapoints_added = new AtomicLong(); /** * Constructor @@ -563,6 +564,13 @@ public void collectStats(final StatsCollector collector) { collector.clearExtraTag("class"); } + collector.addExtraTag("class", "TSDB"); + try { + collector.record("datapoints.added", datapoints_added, "type=all"); + } finally { + collector.clearExtraTag("class"); + } + collector.addExtraTag("class", "TsdbQuery"); try { collector.record("hbase.latency", TsdbQuery.scanlatency, "method=scan"); @@ -725,8 +733,7 @@ public Deferred addPoint(final String metric, } else { v = Bytes.fromLong(value); } - collector.record("datapoints.received", datapoints_received, "type=all"); - datapoints_received.incrementAndGet(); + final short flags = (short) (v.length - 1); // Just the length. return addPointInternal(metric, timestamp, v, tags, flags); } @@ -832,7 +839,7 @@ private Deferred addPointInternal(final String metric, Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); RowKey.prefixKeyWithSalt(row); - + Deferred result = null; if (config.enable_appends()) { final AppendDataPoints kv = new AppendDataPoints(qualifier, value); @@ -844,7 +851,11 @@ private Deferred addPointInternal(final String metric, final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); result = client.put(point); } - + + // Count all added datapoints, not just those that came in through PUT rpc + // Will there be others? Well, something could call addPoint programatically right? + datapoints_added.incrementAndGet(); + // TODO(tsuna): Add a callback to time the latency of HBase and store the // timing in a moving Histogram (once we have a class for this). @@ -871,7 +882,7 @@ private Deferred addPointInternal(final String metric, TSMeta.incrementAndGetCounter(TSDB.this, tsuid); } } - + if (rt_publisher != null) { rt_publisher.sinkDataPoint(metric, timestamp, value, tags, tsuid, flags); } From 1236dff1c757b3847d3f8892d4bd8519f480fd84 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 16 Mar 2016 00:18:12 -0500 Subject: [PATCH 447/826] Need to actually collect the stats from the Startup plugin --- src/core/TSDB.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 3a7a7cedeb..4e58e692ee 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -624,6 +624,14 @@ public void collectStats(final StatsCollector collector) { compactionq.collectStats(collector); // Collect Stats from Plugins + if (startup != null) { + try { + collector.addExtraTag("plugin", "startup"); + startup.collectStats(collector); + } finally { + collector.clearExtraTag("plugin"); + } + } if (rt_publisher != null) { try { collector.addExtraTag("plugin", "publish"); From 499e35c803be135dd3ae2f10dee506035d555f12 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 16 Mar 2016 01:38:35 -0500 Subject: [PATCH 448/826] Implement default values for startup plugin --- src/utils/Config.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/Config.java b/src/utils/Config.java index 3191b1602a..7655260bce 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -508,6 +508,8 @@ protected void setDefaults() { default_map.put("tsd.search.enable", "false"); default_map.put("tsd.search.plugin", ""); default_map.put("tsd.stats.canonical", "false"); + default_map.put("tsd.startup.enable", "false"); + default_map.put("tsd.startup.plugin", ""); default_map.put("tsd.storage.hbase.scanner.maxNumRows", "128"); default_map.put("tsd.storage.fix_duplicates", "false"); default_map.put("tsd.storage.flush_interval", "1000"); From 17132894e52a145abb2d157b2944a7a19ef438d7 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 16 Mar 2016 01:40:03 -0500 Subject: [PATCH 449/826] Make sure value used matches config value --- src/tools/TSDMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index c804c26adc..b23008d46d 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -230,7 +230,7 @@ private static StartupPlugin loadStartupPlugins(Config config) { // load the startup plugin if enabled StartupPlugin startup = null; - if (config.getBoolean("tsd.startup.enabled")) { + if (config.getBoolean("tsd.startup.enable")) { log.debug("Startup Plugin is Enabled"); final String plugin_path = config.getString("tsd.core.plugin_path"); final String plugin_class = config.getString("tsd.startup.plugin"); From 6e19c1d54128ce1bdbe8e53bff4844a28ba642da Mon Sep 17 00:00:00 2001 From: Simon Matic Langford Date: Thu, 17 Mar 2016 22:02:01 +0000 Subject: [PATCH 450/826] Add checkbox for showing global annotations on ui --- src/tsd/client/QueryUi.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index cf78c6feca..3c063745de 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -138,6 +138,7 @@ public class QueryUi implements EntryPoint, HistoryListener { private final ValidatedTextBox yformat = new ValidatedTextBox(); private final ValidatedTextBox y2format = new ValidatedTextBox(); private final ValidatedTextBox wxh = new ValidatedTextBox(); + private final CheckBox global_annotations = new CheckBox("Global annotations"); private String keypos = ""; // Position of the key on the graph. private final CheckBox horizontalkey = new CheckBox("Horizontal layout"); @@ -286,6 +287,8 @@ public void onValueChange(final ValueChangeEvent event) { y2format.addKeyPressHandler(refreshgraph); wxh.addBlurHandler(refreshgraph); wxh.addKeyPressHandler(refreshgraph); + global_annotations.addBlurHandler(refreshgraph); + global_annotations.addKeyPressHandler(refreshgraph); horizontalkey.addClickHandler(refreshgraph); keybox.addClickHandler(refreshgraph); nokey.addClickHandler(refreshgraph); @@ -378,6 +381,11 @@ public void onValueChange(final ValueChangeEvent event) { hbox.add(wxh); table.setWidget(0, 3, hbox); } + { + final HorizontalPanel hbox = new HorizontalPanel(); + hbox.add(global_annotations); + table.setWidget(0, 4, hbox); + } { addMetricForm("metric 1", 0); metrics.selectTab(0); @@ -404,6 +412,7 @@ public void onBeforeSelection(final BeforeSelectionEvent event) { optpanel.add(makeStylePanel(), "Style"); optpanel.selectTab(0); table.setWidget(1, 3, optpanel); + table.getFlexCellFormatter().setColSpan(1, 3, 2); final DecoratorPanel decorator = new DecoratorPanel(); decorator.setWidget(table); @@ -785,6 +794,7 @@ private void refreshFromQueryString() { maybeSetTextbox(qs, "start", start_datebox.getTextBox()); maybeSetTextbox(qs, "end", end_datebox.getTextBox()); setTextbox(qs, "wxh", wxh); + global_annotations.setValue(qs.containsKey("global_annotations")); autoreload.setValue(qs.containsKey("autoreload"), true); maybeSetTextbox(qs, "autoreload", autoreoload_interval); @@ -901,6 +911,9 @@ private void refreshGraph() { // a special parameter that the server will delete from the query. url.append("&ignore=" + nrequests++); } + if (global_annotations.getValue()) { + url.append("&global_annotations"); + } if(timezone.length() > 1) url.append("&tz=").append(timezone); From 6299ae0f1b468bcf6432e40301c09f86de046de6 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 23 Mar 2016 11:36:33 -0500 Subject: [PATCH 451/826] Added check for malformed, double dot timestamp Fixes #724 --- src/utils/DateTime.java | 6 ++++-- test/utils/TestDateTime.java | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 6690573ddc..20d9d80711 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -124,8 +124,10 @@ public static final long parseDateTimeString(final String datetime, } else { try { long time; - if (datetime.contains(".")) { - if (datetime.charAt(10) != '.' || datetime.length() != 14) { + Boolean containsDot = datetime.contains("."); + Boolean containsTwoDots = datetime.matches(".*\\..*\\..*"); + if (containsDot) { + if (datetime.charAt(10) != '.' || datetime.length() != 14 || containsTwoDots) { throw new IllegalArgumentException("Invalid time: " + datetime + ". Millisecond timestamps must be in the format " + ". where the milliseconds are limited to 3 digits"); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 07facd6567..037bd53148 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -143,6 +143,11 @@ public void parseDateTimeStringUnixSecondsZero() { public void parseDateTimeStringUnixSecondsNegative() { DateTime.parseDateTimeString("-135596160", null); } + + @Test(expected = IllegalArgumentException.class) + public void parseDateTimeStringMultipleDots() { + DateTime.parseDateTimeString("1234567890.2.4", null); + } @Test public void parseDateTimeStringUnixSecondsInvalidLong() { From 20222f39ea9d29f079ffceb1068d6b8e4f235ce4 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 23 Mar 2016 12:20:03 -0500 Subject: [PATCH 452/826] Disable STDOUT logging for packages Fixes #715 --- build-aux/deb/logback.xml | 2 +- build-aux/rpm/logback.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-aux/deb/logback.xml b/build-aux/deb/logback.xml index 7ae2c3fcfc..9c32b2ecbe 100644 --- a/build-aux/deb/logback.xml +++ b/build-aux/deb/logback.xml @@ -67,7 +67,7 @@ - + diff --git a/build-aux/rpm/logback.xml b/build-aux/rpm/logback.xml index 7ae2c3fcfc..9c32b2ecbe 100644 --- a/build-aux/rpm/logback.xml +++ b/build-aux/rpm/logback.xml @@ -67,7 +67,7 @@ - + From 927b50d80d1730f5a7409838d6bbde1646c9d031 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 23 Mar 2016 14:01:07 -0500 Subject: [PATCH 453/826] Updated regex to match dotted timestamp 1234567890.2.3 - no match 1234.56789.1234 - no match 1234567890.1234 - matches 1234.567890.2..3 - no match 1234567890.2..3 - no match Fixes #724 --- src/utils/DateTime.java | 12 +++++++++--- test/utils/TestDateTime.java | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 20d9d80711..da2bcd2c68 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -125,9 +125,14 @@ public static final long parseDateTimeString(final String datetime, try { long time; Boolean containsDot = datetime.contains("."); - Boolean containsTwoDots = datetime.matches(".*\\..*\\..*"); + // [0-9]{10} ten digits + // \\. a dot + // [0-9]{1,3} one to three digits + Boolean isValidDottedMillesecond = datetime.matches("^[0-9]{10}\\.[0-9]{1,3}$"); + // one to ten digits (0-9) + Boolean isValidSeconds = datetime.matches("^[0-9]{1,10}$"); if (containsDot) { - if (datetime.charAt(10) != '.' || datetime.length() != 14 || containsTwoDots) { + if (!isValidDottedMillesecond) { throw new IllegalArgumentException("Invalid time: " + datetime + ". Millisecond timestamps must be in the format " + ". where the milliseconds are limited to 3 digits"); @@ -142,8 +147,9 @@ public static final long parseDateTimeString(final String datetime, } // this is a nasty hack to determine if the incoming request is // in seconds or milliseconds. This will work until November 2286 - if (datetime.length() <= 10) + if (datetime.length() <= 10) { time *= 1000; + } return time; } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid time: " + datetime diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 037bd53148..1a8d1e41f8 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -144,10 +144,28 @@ public void parseDateTimeStringUnixSecondsNegative() { DateTime.parseDateTimeString("-135596160", null); } + /* + 1234567890.418 - match + 1234567890.1235 - no match + 1234567890.12 - matches + 1234.56789.003 - no match + 1234567890.3 - match + */ + @Test(expected = IllegalArgumentException.class) public void parseDateTimeStringMultipleDots() { DateTime.parseDateTimeString("1234567890.2.4", null); } + + @Test(expected = IllegalArgumentException.class) + public void parseDateTimeStringMultipleDotsEarlyDot() { + DateTime.parseDateTimeString("1234.56789.123", null); + } + + @Test(expected = IllegalArgumentException.class) + public void parseDateTimeStringEarlyandExtraDots() { + DateTime.parseDateTimeString("1234.56789.0.3", null); + } @Test public void parseDateTimeStringUnixSecondsInvalidLong() { @@ -167,6 +185,18 @@ public void parseDateTimeStringUnixMSDot() { long t = DateTime.parseDateTimeString("1355961603.418", null); assertEquals(1355961603418L, t); } + + @Test + public void parseDateTimeStringUnixMSDotShorter() { + long t = DateTime.parseDateTimeString("1355961603.41", null); + assertEquals(135596160341L, t); + } + + @Test + public void parseDateTimeStringUnixMSDotShortest() { + long t = DateTime.parseDateTimeString("1355961603.4", null); + assertEquals(13559616034L, t); + } @Test (expected = IllegalArgumentException.class) public void parseDateTimeStringUnixMSDotInvalid() { From 358a70e42c5b3d9371a55d60012a59878f121a1c Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 17 Feb 2016 15:49:18 -0600 Subject: [PATCH 454/826] Splicer needs these modifications --- src/core/AggregationIterator.java | 12 ++-- src/core/Aggregators.java | 101 +++++++++++++++++++++++++++ src/core/MutableDataPoint.java | 10 +++ src/query/expression/HighestMax.java | 2 +- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 03bc93b8cf..4c4a917c3d 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -129,7 +129,7 @@ public class AggregationIterator implements SeekableView, DataPoint, * possibly store, provided that the most significant bit is reserved by * FLAG_FLOAT. */ - private static final long TIME_MASK = 0x7FFFFFFFFFFFFFFFL; + protected static final long TIME_MASK = 0x7FFFFFFFFFFFFFFFL; /** Aggregator to use to aggregate data points from different Spans. */ private final Aggregator aggregator; @@ -148,13 +148,13 @@ public class AggregationIterator implements SeekableView, DataPoint, * Once we reach the end of a Span, we'll null out its iterator from this * array. */ - private final SeekableView[] iterators; + protected final SeekableView[] iterators; /** Start time (UNIX timestamp in seconds or ms) on 32 bits ("unsigned" int). */ - private final long start_time; + protected final long start_time; /** End time (UNIX timestamp in seconds or ms) on 32 bits ("unsigned" int). */ - private final long end_time; + protected final long end_time; /** * The current and previous timestamps for the data points being used. @@ -178,7 +178,7 @@ public class AggregationIterator implements SeekableView, DataPoint, * linear interpolation anymore. * */ - private final long[] timestamps; // 32 bit unsigned + flag + protected final long[] timestamps; // 32 bit unsigned + flag /** * The current and next values for the data points being used. @@ -186,7 +186,7 @@ public class AggregationIterator implements SeekableView, DataPoint, * This array is also used to store floating point values, in which case * their binary representation just happens to be stored in a {@code long}. */ - private final long[] values; + protected final long[] values; /** The index in {@link #iterators} of the current Span being used. */ private int current; diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 05eabf4e45..1d846301a5 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -15,6 +15,8 @@ import java.util.HashMap; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Iterator; +import java.util.LinkedList; import org.apache.commons.math3.stat.descriptive.rank.Percentile; import org.apache.commons.math3.stat.descriptive.rank.Percentile.EstimationType; @@ -552,5 +554,104 @@ public double runDouble(final Doubles values) { } } + public static final class MovingAverage extends Aggregator { + private LinkedList list = new LinkedList(); + private final long numPoints; + private final boolean isTimeUnit; + public MovingAverage(final Interpolation method, final String name, long numPoints, boolean isTimeUnit) { + super(method, name); + this.numPoints = numPoints; + this.isTimeUnit = isTimeUnit; + } + + public long runLong(final Longs values) { + long sum = values.nextLongValue(); + while (values.hasNextValue()) { + sum += values.nextLongValue(); + } + + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + list.addFirst(new SumPoint(ts, sum)); + } + + long result = 0; + int count = 0; + + Iterator iter = list.iterator(); + SumPoint first = iter.next(); + boolean conditionMet = false; + + // now sum up the preceeding points + while (iter.hasNext()) { + SumPoint next = iter.next(); + result += (Long) next.val; + count++; + if (!isTimeUnit && count >= numPoints) { + conditionMet = true; + break; + } else if (isTimeUnit && ((first.ts - next.ts) > numPoints)) { + conditionMet = true; + break; + } + } + + if (!conditionMet || count == 0) { + return 0; + } + + return result / count; + } + + @Override + public double runDouble(Doubles values) { + double sum = values.nextDoubleValue(); + while (values.hasNextValue()) { + sum += values.nextDoubleValue(); + } + + if (values instanceof DataPoint) { + long ts = ((DataPoint) values).timestamp(); + list.addFirst(new SumPoint(ts, sum)); + } + + double result = 0; + int count = 0; + + Iterator iter = list.iterator(); + SumPoint first = iter.next(); + boolean conditionMet = false; + + // now sum up the preceeding points + while (iter.hasNext()) { + SumPoint next = iter.next(); + result += (Double) next.val; + count++; + if (!isTimeUnit && count >= numPoints) { + conditionMet = true; + break; + } else if (isTimeUnit && ((first.ts - next.ts) > numPoints)) { + conditionMet = true; + break; + } + } + + if (!conditionMet || count == 0) { + return 0; + } + + return result / count; + } + + class SumPoint { + long ts; + Object val; + + public SumPoint(long ts, Object val) { + this.ts = ts; + this.val = val; + } + } + } } diff --git a/src/core/MutableDataPoint.java b/src/core/MutableDataPoint.java index 2f51a4e3f9..3620a2da98 100644 --- a/src/core/MutableDataPoint.java +++ b/src/core/MutableDataPoint.java @@ -92,6 +92,16 @@ public static MutableDataPoint ofLongValue(final long timestamp, return dp; } + /** + * Copy constructor + * + * @param value A datapoint value. + */ + public static MutableDataPoint fromPoint(final DataPoint value) { + if (value.isInteger()) return ofLongValue(value.timestamp(), value.longValue()); + else return ofDoubleValue(value.timestamp(), value.doubleValue()); + } + @Override public long timestamp() { return timestamp; diff --git a/src/query/expression/HighestMax.java b/src/query/expression/HighestMax.java index 73029b23f4..0b942d5c1b 100644 --- a/src/query/expression/HighestMax.java +++ b/src/query/expression/HighestMax.java @@ -179,7 +179,7 @@ public String writeStringField(final List query_params, /** * Aggregator that stores the overall maximum value for the entire series */ - static class MaxCacheAggregator extends Aggregator { + public static class MaxCacheAggregator extends Aggregator { /** The total number of series in the result set, including sub queries and * group bys */ private final int total_series; From d46133405b98df52ccb1ad6c0176897c2728f67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=AC=E5=B9=B3?= Date: Tue, 24 Nov 2015 19:43:06 +0800 Subject: [PATCH 455/826] tsd connections limit --- src/tools/TSDMain.java | 8 +++++++- src/tsd/ConnectionManager.java | 17 +++++++++++++++-- src/tsd/PipelineFactory.java | 8 ++++---- src/utils/Config.java | 1 + 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 4f1e3fd43e..37cbd7c3d2 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -138,6 +138,12 @@ public static void main(String[] args) throws IOException { } final ServerSocketChannelFactory factory; + int connectionsLimit = 0; + try { + connectionsLimit = config.getInt("tsd.connections.limit"); + } catch (NumberFormatException nfe) { + usage(argp, "Invalid connections limit", 1); + } if (config.getBoolean("tsd.network.async_io")) { int workers = Runtime.getRuntime().availableProcessors() * 2; if (config.hasProperty("tsd.network.worker_threads")) { @@ -188,7 +194,7 @@ public static void main(String[] args) throws IOException { // here to fail fast. final RpcManager manager = RpcManager.instance(tsdb); - server.setPipelineFactory(new PipelineFactory(tsdb, manager)); + server.setPipelineFactory(new PipelineFactory(tsdb, manager, connectionsLimit)); if (config.hasProperty("tsd.network.backlog")) { server.setOption("backlog", config.getInt("tsd.network.backlog")); } diff --git a/src/tsd/ConnectionManager.java b/src/tsd/ConnectionManager.java index 35c3288bad..b8b3ea2eb0 100644 --- a/src/tsd/ConnectionManager.java +++ b/src/tsd/ConnectionManager.java @@ -41,6 +41,10 @@ final class ConnectionManager extends SimpleChannelHandler { private static final AtomicLong exceptions_closed = new AtomicLong(); private static final AtomicLong exceptions_reset = new AtomicLong(); private static final AtomicLong exceptions_timeout = new AtomicLong(); + /** + * max connections can be serviced by tsd, if over limit, tsd will close new connection. + */ + private int connectionsLimit; private static final DefaultChannelGroup channels = new DefaultChannelGroup("all-channels"); @@ -50,7 +54,9 @@ static void closeAllConnections() { } /** Constructor. */ - public ConnectionManager() { + public ConnectionManager(int connectionsLimit) { + LOG.info("totalConnections limit is set : " + connectionsLimit); + this.connectionsLimit = connectionsLimit; } /** @@ -73,7 +79,14 @@ public static void collectStats(final StatsCollector collector) { @Override public void channelOpen(final ChannelHandlerContext ctx, - final ChannelStateEvent e) { + final ChannelStateEvent e) throws IOException { + if (connectionsLimit > 0) { + int channelsSize = channels.size(); + if (channelsSize >= connectionsLimit) { + e.getChannel().close(); + throw new IOException("now channels size " + channelsSize + " is exceed total connections limit " + connectionsLimit); + } + } channels.add(e.getChannel()); connections_established.incrementAndGet(); } diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 85f66a7fbd..4037388fa8 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -47,7 +47,7 @@ public final class PipelineFactory implements ChannelPipelineFactory { // Those are sharable but maintain some state, so a single instance per // PipelineFactory is needed. - private final ConnectionManager connmgr = new ConnectionManager(); + private final ConnectionManager connmgr; private final DetectHttpOrRpc HTTP_OR_RPC = new DetectHttpOrRpc(); private final Timer timer; private final ChannelHandler timeoutHandler; @@ -70,7 +70,7 @@ public final class PipelineFactory implements ChannelPipelineFactory { * serializers */ public PipelineFactory(final TSDB tsdb) { - this(tsdb, RpcManager.instance(tsdb)); + this(tsdb, RpcManager.instance(tsdb), 0); } /** @@ -82,12 +82,13 @@ public PipelineFactory(final TSDB tsdb) { * @throws Exception if the HttpQuery handler is unable to load * serializers */ - public PipelineFactory(final TSDB tsdb, final RpcManager manager) { + public PipelineFactory(final TSDB tsdb, final RpcManager manager, final int connectionsLimit) { this.tsdb = tsdb; this.socketTimeout = tsdb.getConfig().getInt("tsd.core.socket.timeout"); timer = tsdb.getTimer(); this.timeoutHandler = new IdleStateHandler(timer, 0, 0, this.socketTimeout); this.rpchandler = new RpcHandler(tsdb, manager); + this.connmgr = new ConnectionManager(connectionsLimit); try { HttpQuery.initializeSerializerMaps(tsdb); } catch (RuntimeException e) { @@ -153,4 +154,3 @@ protected Object decode(final ChannelHandlerContext ctx, } } - \ No newline at end of file diff --git a/src/utils/Config.java b/src/utils/Config.java index 7655260bce..1b439c0f61 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -536,6 +536,7 @@ protected void setDefaults() { + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, " + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since"); default_map.put("tsd.query.timeout", "0"); + default_map.put("tsd.connections.limit", "0"); for (Map.Entry entry : default_map.entrySet()) { if (!properties.containsKey(entry.getKey())) From 9d9e4757ce1b7c3cda739f4f7a36a1c9a3ab7a4f Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 23 Mar 2016 11:59:47 -0500 Subject: [PATCH 456/826] A few modifications to PR-638 Fixes #638 Added additional command line argument definition. --- src/tools/CliOptions.java | 2 ++ src/tools/TSDMain.java | 4 +++- src/tsd/ConnectionManager.java | 10 +++++++--- src/tsd/PipelineFactory.java | 2 +- src/utils/Config.java | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index e82c9825fa..1ae0322701 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -148,6 +148,8 @@ static void overloadConfig(final ArgP argp, final Config config) { config.overrideConfig("tsd.network.async_io", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--worker-threads")) { config.overrideConfig("tsd.network.worker_threads", entry.getValue()); + } else if (entry.getKey().toLowerCase().equals("--max-connections")) { + config.overrideConfig("tsd.core.connections.limit", entry.getValue()); } } } diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 37cbd7c3d2..eb61f4a6ac 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -96,6 +96,8 @@ public static void main(String[] args) throws IOException { argp.addOption("--backlog", "NUM", "Size of connection attempt queue (default: 3072 or kernel" + " somaxconn."); + argp.addOption("--max-connections", "NUM", + "Maximum number of connections to accept"); argp.addOption("--flush-interval", "MSEC", "Maximum time for which a new data point can be buffered" + " (default: " + DEFAULT_FLUSH_INTERVAL + ")."); @@ -140,7 +142,7 @@ public static void main(String[] args) throws IOException { final ServerSocketChannelFactory factory; int connectionsLimit = 0; try { - connectionsLimit = config.getInt("tsd.connections.limit"); + connectionsLimit = config.getInt("tsd.core.connections.limit"); } catch (NumberFormatException nfe) { usage(argp, "Invalid connections limit", 1); } diff --git a/src/tsd/ConnectionManager.java b/src/tsd/ConnectionManager.java index b8b3ea2eb0..f4d4523e3a 100644 --- a/src/tsd/ConnectionManager.java +++ b/src/tsd/ConnectionManager.java @@ -37,6 +37,7 @@ final class ConnectionManager extends SimpleChannelHandler { private static final Logger LOG = LoggerFactory.getLogger(ConnectionManager.class); private static final AtomicLong connections_established = new AtomicLong(); + private static final AtomicLong connections_rejected = new AtomicLong(); private static final AtomicLong exceptions_unknown = new AtomicLong(); private static final AtomicLong exceptions_closed = new AtomicLong(); private static final AtomicLong exceptions_reset = new AtomicLong(); @@ -65,6 +66,8 @@ public ConnectionManager(int connectionsLimit) { */ public static void collectStats(final StatsCollector collector) { collector.record("connectionmgr.connections", channels.size(), "type=open"); + collector.record("connectionmgr.connections", connections_rejected, + "type=rejected"); collector.record("connectionmgr.connections", connections_established, "type=total"); collector.record("connectionmgr.exceptions", exceptions_closed, @@ -81,10 +84,11 @@ public static void collectStats(final StatsCollector collector) { public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws IOException { if (connectionsLimit > 0) { - int channelsSize = channels.size(); - if (channelsSize >= connectionsLimit) { + int channelSize = channels.size(); + if (channelSize >= connectionsLimit) { e.getChannel().close(); - throw new IOException("now channels size " + channelsSize + " is exceed total connections limit " + connectionsLimit); + connections_rejected.incrementAndGet(); + throw new IOException("Channel size (" + channelSize + ") exceeds total connection limit (" + connectionsLimit + ")"); } } channels.add(e.getChannel()); diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 4037388fa8..30389c34c6 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -70,7 +70,7 @@ public final class PipelineFactory implements ChannelPipelineFactory { * serializers */ public PipelineFactory(final TSDB tsdb) { - this(tsdb, RpcManager.instance(tsdb), 0); + this(tsdb, RpcManager.instance(tsdb), tsdb.getConfig().getInt("tsd.core.connections.limit")); } /** diff --git a/src/utils/Config.java b/src/utils/Config.java index 1b439c0f61..9f68736cb5 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -487,6 +487,7 @@ protected void setDefaults() { default_map.put("tsd.core.auto_create_metrics", "false"); default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); + default_map.put("tsd.core.connections.limit", "0"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); @@ -536,7 +537,6 @@ protected void setDefaults() { + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, " + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since"); default_map.put("tsd.query.timeout", "0"); - default_map.put("tsd.connections.limit", "0"); for (Map.Entry entry : default_map.entrySet()) { if (!properties.containsKey(entry.getKey())) From a807aa314b6973f3fc0197091971de9b9d60f240 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 25 Mar 2016 20:18:58 -0500 Subject: [PATCH 457/826] Accept now as a time --- src/utils/DateTime.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index da2bcd2c68..2d690e7e17 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -75,6 +75,9 @@ public static final long parseDateTimeString(final String datetime, final String tz) { if (datetime == null || datetime.isEmpty()) return -1; + if (datetime.toLowerCase().equals("now")) { + return System.currentTimeMillis(); + } if (datetime.toLowerCase().endsWith("-ago")) { long interval = DateTime.parseDuration( datetime.substring(0, datetime.length() - 4)); From d3fde219fcff4d062b90fb63f1afe03b3349eb0d Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 25 Mar 2016 20:49:09 -0500 Subject: [PATCH 458/826] Added test for Now timestamp Fixes #192 --- test/utils/TestDateTime.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 1a8d1e41f8..3f46f6b3c8 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -70,6 +70,12 @@ public void getTimezoneNull() { assertNull(DateTime.timezones.get("Nothere")); } + @Test + public void parseDateTimeStringNow() { + long t = DateTime.parseDateTimeString("now", null); + assertEquals(t, 1357300800000L); + } + @Test public void parseDateTimeStringRelativeS() { long t = DateTime.parseDateTimeString("60s-ago", null); From 7c0c5688a322421c106b0671f9a58f483eaabf96 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 25 Mar 2016 21:08:49 -0500 Subject: [PATCH 459/826] Update NEWS --- NEWS | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 5378eb836c..c0950558e1 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,28 @@ OpenTSDB - User visible changes. +* Version 2.3.0 RC1 (2016-03-28) + +Noteworthy Changes: + - Introduced option --max-connection/tsd.core.connections.limit to set the maximum number + of connection a TSD will accept (#638) + - 'tsdb import' can now read from stdin (#580) + - Added datapoints counter (#369) + - Improved metadata storage performance (#699) + - added checkbox for showing global annotations in UI (#736) + - Added startup plugins, can be used for Service Discovery or other integration (#719) + - Added MetaDataCache plugin api + - Added timeshift() function (#175) + - Now align downsampling to Gregorian Calendar (#548, #657) + - Added support for latest Java versions + - Added NONE aggregator + - Added script to build OpenTSDB/HBase on OSX (#674) + - Added First/Last Downsampler + - Added query epxressions (alias(), scale(), absolute(), movingAverage(), highestCurrent(), + highestMax(), timeShift(), divide(), sum(), difference(), multiply()) (#625) +Bug Fixes: + - Some improperly formatted timestamps were allowed (#724) + - removed stdout logging from packaged logback.xml files (#715) + - * Version 2.2.0 (2016-02-14) Noteworthy Changes @@ -297,4 +320,4 @@ along with this library. If not, see . Local Variables: mode: outline -End: \ No newline at end of file +End: From 6071de1b47b38ec1b93c899a2b189a4459c73aee Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 25 Mar 2016 21:14:18 -0500 Subject: [PATCH 460/826] Added config value for max number of rows to be returned per Scanner round trip --- src/opentsdb.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/opentsdb.conf b/src/opentsdb.conf index 11d2a911df..cee3a4b402 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -46,6 +46,9 @@ tsd.http.cachedir = # default is 1,000 # tsd.storage.flush_interval = 1000 +# Max number of rows to be returned per Scanner round trip +# tsd.storage.hbase.scanner.maxNumRows = 128 + # Name of the HBase table where data points are stored, default is "tsdb" #tsd.storage.hbase.data_table = tsdb From 5747796c2a4398c43b332a165fbf39fb30f1d2a8 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Tue, 17 Nov 2015 15:56:04 -0600 Subject: [PATCH 461/826] Apply regex based whitelists to new uid auto-creation --- src/opentsdb.conf | 16 ++++ src/tools/CliOptions.java | 11 +++ src/uid/UniqueId.java | 61 +++++++++++++++ src/utils/Config.java | 43 ++++++++++- test/uid/TestUniqueId.java | 150 +++++++++++++++++++++++++++++++++++-- 5 files changed, 272 insertions(+), 9 deletions(-) diff --git a/src/opentsdb.conf b/src/opentsdb.conf index cee3a4b402..7de2cb9e51 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -38,6 +38,22 @@ tsd.http.cachedir = # is False #tsd.core.auto_create_metrics = false +# Whether or no to evaluate new metric/tagk/tagv items against a whitelist, default +# is False +# tsd.core.auto_create_whitelist = false + +# Comma-Delimited list of regex patterns to match against new metric names, default +# is .*, examples might be ^awesome\..*$,^regexfoo[0-9].*$ +#tsd.core.auto_create_metrics_patterns = .* + +# Comma-Delimited list of regex patterns to match against new tagk names, default +# is .*, examples might be ^awesome\..*$,^regexfoo[0-9].*$ +#tsd.core.auto_create_tagk_patterns = .* + +# Comma-Delimited list of regex patterns to match against new tagv names, default +# is .*, examples might be ^awesome\..*$,^regexfoo[0-9].*$ +#tsd.core.auto_create_tagv_patterns = .* + # --------- STORAGE ---------- # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index 1ae0322701..ec5eee6a4b 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -120,6 +120,17 @@ static void overloadConfig(final ArgP argp, final Config config) { // map the overrides if (entry.getKey().toLowerCase().equals("--auto-metric")) { config.overrideConfig("tsd.core.auto_create_metrics", "true"); + } else if (entry.getKey().toLowerCase().equals("--auto-metric-whitelist")) { + config.overrideConfig("tsd.core.auto_create_whitelist", "true"); + } else if (entry.getKey().toLowerCase().equals("--auto-metric-pattern")) { + config.overrideConfig("tsd.core.auto_create_metrics_patterns", + entry.getValue()); + } else if (entry.getKey().toLowerCase().equals("--auto-tagk-pattern")) { + config.overrideConfig("tsd.core.auto_create_tagk_patterns", + entry.getValue()); + } else if (entry.getKey().toLowerCase().equals("--auto-tagv-pattern")) { + config.overrideConfig("tsd.core.auto_create_tagv_patterns", + entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--table")) { config.overrideConfig("tsd.storage.hbase.data_table", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--uidtable")) { diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index e94c988648..0acc58b836 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import javax.xml.bind.DatatypeConverter; @@ -53,6 +54,13 @@ */ @SuppressWarnings("deprecation") // Dunno why even with this, compiler warns. public final class UniqueId implements UniqueIdInterface { + /** Whether or not to check new UID against configured whitelists **/ + private Boolean useWhitelist = false; + /** Whitelists for various uid types **/ + private String auto_metric_patterns = ".*"; + private String auto_tagk_patterns = ".*"; + private String auto_tagv_patterns = ".*"; + private static final Logger LOG = LoggerFactory.getLogger(UniqueId.class); /** Enumerator for different types of UIDS @since 2.0 */ @@ -188,6 +196,14 @@ public short width() { /** @param tsdb Whether or not to track new UIDMeta objects */ public void setTSDB(final TSDB tsdb) { this.tsdb = tsdb; + try { + this.useWhitelist = tsdb.getConfig().auto_whitelist(); + this.auto_metric_patterns = tsdb.getConfig().auto_metric_patterns(); + this.auto_tagk_patterns = tsdb.getConfig().auto_tagk_patterns(); + this.auto_tagv_patterns = tsdb.getConfig().auto_tagv_patterns(); + } catch (Exception e) { + + } } /** The largest possible ID given the number of bytes the IDs are @@ -631,6 +647,10 @@ public byte[] getOrCreateId(final String name) throws HBaseException { try { return getIdAsync(name).joinUninterruptibly(); } catch (NoSuchUniqueName e) { + if (this.useWhitelist && !checkNameIsValid(name)) { + LOG.info("UID cannot be assigned, name is not acceptable because it fails to match the whitelist: " + name); + throw new RuntimeException("UID cannot be assigned, name is not acceptable because it fails to match the whitelist: " + name); + } Deferred assignment = null; boolean pending = false; synchronized (pending_assignments) { @@ -678,6 +698,47 @@ public byte[] getOrCreateId(final String name) throws HBaseException { } } + /** + * Checks to see if the provided string matches the acceptable + * patterns from the configuration. + *

    + * + * @param name The name to compare to the acceptable name regexes + * @return + */ + public Boolean checkNameIsValid(final String name) throws RuntimeException { + final List rxs = new ArrayList(); + try { + String uid_patterns; + switch (type) { + case METRIC: uid_patterns = this.auto_metric_patterns; + break; + case TAGK: uid_patterns = this.auto_tagk_patterns; + break; + case TAGV: uid_patterns = this.auto_tagv_patterns; + break; + default: + throw new RuntimeException("Should never be here"); + } + String[] patterns = uid_patterns.split(","); + + for (String pattern : patterns) { + rxs.add(Pattern.compile(pattern)); + } + + for (Pattern rx : rxs) { + if (rx.matcher(name).matches()) { + LOG.debug("Accepted name for UID: " + name + " based on '" + rx.toString() + "'"); + return true; + } + } + LOG.debug("Rejected name for UID: " + name); + return false; + } catch (Exception e) { + throw new RuntimeException("Failed to check name (" + name + ") against patterns.", e); + } + } + /** * Finds the ID associated with a given name or creates it. *

    diff --git a/src/utils/Config.java b/src/utils/Config.java index 9f68736cb5..bced392630 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -69,7 +69,20 @@ public class Config { /** tsd.core.auto_create_tagv */ private boolean auto_tagv = true; - + + /** tsd.core.auto_create_whitelist */ + private boolean auto_whitelist = false; + + /** tsd.core.auto_create_metrics_patterns */ + private String auto_metric_patterns = ".*"; + + /** tsd.core.auto_create_tagk_patterns */ + private String auto_tagk_patterns = ".*"; + + /** tsd.core.auto_create_tagv_patterns */ + private String auto_tagv_patterns = ".*"; + + /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; @@ -179,7 +192,25 @@ public boolean auto_tagk() { public boolean auto_tagv() { return auto_tagv; } - + + /** @return the auto_whitelist value */ + public boolean auto_whitelist() { return auto_whitelist; } + + /** @return the auto_metric value */ + public String auto_metric_patterns() { + return auto_metric_patterns; + } + + /** @return the auto_tagk value */ + public String auto_tagk_patterns() { + return auto_tagk_patterns; + } + + /** @return the auto_tagv value */ + public String auto_tagv_patterns() { + return auto_tagv_patterns; + } + /** @param auto_metric whether or not to auto create metrics */ public void setAutoMetric(boolean auto_metric) { this.auto_metric = auto_metric; @@ -487,6 +518,10 @@ protected void setDefaults() { default_map.put("tsd.core.auto_create_metrics", "false"); default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); + default_map.put("tsd.core.auto_create_whitelist", "false"); + default_map.put("tsd.core.auto_create_metrics_patterns", ".*"); + default_map.put("tsd.core.auto_create_tagk_patterns", ".*"); + default_map.put("tsd.core.auto_create_tagv_patterns", ".*"); default_map.put("tsd.core.connections.limit", "0"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); @@ -634,6 +669,10 @@ protected void loadStaticVariables() { auto_metric = this.getBoolean("tsd.core.auto_create_metrics"); auto_tagk = this.getBoolean("tsd.core.auto_create_tagks"); auto_tagv = this.getBoolean("tsd.core.auto_create_tagvs"); + auto_whitelist = this.getBoolean("tsd.core.auto_create_whitelist"); + auto_metric_patterns = this.getString("tsdb.core.auto_create_metrics_patterns"); + auto_tagk_patterns = this.getString("tsdb.core.auto_create_tagk_patterns"); + auto_tagv_patterns = this.getString("tsdb.core.auto_create_tagv_patterns"); enable_compactions = this.getBoolean("tsd.storage.enable_compaction"); enable_appends = this.getBoolean("tsd.storage.enable_appends"); repair_appends = this.getBoolean("tsd.storage.repair_appends"); diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index a35aeeae3c..abc785f076 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -36,17 +36,12 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; - import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.argThat; @@ -271,6 +266,10 @@ public void getOrCreateIdAssignIdWithSuccess() { final byte[] id = { 0, 0, 5 }; final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); + when(config.auto_whitelist()).thenReturn(false); + when(config.auto_metric_patterns()).thenReturn(".*"); + when(config.auto_tagk_patterns()).thenReturn(".*"); + when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); @@ -297,7 +296,130 @@ public void getOrCreateIdAssignIdWithSuccess() { // Reverse + forward mappings. verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); } - + + @Test // Test the creation of an ID with no problem. + public void getOrCreateIdAssignWhitelistedIdWithSuccess() { + uid = new UniqueId(client, table, METRIC, 3); + final byte[] id = { 0, 0, 5 }; + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + when(config.auto_whitelist()).thenReturn(true); + when(config.auto_metric_patterns()).thenReturn(".*"); + when(config.auto_tagk_patterns()).thenReturn(".*"); + when(config.auto_tagv_patterns()).thenReturn(".*"); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + // Watch this! ______,^ I'm writing C++ in Java! + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + assertArrayEquals(id, uid.getOrCreateId("foo")); + // Should be a cache hit since we created that entry. + assertArrayEquals(id, uid.getOrCreateId("foo")); + // Should be a cache hit too for the same reason. + assertEquals("foo", uid.getName(id)); + + verify(client).get(anyGet()); // Initial Get. + verify(client).atomicIncrement(incrementForRow(MAXID)); + // Reverse + forward mappings. + verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); + } + + @Test(expected=RuntimeException.class) + public void getOrCreateIdAssignWhitelistedIdWithFailedWhitelist() { + uid = new UniqueId(client, table, METRIC, 3); + final byte[] id = { 0, 0, 5 }; + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + when(config.auto_whitelist()).thenReturn(true); + when(config.auto_metric_patterns()).thenReturn("^nomatch.*$"); + when(config.auto_tagk_patterns()).thenReturn("^sys\\.cpu\\.*$"); + when(config.auto_tagv_patterns()).thenReturn("^sys\\.cpu\\.*$"); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + // Watch this! ______,^ I'm writing C++ in Java! + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + assertArrayEquals(id, uid.getOrCreateId("foo")); + // Should be a cache hit since we created that entry. + assertArrayEquals(id, uid.getOrCreateId("foo")); + // Should be a cache hit too for the same reason. + assertEquals("foo", uid.getName(id)); + + verify(client).get(anyGet()); // Initial Get. + verify(client).atomicIncrement(incrementForRow(MAXID)); + // Reverse + forward mappings. + verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); + } + + @Test // Test the creation of an ID with no problem. + public void checkMetricAgainstWhitelist() { + setupWhitelists(METRIC); + assertTrue(uid.checkNameIsValid("sys.cpu.user")); + } + + @Test // Test the creation of an ID with no problem. + public void checkTagKAgainstWhitelist() { + setupWhitelists(TAGK); + assertTrue(uid.checkNameIsValid("sys.cpu.user")); + } + + @Test // Test the creation of an ID with no problem. + public void checkTagVAgainstWhitelist() { + setupWhitelists(TAGV); + assertTrue(uid.checkNameIsValid("sys.cpu.user")); + } + + @Test + public void checkMetricAgainstWhitelistFails() { + setupWhitelists(METRIC); + assertFalse(uid.checkNameIsValid("foo.badmetric")); + } + + @Test + public void checkTagKAgainstWhitelistFails() { + setupWhitelists(TAGK); + assertFalse(uid.checkNameIsValid("foo.badmetric")); + } + + @Test + public void checkTagVAgainstWhitelistFails() { + setupWhitelists(TAGV); + assertFalse(uid.checkNameIsValid("foo.badmetric")); + } + + private void setupWhitelists(String type) { + uid = new UniqueId(client, table, type, 3); + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + when(config.auto_whitelist()).thenReturn(true); + when(config.auto_metric_patterns()).thenReturn("sys.*"); + when(config.auto_tagk_patterns()).thenReturn("sys.*"); + when(config.auto_tagv_patterns()).thenReturn("sys.*"); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + } + @Test // Test the creation of an ID when unable to increment MAXID public void getOrCreateIdUnableToIncrementMaxId() throws Exception { PowerMockito.mockStatic(Thread.class); @@ -428,6 +550,10 @@ public void getOrCreateIdWithICVFailure() { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); + when(config.auto_whitelist()).thenReturn(false); + when(config.auto_metric_patterns()).thenReturn(".*"); + when(config.auto_tagk_patterns()).thenReturn(".*"); + when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); @@ -460,6 +586,10 @@ public void getOrCreateIdPutsReverseMappingFirst() { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); + when(config.auto_whitelist()).thenReturn(false); + when(config.auto_metric_patterns()).thenReturn(".*"); + when(config.auto_tagk_patterns()).thenReturn(".*"); + when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); @@ -1180,6 +1310,12 @@ public void deleteNoSuchUniqueName() throws Exception { // ----------------- // private void setupStorage() throws Exception { + final Config config = mock(Config.class); + when(config.auto_whitelist()).thenReturn(false); + when(config.auto_metric_patterns()).thenReturn(".*"); + when(config.auto_tagk_patterns()).thenReturn(".*"); + when(config.auto_tagv_patterns()).thenReturn(".*"); + when(tsdb.getConfig()).thenReturn(config); when(tsdb.getClient()).thenReturn(client); storage = new MockBase(tsdb, client, true, true, true, true); From 4615b9b566e7dad355c870e90198da1c6b288235 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 25 Mar 2016 23:59:10 -0500 Subject: [PATCH 462/826] removed extra try block --- src/uid/UniqueId.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 0acc58b836..8a1838c3bc 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -196,14 +196,10 @@ public short width() { /** @param tsdb Whether or not to track new UIDMeta objects */ public void setTSDB(final TSDB tsdb) { this.tsdb = tsdb; - try { - this.useWhitelist = tsdb.getConfig().auto_whitelist(); - this.auto_metric_patterns = tsdb.getConfig().auto_metric_patterns(); - this.auto_tagk_patterns = tsdb.getConfig().auto_tagk_patterns(); - this.auto_tagv_patterns = tsdb.getConfig().auto_tagv_patterns(); - } catch (Exception e) { - - } + this.useWhitelist = tsdb.getConfig().auto_whitelist(); + this.auto_metric_patterns = tsdb.getConfig().auto_metric_patterns(); + this.auto_tagk_patterns = tsdb.getConfig().auto_tagk_patterns(); + this.auto_tagv_patterns = tsdb.getConfig().auto_tagv_patterns(); } /** The largest possible ID given the number of bytes the IDs are From 176d6caef4fae7e89f53f222e4be31bb3a7fca76 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 25 Mar 2016 21:29:18 -0500 Subject: [PATCH 463/826] Added support for using ms in a timestamp to explictely identify it as a ms timestamp Fixes #696 --- src/utils/DateTime.java | 6 ++++++ test/utils/TestDateTime.java | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 2d690e7e17..33f3ab3db5 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -75,9 +75,15 @@ public static final long parseDateTimeString(final String datetime, final String tz) { if (datetime == null || datetime.isEmpty()) return -1; + + if (datetime.matches("^[0-9]+ms$")) { + return Tags.parseLong(datetime.replaceFirst("^([0-9]+)(ms)$", "$1")); + } + if (datetime.toLowerCase().equals("now")) { return System.currentTimeMillis(); } + if (datetime.toLowerCase().endsWith("-ago")) { long interval = DateTime.parseDuration( datetime.substring(0, datetime.length() - 4)); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 3f46f6b3c8..b21bb25866 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -185,6 +185,18 @@ public void parseDateTimeStringUnixMS() { long t = DateTime.parseDateTimeString("1355961603418", null); assertEquals(1355961603418L, t); } + + @Test + public void parseDateTimeStringShortExplicitMS() { + long t = DateTime.parseDateTimeString("123123ms", null); + assertEquals(123123L, t); + } + + @Test + public void parseDateTimeStringExplicitMS() { + long t = DateTime.parseDateTimeString("1234567890123ms", null); + assertEquals(1234567890123L, t); + } @Test public void parseDateTimeStringUnixMSDot() { From 107d9e9d04abafef970579530415e54665e9c46b Mon Sep 17 00:00:00 2001 From: nickman Date: Sat, 26 Mar 2016 14:27:39 -0400 Subject: [PATCH 464/826] Impl for #743 Impl for #743 --- Makefile.am | 2 + src/stats/StatsCollector.java | 5 +++ src/tools/CliOptions.java | 4 +- src/tools/TSDMain.java | 2 + src/tools/TSDPort.java | 56 +++++++++++++++++++++++ src/utils/Config.java | 11 +++++ test/tsd/TestStatsWithPort.java | 79 +++++++++++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/tools/TSDPort.java create mode 100644 test/tsd/TestStatsWithPort.java diff --git a/Makefile.am b/Makefile.am index edaaf6e37a..68a7a08b9e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -134,6 +134,7 @@ tsdb_SRC := \ src/tools/Search.java \ src/tools/StartupPlugin.java \ src/tools/TSDMain.java \ + src/tools/TSDPort.java \ src/tools/TextImporter.java \ src/tools/TreeSync.java \ src/tools/UidManager.java \ @@ -343,6 +344,7 @@ test_SRC := \ test/tsd/TestSuggestRpc.java \ test/tsd/TestTreeRpc.java \ test/tsd/TestUniqueIdRpc.java \ + test/tsd/TestStatsWithPort.java \ test/uid/TestNoSuchUniqueId.java \ test/uid/TestRandomUniqueId.java \ test/uid/TestUniqueId.java \ diff --git a/src/stats/StatsCollector.java b/src/stats/StatsCollector.java index 6d002e1568..4d9281f623 100644 --- a/src/stats/StatsCollector.java +++ b/src/stats/StatsCollector.java @@ -15,6 +15,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.opentsdb.tools.TSDPort; + import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; @@ -50,6 +52,9 @@ public abstract class StatsCollector { */ public StatsCollector(final String prefix) { this.prefix = prefix; + if(TSDPort.isStatsWithPort()) { + addExtraTag("port", "" + TSDPort.getTSDPort()); + } } /** diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index ec5eee6a4b..8e9fc9b8ad 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -161,7 +161,9 @@ static void overloadConfig(final ArgP argp, final Config config) { config.overrideConfig("tsd.network.worker_threads", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--max-connections")) { config.overrideConfig("tsd.core.connections.limit", entry.getValue()); - } + } else if (entry.getKey().toLowerCase().equals("--statswport")) { + config.overrideConfig("tsd.core.stats_with_port", "true"); + } } } diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index eb61f4a6ac..4036ae3015 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -101,6 +101,7 @@ public static void main(String[] args) throws IOException { argp.addOption("--flush-interval", "MSEC", "Maximum time for which a new data point can be buffered" + " (default: " + DEFAULT_FLUSH_INTERVAL + ")."); + argp.addOption("--statswport", "Force all stats to include the port"); CliOptions.addAutoMetricFlag(argp); args = CliOptions.parse(argp, args); args = null; // free(). @@ -220,6 +221,7 @@ public static void main(String[] args) throws IOException { if (startup != null) { startup.setReady(tsdb); } + TSDPort.set(config); log.info("Ready to serve on " + addr); } catch (Throwable e) { factory.releaseExternalResources(); diff --git a/src/tools/TSDPort.java b/src/tools/TSDPort.java new file mode 100644 index 0000000000..861a1c0d2e --- /dev/null +++ b/src/tools/TSDPort.java @@ -0,0 +1,56 @@ +// 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.tools; + +import net.opentsdb.utils.Config; + +/** + * Static reference to the TSD's listening port and stats port configuration + */ + +public class TSDPort { + /** The RPC listening port */ + private static int rpcPort = -1; + /** Indicates if RPC stats include the listening port. Set by config tsd.core.stats_with_port + or CLI option --statswport. */ + private static boolean statsWithPort = false; + + /** + * Sets the rpc port and stats config on TSD startup + * @param config The final config + */ + static void set(Config config) { + rpcPort = config.getInt("tsd.network.port"); + statsWithPort = config.getBoolean("tsd.core.stats_with_port"); + } + + /** + * Returns the TSD's listening port + * @return the port + */ + public static int getTSDPort() { + return rpcPort; + } + + /** + * Indicates if stats should be reported with the port as a tag + * @return true if stats should be reported with the port as a tag, false otherwise + */ + public static boolean isStatsWithPort() { + return statsWithPort; + } + + + private TSDPort() {} + +} diff --git a/src/utils/Config.java b/src/utils/Config.java index bced392630..5a61be5359 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -107,6 +107,10 @@ public class Config { /** tsd.http.request.enable_chunked */ private boolean enable_chunked_requests = false; + /** tsd.core.stats_with_port */ + private boolean stats_with_port = false; + + /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; @@ -264,6 +268,11 @@ public boolean enable_chunked_requests() { return enable_chunked_requests; } + /** @return whether or not rpc stats should be broken out by port */ + public boolean rpc_stats_withport() { + return stats_with_port; + } + /** @return max incoming chunk size in bytes */ public int max_chunked_requests() { return max_chunked_requests; @@ -563,6 +572,7 @@ protected void setDefaults() { default_map.put("tsd.storage.compaction.min_flush_threshold", "100"); default_map.put("tsd.storage.compaction.max_concurrent_flushes", "10000"); default_map.put("tsd.storage.compaction.flush_speed", "2"); + default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.query.allow_delete", "false"); default_map.put("tsd.http.request.enable_chunked", "false"); @@ -689,6 +699,7 @@ protected void loadStaticVariables() { enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); + stats_with_port = this.getBoolean("tsd.core.stats_with_port"); } diff --git a/test/tsd/TestStatsWithPort.java b/test/tsd/TestStatsWithPort.java new file mode 100644 index 0000000000..85ca4001f1 --- /dev/null +++ b/test/tsd/TestStatsWithPort.java @@ -0,0 +1,79 @@ +// 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 java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.Assert; +import org.junit.Test; + +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.tools.TSDPort; + +public class TestStatsWithPort { + + static final Pattern PORT_MATCH = Pattern.compile(" port="); + + @Test + public void testNoPort() { + setPortConfig(4242, false); + } + + @Test + public void testDefaultPort() { + setPortConfig(4242, true); + } + + + protected void doTest() { + final List lines = new ArrayList(); + StatsCollector sc = new StatsCollector("tsd") { + @Override + public final void emit(final String line) { + lines.add(line); + } + }; + sc.record("foo", -1); + + } + + protected void validateStats(final List lines) { + Pattern portMatch = Pattern.compile(" port=" + TSDPort.getTSDPort()); + for(String s: lines) { + if(!TSDPort.isStatsWithPort()) { + Assert.assertFalse("Stat had a port", PORT_MATCH.matcher(s).find()); + } else { + Assert.assertTrue("Stat did not have port", portMatch.matcher(s).find()); + } + } + } + + + public void setPortConfig(final Integer port, final Boolean statsWithPort) { + try { + Field portField = TSDPort.class.getDeclaredField("rpcPort"); + portField.setAccessible(true); + portField.set(null, port); + Field statsWPortField = TSDPort.class.getDeclaredField("statsWithPort"); + statsWPortField.setAccessible(true); + statsWPortField.set(null, statsWithPort); + } catch (Exception ex) { + throw new RuntimeException("Failed to set TCPPort fields", ex); + } + } + +} From ac1260e81e2214d9e18b7584c6be901de968b6b4 Mon Sep 17 00:00:00 2001 From: Can ZHANG Date: Wed, 3 Feb 2016 10:53:02 +0800 Subject: [PATCH 465/826] Remove extra getFromStorage --- src/meta/TSMeta.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 7c34cbfc2a..8e400b4536 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -338,15 +338,14 @@ public Deferred call(ArrayList validated) } /** - * Attempts to store a new, blank timeseries meta object via a CompareAndSet + * Attempts to store a new, blank timeseries meta object * Note: This should not be called by user accessible methods as it will * overwrite any data already in the column. * Note: This call does not guarantee that the UIDs exist before * storing as it should only be called *after* a data point has been recorded * or during a meta sync. * @param tsdb The TSDB to use for storage access - * @return True if the CAS completed successfully (and no TSMeta existed - * previously), false if something was already stored in the TSMeta column. + * @return True if the TSMeta created(or updated) successfully * @throws HBaseException if there was an issue fetching * @throws IllegalArgumentException if parsing failed * @throws JSONException if the object could not be serialized @@ -587,10 +586,10 @@ public Deferred call(Boolean success) throws Exception { } LOG.info("Successfullly created new TSUID entry for: " + meta); - final Deferred meta = getFromStorage(tsdb, tsuid) - .addCallbackDeferring( - new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))); - return meta.addCallbackDeferring(new FetchNewCB()); + return Deferred.fromResult(meta) + .addCallbackDeferring( + new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + .addCallbackDeferring(new FetchNewCB()); } } From 2892cf8fcb723502101a9d09ee57b031682d3b34 Mon Sep 17 00:00:00 2001 From: Can ZHANG Date: Thu, 4 Feb 2016 15:48:01 +0800 Subject: [PATCH 466/826] Create TSMeta by get then put If enable_tsuid_incrementing is false and config.enable_realtime_ts is true, TSMeta will be created through get, check and put, instead of atomicIncrement and put. Conflicts: src/core/TSDB.java --- src/core/TSDB.java | 4 ++- src/meta/TSMeta.java | 62 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 86270d891c..2726602a38 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -915,8 +915,10 @@ private Deferred addPointInternal(final String metric, 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()) { + } else if (config.enable_tsuid_incrementing() && config.enable_realtime_ts()) { TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } else if (!config.enable_tsuid_incrementing() && config.enable_realtime_ts()) { + TSMeta.storeIfNecessary(TSDB.this, tsuid); } } diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 8e400b4536..47f70ee81d 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -611,6 +611,68 @@ public Deferred call(Boolean success) throws Exception { return tsdb.getClient().atomicIncrement(inc).addCallbackDeferring( new TSMetaCB()); } + + public static void storeIfNecessary(final TSDB tsdb, final byte[] tsuid) { + final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); + get.family(FAMILY); + get.qualifier(META_QUALIFIER); + + final class CreateNewCB implements Callback, Object> { + + @Override + public Deferred call(Object arg0) throws Exception { + final TSMeta meta = new TSMeta(tsuid, System.currentTimeMillis() / 1000); + + final class FetchNewCB implements Callback, TSMeta> { + + @Override + public Deferred call(TSMeta stored_meta) throws Exception { + + // pass to the search plugin + tsdb.indexTSMeta(stored_meta); + + // pass through the trees + tsdb.processTSMetaThroughTrees(stored_meta); + + return Deferred.fromResult(true); + } + } + + final class StoreNewCB implements Callback, Boolean> { + + @Override + public Deferred call(Boolean success) throws Exception { + if (!success) { + LOG.warn("Unable to save metadata: " + meta); + return Deferred.fromResult(false); + } + + LOG.info("Successfullly created new TSUID entry for: " + meta); + return Deferred.fromResult(meta) + .addCallbackDeferring( + new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + .addCallbackDeferring(new FetchNewCB()); + } + } + + return meta.storeNew(tsdb).addCallbackDeferring(new StoreNewCB()); + } + } + + final class ExistsCB implements Callback, ArrayList> { + + @Override + public Deferred call(ArrayList row) throws Exception { + if (row == null || row.isEmpty() || row.get(0).value() == null) { + return Deferred.fromResult(new Object()) + .addCallbackDeferring(new CreateNewCB()); + } + return Deferred.fromResult(true); + } + } + + tsdb.getClient().get(get).addCallbackDeferring(new ExistsCB()); + } /** * Attempts to fetch the timeseries meta data from storage. From 5b5624a3e285253cb9bf7a6d092b7ae9057b93b8 Mon Sep 17 00:00:00 2001 From: Camden Narzt Date: Sat, 27 Feb 2016 09:48:23 -0700 Subject: [PATCH 467/826] Fix #707 mkdir_p is called from in a subdir of build so needs an extra `../` and there is no rule to create the `$(classes)` and everything builds without specifying them as dependencies of the jar. I've only tested these changes on the Makefile.in Conflicts: Makefile.am --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 68a7a08b9e..6f274a2373 100644 --- a/Makefile.am +++ b/Makefile.am @@ -565,7 +565,7 @@ install-data-local: staticroot install-data-lib install-data-tools \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ if test -d "$$dstdir"; then :; else \ - echo " $(mkdir_p) '$$dstdir'"; $(mkdir_p) "$$dstdir"; fi; \ + echo " $(mkdir_p) '$$dstdir'"; ../$(mkdir_p) "$$dstdir"; fi; \ echo " $(INSTALL_DATA) '$$d$$p' '$(DESTDIR)$(staticdir)/$$p'"; \ $(INSTALL_DATA) "$$d$$p" "$(DESTDIR)$(staticdir)/$$p"; \ done @@ -718,7 +718,7 @@ manifest: .javac-stamp .git/HEAD echo "Implementation-Version: $(git_version)"; \ echo "Implementation-Vendor: $(spec_vendor)"; } >"$@" -$(jar): manifest .javac-stamp $(classes) +$(jar): manifest .javac-stamp $(JAR) cfm `basename $(jar)` manifest $(classes_with_nested_classes) $(get_expr_classes) \ || { rv=$$? && rm -f `basename $(jar)` && exit $$rv; } # ^^^^^^^^^^^^^^^^^^^^^^^ From b778e2b3cc9aee81a4bf0ab97a55f09c9fb79b7b Mon Sep 17 00:00:00 2001 From: Kieren Hynd Date: Tue, 29 Mar 2016 19:12:14 +0100 Subject: [PATCH 468/826] Correct some config option underscores in example opentsdb.conf --- src/opentsdb.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/opentsdb.conf b/src/opentsdb.conf index 7de2cb9e51..47c64c10a4 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -8,15 +8,15 @@ tsd.network.port = # Enables Nagel's algorithm to reduce the number of packets sent over the # network, default is True -#tsd.network.tcpnodelay = true +#tsd.network.tcp_no_delay = true # Determines whether or not to send keepalive packets to peers, default # is True -#tsd.network.keepalive = true +#tsd.network.keep_alive = true # Determines if the same socket should be used for new connections, default # is True -#tsd.network.reuseaddress = true +#tsd.network.reuse_address = true # Number of worker threads dedicated to Netty, defaults to # of CPUs * 2 #tsd.network.worker_threads = 8 From ace8ba96e6f0e7e9aae7b00ebe7e866b7d340ca9 Mon Sep 17 00:00:00 2001 From: dominosly Date: Tue, 29 Mar 2016 23:39:38 +0000 Subject: [PATCH 469/826] Fix in regex for parsing the java version to determine ALPN version in response to question on mailing list regarding big table builds failing on Debian (jesssie). Java version on jessie has suffix '-internal' added to version line found in 'java -version'. Regex in make file was testing for EOL ($). This just isn't necessary and will break on any version with a suffix. Works fine just matching for major, minor and sub version. Tested on Debian jessie and Ubuntu 14.04 LTS --- third_party/alpn-boot/include.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk index 13cd19dfa9..83ff6792bd 100644 --- a/third_party/alpn-boot/include.mk +++ b/third_party/alpn-boot/include.mk @@ -22,7 +22,7 @@ ALPN_BOOT_VERSION = $(shell version= ;\ echo "Failed to parse Java version";\ exit 1;\ fi; \ - if [[ $$version =~ ^([0-9]+\.[0-9]+)\.([0-9])[_Uu]([0-9]+)$$ ]]; then \ + if [[ $$version =~ ^([0-9]+\.[0-9]+)\.([0-9])[_Uu]([0-9]+) ]]; then \ major=$${BASH_REMATCH[1]};\ minor=$${BASH_REMATCH[2]}; \ sub=$${BASH_REMATCH[3]}; \ From c33d8397c8de41544c1e76d35d5230dd21598d9b Mon Sep 17 00:00:00 2001 From: Johannes Meixner Date: Tue, 5 Apr 2016 12:22:03 +0300 Subject: [PATCH 470/826] Use /usr/bin/env in shebang line - /usr/bin/python does not exist on FreeBSD - /usr/bin/env python will do the right thing on both Linux and BSD --- tools/check_tsd | 2 +- tools/opentsdb_restart.py | 2 +- tools/tsddrain.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index 101b5d5cec..237ec0e534 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # Script which queries TSDB with a given metric and alerts based on # supplied threshold. Compatible with Nagios output format, so can be diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index 31425750a8..9c63679c34 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python """Restart opentsdb. Called using -XX:OnOutOfMemoryError= Because it's calling the 'service opentsdb' command, should be run as root. diff --git a/tools/tsddrain.py b/tools/tsddrain.py index 9a0fcf9b59..8bd595447c 100755 --- a/tools/tsddrain.py +++ b/tools/tsddrain.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # This little script can be used to replace TSDs while performing prolonged # HBase or HDFS maintenances. It runs a simple, low-end TCP server to accept From 6a20a1db204debefa070e466be9eae48d4484366 Mon Sep 17 00:00:00 2001 From: Johannes Meixner Date: Tue, 5 Apr 2016 12:22:03 +0300 Subject: [PATCH 471/826] Use /usr/bin/env in shebang line - /usr/bin/python does not exist on FreeBSD - /usr/bin/env python will do the right thing on both Linux and BSD --- tools/check_tsd | 2 +- tools/opentsdb_restart.py | 2 +- tools/tsddrain.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index 101b5d5cec..237ec0e534 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # Script which queries TSDB with a given metric and alerts based on # supplied threshold. Compatible with Nagios output format, so can be diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index 31425750a8..9c63679c34 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python """Restart opentsdb. Called using -XX:OnOutOfMemoryError= Because it's calling the 'service opentsdb' command, should be run as root. diff --git a/tools/tsddrain.py b/tools/tsddrain.py index 9a0fcf9b59..8bd595447c 100755 --- a/tools/tsddrain.py +++ b/tools/tsddrain.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # This little script can be used to replace TSDs while performing prolonged # HBase or HDFS maintenances. It runs a simple, low-end TCP server to accept From 8a9ebf7aecf013f8fc4fa3604f194d9639554dc6 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Thu, 7 Apr 2016 13:12:37 -0700 Subject: [PATCH 472/826] Revert "Server should just exit whenever there is some unrecognized option" --- src/tools/CliOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index 49dc34df34..aeccb1bb36 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -77,7 +77,7 @@ static String[] parse(final ArgP argp, String[] args) { args = argp.parse(args); } catch (IllegalArgumentException e) { System.err.println("Invalid usage. " + e.getMessage()); - System.exit(2); + return null; } honorVerboseFlag(argp); return args; From e2d2078d822bd3c3c92ffcd06647840509aa796f Mon Sep 17 00:00:00 2001 From: Pradeep Chhetri Date: Thu, 21 May 2015 01:54:36 +0530 Subject: [PATCH 473/826] Server should just exit whenever there is some unrecognized option --- src/tools/CliOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index 8e9fc9b8ad..2e55a7470d 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -77,7 +77,7 @@ static String[] parse(final ArgP argp, String[] args) { args = argp.parse(args); } catch (IllegalArgumentException e) { System.err.println("Invalid usage. " + e.getMessage()); - return null; + System.exit(2); } honorVerboseFlag(argp); return args; From de803d50a27ba45fc24e822db15a2ca0f29bf170 Mon Sep 17 00:00:00 2001 From: Vitaliy Fuks Date: Wed, 13 Apr 2016 14:59:45 -0400 Subject: [PATCH 474/826] Typo: tsd.network.keepalive is actually called tsd.network.keep_alive according to code and documentation. --- build-aux/deb/opentsdb.conf | 2 +- build-aux/rpm/opentsdb.conf | 2 +- src/opentsdb.conf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index f58d5cf14c..3d7db5bfa3 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -12,7 +12,7 @@ tsd.network.port = 4242 # Determines whether or not to send keepalive packets to peers, default # is True -#tsd.network.keepalive = true +#tsd.network.keep_alive = true # Determines if the same socket should be used for new connections, default # is True diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index a515418a7e..caf4599acc 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -12,7 +12,7 @@ tsd.network.port = 4242 # Determines whether or not to send keepalive packets to peers, default # is True -#tsd.network.keepalive = true +#tsd.network.keep_alive = true # Determines if the same socket should be used for new connections, default # is True diff --git a/src/opentsdb.conf b/src/opentsdb.conf index bed259d587..44be136682 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -12,7 +12,7 @@ tsd.network.port = # Determines whether or not to send keepalive packets to peers, default # is True -#tsd.network.keepalive = true +#tsd.network.keep_alive = true # Determines if the same socket should be used for new connections, default # is True From c9fb82dfa6bbb508cd9e2b832ac72cff09163219 Mon Sep 17 00:00:00 2001 From: Vitaliy Fuks Date: Wed, 13 Apr 2016 14:59:45 -0400 Subject: [PATCH 475/826] Typo: tsd.network.keepalive is actually called tsd.network.keep_alive according to code and documentation. --- build-aux/deb/opentsdb.conf | 2 +- build-aux/rpm/opentsdb.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index f58d5cf14c..3d7db5bfa3 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -12,7 +12,7 @@ tsd.network.port = 4242 # Determines whether or not to send keepalive packets to peers, default # is True -#tsd.network.keepalive = true +#tsd.network.keep_alive = true # Determines if the same socket should be used for new connections, default # is True diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index a515418a7e..caf4599acc 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -12,7 +12,7 @@ tsd.network.port = 4242 # Determines whether or not to send keepalive packets to peers, default # is True -#tsd.network.keepalive = true +#tsd.network.keep_alive = true # Determines if the same socket should be used for new connections, default # is True From 0fd1cf53c245873f180151e3328803058d034dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Str=C3=B6m?= Date: Sat, 16 Apr 2016 12:22:41 +0200 Subject: [PATCH 476/826] check_tsd: unbreak #760 Removes old dead code trying to use bad_percent property check_tsd: add support for new aggregators check_tsd: add support for -N for testing at a specific timestamp check_tsd: let verbose datapoints log indicate if value is ignored Also break early, relevant when running with -N mode --- tools/check_tsd | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index 237ec0e534..ecc42b0551 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -29,6 +29,13 @@ import sys import time from optparse import OptionParser +AGGREGATORS = ('avg', 'count', 'dev', + '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') + def main(argv): """Pulls data out of the TSDB and do very simple alerting from Nagios.""" @@ -71,6 +78,9 @@ def main(argv): parser.add_option('-P', '--percent-over', dest='percent_over', default=0, metavar='PERCENT', type='float', help='Only alarm if PERCENT of the data' ' points violate the threshold.') + parser.add_option('-N', '--now', type='int', default=None, + metavar='UTC', + help='Set unix timestamp for "now", for testing') parser.add_option('-S', '--ssl', default=False, action='store_true', help='Make queries to OpenTSDB via SSL (https)') (options, args) = parser.parse_args(args=argv[1:]) @@ -78,9 +88,9 @@ def main(argv): # argument validation if options.comparator not in ('gt', 'ge', 'lt', 'le', 'eq', 'ne'): parser.error("Comparator '%s' not valid." % options.comparator) - elif options.downsample not in ('none', 'avg', 'min', 'sum', 'max'): + elif options.downsample not in ('none',)+AGGREGATORS: parser.error("Downsample '%s' not valid." % options.downsample) - elif options.aggregator not in ('avg', 'min', 'sum', 'max'): + elif options.aggregator not in AGGREGATORS: parser.error("Aggregator '%s' not valid." % options.aggregator) elif not options.metric: parser.error('You must specify a metric (option -m).') @@ -118,8 +128,16 @@ def main(argv): rate = 'rate:' else: rate = '' - url = ('/q?start=%ss-ago&m=%s:%s%s%s%s&ascii&nagios' - % (options.duration, options.aggregator, downsampling, rate, + + if options.now: + now = options.now + start = '%s' % (now - int(options.duration)) + else: + now = int(time.time()) + start = '%ss-ago' % options.duration + + url = ('/q?start=%s&m=%s:%s%s%s%s&ascii&nagios' + % (start, options.aggregator, downsampling, rate, options.metric, tags)) tsd = '%s:%d' % (options.host, options.port) if options.ssl: # Pick the class to instantiate first. @@ -139,7 +157,7 @@ def main(argv): peer = conn.sock.getpeername() print ('Connected to %s:%d' % (peer[0], peer[1])) conn.set_debuglevel(1) - now = int(time.time()) + try: conn.request('GET', url) res = conn.getresponse() @@ -159,8 +177,6 @@ def main(argv): return 2 # but we won! - if options.verbose: - print (datapoints) datapoints = datapoints.splitlines() def no_data_point(): @@ -182,12 +198,20 @@ def main(argv): nbad = 0 # How many bad values have we seen? ncrit = 0 # How many critical values have we seen? nwarn = 0 # How many warning values have we seen? - for datapoint in datapoints: - datapoint = datapoint.split() + for datapoint_str in datapoints: + datapoint = datapoint_str.split() ts = int(datapoint[1]) delta = now - ts if delta > options.duration or delta <= options.ignore_recent: + if options.verbose: + print "%s (ignored, delta %ds)" % (datapoint_str, delta) + if delta < 0: + break # Skip the rest, we got what we came for. continue # Ignore data points outside of our range. + + if options.verbose: + print datapoint_str + npoints += 1 val = datapoint[2] if '.' in val: @@ -228,13 +252,6 @@ def main(argv): bad_pct = nbad * 100.0 / npoints - if options.bad_percent is not None and rv > 0 \ - and bad_pct < options.bad_percent: - if options.verbose: - print 'ignoring alarm, less than %.1f%% bad values (found %.1f%%)' % \ - (options.bad_percent, bad_pct) - rv = 0 - # in nrpe, pipe character is something special, but it's used in tag # searches. Translate it to something else for the purposes of output. ttags = tags.replace("|",":") From 46bddb99c710e6c2a56a15a254dc6ad6b84ccb50 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 16:54:58 -0700 Subject: [PATCH 477/826] Add comments and UTs to 931242b5db4f8ab79c0346d876c53cc96349ba3d as well as clean up the deferreds a bit. Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 24 +++++++++++--------- test/meta/TestTSMeta.java | 46 ++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 5fdaa21b0c..65e0030dd0 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -587,9 +587,7 @@ public Deferred call(Boolean success) throws Exception { } LOG.info("Successfullly created new TSUID entry for: " + meta); - return Deferred.fromResult(meta) - .addCallbackDeferring( - new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + return new LoadUIDs(tsdb, UniqueId.uidToString(tsuid)).call(meta) .addCallbackDeferring(new FetchNewCB()); } @@ -613,7 +611,16 @@ public Deferred call(Boolean success) throws Exception { new TSMetaCB()); } - public static void storeIfNecessary(final TSDB tsdb, final byte[] tsuid) { + /** + * Attempts to fetch the meta column and if null, attempts to write a new + * column using {@link #storeNew}. + * @param tsdb The TSDB instance to use for access. + * @param tsuid The TSUID of the time series. + * @return A deferred with a true if the meta exists or was created, false + * if the meta did not exist and writing failed. + */ + public static Deferred storeIfNecessary(final TSDB tsdb, + final byte[] tsuid) { final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); get.family(FAMILY); get.qualifier(META_QUALIFIER); @@ -649,9 +656,7 @@ public Deferred call(Boolean success) throws Exception { } LOG.info("Successfullly created new TSUID entry for: " + meta); - return Deferred.fromResult(meta) - .addCallbackDeferring( - new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + return new LoadUIDs(tsdb, UniqueId.uidToString(tsuid)).call(meta) .addCallbackDeferring(new FetchNewCB()); } } @@ -665,14 +670,13 @@ final class ExistsCB implements Callback, ArrayList> @Override public Deferred call(ArrayList row) throws Exception { if (row == null || row.isEmpty() || row.get(0).value() == null) { - return Deferred.fromResult(new Object()) - .addCallbackDeferring(new CreateNewCB()); + return new CreateNewCB().call(null); } return Deferred.fromResult(true); } } - tsdb.getClient().get(get).addCallbackDeferring(new ExistsCB()); + return tsdb.getClient().get(get).addCallbackDeferring(new ExistsCB()); } /** diff --git a/test/meta/TestTSMeta.java b/test/meta/TestTSMeta.java index 49c6e2f390..3984495aee 100644 --- a/test/meta/TestTSMeta.java +++ b/test/meta/TestTSMeta.java @@ -62,6 +62,7 @@ public final class TestTSMeta { private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); private final static byte[] META_TABLE = "tsdb-meta".getBytes(MockBase.ASCII()); private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); + private final static byte[] TSUID = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }; private TSDB tsdb; private Config config; private HBaseClient client = mock(HBaseClient.class); @@ -120,7 +121,7 @@ public void before() throws Exception { "1328140801,\"displayName\":\"Web server 1\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(META_TABLE, TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"" + @@ -128,7 +129,7 @@ public void before() throws Exception { "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(META_TABLE, TSUID, TSMeta.FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); @@ -260,7 +261,7 @@ public void deleteNull() throws Exception { @Test public void syncToStorage() throws Exception { - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + meta = new TSMeta(TSUID, 1357300800000L); meta.setDisplayName("New DN"); meta.syncToStorage(tsdb, false).joinUninterruptibly(); assertEquals("New DN", meta.getDisplayName()); @@ -269,7 +270,7 @@ public void syncToStorage() throws Exception { @Test public void syncToStorageOverwrite() throws Exception { - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + meta = new TSMeta(TSUID, 1357300800000L); meta.setDisplayName("New DN"); meta.syncToStorage(tsdb, true).joinUninterruptibly(); assertEquals("New DN", meta.getDisplayName()); @@ -290,14 +291,14 @@ public void syncToStorageNullTSUID() throws Exception { @Test (expected = IllegalArgumentException.class) public void syncToStorageDoesNotExist() throws Exception { - storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + storage.flushRow(META_TABLE, TSUID); + meta = new TSMeta(TSUID, 1357300800000L); meta.syncToStorage(tsdb, false).joinUninterruptibly(); } @Test public void storeNew() throws Exception { - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + meta = new TSMeta(TSUID, 1357300800000L); meta.setDisplayName("New DN"); meta.storeNew(tsdb); assertEquals("New DN", meta.getDisplayName()); @@ -323,7 +324,7 @@ public void metaExistsInStorage() throws Exception { @Test public void metaExistsInStorageNot() throws Exception { - storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, TSUID); assertFalse(TSMeta.metaExistsInStorage(tsdb, "000001000001000001") .joinUninterruptibly()); } @@ -331,14 +332,14 @@ public void metaExistsInStorageNot() throws Exception { @Test public void counterExistsInStorage() throws Exception { assertTrue(TSMeta.counterExistsInStorage(tsdb, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }).joinUninterruptibly()); + TSUID).joinUninterruptibly()); } @Test public void counterExistsInStorageNot() throws Exception { - storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, TSUID); assertFalse(TSMeta.counterExistsInStorage(tsdb, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }).joinUninterruptibly()); + TSUID).joinUninterruptibly()); } @Test @@ -378,12 +379,27 @@ public void COUNTER_QUALIFIER() throws Exception { TSMeta.COUNTER_QUALIFIER()); } + @Test + public void storeIfNecessaryExists() throws Exception { + assertTrue(TSMeta.storeIfNecessary(tsdb, TSUID).join()); + } + + @Test + public void storeIfNecessaryMissing() throws Exception { + storage.flushRow(META_TABLE, TSUID); + assertNull(storage.getColumn(META_TABLE, TSUID, NAME_FAMILY, + TSMeta.META_QUALIFIER())); + assertTrue(TSMeta.storeIfNecessary(tsdb, TSUID).join()); + assertNotNull(storage.getColumn(META_TABLE, TSUID, NAME_FAMILY, + TSMeta.META_QUALIFIER())); + } + @Test public void parseFromColumn() throws Exception { final KeyValue column = mock(KeyValue.class); - when(column.key()).thenReturn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + when(column.key()).thenReturn(TSUID); when(column.value()).thenReturn(storage.getColumn(META_TABLE, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()))); final TSMeta meta = TSMeta.parseFromColumn(tsdb, column, false) @@ -396,9 +412,9 @@ public void parseFromColumn() throws Exception { @Test public void parseFromColumnWithUIDMeta() throws Exception { final KeyValue column = mock(KeyValue.class); - when(column.key()).thenReturn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + when(column.key()).thenReturn(TSUID); when(column.value()).thenReturn(storage.getColumn(META_TABLE, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()))); final TSMeta meta = TSMeta.parseFromColumn(tsdb, column, true) From 955fb3112d96cfab0179fc8a06f383f9c32d3393 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 16:54:58 -0700 Subject: [PATCH 478/826] Add comments and UTs to 931242b5db4f8ab79c0346d876c53cc96349ba3d as well as clean up the deferreds a bit. Signed-off-by: Chris Larsen --- src/meta/TSMeta.java | 24 +++++++++++--------- test/meta/TestTSMeta.java | 46 ++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 47f70ee81d..e1c72f280c 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -586,9 +586,7 @@ public Deferred call(Boolean success) throws Exception { } LOG.info("Successfullly created new TSUID entry for: " + meta); - return Deferred.fromResult(meta) - .addCallbackDeferring( - new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + return new LoadUIDs(tsdb, UniqueId.uidToString(tsuid)).call(meta) .addCallbackDeferring(new FetchNewCB()); } @@ -612,7 +610,16 @@ public Deferred call(Boolean success) throws Exception { new TSMetaCB()); } - public static void storeIfNecessary(final TSDB tsdb, final byte[] tsuid) { + /** + * Attempts to fetch the meta column and if null, attempts to write a new + * column using {@link #storeNew}. + * @param tsdb The TSDB instance to use for access. + * @param tsuid The TSUID of the time series. + * @return A deferred with a true if the meta exists or was created, false + * if the meta did not exist and writing failed. + */ + public static Deferred storeIfNecessary(final TSDB tsdb, + final byte[] tsuid) { final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid); get.family(FAMILY); get.qualifier(META_QUALIFIER); @@ -648,9 +655,7 @@ public Deferred call(Boolean success) throws Exception { } LOG.info("Successfullly created new TSUID entry for: " + meta); - return Deferred.fromResult(meta) - .addCallbackDeferring( - new LoadUIDs(tsdb, UniqueId.uidToString(tsuid))) + return new LoadUIDs(tsdb, UniqueId.uidToString(tsuid)).call(meta) .addCallbackDeferring(new FetchNewCB()); } } @@ -664,14 +669,13 @@ final class ExistsCB implements Callback, ArrayList> @Override public Deferred call(ArrayList row) throws Exception { if (row == null || row.isEmpty() || row.get(0).value() == null) { - return Deferred.fromResult(new Object()) - .addCallbackDeferring(new CreateNewCB()); + return new CreateNewCB().call(null); } return Deferred.fromResult(true); } } - tsdb.getClient().get(get).addCallbackDeferring(new ExistsCB()); + return tsdb.getClient().get(get).addCallbackDeferring(new ExistsCB()); } /** diff --git a/test/meta/TestTSMeta.java b/test/meta/TestTSMeta.java index 49c6e2f390..3984495aee 100644 --- a/test/meta/TestTSMeta.java +++ b/test/meta/TestTSMeta.java @@ -62,6 +62,7 @@ public final class TestTSMeta { private final static byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); private final static byte[] META_TABLE = "tsdb-meta".getBytes(MockBase.ASCII()); private final static byte[] UID_TABLE = "tsdb-uid".getBytes(MockBase.ASCII()); + private final static byte[] TSUID = new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }; private TSDB tsdb; private Config config; private HBaseClient client = mock(HBaseClient.class); @@ -120,7 +121,7 @@ public void before() throws Exception { "1328140801,\"displayName\":\"Web server 1\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(META_TABLE, TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()), ("{\"tsuid\":\"000001000001000001\",\"" + @@ -128,7 +129,7 @@ public void before() throws Exception { "\"custom\":null,\"units\":\"\",\"retention\":42,\"max\":1.0,\"min\":" + "\"NaN\",\"displayName\":\"Display\",\"dataType\":\"Data\"}") .getBytes(MockBase.ASCII())); - storage.addColumn(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + storage.addColumn(META_TABLE, TSUID, TSMeta.FAMILY, "ts_ctr".getBytes(MockBase.ASCII()), Bytes.fromLong(1L)); @@ -260,7 +261,7 @@ public void deleteNull() throws Exception { @Test public void syncToStorage() throws Exception { - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + meta = new TSMeta(TSUID, 1357300800000L); meta.setDisplayName("New DN"); meta.syncToStorage(tsdb, false).joinUninterruptibly(); assertEquals("New DN", meta.getDisplayName()); @@ -269,7 +270,7 @@ public void syncToStorage() throws Exception { @Test public void syncToStorageOverwrite() throws Exception { - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + meta = new TSMeta(TSUID, 1357300800000L); meta.setDisplayName("New DN"); meta.syncToStorage(tsdb, true).joinUninterruptibly(); assertEquals("New DN", meta.getDisplayName()); @@ -290,14 +291,14 @@ public void syncToStorageNullTSUID() throws Exception { @Test (expected = IllegalArgumentException.class) public void syncToStorageDoesNotExist() throws Exception { - storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + storage.flushRow(META_TABLE, TSUID); + meta = new TSMeta(TSUID, 1357300800000L); meta.syncToStorage(tsdb, false).joinUninterruptibly(); } @Test public void storeNew() throws Exception { - meta = new TSMeta(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, 1357300800000L); + meta = new TSMeta(TSUID, 1357300800000L); meta.setDisplayName("New DN"); meta.storeNew(tsdb); assertEquals("New DN", meta.getDisplayName()); @@ -323,7 +324,7 @@ public void metaExistsInStorage() throws Exception { @Test public void metaExistsInStorageNot() throws Exception { - storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, TSUID); assertFalse(TSMeta.metaExistsInStorage(tsdb, "000001000001000001") .joinUninterruptibly()); } @@ -331,14 +332,14 @@ public void metaExistsInStorageNot() throws Exception { @Test public void counterExistsInStorage() throws Exception { assertTrue(TSMeta.counterExistsInStorage(tsdb, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }).joinUninterruptibly()); + TSUID).joinUninterruptibly()); } @Test public void counterExistsInStorageNot() throws Exception { - storage.flushRow(META_TABLE, new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + storage.flushRow(META_TABLE, TSUID); assertFalse(TSMeta.counterExistsInStorage(tsdb, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }).joinUninterruptibly()); + TSUID).joinUninterruptibly()); } @Test @@ -378,12 +379,27 @@ public void COUNTER_QUALIFIER() throws Exception { TSMeta.COUNTER_QUALIFIER()); } + @Test + public void storeIfNecessaryExists() throws Exception { + assertTrue(TSMeta.storeIfNecessary(tsdb, TSUID).join()); + } + + @Test + public void storeIfNecessaryMissing() throws Exception { + storage.flushRow(META_TABLE, TSUID); + assertNull(storage.getColumn(META_TABLE, TSUID, NAME_FAMILY, + TSMeta.META_QUALIFIER())); + assertTrue(TSMeta.storeIfNecessary(tsdb, TSUID).join()); + assertNotNull(storage.getColumn(META_TABLE, TSUID, NAME_FAMILY, + TSMeta.META_QUALIFIER())); + } + @Test public void parseFromColumn() throws Exception { final KeyValue column = mock(KeyValue.class); - when(column.key()).thenReturn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + when(column.key()).thenReturn(TSUID); when(column.value()).thenReturn(storage.getColumn(META_TABLE, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()))); final TSMeta meta = TSMeta.parseFromColumn(tsdb, column, false) @@ -396,9 +412,9 @@ public void parseFromColumn() throws Exception { @Test public void parseFromColumnWithUIDMeta() throws Exception { final KeyValue column = mock(KeyValue.class); - when(column.key()).thenReturn(new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }); + when(column.key()).thenReturn(TSUID); when(column.value()).thenReturn(storage.getColumn(META_TABLE, - new byte[] { 0, 0, 1, 0, 0, 1, 0, 0, 1 }, + TSUID, NAME_FAMILY, "ts_meta".getBytes(MockBase.ASCII()))); final TSMeta meta = TSMeta.parseFromColumn(tsdb, column, true) From 0aa95ea61964c407f0f0f1252bcc503ce33254ce Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 17:20:28 -0700 Subject: [PATCH 479/826] Fix TSDB write data around the TSMeta and TSUID incrementation. Also update news. Signed-off-by: Chris Larsen --- NEWS | 9 +++++++++ src/core/TSDB.java | 23 ++++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/NEWS b/NEWS index 277824cc13..14bac38ef2 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,14 @@ OpenTSDB - User visible changes. +* Version 2.2.1 (2015-?-?) + +Noteworthy Changes + - Generate an incrementing TSMeta request only if both enable_tsuid_incrementing and + tsd.core.meta.enable_realtime_ts are enabled. Previously, increments would run + regardless of whether or not the real time ts setting was enabled. If tsuid + incrementing is disabled then a get and optional put is executed each time without + modifying the meta counter field. + * Version 2.2.0 (2016-02-14) Noteworthy Changes diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 703d219ca1..6eab7220c6 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -822,17 +822,18 @@ private Deferred addPointInternal(final String metric, 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); - } else if (!config.enable_tsuid_incrementing() && config.enable_realtime_ts()) { - TSMeta.storeIfNecessary(TSDB.this, tsuid); + if (config.enable_tsuid_tracking()) { + if (config.enable_realtime_ts()) { + if (config.enable_tsuid_incrementing()) { + TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } else { + TSMeta.storeIfNecessary(TSDB.this, tsuid); + } + } else { + final PutRequest tracking = new PutRequest(meta_table, tsuid, + TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); + client.put(tracking); + } } if (rt_publisher != null) { From 6de3c0276ed3ad1f1a28988d7e11662ef751b818 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 17:23:20 -0700 Subject: [PATCH 480/826] Bump to 2.2.1-SNAPSHOT Signed-off-by: Chris Larsen --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 9b8c7f5c75..0ca046cf10 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.2.0], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.1-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From a2f4c72ae65fa38e426b1d032e2f2602ea41eab0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 17:20:28 -0700 Subject: [PATCH 481/826] Fix TSDB write data around the TSMeta and TSUID incrementation. Also update news. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index d35c7d2a15..2842b89067 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -908,17 +908,18 @@ private Deferred addPointInternal(final String metric, if (meta_cache != null) { meta_cache.increment(tsuid); } else { - // 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()) { + if (config.enable_tsuid_tracking()) { + if (config.enable_realtime_ts()) { + if (config.enable_tsuid_incrementing()) { + TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } else { + TSMeta.storeIfNecessary(TSDB.this, tsuid); + } + } else { 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); - } else if (!config.enable_tsuid_incrementing() && config.enable_realtime_ts()) { - TSMeta.storeIfNecessary(TSDB.this, tsuid); + } } } From 4675b10d6dca897e49ed4bb274e027433f5eca3d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 17:30:21 -0700 Subject: [PATCH 482/826] Fix indentation in TSDB around the meta data incrementing. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 2842b89067..7ea1a79d37 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -908,18 +908,18 @@ private Deferred addPointInternal(final String metric, if (meta_cache != null) { meta_cache.increment(tsuid); } else { - if (config.enable_tsuid_tracking()) { - if (config.enable_realtime_ts()) { - if (config.enable_tsuid_incrementing()) { - TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + if (config.enable_tsuid_tracking()) { + if (config.enable_realtime_ts()) { + if (config.enable_tsuid_incrementing()) { + TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } else { + TSMeta.storeIfNecessary(TSDB.this, tsuid); + } } else { - TSMeta.storeIfNecessary(TSDB.this, tsuid); + final PutRequest tracking = new PutRequest(meta_table, tsuid, + TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); + client.put(tracking); } - } else { - final PutRequest tracking = new PutRequest(meta_table, tsuid, - TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); - client.put(tracking); - } } } From a6634003da14bcf24ebeae4811d6a881472562bb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 17:39:23 -0700 Subject: [PATCH 483/826] Fix the filter metric and tag resolution chain by making sure the first callback is attached as a deferring callback and returns the proper type Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 5d96c4e1c3..257d3cf018 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -361,14 +361,14 @@ class FilterCB implements Callback> { @Override public Object call(final ArrayList results) throws Exception { findGroupBys(); - return Deferred.fromResult(null); + return null; } } /** Resolve and group by tags after resolving the metric */ - class MetricCB implements Callback { + class MetricCB implements Callback, byte[]> { @Override - public Object call(final byte[] uid) throws Exception { + public Deferred call(final byte[] uid) throws Exception { metric = uid; if (filters != null) { final List> deferreds = @@ -385,7 +385,7 @@ public Object call(final byte[] uid) throws Exception { // fire off the callback chain by resolving the metric first return tsdb.metrics.getIdAsync(sub_query.getMetric()) - .addCallback(new MetricCB()); + .addCallbackDeferring(new MetricCB()); } } From 176c997af4bf34ce6d023a975367ec1c838b98a5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 17:39:23 -0700 Subject: [PATCH 484/826] Fix the filter metric and tag resolution chain by making sure the first callback is attached as a deferring callback and returns the proper type Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 105cecf8c2..7270c0c7c3 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -365,14 +365,14 @@ class FilterCB implements Callback> { @Override public Object call(final ArrayList results) throws Exception { findGroupBys(); - return Deferred.fromResult(null); + return null; } } /** Resolve and group by tags after resolving the metric */ - class MetricCB implements Callback { + class MetricCB implements Callback, byte[]> { @Override - public Object call(final byte[] uid) throws Exception { + public Deferred call(final byte[] uid) throws Exception { metric = uid; if (filters != null) { final List> deferreds = @@ -389,7 +389,7 @@ public Object call(final byte[] uid) throws Exception { // fire off the callback chain by resolving the metric first return tsdb.metrics.getIdAsync(sub_query.getMetric()) - .addCallback(new MetricCB()); + .addCallbackDeferring(new MetricCB()); } } From bd327c1a72a0cef60bea43981c966b4aed013f98 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 18:05:27 -0700 Subject: [PATCH 485/826] Remove typed exceptions from the StartupPlugin abstract and add some Javadocs. Also rename the TSDB Startup Plugin accessors for clarity and modify formatting. Also revert API change in 5cdbf14dbbc50d0ebc06551f343305a447913261. However it will now rethrow the Exception as a RuntimeException while still logging the original exception. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 30 +++++++++++++++++------------- src/tools/StartupPlugin.java | 9 +++++---- src/tools/TSDMain.java | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 7ea1a79d37..9cd291c7c6 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -247,11 +247,12 @@ public static byte[] FAMILY() { * Called by initializePlugins, also used to load startup plugins. * @since 2.3 */ - public static void loadPluginPath(final String plugin_path) throws RuntimeException { + public static void loadPluginPath(final String plugin_path) { if (plugin_path != null && !plugin_path.isEmpty()) { try { PluginLoader.loadJARs(plugin_path); } catch (Exception e) { + LOG.error("Error loading plugins from plugin path: " + plugin_path, e); throw new RuntimeException("Error loading plugins from plugin path: " + plugin_path, e); } @@ -268,13 +269,9 @@ public static void loadPluginPath(final String plugin_path) throws RuntimeExcept * @throws IllegalArgumentException if a plugin could not be initialized * @since 2.0 */ - public void initializePlugins(final boolean init_rpcs) throws RuntimeException { + public void initializePlugins(final boolean init_rpcs) { final String plugin_path = config.getString("tsd.core.plugin_path"); - try { - loadPluginPath(plugin_path); - } catch (RuntimeException e) { - throw e; - } + loadPluginPath(plugin_path); try { TagVFilter.initializeFilterMap(this); @@ -389,17 +386,24 @@ public final HBaseClient getClient() { } /** - * Sets the startup plugin so that it can be shutdown properly. - * @param startup + * Sets the startup plugin so that it can be shutdown properly. + * Note that this method will not initialize or call any other methods + * belonging to the plugin's implementation. + * @param plugin The startup plugin that was used. * @since 2.3 */ - public final void setStartup(StartupPlugin startup) { this.startup = startup; } + public final void setStartupPlugin(final StartupPlugin plugin) { + startup = plugin; + } + /** - * Getter that returns the startup plugin object - * @return The StartupPlugin object + * Getter that returns the startup plugin object. + * @return The StartupPlugin object or null if the plugin was not set. * @since 2.3 */ - public final StartupPlugin getStartup() { return this.startup; } + public final StartupPlugin getStartupPlugin() { + return startup; + } /** * Getter that returns the configuration object diff --git a/src/tools/StartupPlugin.java b/src/tools/StartupPlugin.java index 3c56464a68..cbfa040522 100644 --- a/src/tools/StartupPlugin.java +++ b/src/tools/StartupPlugin.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016 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 @@ -37,16 +37,17 @@ public abstract class StartupPlugin { * up properly. The TSD will then shutdown so the operator can fix the * problem. Please use IllegalArgumentException for configuration issues. * @param tsdb The parent TSDB object + * @return A reference to the same configuration object passed in the parameters + * on success. * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong */ - public abstract Config initialize(Config config) throws IllegalArgumentException, Exception; + public abstract Config initialize(Config config); /** * Called when the TSD is fully initialized and ready to handle traffic. */ - public abstract void setReady(final TSDB tsdb) throws Exception; + public abstract void setReady(final TSDB tsdb); /** * Called to gracefully shutdown the plugin. Implementations should close diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 4036ae3015..d88b56a638 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -180,7 +180,7 @@ public static void main(String[] args) throws IOException { try { tsdb = new TSDB(config); if (startup != null) { - tsdb.setStartup(startup); + tsdb.setStartupPlugin(startup); } tsdb.initializePlugins(true); if (config.getBoolean("tsd.storage.hbase.prefetch_meta")) { From 69ff9212fa783693450d0bc0a9897fa07d7a9415 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 23 Mar 2016 11:36:33 -0500 Subject: [PATCH 486/826] Added check for malformed, double dot timestamp Fixes #724 --- src/utils/DateTime.java | 6 ++++-- test/utils/TestDateTime.java | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index d3d9a08c44..5ea80c6742 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -121,8 +121,10 @@ public static final long parseDateTimeString(final String datetime, } else { try { long time; - if (datetime.contains(".")) { - if (datetime.charAt(10) != '.' || datetime.length() != 14) { + Boolean containsDot = datetime.contains("."); + Boolean containsTwoDots = datetime.matches(".*\\..*\\..*"); + if (containsDot) { + if (datetime.charAt(10) != '.' || datetime.length() != 14 || containsTwoDots) { throw new IllegalArgumentException("Invalid time: " + datetime + ". Millisecond timestamps must be in the format " + ". where the milliseconds are limited to 3 digits"); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 63ae2ffab8..eef640dd00 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -123,6 +123,11 @@ public void parseDateTimeStringUnixSecondsZero() { public void parseDateTimeStringUnixSecondsNegative() { DateTime.parseDateTimeString("-135596160", null); } + + @Test(expected = IllegalArgumentException.class) + public void parseDateTimeStringMultipleDots() { + DateTime.parseDateTimeString("1234567890.2.4", null); + } @Test public void parseDateTimeStringUnixSecondsInvalidLong() { From ed9a7410f55272d915a24cc4e9937a7c1ec97efe Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 18:26:42 -0700 Subject: [PATCH 487/826] Little bit of cleanup in DateTime.parseDateTimeString() by removing a regex that isn't used and formatting the variable names per TSD spec. Signed-off-by: Chris Larsen --- src/utils/DateTime.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 33f3ab3db5..41c10ed805 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -133,15 +133,14 @@ public static final long parseDateTimeString(final String datetime, } else { try { long time; - Boolean containsDot = datetime.contains("."); + final boolean contains_dot = datetime.contains("."); // [0-9]{10} ten digits // \\. a dot // [0-9]{1,3} one to three digits - Boolean isValidDottedMillesecond = datetime.matches("^[0-9]{10}\\.[0-9]{1,3}$"); - // one to ten digits (0-9) - Boolean isValidSeconds = datetime.matches("^[0-9]{1,10}$"); - if (containsDot) { - if (!isValidDottedMillesecond) { + final boolean valid_dotted_ms = + datetime.matches("^[0-9]{10}\\.[0-9]{1,3}$"); + if (contains_dot) { + if (!valid_dotted_ms) { throw new IllegalArgumentException("Invalid time: " + datetime + ". Millisecond timestamps must be in the format " + ". where the milliseconds are limited to 3 digits"); From c55e9b34212e255bb73129d8914954d8524469e5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 19:20:32 -0700 Subject: [PATCH 488/826] Fix up d46133405b98df52ccb1ad6c0176897c2728f67c. Rollback the breaking API changes by adding overloaded ctors. On testing, DefaultChannelGroup.size() doesn't return the current size. It often returns "1" even though there may be 5 open connections from the same host to the TSD. This could be an issue in the Netty implementation or something else. Anyway now we're using an AtomicInteger that will increment and decrement on each channel open and close, respectively. It's ugly but accurate. Also modify the naming styles to fit in with TSD code. Signed-off-by: Chris Larsen --- src/tools/TSDMain.java | 6 +-- src/tsd/ConnectionManager.java | 72 +++++++++++++++++++++++++++------- src/tsd/PipelineFactory.java | 39 ++++++++++++------ 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index d88b56a638..0c6f5d55d4 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -141,9 +141,9 @@ public static void main(String[] args) throws IOException { } final ServerSocketChannelFactory factory; - int connectionsLimit = 0; + int connections_limit = 0; try { - connectionsLimit = config.getInt("tsd.core.connections.limit"); + connections_limit = config.getInt("tsd.core.connections.limit"); } catch (NumberFormatException nfe) { usage(argp, "Invalid connections limit", 1); } @@ -197,7 +197,7 @@ public static void main(String[] args) throws IOException { // here to fail fast. final RpcManager manager = RpcManager.instance(tsdb); - server.setPipelineFactory(new PipelineFactory(tsdb, manager, connectionsLimit)); + server.setPipelineFactory(new PipelineFactory(tsdb, manager, connections_limit)); if (config.hasProperty("tsd.network.backlog")) { server.setOption("backlog", config.getInt("tsd.network.backlog")); } diff --git a/src/tsd/ConnectionManager.java b/src/tsd/ConnectionManager.java index f4d4523e3a..902c31315b 100644 --- a/src/tsd/ConnectionManager.java +++ b/src/tsd/ConnectionManager.java @@ -14,12 +14,14 @@ import java.io.IOException; import java.nio.channels.ClosedChannelException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelException; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; @@ -42,10 +44,15 @@ final class ConnectionManager extends SimpleChannelHandler { private static final AtomicLong exceptions_closed = new AtomicLong(); private static final AtomicLong exceptions_reset = new AtomicLong(); private static final AtomicLong exceptions_timeout = new AtomicLong(); - /** - * max connections can be serviced by tsd, if over limit, tsd will close new connection. - */ - private int connectionsLimit; + + /** Max connections can be serviced by tsd, if over limit, tsd will refuse + * new connections. */ + private final int connections_limit; + + /** A counter used for determining how many channels are open. Something odd + * happens with the DefaultChannelGroup in that .size() doesn't return the + * actual number of open connections. TODO - find out why. */ + private final AtomicInteger open_connections = new AtomicInteger(); private static final DefaultChannelGroup channels = new DefaultChannelGroup("all-channels"); @@ -54,10 +61,21 @@ static void closeAllConnections() { channels.close().awaitUninterruptibly(); } - /** Constructor. */ - public ConnectionManager(int connectionsLimit) { - LOG.info("totalConnections limit is set : " + connectionsLimit); - this.connectionsLimit = connectionsLimit; + /** + * Default Ctor with no concurrent connection limit. + */ + public ConnectionManager() { + connections_limit = 0; + } + + /** + * CTor for setting a limit on concurrent connections. + * @param connections_limit The maximum number of concurrent connections allowed. + * @since 2.3 + */ + public ConnectionManager(final int connections_limit) { + LOG.info("TSD concurrent connection limit set to: " + connections_limit); + this.connections_limit = connections_limit; } /** @@ -83,18 +101,24 @@ public static void collectStats(final StatsCollector collector) { @Override public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws IOException { - if (connectionsLimit > 0) { - int channelSize = channels.size(); - if (channelSize >= connectionsLimit) { - e.getChannel().close(); - connections_rejected.incrementAndGet(); - throw new IOException("Channel size (" + channelSize + ") exceeds total connection limit (" + connectionsLimit + ")"); + if (connections_limit > 0) { + final int channel_size = open_connections.incrementAndGet(); + if (channel_size > connections_limit) { + throw new ConnectionRefusedException("Channel size (" + channel_size + ") exceeds total " + + "connection limit (" + connections_limit + ")"); + // exceptionCaught will close the connection and increment the counter. } } channels.add(e.getChannel()); connections_established.incrementAndGet(); } + @Override + public void channelClosed(final ChannelHandlerContext ctx, + final ChannelStateEvent e) throws IOException { + open_connections.decrementAndGet(); + } + @Override public void handleUpstream(final ChannelHandlerContext ctx, final ChannelEvent e) throws Exception { @@ -126,6 +150,13 @@ public void exceptionCaught(final ChannelHandlerContext ctx, // in Java. Like, people have been bitching about errno for years, // and Java managed to do something *far* worse. That's quite a feat. return; + } else if (cause instanceof ConnectionRefusedException) { + connections_rejected.incrementAndGet(); + if (LOG.isDebugEnabled()) { + LOG.debug("Refusing connection from " + chan, e.getCause()); + } + chan.close(); + return; } } if (cause instanceof CodecEmbedderException) { @@ -139,4 +170,17 @@ public void exceptionCaught(final ChannelHandlerContext ctx, e.getChannel().close(); } + /** Simple exception for refusing a connection. */ + private static class ConnectionRefusedException extends ChannelException { + + /** + * Default ctor with a message. + * @param message A descriptive message for the exception. + */ + public ConnectionRefusedException(final String message) { + super(message); + } + + private static final long serialVersionUID = 5348377149312597939L; + } } diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 30389c34c6..05414f4491 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -14,8 +14,6 @@ import static org.jboss.netty.channel.Channels.pipeline; -import java.util.concurrent.ThreadFactory; - import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; @@ -30,7 +28,6 @@ import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; import org.jboss.netty.handler.timeout.IdleStateHandler; -import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.Timer; import net.opentsdb.core.TSDB; @@ -38,6 +35,9 @@ /** * Creates a newly configured {@link ChannelPipeline} for a new channel. * This class is supposed to be a singleton. + * NOTE: On creation (as of 2.3) the property given in the config for + * "tsd.core.connections.limit" will be used to limit the number of concurrent + * connections supported by the pipeline. The default is zero. */ public final class PipelineFactory implements ChannelPipelineFactory { @@ -70,7 +70,8 @@ public final class PipelineFactory implements ChannelPipelineFactory { * serializers */ public PipelineFactory(final TSDB tsdb) { - this(tsdb, RpcManager.instance(tsdb), tsdb.getConfig().getInt("tsd.core.connections.limit")); + this(tsdb, RpcManager.instance(tsdb), + tsdb.getConfig().getInt("tsd.core.connections.limit")); } /** @@ -79,16 +80,32 @@ public PipelineFactory(final TSDB tsdb) { * @param tsdb The TSDB to use. * @param manager instance of a ready-to-use {@link RpcManager}. * @throws RuntimeException if there is an issue loading plugins - * @throws Exception if the HttpQuery handler is unable to load - * serializers + * @throws Exception if the HttpQuery handler is unable to load serializers + */ + public PipelineFactory(final TSDB tsdb, final RpcManager manager) { + this(tsdb, RpcManager.instance(tsdb), + tsdb.getConfig().getInt("tsd.core.connections.limit")); + } + + /** + * Constructor that initializes the RPC router and loads HTTP formatter + * plugins using an already-configured {@link RpcManager}. + * @param tsdb The TSDB to use. + * @param manager instance of a ready-to-use {@link RpcManager}. + * @param connections_limit The maximum number of concurrent connections + * supported by the TSD. + * @throws RuntimeException if there is an issue loading plugins + * @throws Exception if the HttpQuery handler is unable to load serializers + * @since 2.3 */ - public PipelineFactory(final TSDB tsdb, final RpcManager manager, final int connectionsLimit) { + public PipelineFactory(final TSDB tsdb, final RpcManager manager, + final int connections_limit) { this.tsdb = tsdb; - this.socketTimeout = tsdb.getConfig().getInt("tsd.core.socket.timeout"); + socketTimeout = tsdb.getConfig().getInt("tsd.core.socket.timeout"); timer = tsdb.getTimer(); - this.timeoutHandler = new IdleStateHandler(timer, 0, 0, this.socketTimeout); - this.rpchandler = new RpcHandler(tsdb, manager); - this.connmgr = new ConnectionManager(connectionsLimit); + timeoutHandler = new IdleStateHandler(timer, 0, 0, socketTimeout); + rpchandler = new RpcHandler(tsdb, manager); + connmgr = new ConnectionManager(connections_limit); try { HttpQuery.initializeSerializerMaps(tsdb); } catch (RuntimeException e) { From 43140777a62f687bbdbc6974176f71084908af75 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 19 Apr 2016 13:01:44 -0700 Subject: [PATCH 489/826] Add TagVFilter.getCopy() for creating a duplicate of the filter. Signed-off-by: Chris Larsen --- src/query/filter/TagVFilter.java | 12 ++++++++++++ test/query/filter/TestTagVFilter.java | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java index 0c5e797b22..611a6c8266 100644 --- a/src/query/filter/TagVFilter.java +++ b/src/query/filter/TagVFilter.java @@ -498,11 +498,23 @@ public byte[] getTagkBytes() { return tagk_bytes; } + /** @return a non-null list of tag value UIDs. May be empty. */ @JsonIgnore public List getTagVUids() { return tagv_uids == null ? Collections.emptyList() : tagv_uids; } + /** @return A copy of this filter BEFORE tag resolution, as a new object. */ + @JsonIgnore + public TagVFilter getCopy() { + return Builder() + .setFilter(filter) + .setTagk(tagk) + .setType(getType()) + .setGroupBy(group_by) + .build(); + } + /** @return whether or not to group by the results of this filter */ @JsonIgnore public boolean isGroupBy() { diff --git a/test/query/filter/TestTagVFilter.java b/test/query/filter/TestTagVFilter.java index 477f65aaa9..5c952c83df 100644 --- a/test/query/filter/TestTagVFilter.java +++ b/test/query/filter/TestTagVFilter.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -400,5 +401,21 @@ public void tagsToFiltersSameTagDiffValues() throws Exception { assertEquals(2, filters.size()); } + @Test + public void getCopy() { + final TagVFilter filter = TagVFilter.Builder() + .setFilter("*") + .setTagk(TAGK_STRING) + .setType("wildcard") + .setGroupBy(true) + .build(); + final TagVFilter copy = filter.getCopy(); + assertNotSame(filter, copy); + assertEquals(filter.filter, copy.filter); + assertEquals(filter.tagk, copy.tagk); + assertEquals(filter.getType(), copy.getType()); + assertEquals(filter.group_by, copy.group_by); + } + // TODO - test the plugin loader similar to the other plugins } From 005f186d044000ddf8ecc96446e2dee8c174ba54 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 19 Apr 2016 13:01:44 -0700 Subject: [PATCH 490/826] Add TagVFilter.getCopy() for creating a duplicate of the filter. Signed-off-by: Chris Larsen --- src/query/filter/TagVFilter.java | 12 ++++++++++++ test/query/filter/TestTagVFilter.java | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java index 0c5e797b22..611a6c8266 100644 --- a/src/query/filter/TagVFilter.java +++ b/src/query/filter/TagVFilter.java @@ -498,11 +498,23 @@ public byte[] getTagkBytes() { return tagk_bytes; } + /** @return a non-null list of tag value UIDs. May be empty. */ @JsonIgnore public List getTagVUids() { return tagv_uids == null ? Collections.emptyList() : tagv_uids; } + /** @return A copy of this filter BEFORE tag resolution, as a new object. */ + @JsonIgnore + public TagVFilter getCopy() { + return Builder() + .setFilter(filter) + .setTagk(tagk) + .setType(getType()) + .setGroupBy(group_by) + .build(); + } + /** @return whether or not to group by the results of this filter */ @JsonIgnore public boolean isGroupBy() { diff --git a/test/query/filter/TestTagVFilter.java b/test/query/filter/TestTagVFilter.java index 477f65aaa9..5c952c83df 100644 --- a/test/query/filter/TestTagVFilter.java +++ b/test/query/filter/TestTagVFilter.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -400,5 +401,21 @@ public void tagsToFiltersSameTagDiffValues() throws Exception { assertEquals(2, filters.size()); } + @Test + public void getCopy() { + final TagVFilter filter = TagVFilter.Builder() + .setFilter("*") + .setTagk(TAGK_STRING) + .setType("wildcard") + .setGroupBy(true) + .build(); + final TagVFilter copy = filter.getCopy(); + assertNotSame(filter, copy); + assertEquals(filter.filter, copy.filter); + assertEquals(filter.tagk, copy.tagk); + assertEquals(filter.getType(), copy.getType()); + assertEquals(filter.group_by, copy.group_by); + } + // TODO - test the plugin loader similar to the other plugins } From d1ec89faa7247d69de71605ad1e99372a981d167 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 19 Apr 2016 13:06:01 -0700 Subject: [PATCH 491/826] Fix concurrent modification exceptions with tag filters used in the /api/query/exp endpoint. Signed-off-by: Chris Larsen --- src/tsd/QueryExecutor.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java index 20e81bd76b..22937fcb4b 100644 --- a/src/tsd/QueryExecutor.java +++ b/src/tsd/QueryExecutor.java @@ -49,6 +49,7 @@ import net.opentsdb.query.expression.NumericFillPolicy; import net.opentsdb.query.expression.TimeSyncedIterator; import net.opentsdb.query.expression.VariableIterator.SetOperator; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.query.pojo.Expression; import net.opentsdb.query.pojo.Filter; import net.opentsdb.query.pojo.Metric; @@ -156,21 +157,30 @@ public QueryExecutor(final TSDB tsdb, final Query query) { // filters if (mq.getFilter() != null && !mq.getFilter().isEmpty()) { - Filter filters = null; + List filters = null; + boolean explicit_tags = false; if (query.getFilters() == null || query.getFilters().isEmpty()) { throw new IllegalArgumentException("No filter defined: " + mq.getFilter()); } for (final Filter filter : query.getFilters()) { if (filter.getId().equals(mq.getFilter())) { - filters = filter; + // TODO - it'd be more efficient if we could share the filters but + // for now, this is the only way to avoid concurrent modifications. + filters = new ArrayList(filter.getTags().size()); + for (final TagVFilter f : filter.getTags()) { + filters.add(f.getCopy()); + } + explicit_tags = filter.getExplicitTags(); break; } } sub.setRate(timespan.isRate()); - sub.setFilters(filters.getTags()); sub.setAggregator( mq.getAggregator() != null ? mq.getAggregator() : timespan.getAggregator()); - sub.setExplicitTags(filters.getExplicitTags()); + if (filters != null) { + sub.setFilters(filters); + sub.setExplicitTags(explicit_tags); + } } } From 5c4e9b3bd2a61d6fd421233f31f50772888e43dd Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 22 Apr 2016 13:07:45 -0700 Subject: [PATCH 492/826] Add the WriteableDataPointFilterPlugin and UniqueIdFilterPlugin plugin interfaces. These can be used to prevent time series from being written or UIDs from being assigned. It can also enforce naming standards. Signed-off-by: Chris Larsen --- Makefile.am | 3 + src/core/IncomingDataPoints.java | 131 ++-- src/core/TSDB.java | 227 ++++-- src/core/Tags.java | 30 +- src/core/WriteableDataPointFilterPlugin.java | 99 +++ src/uid/FailedToAssignUniqueIdException.java | 18 + src/uid/UniqueId.java | 129 +++- src/uid/UniqueIdFilterPlugin.java | 101 +++ src/utils/Config.java | 2 + test/core/BaseTsdbTest.java | 12 + test/core/TestTSDB.java | 629 ----------------- test/core/TestTSDBAddPoint.java | 690 +++++++++++++++++++ test/core/TestTags.java | 55 +- test/uid/TestUniqueId.java | 127 +++- 14 files changed, 1485 insertions(+), 768 deletions(-) create mode 100644 src/core/WriteableDataPointFilterPlugin.java create mode 100644 src/uid/UniqueIdFilterPlugin.java create mode 100644 test/core/TestTSDBAddPoint.java diff --git a/Makefile.am b/Makefile.am index 6f274a2373..02ebd0e50d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -68,6 +68,7 @@ tsdb_SRC := \ src/core/TSQuery.java \ src/core/TSSubQuery.java \ src/core/WritableDataPoints.java \ + src/core/WriteableDataPointFilterPlugin.java \ src/graph/Plot.java \ src/meta/Annotation.java \ src/meta/MetaDataCache.java \ @@ -179,6 +180,7 @@ tsdb_SRC := \ src/uid/NoSuchUniqueName.java \ src/uid/RandomUniqueId.java \ src/uid/UniqueId.java \ + src/uid/UniqueIdFilterPlugin.java \ src/uid/UniqueIdInterface.java \ src/utils/ByteArrayPair.java \ src/utils/ByteSet.java \ @@ -256,6 +258,7 @@ test_SRC := \ test/core/TestSpanGroup.java \ test/core/TestTags.java \ test/core/TestTSDB.java \ + test/core/TestTSDBAddDataPoint.java \ test/core/TestTsdbQueryDownsample.java \ test/core/TestTsdbQueryDownsampleSalted.java \ test/core/TestTsdbQuery.java \ diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 6fb9c9ba23..6f6245b780 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -73,6 +73,12 @@ final class IncomingDataPoints implements WritableDataPoints { /** Are we doing a batch import? */ private boolean batch_import; + + /** The metric for this time series */ + private String metric; + + /** Copy of the tags given us by the caller */ + private Map tags; /** * Constructor. @@ -163,7 +169,7 @@ static Deferred rowKeyTemplateAsync(final TSDB tsdb, // Lookup or create the metric ID. final Deferred metric_id; if (tsdb.config.auto_metric()) { - metric_id = tsdb.metrics.getOrCreateIdAsync(metric); + metric_id = tsdb.metrics.getOrCreateIdAsync(metric, metric, tags); } else { metric_id = tsdb.metrics.getIdAsync(metric); } @@ -193,8 +199,8 @@ public Deferred call(final ArrayList tags) { } // Kick off the resolution of all tags. - return Tags.resolveOrCreateAllAsync(tsdb, tags).addCallbackDeferring( - new CopyTagsInRowKeyCB()); + return Tags.resolveOrCreateAllAsync(tsdb, metric, tags) + .addCallbackDeferring(new CopyTagsInRowKeyCB()); } public void setSeries(final String metric, final Map tags) { @@ -207,6 +213,8 @@ public void setSeries(final String metric, final Map tags) { } catch (Exception e) { throw new RuntimeException("Should never happen", e); } + this.metric = metric; + this.tags = tags; size = 0; } @@ -281,57 +289,80 @@ private Deferred addPointInternal(final long timestamp, + " when trying to add value=" + Arrays.toString(value) + " to " + this); } - last_ts = (ms_timestamp ? timestamp : timestamp * 1000); + + /** Callback executed for chaining filter calls to see if the value + * should be written or not. */ + final class WriteCB implements Callback, Boolean> { + @Override + public Deferred call(final Boolean allowed) throws Exception { + if (!allowed) { + return Deferred.fromResult(null); + } + - long base_time = baseTime(); - long incoming_base_time; - if (ms_timestamp) { - // drop the ms timestamp to seconds to calculate the base timestamp - incoming_base_time = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); - } else { - incoming_base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); - } + last_ts = (ms_timestamp ? timestamp : timestamp * 1000); - if (incoming_base_time - base_time >= Const.MAX_TIMESPAN) { - // Need to start a new row as we've exceeded Const.MAX_TIMESPAN. - base_time = updateBaseTime((ms_timestamp ? timestamp / 1000 : timestamp)); - } + long base_time = baseTime(); + long incoming_base_time; + if (ms_timestamp) { + // drop the ms timestamp to seconds to calculate the base timestamp + incoming_base_time = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); + } else { + incoming_base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + } - // Java is so stupid with its auto-promotion of int to float. - final byte[] qualifier = Internal.buildQualifier(timestamp, flags); - - // TODO(tsuna): The following timing is rather useless. First of all, - // the histogram never resets, so it tends to converge to a certain - // distribution and never changes. What we really want is a moving - // histogram so we can see how the latency distribution varies over time. - // The other problem is that the Histogram class isn't thread-safe and - // here we access it from a callback that runs in an unknown thread, so - // we might miss some increments. So let's comment this out until we - // have a proper thread-safe moving histogram. - // final long start_put = System.nanoTime(); - // final Callback cb = new Callback() { - // public Object call(final Object arg) { - // putlatency.add((int) ((System.nanoTime() - start_put) / 1000000)); - // return arg; - // } - // public String toString() { - // return "time put request"; - // } - // }; - - // TODO(tsuna): Add an errback to handle some error cases here. - if (tsdb.getConfig().enable_appends()) { - final AppendDataPoints kv = new AppendDataPoints(qualifier, value); - final AppendRequest point = new AppendRequest(tsdb.table, row, TSDB.FAMILY, - AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); - point.setDurable(!batch_import); - return tsdb.client.append(point);/* .addBoth(cb) */ - } else { - final PutRequest point = new PutRequest(tsdb.table, row, TSDB.FAMILY, - qualifier, value); - point.setDurable(!batch_import); - return tsdb.client.put(point)/* .addBoth(cb) */; + if (incoming_base_time - base_time >= Const.MAX_TIMESPAN) { + // Need to start a new row as we've exceeded Const.MAX_TIMESPAN. + base_time = updateBaseTime((ms_timestamp ? timestamp / 1000 : timestamp)); + } + + // Java is so stupid with its auto-promotion of int to float. + final byte[] qualifier = Internal.buildQualifier(timestamp, flags); + + // TODO(tsuna): The following timing is rather useless. First of all, + // the histogram never resets, so it tends to converge to a certain + // distribution and never changes. What we really want is a moving + // histogram so we can see how the latency distribution varies over time. + // The other problem is that the Histogram class isn't thread-safe and + // here we access it from a callback that runs in an unknown thread, so + // we might miss some increments. So let's comment this out until we + // have a proper thread-safe moving histogram. + // final long start_put = System.nanoTime(); + // final Callback cb = new Callback() { + // public Object call(final Object arg) { + // putlatency.add((int) ((System.nanoTime() - start_put) / 1000000)); + // return arg; + // } + // public String toString() { + // return "time put request"; + // } + // }; + + // TODO(tsuna): Add an errback to handle some error cases here. + if (tsdb.getConfig().enable_appends()) { + final AppendDataPoints kv = new AppendDataPoints(qualifier, value); + final AppendRequest point = new AppendRequest(tsdb.table, row, TSDB.FAMILY, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); + point.setDurable(!batch_import); + return tsdb.client.append(point);/* .addBoth(cb) */ + } 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"; + } + } + + if (tsdb.getTSfilter() != null && tsdb.getTSfilter().filterDataPoints()) { + return tsdb.getTSfilter().allowDataPoint(metric, timestamp, value, tags, flags) + .addCallbackDeferring(new WriteCB()); } + return Deferred.fromResult(true).addCallbackDeferring(new WriteCB()); } private void grow() { diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 9cd291c7c6..ba6839715c 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -48,6 +48,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueIdFilterPlugin; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; import net.opentsdb.utils.DateTime; @@ -134,6 +135,16 @@ public final class TSDB { /** Plugin for dealing with data points that can't be stored */ private StorageExceptionHandler storage_exception_handler = null; + /** A filter plugin for allowing or blocking time series */ + private WriteableDataPointFilterPlugin ts_filter; + + /** A filter plugin for allowing or blocking UIDs */ + private UniqueIdFilterPlugin uid_filter; + + /** Writes rejected by the filter */ + private final AtomicLong rejected_dps = new AtomicLong(); + private final AtomicLong rejected_aggregate_dps = new AtomicLong(); + /** Datapoints Added */ private static final AtomicLong datapoints_added = new AtomicLong(); @@ -194,17 +205,13 @@ public TSDB(final HBaseClient client, final Config config) { meta_table = config.getString("tsd.storage.hbase.meta_table").getBytes(CHARSET); if (config.getBoolean("tsd.core.uid.random_metrics")) { - metrics = new UniqueId(this.client, uidtable, METRICS_QUAL, METRICS_WIDTH, - true); + metrics = new UniqueId(this, uidtable, METRICS_QUAL, METRICS_WIDTH, true); } else { - metrics = new UniqueId(this.client, uidtable, METRICS_QUAL, METRICS_WIDTH); + metrics = new UniqueId(this, uidtable, METRICS_QUAL, METRICS_WIDTH, false); } - tag_names = new UniqueId(this.client, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH); - tag_values = new UniqueId(this.client, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH); + tag_names = new UniqueId(this, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH, false); + tag_values = new UniqueId(this, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH, false); compactionq = new CompactionQueue(this); - metrics.setTSDB(this); - tag_names.setTSDB(this); - tag_values.setTSDB(this); if (config.hasProperty("tsd.core.timezone")) { DateTime.setDefaultTimezone(config.getString("tsd.core.timezone")); @@ -374,6 +381,48 @@ public void initializePlugins(final boolean init_rpcs) { storage_exception_handler.getClass().getCanonicalName() + "] version: " + storage_exception_handler.version()); } + + // Writeable Data Point Filter + if (config.getBoolean("tsd.timeseriesfilter.enable")) { + ts_filter = PluginLoader.loadSpecificPlugin( + config.getString("tsd.timeseriesfilter.plugin"), + WriteableDataPointFilterPlugin.class); + if (ts_filter == null) { + throw new IllegalArgumentException( + "Unable to locate time series filter plugin plugin: " + + config.getString("tsd.timeseriesfilter.plugin")); + } + try { + ts_filter.initialize(this); + } catch (Exception e) { + throw new RuntimeException( + "Failed to initialize time series filter plugin", e); + } + LOG.info("Successfully initialized time series filter plugin [" + + ts_filter.getClass().getCanonicalName() + "] version: " + + ts_filter.version()); + } + + // UID Filter + if (config.getBoolean("tsd.uidfilter.enable")) { + uid_filter = PluginLoader.loadSpecificPlugin( + config.getString("tsd.uidfilter.plugin"), + UniqueIdFilterPlugin.class); + if (uid_filter == null) { + throw new IllegalArgumentException( + "Unable to locate UID filter plugin plugin: " + + config.getString("tsd.uidfilter.plugin")); + } + try { + uid_filter.initialize(this); + } catch (Exception e) { + throw new RuntimeException( + "Failed to initialize UID filter plugin", e); + } + LOG.info("Successfully initialized UID filter plugin [" + + uid_filter.getClass().getCanonicalName() + "] version: " + + uid_filter.version()); + } } /** @@ -423,6 +472,22 @@ public final StorageExceptionHandler getStorageExceptionHandler() { return storage_exception_handler; } + /** + * @return the TS filter object, may be null + * @since 2.3 + */ + public WriteableDataPointFilterPlugin getTSfilter() { + return ts_filter; + } + + /** + * @return The UID filter object, may be null. + * @since 2.3 + */ + public UniqueIdFilterPlugin getUidFilter() { + return uid_filter; + } + /** * Attempts to find the name for a unique identifier given a type * @param type The type of UID @@ -583,6 +648,10 @@ public void collectStats(final StatsCollector collector) { } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } + + collector.record("uid.filter.rejected", rejected_dps.get(), "kind=raw"); + collector.record("uid.filter.rejected", rejected_aggregate_dps.get(), + "kind=aggregate"); { final Runtime runtime = Runtime.getRuntime(); @@ -671,6 +740,22 @@ public void collectStats(final StatsCollector collector) { collector.clearExtraTag("plugin"); } } + if (ts_filter != null) { + try { + collector.addExtraTag("plugin", "timeseriesFilter"); + ts_filter.collectStats(collector); + } finally { + collector.clearExtraTag("plugin"); + } + } + if (uid_filter != null) { + try { + collector.addExtraTag("plugin", "uidFilter"); + uid_filter.collectStats(collector); + } finally { + collector.clearExtraTag("plugin"); + } + } } /** Returns a latency histogram for Put RPCs used to store data points. */ @@ -695,6 +780,8 @@ private static void collectUidStats(final UniqueId uid, collector.record("uid.cache-size", uid.cacheSize(), "kind=" + uid.kind()); collector.record("uid.random-collisions", uid.randomIdCollisions(), "kind=" + uid.kind()); + collector.record("uid.rejected-assignments", uid.rejectedAssignments(), + "kind=" + uid.kind()); } /** @return the width, in bytes, of metric UIDs */ @@ -878,59 +965,81 @@ private Deferred addPointInternal(final String metric, base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } - Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); - RowKey.prefixKeyWithSalt(row); - - Deferred result = null; - if (config.enable_appends()) { - final AppendDataPoints kv = new AppendDataPoints(qualifier, value); - final AppendRequest point = new AppendRequest(table, row, FAMILY, - AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); - result = client.append(point); - } else { - scheduleForCompaction(row, (int) base_time); - final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); - result = client.put(point); - } + /** Callback executed for chaining filter calls to see if the value + * should be written or not. */ + final class WriteCB implements Callback, Boolean> { + @Override + public Deferred call(final Boolean allowed) throws Exception { + if (!allowed) { + rejected_dps.incrementAndGet(); + return Deferred.fromResult(null); + } + + Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); + RowKey.prefixKeyWithSalt(row); + + Deferred result = null; + if (config.enable_appends()) { + final AppendDataPoints kv = new AppendDataPoints(qualifier, value); + final AppendRequest point = new AppendRequest(table, row, FAMILY, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); + result = client.append(point); + } else { + scheduleForCompaction(row, (int) base_time); + final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); + result = client.put(point); + } - // Count all added datapoints, not just those that came in through PUT rpc - // Will there be others? Well, something could call addPoint programatically right? - datapoints_added.incrementAndGet(); + // Count all added datapoints, not just those that came in through PUT rpc + // Will there be others? Well, something could call addPoint programatically right? + datapoints_added.incrementAndGet(); - // TODO(tsuna): Add a callback to time the latency of HBase and store the - // timing in a moving Histogram (once we have a class for this). - - if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && - !config.enable_tsuid_tracking() && rt_publisher == null) { - return result; - } - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, - Const.TIMESTAMP_BYTES); - - // if the meta cache plugin is instantiated then tracking goes through it - if (meta_cache != null) { - meta_cache.increment(tsuid); - } else { - if (config.enable_tsuid_tracking()) { - if (config.enable_realtime_ts()) { - if (config.enable_tsuid_incrementing()) { - TSMeta.incrementAndGetCounter(TSDB.this, tsuid); - } else { - TSMeta.storeIfNecessary(TSDB.this, tsuid); - } + // TODO(tsuna): Add a callback to time the latency of HBase and store the + // timing in a moving Histogram (once we have a class for this). + + if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && + !config.enable_tsuid_tracking() && rt_publisher == null) { + return result; + } + + final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, + Const.TIMESTAMP_BYTES); + + // if the meta cache plugin is instantiated then tracking goes through it + if (meta_cache != null) { + meta_cache.increment(tsuid); } else { - final PutRequest tracking = new PutRequest(meta_table, tsuid, - TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); - client.put(tracking); + if (config.enable_tsuid_tracking()) { + if (config.enable_realtime_ts()) { + if (config.enable_tsuid_incrementing()) { + TSMeta.incrementAndGetCounter(TSDB.this, tsuid); + } else { + TSMeta.storeIfNecessary(TSDB.this, tsuid); + } + } else { + final PutRequest tracking = new PutRequest(meta_table, tsuid, + TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); + client.put(tracking); + } + } + } + + if (rt_publisher != null) { + rt_publisher.sinkDataPoint(metric, timestamp, value, tags, tsuid, flags); } + return result; + } + @Override + public String toString() { + return "addPointInternal Write Callback"; } } - - if (rt_publisher != null) { - rt_publisher.sinkDataPoint(metric, timestamp, value, tags, tsuid, flags); + + if (ts_filter != null && ts_filter.filterDataPoints()) { + return ts_filter.allowDataPoint(metric, timestamp, value, tags, flags) + .addCallbackDeferring(new WriteCB()); } - return result; + return Deferred.fromResult(true).addCallbackDeferring(new WriteCB()); } /** @@ -1078,6 +1187,16 @@ public Object call(ArrayList compactions) throws Exception { storage_exception_handler.getClass().getCanonicalName()); deferreds.add(storage_exception_handler.shutdown()); } + if (ts_filter != null) { + LOG.info("Shutting down time series filter plugin: " + + ts_filter.getClass().getCanonicalName()); + deferreds.add(ts_filter.shutdown()); + } + if (uid_filter != null) { + LOG.info("Shutting down UID filter plugin: " + + uid_filter.getClass().getCanonicalName()); + deferreds.add(uid_filter.shutdown()); + } // wait for plugins to shutdown before we close the client return deferreds.size() > 0 diff --git a/src/core/Tags.java b/src/core/Tags.java index 462196e152..6aa5cc2074 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -587,9 +587,9 @@ public static ArrayList resolveAll(final TSDB tsdb, */ public static Deferred> resolveAllAsync(final TSDB tsdb, final Map tags) { - return resolveAllInternalAsync(tsdb, tags, false); - } - + return resolveAllInternalAsync(tsdb, null, tags, false); + } + /** * Resolves (and creates, if necessary) all the tags (name=value) into the a * sorted byte arrays. @@ -626,7 +626,6 @@ static ArrayList resolveAllInternal(final TSDB tsdb, return tag_ids; } - /** * Resolves (and creates, if necessary) all the tags (name=value) into the a * sorted byte arrays. @@ -638,11 +637,28 @@ static ArrayList resolveAllInternal(final TSDB tsdb, */ static Deferred> resolveOrCreateAllAsync(final TSDB tsdb, final Map tags) { - return resolveAllInternalAsync(tsdb, tags, true); + return resolveAllInternalAsync(tsdb, null, tags, true); + } + + /** + * Resolves (and creates, if necessary) all the tags (name=value) into the a + * sorted byte arrays. + * @param tsdb The TSDB to use for UniqueId lookups. + * @param metric The metric associated with this tag set for filtering. + * @param tags The tags to resolve. If a new tag name or tag value is + * seen, it will be assigned an ID. + * @return an array of sorted tags (tag id, tag name). + * @since 2.3 + */ + static Deferred> + resolveOrCreateAllAsync(final TSDB tsdb, final String metric, + final Map tags) { + return resolveAllInternalAsync(tsdb, metric, tags, true); } private static Deferred> resolveAllInternalAsync(final TSDB tsdb, + final String metric, final Map tags, final boolean create) { final ArrayList> tag_ids = @@ -651,10 +667,10 @@ static ArrayList resolveAllInternal(final TSDB tsdb, // For each tag, start resolving the tag name and the tag value. for (final Map.Entry entry : tags.entrySet()) { final Deferred name_id = create - ? tsdb.tag_names.getOrCreateIdAsync(entry.getKey()) + ? tsdb.tag_names.getOrCreateIdAsync(entry.getKey(), metric, tags) : tsdb.tag_names.getIdAsync(entry.getKey()); final Deferred value_id = create - ? tsdb.tag_values.getOrCreateIdAsync(entry.getValue()) + ? tsdb.tag_values.getOrCreateIdAsync(entry.getValue(), metric, tags) : tsdb.tag_values.getIdAsync(entry.getValue()); // Then once the tag name is resolved, get the resolved tag value. diff --git a/src/core/WriteableDataPointFilterPlugin.java b/src/core/WriteableDataPointFilterPlugin.java new file mode 100644 index 0000000000..8a01159667 --- /dev/null +++ b/src/core/WriteableDataPointFilterPlugin.java @@ -0,0 +1,99 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.Map; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.stats.StatsCollector; + +/** + * A filter that can determine whether or not time series should be allowed + * assignment based on their metric and tags. This is useful for such + * situations as: + *
    • Enforcing naming standards
    • + *
    • Blacklisting certain names or properties
    • + *
    • Preventing cardinality explosions
    + * Note: Implementations must have a parameterless constructor. The + * {@link #initialize(TSDB)} method will be called immediately after the plugin is + * instantiated and before any other methods are called. + * @since 2.3 + */ +public abstract class WriteableDataPointFilterPlugin { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. "2.3.1". The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + + /** + * Determine whether or not the data point should be stored. + * If the data should not be stored, the implementation can return false or an + * exception in the deferred object. Otherwise it should return true and the + * data point will be written to storage. + * @param metric The metric name for the data point + * @param timestamp The timestamp of the data + * @param value The value encoded as either an integer or floating point value + * @param tags The tags associated with the data point + * @param flags Encoding flags for the value + * @return True if the data should be written, false if it should be rejected. + */ + public abstract Deferred allowDataPoint( + final String metric, + final long timestamp, + final byte[] value, + final Map tags, + final short flags); + + /** + * Whether or not the filter should process data points. + * @return False if {@link #allowDataPoint(String, long, byte[], Map, short)} + * should NOT be called, true if it should. + */ + public abstract boolean filterDataPoints(); +} diff --git a/src/uid/FailedToAssignUniqueIdException.java b/src/uid/FailedToAssignUniqueIdException.java index 7ffa1a999d..fb79e6355c 100644 --- a/src/uid/FailedToAssignUniqueIdException.java +++ b/src/uid/FailedToAssignUniqueIdException.java @@ -28,6 +28,23 @@ public FailedToAssignUniqueIdException(final String kind, final String name, this.attempts = attempts; } + /** + * CTor + * @param kind The kind of object that couldn't be assigned + * @param name The name of the object that couldn't be assigned + * @param attempts How many attempts were made to assign + * @param msg A message to append + * @since 2.3 + */ + public FailedToAssignUniqueIdException(final String kind, final String name, + final int attempts, final String msg) { + super("Failed to assign ID for kind='" + kind + "' name='" + + name + "' after " + attempts + " attempts due to: " + msg); + this.kind = kind; + this.name = name; + this.attempts = attempts; + } + /** * CTor * @param kind The kind of object that couldn't be assigned @@ -44,6 +61,7 @@ public FailedToAssignUniqueIdException(final String kind, final String name, this.attempts = attempts; } + /** @return Returns the kind of unique ID that couldn't be assigned. */ public String kind() { return kind; diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index bd218900a4..666190d007 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -126,8 +126,10 @@ public enum UniqueIdType { /** How many times we collided with an existing ID when attempting to * generate a new UID */ private volatile int random_id_collisions; - - /** Whether or not to generate new UIDMetas */ + /** How many times assignments have been rejected by the UID filter */ + private volatile int rejected_assignments; + + /** TSDB object used for filtering and/or meta generation. */ private TSDB tsdb; /** @@ -170,6 +172,34 @@ public UniqueId(final HBaseClient client, final byte[] table, final String kind, this.id_width = (short) width; this.randomize_id = randomize_id; } + + /** + * Constructor. + * @param tsdb The TSDB this UID object belongs to + * @param table The name of the HBase table to use. + * @param kind The kind of Unique ID this instance will deal with. + * @param width The number of bytes on which Unique IDs should be encoded. + * @param Whether or not to randomize new UIDs + * @throws IllegalArgumentException if width is negative or too small/large + * or if kind is an empty string. + * @since 2.3 + */ + public UniqueId(final TSDB tsdb, final byte[] table, final String kind, + final int width, final boolean randomize_id) { + this.client = tsdb.getClient(); + this.tsdb = tsdb; + this.table = table; + if (kind.isEmpty()) { + throw new IllegalArgumentException("Empty string as 'kind' argument!"); + } + this.kind = toBytes(kind); + type = stringToUniqueIdType(kind); + if (width < 1 || width > 8) { + throw new IllegalArgumentException("Invalid width: " + width); + } + this.id_width = (short) width; + this.randomize_id = randomize_id; + } /** The number of times we avoided reading from HBase thanks to the cache. */ public int cacheHits() { @@ -191,6 +221,11 @@ public int randomIdCollisions() { return random_id_collisions; } + /** Returns the number of UID assignments rejected by the filter */ + public int rejectedAssignments() { + return rejected_assignments; + } + public String kind() { return fromBytes(kind); } @@ -754,6 +789,25 @@ public Boolean checkNameIsValid(final String name) throws RuntimeException { * @since 1.2 */ public Deferred getOrCreateIdAsync(final String name) { + return getOrCreateIdAsync(name, null, null); + } + + /** + * Finds the ID associated with a given name or creates it. + *

    + * The length of the byte array is fixed in advance by the implementation. + * + * @param name The name to lookup in the table or to assign an ID to. + * @param metric Name of the metric associated with the UID for filtering. + * @param tags Tag set associated with the UID for filtering. + * @throws HBaseException if there is a problem communicating with HBase. + * @throws IllegalStateException if all possible IDs are already assigned. + * @throws IllegalStateException if the ID found in HBase is encoded on the + * wrong number of bytes. + * @since 2.3 + */ + public Deferred getOrCreateIdAsync(final String name, + final String metric, final Map tags) { // Look in the cache first. final byte[] id = getIdFromCache(name); if (id != null) { @@ -762,29 +816,62 @@ public Deferred getOrCreateIdAsync(final String name) { } // Not found in our cache, so look in HBase instead. + /** Triggers the assignment if allowed through the filter */ + class AssignmentAllowedCB implements Callback, Boolean> { + @Override + public Deferred call(final Boolean allowed) throws Exception { + if (!allowed) { + rejected_assignments++; + return Deferred.fromError(new FailedToAssignUniqueIdException( + new String(kind), name, 0, "Blocked by UID filter.")); + } + + Deferred assignment = null; + synchronized (pending_assignments) { + assignment = pending_assignments.get(name); + if (assignment == null) { + // to prevent UID leaks that can be caused when multiple time + // series for the same metric or tags arrive, we need to write a + // deferred to the pending map as quickly as possible. Then we can + // start the assignment process after we've stashed the deferred + // and released the lock + assignment = new Deferred(); + pending_assignments.put(name, assignment); + } else { + LOG.info("Already waiting for UID assignment: " + name); + return assignment; + } + } + + // start the assignment dance after stashing the deferred + if (metric != null && LOG.isDebugEnabled()) { + LOG.debug("Assigning UID for '" + name + "' of type '" + type + + "' for series '" + metric + ", " + tags + "'"); + } + + // start the assignment dance after stashing the deferred + return new UniqueIdAllocator(name, assignment).tryAllocate(); + } + @Override + public String toString() { + return "AssignmentAllowedCB"; + } + } + + /** Triggers an assignment (possibly through the filter) if the exception + * returned was a NoSuchUniqueName. */ class HandleNoSuchUniqueNameCB implements Callback { public Object call(final Exception e) { if (e instanceof NoSuchUniqueName) { - - Deferred assignment = null; - synchronized (pending_assignments) { - assignment = pending_assignments.get(name); - if (assignment == null) { - // to prevent UID leaks that can be caused when multiple time - // series for the same metric or tags arrive, we need to write a - // deferred to the pending map as quickly as possible. Then we can - // start the assignment process after we've stashed the deferred - // and released the lock - assignment = new Deferred(); - pending_assignments.put(name, assignment); - } else { - LOG.info("Already waiting for UID assignment: " + name); - return assignment; - } + if (tsdb != null && tsdb.getUidFilter() != null && + tsdb.getUidFilter().fillterUIDAssignments()) { + return tsdb.getUidFilter() + .allowUIDAssignment(type, name, metric, tags) + .addCallbackDeferring(new AssignmentAllowedCB()); + } else { + return Deferred.fromResult(true) + .addCallbackDeferring(new AssignmentAllowedCB()); } - - // start the assignment dance after stashing the deferred - return new UniqueIdAllocator(name, assignment).tryAllocate(); } return e; // Other unexpected exception, let it bubble up. } diff --git a/src/uid/UniqueIdFilterPlugin.java b/src/uid/UniqueIdFilterPlugin.java new file mode 100644 index 0000000000..b0fd0c5e2a --- /dev/null +++ b/src/uid/UniqueIdFilterPlugin.java @@ -0,0 +1,101 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.uid; + +import java.util.Map; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.uid.UniqueId.UniqueIdType; + +/** + * A filter that can determine whether or not UIDs should be allowed assignment + * based on their metric and tags. This is useful for such situations as: + *

    • Enforcing naming standards
    • + *
    • Blacklisting certain names or properties
    • + *
    • Preventing cardinality explosions
    + * Note: Implementations must have a parameterless constructor. The + * {@link #initialize(TSDB)} method will be called immediately after the plugin is + * instantiated and before any other methods are called. + * @since 2.3 + */ +public abstract class UniqueIdFilterPlugin { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. "2.3.1". The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + + /** + * Determine whether or not the UID should be assigned. + * If the UID should not be assigned a value, the implementation can return + * false or an exception in the deferred object. Otherwise it should return + * true and the UID will proceed with assignment. + * NOTE: In some cases the metric and tags may be null, particularly if the + * synchronous APIs were called. In such a situation, make sure the + * implementation handles it properly. + * @param type The type of UID being assigned + * @param value The string value of the UID + * @param metric The metric name associated with the UID for assignment + * @param tags The tag set associated with the UID + * @return True if the UID should be assigned, false if not. + */ + public abstract Deferred allowUIDAssignment( + final UniqueIdType type, + final String value, + final String metric, + final Map tags); + + /** + * Whether or not the filter should process UIDs. + * @return True if {@link #allowUIDAssignment(UniqueIdType, String, String, Map)} + * should be called, false if not. + */ + public abstract boolean fillterUIDAssignments(); +} diff --git a/src/utils/Config.java b/src/utils/Config.java index 5a61be5359..96be913863 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -572,6 +572,8 @@ protected void setDefaults() { default_map.put("tsd.storage.compaction.min_flush_threshold", "100"); default_map.put("tsd.storage.compaction.max_concurrent_flushes", "10000"); default_map.put("tsd.storage.compaction.flush_speed", "2"); + default_map.put("tsd.timeseriesfilter.enable", "false"); + default_map.put("tsd.uidfilter.enable", "false"); default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.query.allow_delete", "false"); diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 2f83b61243..7c5f209abb 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -674,4 +674,16 @@ public boolean continuePausedTask() { } } } + + /** + * A little class used to throw a very specific type of exception for matching + * in Unit Tests. + */ + public static class UnitTestException extends RuntimeException { + public UnitTestException() { } + public UnitTestException(final String msg) { + super(msg); + } + private static final long serialVersionUID = -4404095849459619922L; + } } \ No newline at end of file diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 91a97ec96e..64c4e64738 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -28,7 +28,6 @@ import net.opentsdb.utils.Config; import org.hbase.async.AtomicIncrementRequest; -import org.hbase.async.Bytes; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; @@ -37,11 +36,9 @@ 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 org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @@ -453,632 +450,6 @@ public void uidTable() { assertArrayEquals("tsdb-uid".getBytes(), tsdb.uidTable()); } - @Test - public void addPointLong1Byte() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointLong1ByteNegative() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, -42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(-42, value[0]); - } - - @Test - public void addPointLong2Bytes() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 257, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); - assertNotNull(value); - assertEquals(257, Bytes.getShort(value)); - } - - @Test - public void addPointLong2BytesNegative() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, -257, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); - assertNotNull(value); - assertEquals(-257, Bytes.getShort(value)); - } - - @Test - public void addPointLong4Bytes() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 65537, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); - assertNotNull(value); - assertEquals(65537, Bytes.getInt(value)); - } - - @Test - public void addPointLong4BytesNegative() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, -65537, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); - assertNotNull(value); - assertEquals(-65537, Bytes.getInt(value)); - } - - @Test - public void addPointLong8Bytes() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 4294967296L, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); - assertNotNull(value); - assertEquals(4294967296L, Bytes.getLong(value)); - } - - @Test - public void addPointLong8BytesNegative() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, -4294967296L, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); - assertNotNull(value); - assertEquals(-4294967296L, Bytes.getLong(value)); - } - - @Test - public void addPointLongMs() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointLongMany() throws Exception { - setupAddPointStorage(); - - long timestamp = 1356998400; - for (int i = 1; i <= 50; i++) { - tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); - } - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(1, value[0]); - assertEquals(50, storage.numColumns(row)); - } - - @Test - public void addPointLongManyMs() throws Exception { - setupAddPointStorage(); - - long timestamp = 1356998400500L; - for (int i = 1; i <= 50; i++) { - tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); - } - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); - assertNotNull(value); - assertEquals(1, value[0]); - assertEquals(50, storage.numColumns(row)); - } - - @Test - public void addPointLongEndOfRow() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1357001999, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, - (byte) 0xF0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointLongOverwrite() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998400, 24, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(24, value[0]); - } - - @Test (expected = NoSuchUniqueName.class) - public void addPointNoAutoMetric() throws Exception { - setupAddPointStorage(); - tsdb.addPoint(NSUN_METRIC, 1356998400, 42, tags).joinUninterruptibly(); - } - - @Test - public void addPointSecondZero() throws Exception { - // Thu, 01 Jan 1970 00:00:00 GMT - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 0, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointSecondOne() throws Exception { - // hey, it's valid *shrug* Thu, 01 Jan 1970 00:00:01 GMT - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 16 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointSecond2106() throws Exception { - // Sun, 07 Feb 2106 06:28:15 GMT - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 4294967295L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, - 0x60, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0x69, (byte) 0xF0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test (expected = IllegalArgumentException.class) - public void addPointSecondNegative() throws Exception { - // Fri, 13 Dec 1901 20:45:52 GMT - // may support in the future, but 1.0 didn't - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, -2147483648, 42, tags).joinUninterruptibly(); - } - - @Test (expected = IllegalArgumentException.class) - public void emptyTagValue() throws Exception { - setupAddPointStorage(); - - tags.put(TAGK_STRING, ""); - tsdb.addPoint(METRIC_STRING, 1234567890, 42, tags).joinUninterruptibly(); - } - - @Test - public void addPointMS1970() throws Exception { - // Since it's just over Integer.MAX_VALUE, OpenTSDB will treat this as - // a millisecond timestamp since it doesn't fit in 4 bytes. - // Base time is 4294800 which is Thu, 19 Feb 1970 17:00:00 GMT - // offset = F0A36000 or 167296 ms - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 4294967296L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0, (byte) 0x41, (byte) 0x88, - (byte) 0x90, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, - (byte) 0xA3, 0x60, 0}); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointMS2106() throws Exception { - // Sun, 07 Feb 2106 06:28:15.000 GMT - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 4294967295000L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, - 0x60, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF6, - (byte) 0x77, 0x46, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointMS2286() throws Exception { - // It's an artificial limit and more thought needs to be put into it - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 9999999999999L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, (byte) 0x54, (byte) 0x0B, (byte) 0xD9, - 0x10, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xFA, - (byte) 0xAE, 0x5F, (byte) 0xC0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test (expected = IllegalArgumentException.class) - public void addPointMSTooLarge() throws Exception { - // It's an artificial limit and more thought needs to be put into it - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 10000000000000L, 42, tags).joinUninterruptibly(); - } - - @Test (expected = IllegalArgumentException.class) - public void addPointMSNegative() throws Exception { - // Fri, 13 Dec 1901 20:45:52 GMT - // may support in the future, but 1.0 didn't - setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint(METRIC_STRING, -2147483648000L, 42, tags).joinUninterruptibly(); - } - - @Test - public void addPointFloat() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointFloatNegative() throws Exception { - setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint(METRIC_STRING, 1356998400, -42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(-42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointFloatMs() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - new byte[] { (byte) 0xF0, 0, 0x7D, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointFloatEndOfRow() throws Exception { - setupAddPointStorage(); - HashMap tags = new HashMap(1); - tags.put("host", "web01"); - tsdb.addPoint(METRIC_STRING, 1357001999, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, - (byte) 0xFB }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointFloatPrecision() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42.5123459999F, tags) - .joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42.512345F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointFloatOverwrite() throws Exception { - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998400, 25.4F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(25.4F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointBothSameTimeIntAndFloat() throws Exception { - // this is an odd situation that can occur if the user puts an int and then - // a float (or vice-versa) with the same timestamp. What happens in the - // aggregators when this occurs? - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertEquals(2, storage.numColumns(row)); - assertNotNull(value); - assertEquals(42, value[0]); - value = storage.getColumn(row, new byte[] { 0, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointBothSameTimeIntAndFloatMs() throws Exception { - // this is an odd situation that can occur if the user puts an int and then - // a float (or vice-versa) with the same timestamp. What happens in the - // aggregators when this occurs? - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); - assertEquals(2, storage.numColumns(row)); - assertNotNull(value); - assertEquals(42, value[0]); - value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 11 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); - } - - @Test - public void addPointBothSameTimeSecondAndMs() throws Exception { - // this can happen if a second and an ms data point are stored for the same - // timestamp. - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998400000L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertEquals(2, storage.numColumns(row)); - assertNotNull(value); - assertEquals(42, value[0]); - value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0, 0 }); - assertNotNull(value); - // should have 7 digits of precision - assertEquals(42, value[0]); - } - - @Test - public void addPointWithSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointWithSaltDifferentTags() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - - setupAddPointStorage(); - tags.put(TAGK_STRING, TAGV_B_STRING); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 9, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 2}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointWithSaltDifferentTime() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1359680400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x51, (byte) 0x0B, 0x13, - (byte) 0x90, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointAppend() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42 }, value); - } - - @Test - public void addPointAppendWithOffset() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998430, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 1, -32, 42 }, value); - } - - @Test - public void addPointAppendAppending() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); - } - - @Test - public void addPointAppendAppendingOutOfOrder() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42, 3, -64, 1, 1, -32, 24 }, value); - } - - @Test - public void addPointAppendAppendingDuplicates() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 1, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 1, -32, 1 }, value); - } - - @Test - public void addPointAppendMS() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400050L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { (byte) 0xF0, 0, 12, -128, 42 }, value); - } - - @Test - public void addPointAppendAppendingMixMS() throws Exception { - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998400050L, 1, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { - 0, 0, 42, (byte) 0xF0, 0, 12, -128, 1, 1, -32, 24 }, value); - } - - @Test - public void addPointAppendWithSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42 }, value); - } - - @Test - public void addPointAppendAppendingWithSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - Whitebox.setInternalState(config, "enable_appends", true); - setupAddPointStorage(); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); - } - /** * Helper to mock the UID caches with valid responses */ diff --git a/test/core/TestTSDBAddPoint.java b/test/core/TestTSDBAddPoint.java new file mode 100644 index 0000000000..6e16f08622 --- /dev/null +++ b/test/core/TestTSDBAddPoint.java @@ -0,0 +1,690 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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 static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyShort; +import static org.mockito.Matchers.eq; +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.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; + +import org.hbase.async.Bytes; +import org.junit.Before; +import org.junit.Test; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.uid.NoSuchUniqueName; + +public class TestTSDBAddPoint extends BaseTsdbTest { + + @Before + public void beforeLocal() throws Exception { + setDataPointStorage(); + } + + @Test + public void addPointLong1Byte() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointLong1ByteNegative() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, -42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(-42, value[0]); + } + + @Test + public void addPointLong2Bytes() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 257, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); + assertNotNull(value); + assertEquals(257, Bytes.getShort(value)); + } + + @Test + public void addPointLong2BytesNegative() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, -257, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); + assertNotNull(value); + assertEquals(-257, Bytes.getShort(value)); + } + + @Test + public void addPointLong4Bytes() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 65537, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); + assertNotNull(value); + assertEquals(65537, Bytes.getInt(value)); + } + + @Test + public void addPointLong4BytesNegative() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, -65537, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); + assertNotNull(value); + assertEquals(-65537, Bytes.getInt(value)); + } + + @Test + public void addPointLong8Bytes() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 4294967296L, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); + assertNotNull(value); + assertEquals(4294967296L, Bytes.getLong(value)); + } + + @Test + public void addPointLong8BytesNegative() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, -4294967296L, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); + assertNotNull(value); + assertEquals(-4294967296L, Bytes.getLong(value)); + } + + @Test + public void addPointLongMs() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointLongMany() throws Exception { + long timestamp = 1356998400; + for (int i = 1; i <= 50; i++) { + tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); + } + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(1, value[0]); + assertEquals(50, storage.numColumns(row)); + } + + @Test + public void addPointLongManyMs() throws Exception { + long timestamp = 1356998400500L; + for (int i = 1; i <= 50; i++) { + tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); + } + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); + assertNotNull(value); + assertEquals(1, value[0]); + assertEquals(50, storage.numColumns(row)); + } + + @Test + public void addPointLongEndOfRow() throws Exception { + tsdb.addPoint(METRIC_STRING, 1357001999, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, + (byte) 0xF0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointLongOverwrite() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, 24, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(24, value[0]); + } + + @Test (expected = NoSuchUniqueName.class) + public void addPointNoAutoMetric() throws Exception { + tsdb.addPoint(NSUN_METRIC, 1356998400, 42, tags).joinUninterruptibly(); + } + + @Test + public void addPointSecondZero() throws Exception { + // Thu, 01 Jan 1970 00:00:00 GMT + tsdb.addPoint(METRIC_STRING, 0, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointSecondOne() throws Exception { + // hey, it's valid *shrug* Thu, 01 Jan 1970 00:00:01 GMT + tsdb.addPoint(METRIC_STRING, 1, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 16 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointSecond2106() throws Exception { + // Sun, 07 Feb 2106 06:28:15 GMT + tsdb.addPoint(METRIC_STRING, 4294967295L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, + 0x60, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0x69, (byte) 0xF0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test (expected = IllegalArgumentException.class) + public void addPointSecondNegative() throws Exception { + // Fri, 13 Dec 1901 20:45:52 GMT + // may support in the future, but 1.0 didn't + tsdb.addPoint(METRIC_STRING, -2147483648, 42, tags).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void emptyTagValue() throws Exception { + tags.put(TAGK_STRING, ""); + tsdb.addPoint(METRIC_STRING, 1234567890, 42, tags).joinUninterruptibly(); + } + + @Test + public void addPointMS1970() throws Exception { + // Since it's just over Integer.MAX_VALUE, OpenTSDB will treat this as + // a millisecond timestamp since it doesn't fit in 4 bytes. + // Base time is 4294800 which is Thu, 19 Feb 1970 17:00:00 GMT + // offset = F0A36000 or 167296 ms + tsdb.addPoint(METRIC_STRING, 4294967296L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0, (byte) 0x41, (byte) 0x88, + (byte) 0x90, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, + (byte) 0xA3, 0x60, 0}); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointMS2106() throws Exception { + // Sun, 07 Feb 2106 06:28:15.000 GMT + tsdb.addPoint(METRIC_STRING, 4294967295000L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, + 0x60, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF6, + (byte) 0x77, 0x46, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointMS2286() throws Exception { + // It's an artificial limit and more thought needs to be put into it + tsdb.addPoint(METRIC_STRING, 9999999999999L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, (byte) 0x54, (byte) 0x0B, (byte) 0xD9, + 0x10, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xFA, + (byte) 0xAE, 0x5F, (byte) 0xC0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test (expected = IllegalArgumentException.class) + public void addPointMSTooLarge() throws Exception { + // It's an artificial limit and more thought needs to be put into it + tsdb.addPoint(METRIC_STRING, 10000000000000L, 42, tags).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addPointMSNegative() throws Exception { + // Fri, 13 Dec 1901 20:45:52 GMT + // may support in the future, but 1.0 didn't + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + tsdb.addPoint(METRIC_STRING, -2147483648000L, 42, tags).joinUninterruptibly(); + } + + @Test + public void addPointFloat() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointFloatNegative() throws Exception { + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + tsdb.addPoint(METRIC_STRING, 1356998400, -42.5F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(-42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointFloatMs() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + new byte[] { (byte) 0xF0, 0, 0x7D, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointFloatEndOfRow() throws Exception { + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + tsdb.addPoint(METRIC_STRING, 1357001999, 42.5F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, + (byte) 0xFB }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointFloatPrecision() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5123459999F, tags) + .joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42.512345F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointFloatOverwrite() throws Exception { + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, 25.4F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(25.4F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointBothSameTimeIntAndFloat() throws Exception { + // this is an odd situation that can occur if the user puts an int and then + // a float (or vice-versa) with the same timestamp. What happens in the + // aggregators when this occurs? + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertEquals(2, storage.numColumns(row)); + assertNotNull(value); + assertEquals(42, value[0]); + value = storage.getColumn(row, new byte[] { 0, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointBothSameTimeIntAndFloatMs() throws Exception { + // this is an odd situation that can occur if the user puts an int and then + // a float (or vice-versa) with the same timestamp. What happens in the + // aggregators when this occurs? + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); + assertEquals(2, storage.numColumns(row)); + assertNotNull(value); + assertEquals(42, value[0]); + value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 11 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42.5F, Float.intBitsToFloat(Bytes.getInt(value)), 0.0000001); + } + + @Test + public void addPointBothSameTimeSecondAndMs() throws Exception { + // this can happen if a second and an ms data point are stored for the same + // timestamp. + tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400000L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertEquals(2, storage.numColumns(row)); + assertNotNull(value); + assertEquals(42, value[0]); + value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0, 0 }); + assertNotNull(value); + // should have 7 digits of precision + assertEquals(42, value[0]); + } + + @Test + public void addPointWithSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointWithSaltDifferentTags() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + tags.put(TAGK_STRING, TAGV_B_STRING); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 9, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 2}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointWithSaltDifferentTime() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + tsdb.addPoint(METRIC_STRING, 1359680400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x51, (byte) 0x0B, 0x13, + (byte) 0x90, 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + } + + @Test + public void addPointAppend() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42 }, value); + } + + @Test + public void addPointAppendWithOffset() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998430, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 1, -32, 42 }, value); + } + + @Test + public void addPointAppendAppending() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); + } + + @Test + public void addPointAppendAppendingOutOfOrder() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 3, -64, 1, 1, -32, 24 }, value); + } + + @Test + public void addPointAppendAppendingDuplicates() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 1, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 1, -32, 1 }, value); + } + + @Test + public void addPointAppendMS() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400050L, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { (byte) 0xF0, 0, 12, -128, 42 }, value); + } + + @Test + public void addPointAppendAppendingMixMS() throws Exception { + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998400050L, 1, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { + 0, 0, 42, (byte) 0xF0, 0, 12, -128, 1, 1, -32, 24 }, value); + } + + @Test + public void addPointAppendWithSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42 }, value); + } + + @Test + public void addPointAppendAppendingWithSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + Whitebox.setInternalState(config, "enable_appends", true); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, + AppendDataPoints.APPEND_COLUMN_QUALIFIER); + assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); + } + + @Test + public void dpFilterOK() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenReturn(Deferred.fromResult(true)); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNotNull(value); + assertEquals(42, value[0]); + + verify(filter, times(1)).filterDataPoints(); + verify(filter, times(1)).allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort()); + } + + @Test + public void dpFilterBlocked() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenReturn(Deferred.fromResult(false)); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNull(value); + + verify(filter, times(1)).filterDataPoints(); + verify(filter, times(1)).allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort()); + } + + @Test + public void dpFilterReturnsException() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenReturn(Deferred.fromError(new UnitTestException("Boo!"))); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + final Deferred deferred = + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags); + try { + deferred.join(); + fail("Expected an UnitTestException"); + } catch (UnitTestException e) { }; + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNull(value); + + verify(filter, times(1)).filterDataPoints(); + verify(filter, times(1)).allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort()); + } + + @Test + public void uidFilterThrowsException() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenThrow(new UnitTestException("Boo!")); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + try { + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags); + fail("Expected an UnitTestException"); + } catch (UnitTestException e) { }; + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); + assertNull(value); + + verify(filter, times(1)).filterDataPoints(); + verify(filter, times(1)).allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort()); + } +} diff --git a/test/core/TestTags.java b/test/core/TestTags.java index 3618c1ac7f..f6bea0e828 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -24,6 +24,7 @@ import net.opentsdb.query.filter.TagVRegexFilter; import net.opentsdb.query.filter.TagVWildcardFilter; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.FailedToAssignUniqueIdException; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; @@ -45,6 +46,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -52,6 +54,9 @@ 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.Matchers.anyMapOf; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -756,6 +761,32 @@ public void resolveOrCreateTagvNotAllowedBlocked() throws Exception { Tags.resolveOrCreateAll(tsdb, tags); } + @Test + public void resolveOrCreateAllAsync() throws Exception { + setupStorage(); + setupResolveAll(); + + final Map tags = new HashMap(1); + tags.put("host", "nohost"); + final List uids = Tags.resolveOrCreateAllAsync(tsdb, "metric", tags).join(); + assertEquals(1, uids.size()); + assertArrayEquals(new byte[] { 0, 0, 1, 0, 0, 3}, uids.get(0)); + } + + @Test (expected = DeferredGroupException.class) + public void resolveOrCreateAllAsyncFilterBlocked() throws Exception { + setupStorage(); + setupResolveAll(); + when(tag_names.getOrCreateIdAsync(eq("host"), anyString(), + anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromError(new FailedToAssignUniqueIdException( + "tagk", "host", 0, "Blocked by UID filter."))); + + final Map tags = new HashMap(1); + tags.put("host", "nohost"); + Tags.resolveOrCreateAllAsync(tsdb, "metric", tags).join(); + } + // PRIVATE helpers to setup unit tests private void setupStorage() throws Exception { @@ -799,16 +830,30 @@ private void setupResolveIds() throws Exception { } private void setupResolveAll() throws Exception { - when(tag_names.getOrCreateId("host")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_names.getOrCreateId("doesnotexist")) + when(tag_names.getOrCreateId(eq("host"))) + .thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getOrCreateIdAsync(eq("host"), anyString(), + anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_names.getOrCreateId(eq("doesnotexist"))) .thenReturn(new byte[] { 0, 0, 3 }); + when(tag_names.getOrCreateIdAsync(eq("doesnotexist"), anyString(), + anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); when(tag_names.getId("pop")).thenReturn(new byte[] { 0, 0, 2 }); when(tag_names.getId("nonesuch")) .thenThrow(new NoSuchUniqueName("tagv", "nonesuch")); - when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] { 0, 0, 1 }); - when(tag_values.getOrCreateId("nohost")) - .thenReturn(new byte[] { 0, 0, 3 }); + when(tag_values.getOrCreateId(eq("web01"))) + .thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getOrCreateIdAsync(eq("web01"), anyString(), + anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 1 })); + when(tag_values.getOrCreateId(eq("nohost"))) + .thenReturn(new byte[] { 0, 0, 3 }); + when(tag_values.getOrCreateIdAsync(eq("nohost"), anyString(), + anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(new byte[] { 0, 0, 3 })); when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); when(tag_values.getId("invalidhost")) .thenThrow(new NoSuchUniqueName("tagk", "invalidhost")); diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index d0990bdd77..4423b3dd65 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -22,7 +22,9 @@ import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; +import net.opentsdb.core.BaseTsdbTest.UnitTestException; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; import org.hbase.async.AtomicIncrementRequest; @@ -44,6 +46,8 @@ import static org.junit.Assert.*; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.eq; @@ -372,6 +376,127 @@ public void getOrCreateIdAssignWhitelistedIdWithFailedWhitelist() { verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); } + @Test + public void getOrCreateIdAsyncAssignFilterOK() throws Exception { + uid = new UniqueId(client, table, METRIC, 3); + final byte[] id = { 0, 0, 5 }; + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(true)); + when(tsdb.getUidFilter()).thenReturn(filter); + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + // Watch this! ______,^ I'm writing C++ in Java! + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + assertArrayEquals(id, uid.getOrCreateIdAsync("foo").join()); + // Should be a cache hit since we created that entry. + assertArrayEquals(id, uid.getOrCreateIdAsync("foo").join()); + // Should be a cache hit too for the same reason. + assertEquals("foo", uid.getName(id)); + + verify(client).get(anyGet()); // Initial Get. + verify(client).atomicIncrement(incrementForRow(MAXID)); + // Reverse + forward mappings. + verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); + verify(filter, times(1)).allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class)); + } + + @Test (expected = FailedToAssignUniqueIdException.class) + public void getOrCreateIdAssignFilterBlocked() throws Exception { + uid = new UniqueId(client, table, METRIC, 3); + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(false)); + when(tsdb.getUidFilter()).thenReturn(filter); + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + uid.getOrCreateIdAsync("foo").join(); + } + + @Test (expected = UnitTestException.class) + public void getOrCreateIdAssignFilterReturnException() throws Exception { + uid = new UniqueId(client, table, METRIC, 3); + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromError(new UnitTestException())); + when(tsdb.getUidFilter()).thenReturn(filter); + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + uid.getOrCreateIdAsync("foo").join(); + } + + @Test (expected = UnitTestException.class) + public void getOrCreateIdAssignFilterThrowsException() throws Exception { + uid = new UniqueId(client, table, METRIC, 3); + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(),anyMapOf(String.class, String.class))) + .thenThrow(new UnitTestException()); + when(tsdb.getUidFilter()).thenReturn(filter); + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + uid.getOrCreateIdAsync("foo").join(); + } + @Test // Test the creation of an ID with no problem. public void checkMetricAgainstWhitelist() { setupWhitelists(METRIC); @@ -954,7 +1079,6 @@ public void getTagPairsFromTSUIDString() { assertArrayEquals(new byte[] { 0, 0, 3, 0, 0, 4 }, tags.get(1)); } - @Test public void getTagPairsFromTSUIDStringNonStandardWidth() { PowerMockito.mockStatic(TSDB.class); @@ -1010,7 +1134,6 @@ public void getTagPairsFromTSUIDBytes() { assertArrayEquals(new byte[] { 0, 0, 3, 0, 0, 4 }, tags.get(1)); } - @Test public void getTagPairsFromTSUIDBytesNonStandardWidth() { PowerMockito.mockStatic(TSDB.class); From 7b3db064a1179c6a73a56b145e4ac224754ba213 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 22 Apr 2016 20:57:31 -0700 Subject: [PATCH 493/826] Rollback 5747796c2a4398c43b332a165fbf39fb30f1d2a8 as we'll move the whitelist functionality into the new UniqueIdFilter class. Signed-off-by: Chris Larsen --- src/opentsdb.conf | 16 ---- src/tools/CliOptions.java | 11 --- src/uid/UniqueId.java | 69 +++++----------- src/utils/Config.java | 43 +--------- test/uid/TestUniqueId.java | 162 ++++++++++++++----------------------- 5 files changed, 80 insertions(+), 221 deletions(-) diff --git a/src/opentsdb.conf b/src/opentsdb.conf index 47c64c10a4..aa9cf9bffb 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -38,22 +38,6 @@ tsd.http.cachedir = # is False #tsd.core.auto_create_metrics = false -# Whether or no to evaluate new metric/tagk/tagv items against a whitelist, default -# is False -# tsd.core.auto_create_whitelist = false - -# Comma-Delimited list of regex patterns to match against new metric names, default -# is .*, examples might be ^awesome\..*$,^regexfoo[0-9].*$ -#tsd.core.auto_create_metrics_patterns = .* - -# Comma-Delimited list of regex patterns to match against new tagk names, default -# is .*, examples might be ^awesome\..*$,^regexfoo[0-9].*$ -#tsd.core.auto_create_tagk_patterns = .* - -# Comma-Delimited list of regex patterns to match against new tagv names, default -# is .*, examples might be ^awesome\..*$,^regexfoo[0-9].*$ -#tsd.core.auto_create_tagv_patterns = .* - # --------- STORAGE ---------- # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index 2e55a7470d..377225ccef 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -120,17 +120,6 @@ static void overloadConfig(final ArgP argp, final Config config) { // map the overrides if (entry.getKey().toLowerCase().equals("--auto-metric")) { config.overrideConfig("tsd.core.auto_create_metrics", "true"); - } else if (entry.getKey().toLowerCase().equals("--auto-metric-whitelist")) { - config.overrideConfig("tsd.core.auto_create_whitelist", "true"); - } else if (entry.getKey().toLowerCase().equals("--auto-metric-pattern")) { - config.overrideConfig("tsd.core.auto_create_metrics_patterns", - entry.getValue()); - } else if (entry.getKey().toLowerCase().equals("--auto-tagk-pattern")) { - config.overrideConfig("tsd.core.auto_create_tagk_patterns", - entry.getValue()); - } else if (entry.getKey().toLowerCase().equals("--auto-tagv-pattern")) { - config.overrideConfig("tsd.core.auto_create_tagv_patterns", - entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--table")) { config.overrideConfig("tsd.storage.hbase.data_table", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--uidtable")) { diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 666190d007..9b218e2e00 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; import javax.xml.bind.DatatypeConverter; @@ -237,10 +236,6 @@ public short width() { /** @param tsdb Whether or not to track new UIDMeta objects */ public void setTSDB(final TSDB tsdb) { this.tsdb = tsdb; - this.useWhitelist = tsdb.getConfig().auto_whitelist(); - this.auto_metric_patterns = tsdb.getConfig().auto_metric_patterns(); - this.auto_tagk_patterns = tsdb.getConfig().auto_tagk_patterns(); - this.auto_tagv_patterns = tsdb.getConfig().auto_tagv_patterns(); } /** The largest possible ID given the number of bytes the IDs are @@ -684,10 +679,25 @@ public byte[] getOrCreateId(final String name) throws HBaseException { try { return getIdAsync(name).joinUninterruptibly(); } catch (NoSuchUniqueName e) { - if (this.useWhitelist && !checkNameIsValid(name)) { - LOG.info("UID cannot be assigned, name is not acceptable because it fails to match the whitelist: " + name); - throw new RuntimeException("UID cannot be assigned, name is not acceptable because it fails to match the whitelist: " + name); + if (tsdb != null && tsdb.getUidFilter() != null && + tsdb.getUidFilter().fillterUIDAssignments()) { + try { + if (!tsdb.getUidFilter().allowUIDAssignment(type, name, null, null) + .join()) { + rejected_assignments++; + throw new FailedToAssignUniqueIdException(new String(kind), name, 0, + "Blocked by UID filter."); + } + } catch (FailedToAssignUniqueIdException e1) { + throw e1; + } catch (InterruptedException e1) { + LOG.error("Interrupted", e1); + Thread.currentThread().interrupt(); + } catch (Exception e1) { + throw new RuntimeException("Should never be here", e1); + } } + Deferred assignment = null; boolean pending = false; synchronized (pending_assignments) { @@ -734,48 +744,7 @@ public byte[] getOrCreateId(final String name) throws HBaseException { throw new RuntimeException("Should never be here", e); } } - - /** - * Checks to see if the provided string matches the acceptable - * patterns from the configuration. - *

    - * - * @param name The name to compare to the acceptable name regexes - * @return - */ - public Boolean checkNameIsValid(final String name) throws RuntimeException { - final List rxs = new ArrayList(); - try { - String uid_patterns; - switch (type) { - case METRIC: uid_patterns = this.auto_metric_patterns; - break; - case TAGK: uid_patterns = this.auto_tagk_patterns; - break; - case TAGV: uid_patterns = this.auto_tagv_patterns; - break; - default: - throw new RuntimeException("Should never be here"); - } - String[] patterns = uid_patterns.split(","); - - for (String pattern : patterns) { - rxs.add(Pattern.compile(pattern)); - } - - for (Pattern rx : rxs) { - if (rx.matcher(name).matches()) { - LOG.debug("Accepted name for UID: " + name + " based on '" + rx.toString() + "'"); - return true; - } - } - LOG.debug("Rejected name for UID: " + name); - return false; - } catch (Exception e) { - throw new RuntimeException("Failed to check name (" + name + ") against patterns.", e); - } - } - + /** * Finds the ID associated with a given name or creates it. *

    diff --git a/src/utils/Config.java b/src/utils/Config.java index 96be913863..a8e451723c 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -69,20 +69,7 @@ public class Config { /** tsd.core.auto_create_tagv */ private boolean auto_tagv = true; - - /** tsd.core.auto_create_whitelist */ - private boolean auto_whitelist = false; - - /** tsd.core.auto_create_metrics_patterns */ - private String auto_metric_patterns = ".*"; - - /** tsd.core.auto_create_tagk_patterns */ - private String auto_tagk_patterns = ".*"; - - /** tsd.core.auto_create_tagv_patterns */ - private String auto_tagv_patterns = ".*"; - - + /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; @@ -196,25 +183,7 @@ public boolean auto_tagk() { public boolean auto_tagv() { return auto_tagv; } - - /** @return the auto_whitelist value */ - public boolean auto_whitelist() { return auto_whitelist; } - - /** @return the auto_metric value */ - public String auto_metric_patterns() { - return auto_metric_patterns; - } - - /** @return the auto_tagk value */ - public String auto_tagk_patterns() { - return auto_tagk_patterns; - } - - /** @return the auto_tagv value */ - public String auto_tagv_patterns() { - return auto_tagv_patterns; - } - + /** @param auto_metric whether or not to auto create metrics */ public void setAutoMetric(boolean auto_metric) { this.auto_metric = auto_metric; @@ -527,10 +496,6 @@ protected void setDefaults() { default_map.put("tsd.core.auto_create_metrics", "false"); default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); - default_map.put("tsd.core.auto_create_whitelist", "false"); - default_map.put("tsd.core.auto_create_metrics_patterns", ".*"); - default_map.put("tsd.core.auto_create_tagk_patterns", ".*"); - default_map.put("tsd.core.auto_create_tagv_patterns", ".*"); default_map.put("tsd.core.connections.limit", "0"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); @@ -681,10 +646,6 @@ protected void loadStaticVariables() { auto_metric = this.getBoolean("tsd.core.auto_create_metrics"); auto_tagk = this.getBoolean("tsd.core.auto_create_tagks"); auto_tagv = this.getBoolean("tsd.core.auto_create_tagvs"); - auto_whitelist = this.getBoolean("tsd.core.auto_create_whitelist"); - auto_metric_patterns = this.getString("tsdb.core.auto_create_metrics_patterns"); - auto_tagk_patterns = this.getString("tsdb.core.auto_create_tagk_patterns"); - auto_tagv_patterns = this.getString("tsdb.core.auto_create_tagv_patterns"); enable_compactions = this.getBoolean("tsd.storage.enable_compaction"); enable_appends = this.getBoolean("tsd.storage.enable_appends"); repair_appends = this.getBoolean("tsd.storage.repair_appends"); diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index 4423b3dd65..0b0393f855 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -266,18 +266,20 @@ public void getOrCreateIdWithExistingId() { } @Test // Test the creation of an ID with no problem. - public void getOrCreateIdAssignIdWithSuccess() { + public void getOrCreateIdAssignFilterOK() { uid = new UniqueId(client, table, METRIC, 3); final byte[] id = { 0, 0, 5 }; final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); - when(config.auto_whitelist()).thenReturn(false); - when(config.auto_metric_patterns()).thenReturn(".*"); - when(config.auto_tagk_patterns()).thenReturn(".*"); - when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(true)); + when(tsdb.getUidFilter()).thenReturn(filter); when(client.get(anyGet())) // null => ID doesn't exist. .thenReturn(Deferred.>fromResult(null)); @@ -300,22 +302,25 @@ public void getOrCreateIdAssignIdWithSuccess() { verify(client).atomicIncrement(incrementForRow(MAXID)); // Reverse + forward mappings. verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); + verify(filter, times(1)).allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class)); } - @Test // Test the creation of an ID with no problem. - public void getOrCreateIdAssignWhitelistedIdWithSuccess() { + @Test (expected = FailedToAssignUniqueIdException.class) + public void getOrCreateIdAssignFilterBlocked() { uid = new UniqueId(client, table, METRIC, 3); - final byte[] id = { 0, 0, 5 }; final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); - when(config.auto_whitelist()).thenReturn(true); - when(config.auto_metric_patterns()).thenReturn(".*"); - when(config.auto_tagk_patterns()).thenReturn(".*"); - when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); - + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromResult(false)); + when(tsdb.getUidFilter()).thenReturn(filter); + when(client.get(anyGet())) // null => ID doesn't exist. .thenReturn(Deferred.>fromResult(null)); // Watch this! ______,^ I'm writing C++ in Java! @@ -327,31 +332,23 @@ public void getOrCreateIdAssignWhitelistedIdWithSuccess() { .thenReturn(Deferred.fromResult(true)) .thenReturn(Deferred.fromResult(true)); - assertArrayEquals(id, uid.getOrCreateId("foo")); - // Should be a cache hit since we created that entry. - assertArrayEquals(id, uid.getOrCreateId("foo")); - // Should be a cache hit too for the same reason. - assertEquals("foo", uid.getName(id)); - - verify(client).get(anyGet()); // Initial Get. - verify(client).atomicIncrement(incrementForRow(MAXID)); - // Reverse + forward mappings. - verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); + uid.getOrCreateId("foo"); } - @Test(expected=RuntimeException.class) - public void getOrCreateIdAssignWhitelistedIdWithFailedWhitelist() { + @Test(expected = RuntimeException.class) + public void getOrCreateIdAssignFilterReturnException() { uid = new UniqueId(client, table, METRIC, 3); - final byte[] id = { 0, 0, 5 }; final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); - when(config.auto_whitelist()).thenReturn(true); - when(config.auto_metric_patterns()).thenReturn("^nomatch.*$"); - when(config.auto_tagk_patterns()).thenReturn("^sys\\.cpu\\.*$"); - when(config.auto_tagv_patterns()).thenReturn("^sys\\.cpu\\.*$"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenReturn(Deferred.fromError(new UnitTestException())); + when(tsdb.getUidFilter()).thenReturn(filter); when(client.get(anyGet())) // null => ID doesn't exist. .thenReturn(Deferred.>fromResult(null)); @@ -364,16 +361,36 @@ public void getOrCreateIdAssignWhitelistedIdWithFailedWhitelist() { .thenReturn(Deferred.fromResult(true)) .thenReturn(Deferred.fromResult(true)); - assertArrayEquals(id, uid.getOrCreateId("foo")); - // Should be a cache hit since we created that entry. - assertArrayEquals(id, uid.getOrCreateId("foo")); - // Should be a cache hit too for the same reason. - assertEquals("foo", uid.getName(id)); + uid.getOrCreateId("foo"); + } + + @Test(expected = RuntimeException.class) + public void getOrCreateIdAssignFilterThrowsException() { + uid = new UniqueId(client, table, METRIC, 3); + final Config config = mock(Config.class); + when(config.enable_realtime_uid()).thenReturn(false); + final TSDB tsdb = mock(TSDB.class); + when(tsdb.getConfig()).thenReturn(config); + uid.setTSDB(tsdb); + final UniqueIdFilterPlugin filter = mock(UniqueIdFilterPlugin.class); + when(filter.fillterUIDAssignments()).thenReturn(true); + when(filter.allowUIDAssignment(any(UniqueIdType.class), anyString(), + anyString(), anyMapOf(String.class, String.class))) + .thenThrow(new UnitTestException()); + when(tsdb.getUidFilter()).thenReturn(filter); - verify(client).get(anyGet()); // Initial Get. - verify(client).atomicIncrement(incrementForRow(MAXID)); - // Reverse + forward mappings. - verify(client, times(2)).compareAndSet(anyPut(), emptyArray()); + when(client.get(anyGet())) // null => ID doesn't exist. + .thenReturn(Deferred.>fromResult(null)); + // Watch this! ______,^ I'm writing C++ in Java! + + when(client.atomicIncrement(incrementForRow(MAXID))) + .thenReturn(Deferred.fromResult(5L)); + + when(client.compareAndSet(anyPut(), emptyArray())) + .thenReturn(Deferred.fromResult(true)) + .thenReturn(Deferred.fromResult(true)); + + uid.getOrCreateId("foo"); } @Test @@ -417,7 +434,7 @@ public void getOrCreateIdAsyncAssignFilterOK() throws Exception { } @Test (expected = FailedToAssignUniqueIdException.class) - public void getOrCreateIdAssignFilterBlocked() throws Exception { + public void getOrCreateIdAsyncAssignFilterBlocked() throws Exception { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); @@ -444,7 +461,7 @@ public void getOrCreateIdAssignFilterBlocked() throws Exception { } @Test (expected = UnitTestException.class) - public void getOrCreateIdAssignFilterReturnException() throws Exception { + public void getOrCreateIdAsyncAssignFilterReturnException() throws Exception { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); @@ -471,7 +488,7 @@ public void getOrCreateIdAssignFilterReturnException() throws Exception { } @Test (expected = UnitTestException.class) - public void getOrCreateIdAssignFilterThrowsException() throws Exception { + public void getOrCreateIdAsyncAssignFilterThrowsException() throws Exception { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); @@ -497,55 +514,6 @@ public void getOrCreateIdAssignFilterThrowsException() throws Exception { uid.getOrCreateIdAsync("foo").join(); } - @Test // Test the creation of an ID with no problem. - public void checkMetricAgainstWhitelist() { - setupWhitelists(METRIC); - assertTrue(uid.checkNameIsValid("sys.cpu.user")); - } - - @Test // Test the creation of an ID with no problem. - public void checkTagKAgainstWhitelist() { - setupWhitelists(TAGK); - assertTrue(uid.checkNameIsValid("sys.cpu.user")); - } - - @Test // Test the creation of an ID with no problem. - public void checkTagVAgainstWhitelist() { - setupWhitelists(TAGV); - assertTrue(uid.checkNameIsValid("sys.cpu.user")); - } - - @Test - public void checkMetricAgainstWhitelistFails() { - setupWhitelists(METRIC); - assertFalse(uid.checkNameIsValid("foo.badmetric")); - } - - @Test - public void checkTagKAgainstWhitelistFails() { - setupWhitelists(TAGK); - assertFalse(uid.checkNameIsValid("foo.badmetric")); - } - - @Test - public void checkTagVAgainstWhitelistFails() { - setupWhitelists(TAGV); - assertFalse(uid.checkNameIsValid("foo.badmetric")); - } - - private void setupWhitelists(String type) { - uid = new UniqueId(client, table, type, 3); - final Config config = mock(Config.class); - when(config.enable_realtime_uid()).thenReturn(false); - when(config.auto_whitelist()).thenReturn(true); - when(config.auto_metric_patterns()).thenReturn("sys.*"); - when(config.auto_tagk_patterns()).thenReturn("sys.*"); - when(config.auto_tagv_patterns()).thenReturn("sys.*"); - final TSDB tsdb = mock(TSDB.class); - when(tsdb.getConfig()).thenReturn(config); - uid.setTSDB(tsdb); - } - @Test // Test the creation of an ID when unable to increment MAXID public void getOrCreateIdUnableToIncrementMaxId() throws Exception { PowerMockito.mockStatic(Thread.class); @@ -676,10 +644,6 @@ public void getOrCreateIdWithICVFailure() { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); - when(config.auto_whitelist()).thenReturn(false); - when(config.auto_metric_patterns()).thenReturn(".*"); - when(config.auto_tagk_patterns()).thenReturn(".*"); - when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); @@ -712,10 +676,6 @@ public void getOrCreateIdPutsReverseMappingFirst() { uid = new UniqueId(client, table, METRIC, 3); final Config config = mock(Config.class); when(config.enable_realtime_uid()).thenReturn(false); - when(config.auto_whitelist()).thenReturn(false); - when(config.auto_metric_patterns()).thenReturn(".*"); - when(config.auto_tagk_patterns()).thenReturn(".*"); - when(config.auto_tagv_patterns()).thenReturn(".*"); final TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); uid.setTSDB(tsdb); @@ -1511,10 +1471,6 @@ public void deleteNoSuchUniqueName() throws Exception { private void setupStorage() throws Exception { final Config config = mock(Config.class); - when(config.auto_whitelist()).thenReturn(false); - when(config.auto_metric_patterns()).thenReturn(".*"); - when(config.auto_tagk_patterns()).thenReturn(".*"); - when(config.auto_tagv_patterns()).thenReturn(".*"); when(tsdb.getConfig()).thenReturn(config); when(tsdb.getClient()).thenReturn(client); storage = new MockBase(tsdb, client, true, true, true, true); From d4fe65b7aeb40cc9e1e1533e458f2abd64f1ebb6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 Apr 2016 10:42:53 -0700 Subject: [PATCH 494/826] Clean up 107d9e9d04abafef970579530415e54665e9c46b a bit. We can use the StatsCollector class for the static settings and make it a a bit more flexible in case we need to add more stats in the future. Also remove the CLI options as we want folks to use configs instead of clogging up the command line. Signed-off-by: Chris Larsen --- Makefile.am | 4 +- src/core/TSDB.java | 3 ++ src/stats/StatsCollector.java | 33 +++++++++++--- src/tools/CliOptions.java | 6 +-- src/tools/TSDMain.java | 1 - src/tools/TSDPort.java | 56 ----------------------- src/utils/Config.java | 11 ----- test/tsd/TestStatsRpc.java | 29 ++++++++++++ test/tsd/TestStatsWithPort.java | 79 --------------------------------- 9 files changed, 62 insertions(+), 160 deletions(-) delete mode 100644 src/tools/TSDPort.java delete mode 100644 test/tsd/TestStatsWithPort.java diff --git a/Makefile.am b/Makefile.am index 02ebd0e50d..cfbd5e4bed 100644 --- a/Makefile.am +++ b/Makefile.am @@ -133,9 +133,8 @@ tsdb_SRC := \ src/tools/MetaPurge.java \ src/tools/MetaSync.java \ src/tools/Search.java \ - src/tools/StartupPlugin.java \ + src/tools/StartupPlugin.java \ src/tools/TSDMain.java \ - src/tools/TSDPort.java \ src/tools/TextImporter.java \ src/tools/TreeSync.java \ src/tools/UidManager.java \ @@ -347,7 +346,6 @@ test_SRC := \ test/tsd/TestSuggestRpc.java \ test/tsd/TestTreeRpc.java \ test/tsd/TestUniqueIdRpc.java \ - test/tsd/TestStatsWithPort.java \ test/uid/TestNoSuchUniqueId.java \ test/uid/TestRandomUniqueId.java \ test/uid/TestUniqueId.java \ diff --git a/src/core/TSDB.java b/src/core/TSDB.java index ba6839715c..4f20acb0dc 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -233,6 +233,9 @@ public TSDB(final HBaseClient client, final Config config) { // load up the functions that require the TSDB object ExpressionFactory.addTSDBFunctions(this); + // set any extra tags from the config for stats + StatsCollector.setGlobalTags(config); + LOG.debug(config.dumpConfiguration()); } diff --git a/src/stats/StatsCollector.java b/src/stats/StatsCollector.java index 4d9281f623..6170533fff 100644 --- a/src/stats/StatsCollector.java +++ b/src/stats/StatsCollector.java @@ -15,12 +15,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.tools.TSDPort; +import net.opentsdb.utils.Config; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; /** * Receives various stats/metrics from the current process. @@ -36,6 +37,9 @@ public abstract class StatsCollector { private static final Logger LOG = LoggerFactory.getLogger(StatsCollector.class); + /** Tags to add to every stat emitted by the collector */ + private static Map global_tags; + /** Prefix to add to every metric name, for example `tsd'. */ protected final String prefix; @@ -44,7 +48,7 @@ public abstract class StatsCollector { /** Buffer used to build lines emitted. */ private final StringBuilder buf = new StringBuilder(); - + /** * Constructor. * @param prefix A prefix to add to every metric name, for example @@ -52,11 +56,13 @@ public abstract class StatsCollector { */ public StatsCollector(final String prefix) { this.prefix = prefix; - if(TSDPort.isStatsWithPort()) { - addExtraTag("port", "" + TSDPort.getTSDPort()); + if (global_tags != null && !global_tags.isEmpty()) { + for (final Entry entry : global_tags.entrySet()) { + addExtraTag(entry.getKey(), entry.getValue()); + } } } - + /** * Method to override to actually emit a data point. * @param datapoint A data point in a format suitable for a text @@ -245,4 +251,21 @@ public final void clearExtraTag(final String name) { extratags.remove(name); } + /** + * Parses the configuration to determine if any extra tags should be included + * with every stat emitted. + * @param config The config object to parse + * @throws IllegalArgumentException if the config is null. Other exceptions + * may be thrown if the config values are unparseable. + */ + public static final void setGlobalTags(final Config config) { + if (config == null) { + throw new IllegalArgumentException("Configuration cannot be null."); + } + + if (config.getBoolean("tsd.core.stats_with_port")) { + global_tags = new HashMap(1); + global_tags.put("port", config.getString("tsd.network.port")); + } + } } diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index 377225ccef..cf5d58b4e0 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -148,11 +148,7 @@ static void overloadConfig(final ArgP argp, final Config config) { config.overrideConfig("tsd.network.async_io", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--worker-threads")) { config.overrideConfig("tsd.network.worker_threads", entry.getValue()); - } else if (entry.getKey().toLowerCase().equals("--max-connections")) { - config.overrideConfig("tsd.core.connections.limit", entry.getValue()); - } else if (entry.getKey().toLowerCase().equals("--statswport")) { - config.overrideConfig("tsd.core.stats_with_port", "true"); - } + } } } diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index 0c6f5d55d4..f26a895419 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -221,7 +221,6 @@ public static void main(String[] args) throws IOException { if (startup != null) { startup.setReady(tsdb); } - TSDPort.set(config); log.info("Ready to serve on " + addr); } catch (Throwable e) { factory.releaseExternalResources(); diff --git a/src/tools/TSDPort.java b/src/tools/TSDPort.java deleted file mode 100644 index 861a1c0d2e..0000000000 --- a/src/tools/TSDPort.java +++ /dev/null @@ -1,56 +0,0 @@ -// 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.tools; - -import net.opentsdb.utils.Config; - -/** - * Static reference to the TSD's listening port and stats port configuration - */ - -public class TSDPort { - /** The RPC listening port */ - private static int rpcPort = -1; - /** Indicates if RPC stats include the listening port. Set by config tsd.core.stats_with_port - or CLI option --statswport. */ - private static boolean statsWithPort = false; - - /** - * Sets the rpc port and stats config on TSD startup - * @param config The final config - */ - static void set(Config config) { - rpcPort = config.getInt("tsd.network.port"); - statsWithPort = config.getBoolean("tsd.core.stats_with_port"); - } - - /** - * Returns the TSD's listening port - * @return the port - */ - public static int getTSDPort() { - return rpcPort; - } - - /** - * Indicates if stats should be reported with the port as a tag - * @return true if stats should be reported with the port as a tag, false otherwise - */ - public static boolean isStatsWithPort() { - return statsWithPort; - } - - - private TSDPort() {} - -} diff --git a/src/utils/Config.java b/src/utils/Config.java index a8e451723c..4927d43fbb 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -94,10 +94,6 @@ public class Config { /** tsd.http.request.enable_chunked */ private boolean enable_chunked_requests = false; - /** tsd.core.stats_with_port */ - private boolean stats_with_port = false; - - /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; @@ -237,11 +233,6 @@ public boolean enable_chunked_requests() { return enable_chunked_requests; } - /** @return whether or not rpc stats should be broken out by port */ - public boolean rpc_stats_withport() { - return stats_with_port; - } - /** @return max incoming chunk size in bytes */ public int max_chunked_requests() { return max_chunked_requests; @@ -662,8 +653,6 @@ protected void loadStaticVariables() { enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); - stats_with_port = this.getBoolean("tsd.core.stats_with_port"); - } /** diff --git a/test/tsd/TestStatsRpc.java b/test/tsd/TestStatsRpc.java index fa90729245..2b09c73606 100644 --- a/test/tsd/TestStatsRpc.java +++ b/test/tsd/TestStatsRpc.java @@ -13,6 +13,7 @@ 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; @@ -21,6 +22,7 @@ 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; @@ -45,6 +47,33 @@ public void before() throws Exception { when(tsdb.getClient()).thenReturn(client); } + @Test + public void statsWithOutPort() throws Exception { + final StatsRpc rpc = new StatsRpc(); + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertFalse(json.contains("port=4242")); + } + + @Test + public void statsWithPort() throws Exception { + when(tsdb.getConfig().getBoolean("tsd.core.stats_with_port")) + .thenReturn(true); + when(tsdb.getConfig().getString("tsd.network.port")) + .thenReturn("4242"); + StatsCollector.setGlobalTags(tsdb.getConfig()); + final StatsRpc rpc = new StatsRpc(); + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats"); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("port=4242")); + } + @Test public void printThreadStats() throws Exception { final StatsRpc rpc = new StatsRpc(); diff --git a/test/tsd/TestStatsWithPort.java b/test/tsd/TestStatsWithPort.java deleted file mode 100644 index 85ca4001f1..0000000000 --- a/test/tsd/TestStatsWithPort.java +++ /dev/null @@ -1,79 +0,0 @@ -// 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 java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -import org.junit.Assert; -import org.junit.Test; - -import net.opentsdb.stats.StatsCollector; -import net.opentsdb.tools.TSDPort; - -public class TestStatsWithPort { - - static final Pattern PORT_MATCH = Pattern.compile(" port="); - - @Test - public void testNoPort() { - setPortConfig(4242, false); - } - - @Test - public void testDefaultPort() { - setPortConfig(4242, true); - } - - - protected void doTest() { - final List lines = new ArrayList(); - StatsCollector sc = new StatsCollector("tsd") { - @Override - public final void emit(final String line) { - lines.add(line); - } - }; - sc.record("foo", -1); - - } - - protected void validateStats(final List lines) { - Pattern portMatch = Pattern.compile(" port=" + TSDPort.getTSDPort()); - for(String s: lines) { - if(!TSDPort.isStatsWithPort()) { - Assert.assertFalse("Stat had a port", PORT_MATCH.matcher(s).find()); - } else { - Assert.assertTrue("Stat did not have port", portMatch.matcher(s).find()); - } - } - } - - - public void setPortConfig(final Integer port, final Boolean statsWithPort) { - try { - Field portField = TSDPort.class.getDeclaredField("rpcPort"); - portField.setAccessible(true); - portField.set(null, port); - Field statsWPortField = TSDPort.class.getDeclaredField("statsWithPort"); - statsWPortField.setAccessible(true); - statsWPortField.set(null, statsWithPort); - } catch (Exception ex) { - throw new RuntimeException("Failed to set TCPPort fields", ex); - } - } - -} From a4cf501bb2750b7de382c034cf981086a70309da Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 Apr 2016 12:26:08 -0700 Subject: [PATCH 495/826] Add the UniqueIdWhitelistFilter plugin directly to the TSD code base so users can instantiate it without loading a separate file. Signed-off-by: Chris Larsen --- src/META-INF/MANIFEST.MF | 1 + .../net.opentsdb.uid.UniqueIdFilterPlugin | 1 + src/uid/UniqueIdWhitelistFilter.java | 200 ++++++++++++++++++ test/uid/TestUniqueIdWhitelistFilter.java | 165 +++++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 src/META-INF/MANIFEST.MF create mode 100644 src/META-INF/services/net.opentsdb.uid.UniqueIdFilterPlugin create mode 100644 src/uid/UniqueIdWhitelistFilter.java create mode 100644 test/uid/TestUniqueIdWhitelistFilter.java diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..348f1bdd38 --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1 @@ +Manifest-Version: 1.0 \ No newline at end of file diff --git a/src/META-INF/services/net.opentsdb.uid.UniqueIdFilterPlugin b/src/META-INF/services/net.opentsdb.uid.UniqueIdFilterPlugin new file mode 100644 index 0000000000..010e95484c --- /dev/null +++ b/src/META-INF/services/net.opentsdb.uid.UniqueIdFilterPlugin @@ -0,0 +1 @@ +net.opentsdb.uid.UniqueIdWhitelistFilter \ No newline at end of file diff --git a/src/uid/UniqueIdWhitelistFilter.java b/src/uid/UniqueIdWhitelistFilter.java new file mode 100644 index 0000000000..371ea7bd50 --- /dev/null +++ b/src/uid/UniqueIdWhitelistFilter.java @@ -0,0 +1,200 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.uid; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import com.google.common.annotations.VisibleForTesting; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; + +/** + * A UID filter implementation using regular expression based whitelists. + * Multiple regular expressions can be provided in the configuration file with + * a configurable delimiter. Each expression is compiled into a list per UID + * type and when a new UID passes through the filter, each expression in the + * list is compared to make sure the name satisfies all expressions. + */ +public class UniqueIdWhitelistFilter extends UniqueIdFilterPlugin { + + /** Default delimiter */ + public static final String DEFAULT_REGEX_DELIMITER = ","; + + /** Lists of patterns for each type. */ + private List metric_patterns; + private List tagk_patterns; + private List tagv_patterns; + + /** Counters for tracking stats */ + private final AtomicLong metrics_rejected = new AtomicLong(); + private final AtomicLong metrics_allowed = new AtomicLong(); + private final AtomicLong tagks_rejected = new AtomicLong(); + private final AtomicLong tagks_allowed = new AtomicLong(); + private final AtomicLong tagvs_rejected = new AtomicLong(); + private final AtomicLong tagvs_allowed = new AtomicLong(); + + @Override + public void initialize(final TSDB tsdb) { + final Config config = tsdb.getConfig(); + String delimiter = config.getString("tsd.uidfilter.whitelist.delimiter"); + if (delimiter == null) { + delimiter = DEFAULT_REGEX_DELIMITER; + } + + String raw = config.getString("tsd.uidfilter.whitelist.metric_patterns"); + if (raw != null) { + final String[] splits = raw.split(delimiter); + metric_patterns = new ArrayList(splits.length); + for (final String pattern : splits) { + try { + metric_patterns.add(Pattern.compile(pattern)); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("The metric whitelist pattern [" + + pattern + "] does not compile.", e); + } + } + } + + raw = config.getString("tsd.uidfilter.whitelist.tagk_patterns"); + if (raw != null) { + final String[] splits = raw.split(delimiter); + tagk_patterns = new ArrayList(splits.length); + for (final String pattern : splits) { + try { + tagk_patterns.add(Pattern.compile(pattern)); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("The tagk whitelist pattern [" + + pattern + "] does not compile.", e); + } + } + } + + raw = config.getString("tsd.uidfilter.whitelist.tagv_patterns"); + if (raw != null) { + final String[] splits = raw.split(delimiter); + tagv_patterns = new ArrayList(splits.length); + for (final String pattern : splits) { + try { + tagv_patterns.add(Pattern.compile(pattern)); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("The tagv whitelist pattern [" + + pattern + "] does not compile.", e); + } + } + } + } + + @Override + public Deferred shutdown() { + return Deferred.fromResult(null); + } + + @Override + public String version() { + return "2.3.0"; + } + + @Override + public void collectStats(final StatsCollector collector) { + collector.record("uid.filter.whitelist.accepted", metrics_allowed.get(), + "type=metrics"); + collector.record("uid.filter.whitelist.accepted", tagks_allowed.get(), + "type=tagk"); + collector.record("uid.filter.whitelist.accepted", tagvs_allowed.get(), + "type=tagv"); + collector.record("uid.filter.whitelist.rejected", metrics_rejected.get(), + "type=metrics"); + collector.record("uid.filter.whitelist.rejected", tagks_rejected.get(), + "type=tagk"); + collector.record("uid.filter.whitelist.rejected", tagvs_rejected.get(), + "type=tagv"); + } + + @Override + public Deferred allowUIDAssignment( + final UniqueIdType type, + final String value, + final String metric, + final Map tags) { + + switch (type) { + case METRIC: + if (metric_patterns != null) { + for (final Pattern pattern : metric_patterns) { + if (!pattern.matcher(value).find()) { + metrics_rejected.incrementAndGet(); + return Deferred.fromResult(false); + } + } + } + metrics_allowed.incrementAndGet(); + break; + + case TAGK: + if (tagk_patterns != null) { + for (final Pattern pattern : tagk_patterns) { + if (!pattern.matcher(value).find()) { + tagks_rejected.incrementAndGet(); + return Deferred.fromResult(false); + } + } + } + tagks_allowed.incrementAndGet(); + break; + + case TAGV: + if (tagv_patterns != null) { + for (final Pattern pattern : tagv_patterns) { + if (!pattern.matcher(value).find()) { + tagvs_rejected.incrementAndGet(); + return Deferred.fromResult(false); + } + } + } + tagvs_allowed.incrementAndGet(); + break; + } + + // all patterns passed, yay! + return Deferred.fromResult(true); + } + + @Override + public boolean fillterUIDAssignments() { + return true; + } + + @VisibleForTesting + List metricPatterns() { + return metric_patterns; + } + + @VisibleForTesting + List tagkPatterns() { + return tagk_patterns; + } + + @VisibleForTesting + List tagvPatterns() { + return tagv_patterns; + } +} diff --git a/test/uid/TestUniqueIdWhitelistFilter.java b/test/uid/TestUniqueIdWhitelistFilter.java new file mode 100644 index 0000000000..c07f9c26e2 --- /dev/null +++ b/test/uid/TestUniqueIdWhitelistFilter.java @@ -0,0 +1,165 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.uid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +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 net.opentsdb.core.TSDB; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, Config.class }) +public class TestUniqueIdWhitelistFilter { + + private TSDB tsdb; + private Config config; + private UniqueIdWhitelistFilter filter; + + @Before + public void before() throws Exception { + tsdb = PowerMockito.mock(TSDB.class); + config = new Config(false); + when(tsdb.getConfig()).thenReturn(config); + filter = new UniqueIdWhitelistFilter(); + + config.overrideConfig("tsd.uidfilter.whitelist.metric_patterns", ".*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagk_patterns", ".*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagv_patterns", ".*"); + } + + @Test + public void ctor() throws Exception { + assertNull(filter.metricPatterns()); + assertNull(filter.tagkPatterns()); + assertNull(filter.tagvPatterns()); + } + + @Test + public void initalize() throws Exception { + filter.initialize(tsdb); + assertEquals(1, filter.metricPatterns().size()); + assertEquals(".*", filter.metricPatterns().get(0).pattern()); + assertEquals(1, filter.tagkPatterns().size()); + assertEquals(".*", filter.tagkPatterns().get(0).pattern()); + assertEquals(1, filter.tagvPatterns().size()); + assertEquals(".*", filter.tagvPatterns().get(0).pattern()); + } + + @Test + public void initalizeMultiplePatterns() throws Exception { + config.overrideConfig("tsd.uidfilter.whitelist.metric_patterns", ".*,^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagk_patterns", ".*,^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagv_patterns", ".*,^test.*"); + filter.initialize(tsdb); + assertEquals(2, filter.metricPatterns().size()); + assertEquals(".*", filter.metricPatterns().get(0).pattern()); + assertEquals("^test.*", filter.metricPatterns().get(1).pattern()); + assertEquals(2, filter.tagkPatterns().size()); + assertEquals(".*", filter.tagkPatterns().get(0).pattern()); + assertEquals("^test.*", filter.tagkPatterns().get(1).pattern()); + assertEquals(2, filter.tagvPatterns().size()); + assertEquals(".*", filter.tagvPatterns().get(0).pattern()); + assertEquals("^test.*", filter.tagvPatterns().get(1).pattern()); + } + + @Test + public void initalizeMultiplePatternsAlternateDelimiter() throws Exception { + config.overrideConfig("tsd.uidfilter.whitelist.delimiter", "\\|"); + config.overrideConfig("tsd.uidfilter.whitelist.metric_patterns", ".*|^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagk_patterns", ".*|^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagv_patterns", ".*|^test.*"); + filter.initialize(tsdb); + assertEquals(2, filter.metricPatterns().size()); + assertEquals(".*", filter.metricPatterns().get(0).pattern()); + assertEquals("^test.*", filter.metricPatterns().get(1).pattern()); + assertEquals(2, filter.tagkPatterns().size()); + assertEquals(".*", filter.tagkPatterns().get(0).pattern()); + assertEquals("^test.*", filter.tagkPatterns().get(1).pattern()); + assertEquals(2, filter.tagvPatterns().size()); + assertEquals(".*", filter.tagvPatterns().get(0).pattern()); + assertEquals("^test.*", filter.tagvPatterns().get(1).pattern()); + } + + @Test (expected = IllegalArgumentException.class) + public void initalizeBadRegex() throws Exception { + config.overrideConfig("tsd.uidfilter.whitelist.metric_patterns", "grp[start"); + filter.initialize(tsdb); + } + + @Test + public void shutdown() throws Exception { + assertNull(filter.shutdown().join()); + } + + @Test + public void allowUIDAssignment() throws Exception { + config.overrideConfig("tsd.uidfilter.whitelist.metric_patterns", "^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagk_patterns", "^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagv_patterns", "^test.*"); + filter.initialize(tsdb); + assertTrue(filter.allowUIDAssignment(UniqueIdType.METRIC, "test_metric", + null, null).join()); + assertFalse(filter.allowUIDAssignment(UniqueIdType.METRIC, "metric", + null, null).join()); + assertTrue(filter.allowUIDAssignment(UniqueIdType.TAGK, "test_tagk", + null, null).join()); + assertFalse(filter.allowUIDAssignment(UniqueIdType.TAGK, "tagk", + null, null).join()); + assertTrue(filter.allowUIDAssignment(UniqueIdType.TAGV, "test_tagv", + null, null).join()); + assertFalse(filter.allowUIDAssignment(UniqueIdType.TAGV, "tagv", + null, null).join()); + } + + @Test + public void allowUIDAssignmentMultiplePaterns() throws Exception { + config.overrideConfig("tsd.uidfilter.whitelist.metric_patterns", ".*,^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagk_patterns", ".*,^test.*"); + config.overrideConfig("tsd.uidfilter.whitelist.tagv_patterns", ".*,^test.*"); + filter.initialize(tsdb); + assertTrue(filter.allowUIDAssignment(UniqueIdType.METRIC, "test_metric", + null, null).join()); + assertFalse(filter.allowUIDAssignment(UniqueIdType.METRIC, "metric", + null, null).join()); + assertTrue(filter.allowUIDAssignment(UniqueIdType.TAGK, "test_tagk", + null, null).join()); + assertFalse(filter.allowUIDAssignment(UniqueIdType.TAGK, "tagk", + null, null).join()); + assertTrue(filter.allowUIDAssignment(UniqueIdType.TAGV, "test_tagv", + null, null).join()); + assertFalse(filter.allowUIDAssignment(UniqueIdType.TAGV, "tagv", + null, null).join()); + } + + @Test + public void fillterUIDAssignments() throws Exception { + assertTrue(filter.fillterUIDAssignments()); + } +} From 971decfde75c1a2ec02e26fd287b4ae1790fecdf Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 Apr 2016 13:07:03 -0700 Subject: [PATCH 496/826] Remove leftover defaults from the UniqueId class. Signed-off-by: Chris Larsen --- src/uid/UniqueId.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 9b218e2e00..22a384fc82 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -56,13 +56,6 @@ */ @SuppressWarnings("deprecation") // Dunno why even with this, compiler warns. public final class UniqueId implements UniqueIdInterface { - /** Whether or not to check new UID against configured whitelists **/ - private Boolean useWhitelist = false; - /** Whitelists for various uid types **/ - private String auto_metric_patterns = ".*"; - private String auto_tagk_patterns = ".*"; - private String auto_tagv_patterns = ".*"; - private static final Logger LOG = LoggerFactory.getLogger(UniqueId.class); /** Enumerator for different types of UIDS @since 2.0 */ From 866920fd1259295f02037d94bd39a0537361cdf4 Mon Sep 17 00:00:00 2001 From: Isaiah Choe Date: Thu, 15 Oct 2015 17:24:57 +0900 Subject: [PATCH 497/826] Allow to define special characters for keys and values of the tag via config tsd.core.tag.allow_specialchars Signed-off-by: Chris Larsen --- src/core/TSDB.java | 4 ++++ src/core/Tags.java | 19 ++++++++++++++++++- test/core/TestTags.java | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 4f20acb0dc..57e591ef07 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -230,6 +230,10 @@ public TSDB(final HBaseClient client, final Config config) { UniqueId.preloadUidCache(this, uid_cache_map); } + if (config.getString("tsd.core.tag.allow_specialchars") != null) { + Tags.setAllowSpecialChars(config.getString("tsd.core.tag.allow_specialchars")); + } + // load up the functions that require the TSDB object ExpressionFactory.addTSDBFunctions(this); diff --git a/src/core/Tags.java b/src/core/Tags.java index 6aa5cc2074..ff1a4b2125 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -37,6 +37,7 @@ public final class Tags { private static final Logger LOG = LoggerFactory.getLogger(Tags.class); + private static String allowSpecialChars = ""; private Tags() { // Can't create instances of this utility class. @@ -547,7 +548,7 @@ public static void validateString(final String what, final String s) { final char c = s.charAt(i); if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '-' || c == '_' || c == '.' - || c == '/' || Character.isLetter(c))) { + || c == '/' || Character.isLetter(c) || isAllowSpecialChars(c))) { throw new IllegalArgumentException("Invalid " + what + " (\"" + s + "\"): illegal character: " + c); } @@ -808,4 +809,20 @@ public static boolean looksLikeInteger(final String value) { return true; } + /** + * Set the special characters due to allowing for a key or a value of the tag. + * @param characters character sequences as a string + */ + public static void setAllowSpecialChars(String characters) { + allowSpecialChars = characters == null ? "" : characters; + } + + /** + * Returns true if the character can be used a tag name or a tag value. + * @param character + * @return + */ + static boolean isAllowSpecialChars(char character) { + return allowSpecialChars.indexOf(character) != -1; + } } diff --git a/test/core/TestTags.java b/test/core/TestTags.java index f6bea0e828..2898807149 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -942,4 +942,20 @@ public void getTagUidsEmptyRow() throws Exception { final ByteMap uids = Tags.getTagUids(new byte[] {}); assertEquals(0, uids.size()); } + + @Test + public void setAllowSpecialChars() throws Exception { + assertFalse(Tags.isAllowSpecialChars('!')); + + Tags.setAllowSpecialChars(null); + assertFalse(Tags.isAllowSpecialChars('!')); + + Tags.setAllowSpecialChars(""); + assertFalse(Tags.isAllowSpecialChars('!')); + + Tags.setAllowSpecialChars("!)(%"); + assertTrue(Tags.isAllowSpecialChars('!')); + assertTrue(Tags.isAllowSpecialChars('(')); + assertTrue(Tags.isAllowSpecialChars('%')); + } } From 539e4b0d3a3ea1bb9f7e5d49df26df84bf3ab8cc Mon Sep 17 00:00:00 2001 From: HugoMFernandes Date: Fri, 15 Apr 2016 15:12:39 +0100 Subject: [PATCH 498/826] Fix #773 - Add global_annotations to graphs Fixed a bug in which global_annotations were ignored by GraphHandler.java (and thus, were not rendered by gnuplot). Added async call to hbase to retrieve global annotations when required in a similar fashion as that of QueryRpc.java. Note: Since both GraphHandler.java and QueryRpc.java implement (similar) query-processing logic, all of this should be unified in the future (requires a large refactor to split all query logic with visualization logic). Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 71 +++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 6f02526a3b..3df3086a90 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -45,11 +45,15 @@ import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.graph.Plot; +import net.opentsdb.meta.Annotation; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + /** * Stateless handler of HTTP graph requests (the {@code /q} endpoint). */ @@ -123,6 +127,10 @@ public void execute(final TSDB tsdb, final HttpQuery query) { } } + // TODO(HugoMFernandes): Most of this (query-related) logic is implemented in + // net.opentsdb.tsd.QueryRpc.java (which actually does this asynchronously), + // so we should refactor both classes to split the actual logic used to + // generate the data from the actual visualization (removing all duped code). private void doGraph(final TSDB tsdb, final HttpQuery query) throws IOException { final String basepath = getGnuplotBasePath(tsdb, query); @@ -154,10 +162,15 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) if (!nocache && isDiskCacheHit(query, end_time, max_age, basepath)) { return; } - Query[] tsdbqueries; - List options; - tsdbqueries = parseQuery(tsdb, query); - options = query.getQueryStringParams("o"); + + // Parse TSQuery from HTTP query + final TSQuery tsquery = QueryRpc.parseQuery(tsdb, query); + tsquery.validateAndSetQuery(); + + // Build the queries for the parsed TSQuery + Query[] tsdbqueries = tsquery.buildQueries(tsdb); + + List options = query.getQueryStringParams("o"); if (options == null) { options = new ArrayList(tsdbqueries.length); for (int i = 0; i < tsdbqueries.length; i++) { @@ -212,9 +225,37 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) return; } + final RunGnuplot rungnuplot = new RunGnuplot(query, max_age, plot, basepath, + aggregated_tags, npoints); + + class ErrorCB implements Callback { + public Object call(final Exception e) throws Exception { + LOG.info("Failed to retrieve global annotations: ", e); + throw e; + } + } + + class GlobalCB implements Callback> { + public Object call(final List globalAnnotations) throws Exception { + rungnuplot.plot.setGlobals(globalAnnotations); + execGnuplot(rungnuplot, query); + + return null; + } + } + + // Fetch global annotations, if needed + if (!tsquery.getNoAnnotations() && tsquery.getGlobalAnnotations()) { + Annotation.getGlobalAnnotations(tsdb, start_time, end_time) + .addCallback(new GlobalCB()).addErrback(new ErrorCB()); + } else { + execGnuplot(rungnuplot, query); + } + } + + private void execGnuplot(RunGnuplot rungnuplot, HttpQuery query) { try { - gnuplot.execute(new RunGnuplot(query, max_age, plot, basepath, - aggregated_tags, npoints)); + gnuplot.execute(rungnuplot); } catch (RejectedExecutionException e) { query.internalError(new Exception("Too many requests pending," + " please try again later", e)); @@ -354,7 +395,7 @@ private String getGnuplotBasePath(final TSDB tsdb, final HttpQuery query) { qs.remove("png"); qs.remove("json"); qs.remove("ascii"); - return tsdb.getConfig().getDirectoryName("tsd.http.cachedir") + + return tsdb.getConfig().getDirectoryName("tsd.http.cachedir") + Integer.toHexString(qs.hashCode()); } @@ -841,21 +882,7 @@ private static void printMetricHeader(final PrintWriter writer, final String met writer.print(timestamp / 1000L); writer.print(' '); } - - /** - * Parses the {@code /q} query in a list of {@link Query} objects. - * @param tsdb The TSDB to use. - * @param query The HTTP query for {@code /q}. - * @return The corresponding {@link Query} objects. - * @throws BadRequestException if the query was malformed. - * @throws IllegalArgumentException if the metric or tags were malformed. - */ - private static Query[] parseQuery(final TSDB tsdb, final HttpQuery query) { - final TSQuery q = QueryRpc.parseQuery(tsdb, query); - q.validateAndSetQuery(); - return q.buildQueries(tsdb); - } - + private static final PlotThdFactory thread_factory = new PlotThdFactory(); private static final class PlotThdFactory implements ThreadFactory { From 463c01be8abb52f6e33025c8a4f016ae93716c9b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 Apr 2016 14:25:49 -0700 Subject: [PATCH 499/826] Add annotation flags to the UI to pass URI params properly for Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 6 +++--- src/tsd/client/QueryUi.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 3df3086a90..2669d6b8c6 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -230,14 +230,14 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) class ErrorCB implements Callback { public Object call(final Exception e) throws Exception { - LOG.info("Failed to retrieve global annotations: ", e); + LOG.warn("Failed to retrieve global annotations: ", e); throw e; } } class GlobalCB implements Callback> { - public Object call(final List globalAnnotations) throws Exception { - rungnuplot.plot.setGlobals(globalAnnotations); + public Object call(final List global_annotations) throws Exception { + rungnuplot.plot.setGlobals(global_annotations); execGnuplot(rungnuplot, query); return null; diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index cf78c6feca..e81a50ac70 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -148,6 +148,10 @@ public class QueryUi implements EntryPoint, HistoryListener { private final CheckBox smooth = new CheckBox(); private final ListBox styles = new ListBox(); private String timezone = ""; + + // Annotations handling flags. + private boolean hide_annotations = false; + private boolean show_global_annotations = false; /** * Handles every change to the query form and gets a new graph. @@ -788,6 +792,9 @@ private void refreshFromQueryString() { autoreload.setValue(qs.containsKey("autoreload"), true); maybeSetTextbox(qs, "autoreload", autoreoload_interval); + show_global_annotations = qs.containsKey("global_annotations") ? true : false; + hide_annotations = qs.containsKey("no_annotations") ? true : false; + //get the tz param value final ArrayList tzvalues = qs.get("tz"); if (tzvalues == null) @@ -931,6 +938,14 @@ private void refreshGraph() { url.append("&smooth=csplines"); } url.append("&style=").append(styles.getValue(styles.getSelectedIndex())); + + if (hide_annotations) { + url.append("&no_annotations=true"); + } + if (show_global_annotations) { + url.append("&global_annotations=true"); + } + final String unencodedUri = url.toString(); final String uri = URL.encode(unencodedUri); if (uri.equals(lastgraphuri)) { From 3d558060a0f4483f65348c8c9a78b9a31554159d Mon Sep 17 00:00:00 2001 From: HugoMFernandes Date: Fri, 15 Apr 2016 15:12:39 +0100 Subject: [PATCH 500/826] Fix #773 - Add global_annotations to graphs Fixed a bug in which global_annotations were ignored by GraphHandler.java (and thus, were not rendered by gnuplot). Added async call to hbase to retrieve global annotations when required in a similar fashion as that of QueryRpc.java. Note: Since both GraphHandler.java and QueryRpc.java implement (similar) query-processing logic, all of this should be unified in the future (requires a large refactor to split all query logic with visualization logic). Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 71 +++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index d6ce433e13..3df3086a90 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -45,11 +45,15 @@ import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.graph.Plot; +import net.opentsdb.meta.Annotation; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + /** * Stateless handler of HTTP graph requests (the {@code /q} endpoint). */ @@ -123,6 +127,10 @@ public void execute(final TSDB tsdb, final HttpQuery query) { } } + // TODO(HugoMFernandes): Most of this (query-related) logic is implemented in + // net.opentsdb.tsd.QueryRpc.java (which actually does this asynchronously), + // so we should refactor both classes to split the actual logic used to + // generate the data from the actual visualization (removing all duped code). private void doGraph(final TSDB tsdb, final HttpQuery query) throws IOException { final String basepath = getGnuplotBasePath(tsdb, query); @@ -154,10 +162,15 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) if (!nocache && isDiskCacheHit(query, end_time, max_age, basepath)) { return; } - Query[] tsdbqueries; - List options; - tsdbqueries = parseQuery(tsdb, query); - options = query.getQueryStringParams("o"); + + // Parse TSQuery from HTTP query + final TSQuery tsquery = QueryRpc.parseQuery(tsdb, query); + tsquery.validateAndSetQuery(); + + // Build the queries for the parsed TSQuery + Query[] tsdbqueries = tsquery.buildQueries(tsdb); + + List options = query.getQueryStringParams("o"); if (options == null) { options = new ArrayList(tsdbqueries.length); for (int i = 0; i < tsdbqueries.length; i++) { @@ -212,9 +225,37 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) return; } + final RunGnuplot rungnuplot = new RunGnuplot(query, max_age, plot, basepath, + aggregated_tags, npoints); + + class ErrorCB implements Callback { + public Object call(final Exception e) throws Exception { + LOG.info("Failed to retrieve global annotations: ", e); + throw e; + } + } + + class GlobalCB implements Callback> { + public Object call(final List globalAnnotations) throws Exception { + rungnuplot.plot.setGlobals(globalAnnotations); + execGnuplot(rungnuplot, query); + + return null; + } + } + + // Fetch global annotations, if needed + if (!tsquery.getNoAnnotations() && tsquery.getGlobalAnnotations()) { + Annotation.getGlobalAnnotations(tsdb, start_time, end_time) + .addCallback(new GlobalCB()).addErrback(new ErrorCB()); + } else { + execGnuplot(rungnuplot, query); + } + } + + private void execGnuplot(RunGnuplot rungnuplot, HttpQuery query) { try { - gnuplot.execute(new RunGnuplot(query, max_age, plot, basepath, - aggregated_tags, npoints)); + gnuplot.execute(rungnuplot); } catch (RejectedExecutionException e) { query.internalError(new Exception("Too many requests pending," + " please try again later", e)); @@ -354,7 +395,7 @@ private String getGnuplotBasePath(final TSDB tsdb, final HttpQuery query) { qs.remove("png"); qs.remove("json"); qs.remove("ascii"); - return tsdb.getConfig().getDirectoryName("tsd.http.cachedir") + + return tsdb.getConfig().getDirectoryName("tsd.http.cachedir") + Integer.toHexString(qs.hashCode()); } @@ -841,21 +882,7 @@ private static void printMetricHeader(final PrintWriter writer, final String met writer.print(timestamp / 1000L); writer.print(' '); } - - /** - * Parses the {@code /q} query in a list of {@link Query} objects. - * @param tsdb The TSDB to use. - * @param query The HTTP query for {@code /q}. - * @return The corresponding {@link Query} objects. - * @throws BadRequestException if the query was malformed. - * @throws IllegalArgumentException if the metric or tags were malformed. - */ - private static Query[] parseQuery(final TSDB tsdb, final HttpQuery query) { - final TSQuery q = QueryRpc.parseQuery(tsdb, query, null); - q.validateAndSetQuery(); - return q.buildQueries(tsdb); - } - + private static final PlotThdFactory thread_factory = new PlotThdFactory(); private static final class PlotThdFactory implements ThreadFactory { From 95a67b2f3466a880ee5d05713378726540d38356 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 Apr 2016 14:25:49 -0700 Subject: [PATCH 501/826] Add annotation flags to the UI to pass URI params properly for Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 6 +++--- src/tsd/client/QueryUi.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 3df3086a90..2669d6b8c6 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -230,14 +230,14 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) class ErrorCB implements Callback { public Object call(final Exception e) throws Exception { - LOG.info("Failed to retrieve global annotations: ", e); + LOG.warn("Failed to retrieve global annotations: ", e); throw e; } } class GlobalCB implements Callback> { - public Object call(final List globalAnnotations) throws Exception { - rungnuplot.plot.setGlobals(globalAnnotations); + public Object call(final List global_annotations) throws Exception { + rungnuplot.plot.setGlobals(global_annotations); execGnuplot(rungnuplot, query); return null; diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 3c063745de..23e4952c1c 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -149,6 +149,10 @@ public class QueryUi implements EntryPoint, HistoryListener { private final CheckBox smooth = new CheckBox(); private final ListBox styles = new ListBox(); private String timezone = ""; + + // Annotations handling flags. + private boolean hide_annotations = false; + private boolean show_global_annotations = false; /** * Handles every change to the query form and gets a new graph. @@ -798,6 +802,9 @@ private void refreshFromQueryString() { autoreload.setValue(qs.containsKey("autoreload"), true); maybeSetTextbox(qs, "autoreload", autoreoload_interval); + show_global_annotations = qs.containsKey("global_annotations") ? true : false; + hide_annotations = qs.containsKey("no_annotations") ? true : false; + //get the tz param value final ArrayList tzvalues = qs.get("tz"); if (tzvalues == null) @@ -944,6 +951,14 @@ private void refreshGraph() { url.append("&smooth=csplines"); } url.append("&style=").append(styles.getValue(styles.getSelectedIndex())); + + if (hide_annotations) { + url.append("&no_annotations=true"); + } + if (show_global_annotations) { + url.append("&global_annotations=true"); + } + final String unencodedUri = url.toString(); final String uri = URL.encode(unencodedUri); if (uri.equals(lastgraphuri)) { From cb836d9a5377b1ce2085eb521b745f982283b48e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 23 Apr 2016 14:35:03 -0700 Subject: [PATCH 502/826] Restore the 2.2 API for Query parsing. Signed-off-by: Chris Larsen --- src/tsd/QueryRpc.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 9a321025d7..2f6d92be24 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -494,6 +494,18 @@ public String toString() { } } + /** + * Parses a query string legacy style query from the URI + * @param tsdb The TSDB we belong to + * @param query The HTTP Query for parsing + * @return A TSQuery if parsing was successful + * @throws BadRequestException if parsing was unsuccessful + * @since 2.3 + */ + public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query) { + return parseQuery(tsdb, query, null); + } + /** * Parses a query string legacy style query from the URI * @param tsdb The TSDB we belong to @@ -502,6 +514,7 @@ public String toString() { * If this is null, it means any expressions in the URI will be skipped. * @return A TSQuery if parsing was successful * @throws BadRequestException if parsing was unsuccessful + * @since 2.3 */ public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, final List expressions) { From 7d4f779b66bdfb1029e38460bcb6e5c522be4ba4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 24 Apr 2016 12:33:24 -0700 Subject: [PATCH 503/826] Add the "short-hostname" flag to travis to help the builds. Signed-off-by: Chris Larsen --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1126638b1e..35b57e08a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: java before_script: ./build.sh pom.xml script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet +addons: + hostname: short-hostname jdk: - oraclejdk7 - openjdk6 From 8e9769537802f661f83200113fa037ca3bc572ad Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 24 Apr 2016 12:33:24 -0700 Subject: [PATCH 504/826] Add the "short-hostname" flag to travis to help the builds. Signed-off-by: Chris Larsen --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1126638b1e..35b57e08a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: java before_script: ./build.sh pom.xml script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet +addons: + hostname: short-hostname jdk: - oraclejdk7 - openjdk6 From 535b7e553812d1f2c13377d57e352e5c131a4097 Mon Sep 17 00:00:00 2001 From: Andy Flury Date: Wed, 9 Sep 2015 10:29:32 -0400 Subject: [PATCH 505/826] add first and last aggregator Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 56 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 1d846301a5..ef1371bc68 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -81,7 +81,7 @@ public enum Interpolation { * if timestamps don't line up instead of interpolating. */ public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); - + /** Aggregator that returns the number of data points. * WARNING: This currently interpolates with zero-if-missing. In this case * counts will be off when counting multiple time series. Only use this when @@ -89,6 +89,12 @@ public enum Interpolation { * @since 2.2 */ public static final Aggregator COUNT = new Count(Interpolation.ZIM, "count"); + /** Aggregator that returns the first data point. */ + public static final Aggregator FIRST = new First(Interpolation.ZIM, "first"); + + /** Aggregator that returns the first data point. */ + public static final Aggregator LAST = new Last(Interpolation.ZIM, "last"); + /** Maps an aggregator name to its instance. */ private static final HashMap aggregators; @@ -156,6 +162,8 @@ public enum Interpolation { aggregators.put("zimsum", ZIMSUM); aggregators.put("mimmin", MIMMIN); aggregators.put("mimmax", MIMMAX); + aggregators.put("first", FIRST); + aggregators.put("last", LAST); PercentileAgg[] percentiles = { p999, p99, p95, p90, p75, p50, @@ -643,7 +651,7 @@ public double runDouble(Doubles values) { return result / count; } - + class SumPoint { long ts; Object val; @@ -654,4 +662,48 @@ public SumPoint(long ts, Object val) { } } } + + private static final class First extends Aggregator { + public First(final Interpolation method, final String name) { + super(method, name); + } + + public long runLong(final Longs values) { + long val = values.nextLongValue(); + while (values.hasNextValue()) { + values.nextLongValue(); + } + return val; + } + + public double runDouble(final Doubles values) { + double val = values.nextDoubleValue(); + while (values.hasNextValue()) { + values.nextDoubleValue(); + } + return val; + } + } + + private static final class Last extends Aggregator { + public Last(final Interpolation method, final String name) { + super(method, name); + } + + public long runLong(final Longs values) { + long val = values.nextLongValue(); + while (values.hasNextValue()) { + val = values.nextLongValue(); + } + return val; + } + + public double runDouble(final Doubles values) { + double val = values.nextDoubleValue(); + while (values.hasNextValue()) { + val = values.nextDoubleValue(); + } + return val; + } + } } From 21f7e4606410a60802c8d639849183ff367bf8d1 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 24 Apr 2016 12:48:08 -0700 Subject: [PATCH 506/826] Add unit tests for the first and last aggregators. Signed-off-by: Chris Larsen --- test/core/TestAggregators.java | 116 ++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/test/core/TestAggregators.java b/test/core/TestAggregators.java index 5f51327046..7f4f843517 100644 --- a/test/core/TestAggregators.java +++ b/test/core/TestAggregators.java @@ -12,6 +12,8 @@ // see . package net.opentsdb.core; +import static org.junit.Assert.assertEquals; + import java.util.Random; import org.junit.Assert; @@ -36,26 +38,37 @@ public final class TestAggregators { /** Helper class to hold a bunch of numbers we can iterate on. */ private static final class Numbers implements Aggregator.Longs, Aggregator.Doubles { - private final long[] numbers; + private final long[] longs; + private final double[] doubles; private int i = 0; public Numbers(final long[] numbers) { - this.numbers = numbers; + longs = numbers; + doubles = null; + } + + public Numbers(final double[] numbers) { + longs = null; + doubles = numbers; } + public boolean isInteger() { + return longs != null ? true : false; + } + @Override public boolean hasNextValue() { - return i < numbers.length; + return longs != null ? i < longs.length : i < doubles.length; } @Override public long nextLongValue() { - return numbers[i++]; + return longs[i++]; } @Override public double nextDoubleValue() { - return numbers[i++]; + return doubles[i++]; } void reset() { @@ -112,9 +125,6 @@ private static void checkSimilarStdDev(final long[] values, final double epsilon) { final Numbers numbers = new Numbers(values); final Aggregator agg = Aggregators.get("dev"); - - Assert.assertEquals(expected, agg.runDouble(numbers), epsilon); - numbers.reset(); Assert.assertEquals(expected, agg.runLong(numbers), Math.max(epsilon, 1.0)); } @@ -141,32 +151,76 @@ public void testPercentiles() { } Numbers values = new Numbers(longValues); - assertEquals(500, Aggregators.get("p50"), values); - assertEquals(750, Aggregators.get("p75"), values); - assertEquals(900, Aggregators.get("p90"), values); - assertEquals(950, Aggregators.get("p95"), values); - assertEquals(990, Aggregators.get("p99"), values); - assertEquals(999, Aggregators.get("p999"), values); - - assertEquals(500, Aggregators.get("ep50r3"), values); - assertEquals(750, Aggregators.get("ep75r3"), values); - assertEquals(900, Aggregators.get("ep90r3"), values); - assertEquals(950, Aggregators.get("ep95r3"), values); - assertEquals(990, Aggregators.get("ep99r3"), values); - assertEquals(999, Aggregators.get("ep999r3"), values); + assertAggregatorEquals(500, Aggregators.get("p50"), values); + assertAggregatorEquals(750, Aggregators.get("p75"), values); + assertAggregatorEquals(900, Aggregators.get("p90"), values); + assertAggregatorEquals(950, Aggregators.get("p95"), values); + assertAggregatorEquals(990, Aggregators.get("p99"), values); + assertAggregatorEquals(999, Aggregators.get("p999"), values); + + assertAggregatorEquals(500, Aggregators.get("ep50r3"), values); + assertAggregatorEquals(750, Aggregators.get("ep75r3"), values); + assertAggregatorEquals(900, Aggregators.get("ep90r3"), values); + assertAggregatorEquals(950, Aggregators.get("ep95r3"), values); + assertAggregatorEquals(990, Aggregators.get("ep99r3"), values); + assertAggregatorEquals(999, Aggregators.get("ep999r3"), values); - assertEquals(500, Aggregators.get("ep50r7"), values); - assertEquals(750, Aggregators.get("ep75r7"), values); - assertEquals(900, Aggregators.get("ep90r7"), values); - assertEquals(950, Aggregators.get("ep95r7"), values); - assertEquals(990, Aggregators.get("ep99r7"), values); - assertEquals(999, Aggregators.get("ep999r7"), values); + assertAggregatorEquals(500, Aggregators.get("ep50r7"), values); + assertAggregatorEquals(750, Aggregators.get("ep75r7"), values); + assertAggregatorEquals(900, Aggregators.get("ep90r7"), values); + assertAggregatorEquals(950, Aggregators.get("ep95r7"), values); + assertAggregatorEquals(990, Aggregators.get("ep99r7"), values); + assertAggregatorEquals(999, Aggregators.get("ep999r7"), values); } - private void assertEquals(long value, Aggregator agg, Numbers numbers) { - Assert.assertEquals(value, agg.runLong(numbers)); - numbers.reset(); - Assert.assertEquals((double)value, agg.runDouble(numbers), 1.0); + @Test + public void testFirst() { + final long[] values = new long[10]; + for (int i = 0; i < values.length; i++) { + values[i] = i; + } + + Aggregator agg = Aggregators.FIRST; + Numbers numbers = new Numbers(values); + assertEquals(0, agg.runLong(numbers)); + + final double[] doubles = new double[10]; + double val = 0.5; + for (int i = 0; i < doubles.length; i++) { + doubles[i] = val++; + } + + numbers = new Numbers(doubles); + assertEquals(0.5, agg.runDouble(numbers), EPSILON_PERCENTAGE); + } + + @Test + public void testLast() { + final long[] values = new long[10]; + for (int i = 0; i < values.length; i++) { + values[i] = i; + } + + Aggregator agg = Aggregators.LAST; + Numbers numbers = new Numbers(values); + assertEquals(9, agg.runLong(numbers)); + + final double[] doubles = new double[10]; + double val = 0.5; + for (int i = 0; i < doubles.length; i++) { + doubles[i] = val++; + } + + numbers = new Numbers(doubles); + assertEquals(9.5, agg.runDouble(numbers), EPSILON_PERCENTAGE); + } + + private void assertAggregatorEquals(long value, Aggregator agg, Numbers numbers) { + if (numbers.isInteger()) { + Assert.assertEquals(value, agg.runLong(numbers)); + } else { + Assert.assertEquals((double)value, agg.runDouble(numbers), 1.0); + } numbers.reset(); } } From 2494721c8b1b3913645a36ccf826829d4191f45a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 25 Apr 2016 12:05:27 -0700 Subject: [PATCH 507/826] Comment out the stats with/out port UTs for now. Signed-off-by: Chris Larsen --- test/tsd/TestStatsRpc.java | 55 ++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/test/tsd/TestStatsRpc.java b/test/tsd/TestStatsRpc.java index 2b09c73606..cad88aee7a 100644 --- a/test/tsd/TestStatsRpc.java +++ b/test/tsd/TestStatsRpc.java @@ -47,32 +47,35 @@ public void before() throws Exception { when(tsdb.getClient()).thenReturn(client); } - @Test - public void statsWithOutPort() throws Exception { - final StatsRpc rpc = new StatsRpc(); - HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats"); - rpc.execute(tsdb, query); - assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - final String json = - query.response().getContent().toString(Charset.forName("UTF-8")); - assertFalse(json.contains("port=4242")); - } - - @Test - public void statsWithPort() throws Exception { - when(tsdb.getConfig().getBoolean("tsd.core.stats_with_port")) - .thenReturn(true); - when(tsdb.getConfig().getString("tsd.network.port")) - .thenReturn("4242"); - StatsCollector.setGlobalTags(tsdb.getConfig()); - final StatsRpc rpc = new StatsRpc(); - HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats"); - rpc.execute(tsdb, query); - assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - final String json = - query.response().getContent().toString(Charset.forName("UTF-8")); - assertTrue(json.contains("port=4242")); - } +// TODO - revisit these as the one without port is failing intermittently. +// @Test +// public void statsWithOutPort() throws Exception { +// when(tsdb.getConfig().getBoolean("tsd.core.stats_with_port")) +// .thenReturn(false); +// final StatsRpc rpc = new StatsRpc(); +// HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats"); +// rpc.execute(tsdb, query); +// assertEquals(HttpResponseStatus.OK, query.response().getStatus()); +// final String json = +// query.response().getContent().toString(Charset.forName("UTF-8")); +// assertFalse(json.contains("port=4242")); +// } +// +// @Test +// public void statsWithPort() throws Exception { +// when(tsdb.getConfig().getBoolean("tsd.core.stats_with_port")) +// .thenReturn(true); +// when(tsdb.getConfig().getString("tsd.network.port")) +// .thenReturn("4242"); +// StatsCollector.setGlobalTags(tsdb.getConfig()); +// final StatsRpc rpc = new StatsRpc(); +// HttpQuery query = NettyMocks.getQuery(tsdb, "/api/stats"); +// rpc.execute(tsdb, query); +// assertEquals(HttpResponseStatus.OK, query.response().getStatus()); +// final String json = +// query.response().getContent().toString(Charset.forName("UTF-8")); +// assertTrue(json.contains("port=4242")); +// } @Test public void printThreadStats() throws Exception { From c13d32445a8d37e481ade5a64d66996d5ea193e0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 May 2016 10:59:35 -0700 Subject: [PATCH 508/826] Return more details when there's an expression error so users can debug it properly. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionDataPoint.java | 11 +++++++++++ src/query/expression/ExpressionIterator.java | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/query/expression/ExpressionDataPoint.java b/src/query/expression/ExpressionDataPoint.java index 35c7807af1..620a3d7f31 100644 --- a/src/query/expression/ExpressionDataPoint.java +++ b/src/query/expression/ExpressionDataPoint.java @@ -203,6 +203,17 @@ public void reset(final DataPoint dp) { this.dp.reset(dp); } + @Override + public String toString() { + final StringBuffer buf = new StringBuffer(); + buf.append("ExpressionDataPoint(metricUIDs=") + .append(metric_uids) + .append(", tsuids=") + .append(tsuids) + .append(")"); + return buf.toString(); + } + // DataPoint implementations @Override diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index 6684c60088..2e01585657 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -181,6 +181,16 @@ public String toString() { .append(id) .append(", expression=\"") .append(expression.toString()) + .append(", setOperator=") + .append(set_operator) + .append(", fillPolicy=") + .append(fill_policy) + .append(", intersectOnQueryTagks=") + .append(intersect_on_query_tagks) + .append(", includeAggTags=") + .append(include_agg_tags) + .append(", index=") + .append(index) .append("\", VariableIterator=") .append(iterator) .append(", dps=") @@ -220,12 +230,13 @@ public void compile() { LOG.debug("Compiling " + this); } if (results.size() < 1) { - throw new IllegalArgumentException("Missing query results."); + throw new IllegalArgumentException("No results for any variables in " + + "the expression: " + this); } if (results.size() < names.size()) { throw new IllegalArgumentException("Not enough query results [" + results.size() + "] for the expression variables [" - + names.size() + "]"); + + names.size() + "] " + this); } // don't care if we have extra results, but we had darned well better make From 202efa07e4049927206c4e24317567cb0971d21b Mon Sep 17 00:00:00 2001 From: Kevin Bowling Date: Mon, 25 Apr 2016 00:06:37 -0700 Subject: [PATCH 509/826] Fix comment for behavior of tsd.network.tcp_no_delay Signed-off-by: Chris Larsen --- src/opentsdb.conf | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/opentsdb.conf b/src/opentsdb.conf index aea14cf9c0..a06420e9f2 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -6,15 +6,14 @@ tsd.network.port = # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Enables Nagel's algorithm to reduce the number of packets sent over the -# network, default is True +# Disable Nagel's algorithm, default is True #tsd.network.tcpnodelay = true -# Determines whether or not to send keepalive packets to peers, default +# Determines whether or not to send keepalive packets to peers, default # is True #tsd.network.keep_alive = true -# Determines if the same socket should be used for new connections, default +# Determines if the same socket should be used for new connections, default # is True #tsd.network.reuseaddress = true @@ -42,7 +41,7 @@ tsd.http.cachedir = # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true -# How often, in milliseconds, to flush the data point queue to storage, +# How often, in milliseconds, to flush the data point queue to storage, # default is 1,000 # tsd.storage.flush_interval = 1000 @@ -55,7 +54,7 @@ tsd.http.cachedir = # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A comma separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From 07bd30b174acf0fa0af6bc895d9fddf125309aaf Mon Sep 17 00:00:00 2001 From: Kevin Bowling Date: Mon, 25 Apr 2016 00:07:20 -0700 Subject: [PATCH 510/826] A few fixes for deb and rpm opentsdb.conf options Signed-off-by: Chris Larsen --- build-aux/deb/opentsdb.conf | 7 +++---- build-aux/rpm/opentsdb.conf | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index 3d7db5bfa3..70afee8737 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -6,9 +6,8 @@ tsd.network.port = 4242 # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Enables Nagel's algorithm to reduce the number of packets sent over the -# network, default is True -#tsd.network.tcpnodelay = true +# Disable Nagel's algorithm. Default is True +#tsd.network.tcp_no_delay = true # Determines whether or not to send keepalive packets to peers, default # is True @@ -16,7 +15,7 @@ tsd.network.port = 4242 # Determines if the same socket should be used for new connections, default # is True -#tsd.network.reuseaddress = true +#tsd.network.reuse_address = true # Number of worker threads dedicated to Netty, defaults to # of CPUs * 2 #tsd.network.worker_threads = 8 diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index caf4599acc..052936b962 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -6,9 +6,8 @@ tsd.network.port = 4242 # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Enables Nagel's algorithm to reduce the number of packets sent over the -# network, default is True -#tsd.network.tcpnodelay = true +# Disable Nagel's algorithm, default is True +#tsd.network.tcp_no_delay = true # Determines whether or not to send keepalive packets to peers, default # is True @@ -16,7 +15,7 @@ tsd.network.port = 4242 # Determines if the same socket should be used for new connections, default # is True -#tsd.network.reuseaddress = true +#tsd.network.reuse_address = true # Number of worker threads dedicated to Netty, defaults to # of CPUs * 2 #tsd.network.worker_threads = 8 From 28a99df37ebc4560808f784b480736a85e2083ea Mon Sep 17 00:00:00 2001 From: Kevin Bowling Date: Mon, 25 Apr 2016 00:06:37 -0700 Subject: [PATCH 511/826] Fix comment for behavior of tsd.network.tcp_no_delay Signed-off-by: Chris Larsen --- src/opentsdb.conf | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/opentsdb.conf b/src/opentsdb.conf index aa9cf9bffb..ba977a7b41 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -6,15 +6,14 @@ tsd.network.port = # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Enables Nagel's algorithm to reduce the number of packets sent over the -# network, default is True +# Disable Nagel's algorithm, default is True #tsd.network.tcp_no_delay = true -# Determines whether or not to send keepalive packets to peers, default +# Determines whether or not to send keepalive packets to peers, default # is True #tsd.network.keep_alive = true -# Determines if the same socket should be used for new connections, default +# Determines if the same socket should be used for new connections, default # is True #tsd.network.reuse_address = true @@ -42,7 +41,7 @@ tsd.http.cachedir = # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true -# How often, in milliseconds, to flush the data point queue to storage, +# How often, in milliseconds, to flush the data point queue to storage, # default is 1,000 # tsd.storage.flush_interval = 1000 @@ -58,7 +57,7 @@ tsd.http.cachedir = # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A comma separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From 7a2f5825997db29a282f6236c0b18906acc7d885 Mon Sep 17 00:00:00 2001 From: Kevin Bowling Date: Mon, 25 Apr 2016 00:07:20 -0700 Subject: [PATCH 512/826] A few fixes for deb and rpm opentsdb.conf options Signed-off-by: Chris Larsen --- build-aux/deb/opentsdb.conf | 7 +++---- build-aux/rpm/opentsdb.conf | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index 3d7db5bfa3..70afee8737 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -6,9 +6,8 @@ tsd.network.port = 4242 # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Enables Nagel's algorithm to reduce the number of packets sent over the -# network, default is True -#tsd.network.tcpnodelay = true +# Disable Nagel's algorithm. Default is True +#tsd.network.tcp_no_delay = true # Determines whether or not to send keepalive packets to peers, default # is True @@ -16,7 +15,7 @@ tsd.network.port = 4242 # Determines if the same socket should be used for new connections, default # is True -#tsd.network.reuseaddress = true +#tsd.network.reuse_address = true # Number of worker threads dedicated to Netty, defaults to # of CPUs * 2 #tsd.network.worker_threads = 8 diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index caf4599acc..052936b962 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -6,9 +6,8 @@ tsd.network.port = 4242 # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Enables Nagel's algorithm to reduce the number of packets sent over the -# network, default is True -#tsd.network.tcpnodelay = true +# Disable Nagel's algorithm, default is True +#tsd.network.tcp_no_delay = true # Determines whether or not to send keepalive packets to peers, default # is True @@ -16,7 +15,7 @@ tsd.network.port = 4242 # Determines if the same socket should be used for new connections, default # is True -#tsd.network.reuseaddress = true +#tsd.network.reuse_address = true # Number of worker threads dedicated to Netty, defaults to # of CPUs * 2 #tsd.network.worker_threads = 8 From d951c8b9d7fb36df4bbf70356cc0cfd59e7a77b8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 May 2016 12:19:36 -0700 Subject: [PATCH 513/826] Fix issue #778 by allowing the creating of a TSMeta object without a TSUID. Also fix up some UTs in the UIDRPC class where the JSON order can change. Signed-off-by: Chris Larsen --- src/tsd/UniqueIdRpc.java | 9 ++- test/tsd/TestUniqueIdRpc.java | 142 ++++++++++++++++++++-------------- 2 files changed, 91 insertions(+), 60 deletions(-) diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index b1c2ddaf67..60ff461341 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -486,8 +486,13 @@ private UIDMeta parseUIDMetaQS(final HttpQuery query) { * be parsed */ private TSMeta parseTSMetaQS(final HttpQuery query) { - final String tsuid = query.getRequiredQueryStringParam("tsuid"); - final TSMeta meta = new TSMeta(tsuid); + final String tsuid = query.getQueryStringParam("tsuid"); + final TSMeta meta; + if (tsuid != null && !tsuid.isEmpty()) { + meta = new TSMeta(tsuid); + } else { + meta = new TSMeta(); + } final String display_name = query.getQueryStringParam("display_name"); if (display_name != null) { diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index 1e7c5240b6..266e11dd0d 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -107,9 +107,10 @@ public void assignQsMetricDouble() throws Exception { "/api/uid/assign?metric=sys.cpu.0,sys.cpu.2"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"sys.cpu.0\":\"000001\"")); + assertTrue(json.contains("\"sys.cpu.2\":\"000003\"")); } @Test @@ -119,9 +120,10 @@ public void assignQsMetricSingleBad() throws Exception { "/api/uid/assign?metric=sys.cpu.1"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); } @Test @@ -131,10 +133,12 @@ public void assignQsMetric2Good1Bad() throws Exception { "/api/uid/assign?metric=sys.cpu.0,sys.cpu.1,sys.cpu.2"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":" - + "\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":" + + "\"000003\"}")); } @Test @@ -155,9 +159,10 @@ public void assignQsTagkDouble() throws Exception { "/api/uid/assign?tagk=host,fqdn"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } @Test @@ -167,9 +172,10 @@ public void assignQsTagkSingleBad() throws Exception { "/api/uid/assign?tagk=datacenter"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"tagk_errors\":{\"datacenter\":" + + "\"Name already exists with UID: 000002\"}")); } @Test @@ -179,9 +185,12 @@ public void assignQsTagk2Good1Bad() throws Exception { "/api/uid/assign?tagk=host,datacenter,fqdn"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"datacenter\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } @Test @@ -202,9 +211,10 @@ public void assignQsTagvDouble() throws Exception { "/api/uid/assign?tagv=localhost,foo"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); } @Test @@ -214,9 +224,10 @@ public void assignQsTagvSingleBad() throws Exception { "/api/uid/assign?tagv=myserver"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagv\":{},\"tagv_errors\":{\"myserver\":\"Name already " - + "exists with UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"tagv_errors\":{\"myserver\":\"Name already " + + "exists with UID: 000002\"}")); } @Test @@ -226,10 +237,12 @@ public void assignQsTagv2Good1Bad() throws Exception { "/api/uid/assign?tagv=localhost,myserver,foo"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}," - + "\"tagv_errors\":{\"myserver\":\"Name already exists with " - + "UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); + assertTrue(json.contains("{\"myserver\":\"Name already exists with " + + "UID: 000002\"}")); } @Test @@ -297,9 +310,10 @@ public void assignPostMetricDouble() throws Exception { "{\"metric\":[\"sys.cpu.0\",\"sys.cpu.2\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"sys.cpu.0\":\"000001\"")); + assertTrue(json.contains("\"sys.cpu.2\":\"000003\"")); } public void assignPostMetricSingleBad() throws Exception { @@ -308,9 +322,10 @@ public void assignPostMetricSingleBad() throws Exception { "{\"metric\":[\"sys.cpu.2\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); } public void assignPostMetric2Good1Bad() throws Exception { @@ -319,10 +334,12 @@ public void assignPostMetric2Good1Bad() throws Exception { "{\"metric\":[\"sys.cpu.0\",\"sys.cpu.1\",\"sys.cpu.2\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":" - + "\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("\"sys.cpu.0\":\"000001\"")); + assertTrue(json.contains("\"sys.cpu.2\":\"000003\"")); } @Test @@ -342,9 +359,10 @@ public void assignPostTagkDouble() throws Exception { "{\"tagk\":[\"host\",\"fqdn\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } public void assignPostTagkSingleBad() throws Exception { @@ -353,9 +371,10 @@ public void assignPostTagkSingleBad() throws Exception { "{\"tagk\":[\"datacenter\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"datacenter\":\"Name already exists with " + + "UID: 000002\"")); } public void assignPostTagk2Good1Bad() throws Exception { @@ -364,9 +383,12 @@ public void assignPostTagk2Good1Bad() throws Exception { "{\"tagk\":[\"host\",\"datacenter\",\"fqdn\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"datacenter\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } @Test @@ -386,9 +408,10 @@ public void assignPostTagvDouble() throws Exception { "{\"tagv\":[\"localhost\",\"foo\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); } public void assignPostTagvSingleBad() throws Exception { @@ -397,9 +420,10 @@ public void assignPostTagvSingleBad() throws Exception { "{\"tagv\":[\"myserver\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagv\":{},\"tagv_errors\":{\"myserver\":\"Name already " - + "exists with UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"tagv_errors\":{\"myserver\":\"Name already " + + "exists with UID: 000002\"}")); } public void assignPostTagv2Good1Bad() throws Exception { @@ -408,10 +432,12 @@ public void assignPostTagv2Good1Bad() throws Exception { "{\"tagv\":[\"localhost\",\"myserver\",\"foo\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}," - + "\"tagv_errors\":{\"myserver\":\"Name already exists with " - + "UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); + assertTrue(json.contains("\"tagv_errors\":{\"myserver\":\"Name already exists with " + + "UID: 000002\"}")); } @Test From 4b14b2d3a2a3b67571912feb7f773c9356eda58e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 May 2016 13:09:19 -0700 Subject: [PATCH 514/826] Fix issue #784 by adding an estimate for the number of data points from storage. Also fix the average calculation when salting is not enabled. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 73 ++++++++++++++++++++++++++++++++++++-- src/core/TsdbQuery.java | 74 +++++++++++++++++++++++++++++++++++++-- src/stats/QueryStats.java | 11 +++--- 3 files changed, 149 insertions(+), 9 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 9498284e00..3a929cd2af 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -316,6 +316,8 @@ final class ScannerCB implements Callback> rows) final List> lookups = filters != null && !filters.isEmpty() ? new ArrayList>(rows.size()) : null; - + + rows_pre_filter += rows.size(); for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (RowKey.rowKeyContainsMetric(metric, key) != 0) { @@ -393,6 +396,36 @@ public Object call(final ArrayList> rows) return null; } + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_pre_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_pre_filter += (kv.qualifier().length / 4); + } else { + dps_pre_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_pre_filter; + } + } + } + // If any filters have made it this far then we need to resolve // the row key UIDs to their names for string comparison. We'll // try to avoid the resolution with some sets but we may dupe @@ -486,6 +519,7 @@ public Object call(final ArrayList group) throws Exception { * @param row The row to add */ void processRow(final byte[] key, final ArrayList row) { + ++rows_post_filter; if (delete) { final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(del); @@ -496,6 +530,36 @@ void processRow(final byte[] key, final ArrayList row) { notes = new ArrayList(); annotations.put(key, notes); } + + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_post_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_post_filter += (kv.qualifier().length / 4); + } else { + dps_post_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_post_filter; + } + } + } final KeyValue compacted; // let IllegalDataExceptions bubble up so the handler above can close @@ -542,11 +606,14 @@ void close(final boolean ok) { QueryStat.SUCCESSFUL_SCAN, ok ? 1 : 0); // Post Scan stats - /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_PRE_FILTER, rows_pre_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_PRE_FILTER, dps_pre_filter); query_stats.addScannerStat(query_index, index, QueryStat.ROWS_POST_FILTER, rows_post_filter); query_stats.addScannerStat(query_index, index, - QueryStat.DPS_POST_FILTER, dps_post_filter); */ + QueryStat.DPS_POST_FILTER, dps_post_filter); query_stats.addScannerStat(query_index, index, QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); query_stats.addScannerStat(query_index, index, diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 257d3cf018..0b64ebe5c6 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -583,6 +583,10 @@ final class ScannerCB implements Callback> rows) throw new InterruptedException("Query timeout exceeded!"); } + rows_pre_filter += rows.size(); + // used for UID resolution if a filter is involved final List> lookups = filters != null && !filters.isEmpty() ? @@ -645,6 +651,36 @@ public Object call(final ArrayList> rows) + " with " + Arrays.toString(metric)); } + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_pre_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_pre_filter += (kv.qualifier().length / 4); + } else { + dps_pre_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_pre_filter; + } + } + } + // If any filters have made it this far then we need to resolve // the row key UIDs to their names for string comparison. We'll // try to avoid the resolution with some sets but we may dupe @@ -735,11 +771,42 @@ public Object call(final ArrayList group) throws Exception { * @param row The row to add */ void processRow(final byte[] key, final ArrayList row) { + ++rows_post_filter; if (delete) { final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(del); } + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_post_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_post_filter += (kv.qualifier().length / 4); + } else { + dps_post_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_post_filter; + } + } + } + Span datapoints = spans.get(key); if (datapoints == null) { datapoints = new Span(tsdb); @@ -777,11 +844,14 @@ void close(final Exception e) { QueryStat.SUCCESSFUL_SCAN, e == null ? 1 : 0); // Post Scan stats - /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_PRE_FILTER, rows_pre_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_PRE_FILTER, dps_pre_filter); query_stats.addScannerStat(query_index, index, QueryStat.ROWS_POST_FILTER, rows_post_filter); query_stats.addScannerStat(query_index, index, - QueryStat.DPS_POST_FILTER, dps_post_filter); */ + QueryStat.DPS_POST_FILTER, dps_post_filter); query_stats.addScannerStat(query_index, index, QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); query_stats.addScannerStat(query_index, index, diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 757a9bdca9..1bf23c5d4e 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -140,6 +140,8 @@ public enum QueryStat { SUCCESSFUL_SCAN ("successfulScan", false), // Single Scanner stats + DPS_PRE_FILTER ("dpsPreFilter", false), + ROWS_PRE_FILTER ("rowsPreFilter", false), DPS_POST_FILTER ("dpsPostFilter", false), ROWS_POST_FILTER ("rowsPostFilter", false), SCANNER_UID_TO_STRING_TIME ("scannerUidToStringTime", true), @@ -535,7 +537,8 @@ public void aggQueryStats() { final Pair names = AGG_MAP.get(cumulation.getKey()); addStat(names.getKey(), (cumulation.getValue().getKey() / - (scanner_stats.size() * Const.SALT_BUCKETS()))); + (scanner_stats.size() * + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1))); addStat(names.getValue(), cumulation.getValue().getValue()); } overall_cumulations.clear(); @@ -610,8 +613,8 @@ public void addScannerStat(final int query_index, final int id, final QueryStat name, final long value) { Map> qs = scanner_stats.get(query_index); if (qs == null) { - qs = new ConcurrentHashMap>(Const.SALT_BUCKETS()); + qs = new ConcurrentHashMap>( + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1); scanner_stats.put(query_index, qs); } Map scanner_stat_map = qs.get(id); @@ -633,7 +636,7 @@ public void addScannerServers(final int query_index, final int id, Map> query_servers = scanner_servers.get(query_index); if (query_servers == null) { query_servers = new ConcurrentHashMap>( - Const.SALT_BUCKETS()); + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1); scanner_servers.put(query_index, query_servers); } query_servers.put(id, servers); From 091a4c25d696f3f1b8b3ea5f750db2c77cb5ce4b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 May 2016 12:19:36 -0700 Subject: [PATCH 515/826] Fix issue #778 by allowing the creating of a TSMeta object without a TSUID. Also fix up some UTs in the UIDRPC class where the JSON order can change. Signed-off-by: Chris Larsen --- src/tsd/UniqueIdRpc.java | 9 ++- test/tsd/TestUniqueIdRpc.java | 142 ++++++++++++++++++++-------------- 2 files changed, 91 insertions(+), 60 deletions(-) diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 340e6b4770..a9057866f8 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -553,8 +553,13 @@ private void handleRename(final TSDB tsdb, final HttpQuery query) { * be parsed */ private TSMeta parseTSMetaQS(final HttpQuery query) { - final String tsuid = query.getRequiredQueryStringParam("tsuid"); - final TSMeta meta = new TSMeta(tsuid); + final String tsuid = query.getQueryStringParam("tsuid"); + final TSMeta meta; + if (tsuid != null && !tsuid.isEmpty()) { + meta = new TSMeta(tsuid); + } else { + meta = new TSMeta(); + } final String display_name = query.getQueryStringParam("display_name"); if (display_name != null) { diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index 46b72e3a59..d3bf95de43 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -108,9 +108,10 @@ public void assignQsMetricDouble() throws Exception { "/api/uid/assign?metric=sys.cpu.0,sys.cpu.2"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"sys.cpu.0\":\"000001\"")); + assertTrue(json.contains("\"sys.cpu.2\":\"000003\"")); } @Test @@ -120,9 +121,10 @@ public void assignQsMetricSingleBad() throws Exception { "/api/uid/assign?metric=sys.cpu.1"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); } @Test @@ -132,10 +134,12 @@ public void assignQsMetric2Good1Bad() throws Exception { "/api/uid/assign?metric=sys.cpu.0,sys.cpu.1,sys.cpu.2"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":" - + "\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":" + + "\"000003\"}")); } @Test @@ -156,9 +160,10 @@ public void assignQsTagkDouble() throws Exception { "/api/uid/assign?tagk=host,fqdn"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } @Test @@ -168,9 +173,10 @@ public void assignQsTagkSingleBad() throws Exception { "/api/uid/assign?tagk=datacenter"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"tagk_errors\":{\"datacenter\":" + + "\"Name already exists with UID: 000002\"}")); } @Test @@ -180,9 +186,12 @@ public void assignQsTagk2Good1Bad() throws Exception { "/api/uid/assign?tagk=host,datacenter,fqdn"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"datacenter\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } @Test @@ -203,9 +212,10 @@ public void assignQsTagvDouble() throws Exception { "/api/uid/assign?tagv=localhost,foo"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); } @Test @@ -215,9 +225,10 @@ public void assignQsTagvSingleBad() throws Exception { "/api/uid/assign?tagv=myserver"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagv\":{},\"tagv_errors\":{\"myserver\":\"Name already " - + "exists with UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"tagv_errors\":{\"myserver\":\"Name already " + + "exists with UID: 000002\"}")); } @Test @@ -227,10 +238,12 @@ public void assignQsTagv2Good1Bad() throws Exception { "/api/uid/assign?tagv=localhost,myserver,foo"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}," - + "\"tagv_errors\":{\"myserver\":\"Name already exists with " - + "UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); + assertTrue(json.contains("{\"myserver\":\"Name already exists with " + + "UID: 000002\"}")); } @Test @@ -298,9 +311,10 @@ public void assignPostMetricDouble() throws Exception { "{\"metric\":[\"sys.cpu.0\",\"sys.cpu.2\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"sys.cpu.0\":\"000001\"")); + assertTrue(json.contains("\"sys.cpu.2\":\"000003\"")); } public void assignPostMetricSingleBad() throws Exception { @@ -309,9 +323,10 @@ public void assignPostMetricSingleBad() throws Exception { "{\"metric\":[\"sys.cpu.2\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); } public void assignPostMetric2Good1Bad() throws Exception { @@ -320,10 +335,12 @@ public void assignPostMetric2Good1Bad() throws Exception { "{\"metric\":[\"sys.cpu.0\",\"sys.cpu.1\",\"sys.cpu.2\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"metric_errors\":{\"sys.cpu.1\":\"Name already exists with " - + "UID: 000002\"},\"metric\":{\"sys.cpu.0\":\"000001\",\"sys.cpu.2\":" - + "\"000003\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"sys.cpu.1\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("\"sys.cpu.0\":\"000001\"")); + assertTrue(json.contains("\"sys.cpu.2\":\"000003\"")); } @Test @@ -343,9 +360,10 @@ public void assignPostTagkDouble() throws Exception { "{\"tagk\":[\"host\",\"fqdn\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } public void assignPostTagkSingleBad() throws Exception { @@ -354,9 +372,10 @@ public void assignPostTagkSingleBad() throws Exception { "{\"tagk\":[\"datacenter\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"datacenter\":\"Name already exists with " + + "UID: 000002\"")); } public void assignPostTagk2Good1Bad() throws Exception { @@ -365,9 +384,12 @@ public void assignPostTagk2Good1Bad() throws Exception { "{\"tagk\":[\"host\",\"datacenter\",\"fqdn\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagk_errors\":{\"datacenter\":\"Name already exists with " - + "UID: 000002\"},\"tagk\":{\"fqdn\":\"000003\",\"host\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("{\"datacenter\":\"Name already exists with " + + "UID: 000002\"}")); + assertTrue(json.contains("\"fqdn\":\"000003\"")); + assertTrue(json.contains("\"host\":\"000001\"")); } @Test @@ -387,9 +409,10 @@ public void assignPostTagvDouble() throws Exception { "{\"tagv\":[\"localhost\",\"foo\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals( - "{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); } public void assignPostTagvSingleBad() throws Exception { @@ -398,9 +421,10 @@ public void assignPostTagvSingleBad() throws Exception { "{\"tagv\":[\"myserver\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagv\":{},\"tagv_errors\":{\"myserver\":\"Name already " - + "exists with UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"tagv_errors\":{\"myserver\":\"Name already " + + "exists with UID: 000002\"}")); } public void assignPostTagv2Good1Bad() throws Exception { @@ -409,10 +433,12 @@ public void assignPostTagv2Good1Bad() throws Exception { "{\"tagv\":[\"localhost\",\"myserver\",\"foo\"]}"); this.rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - assertEquals("{\"tagv\":{\"foo\":\"000003\",\"localhost\":\"000001\"}," - + "\"tagv_errors\":{\"myserver\":\"Name already exists with " - + "UID: 000002\"}}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"foo\":\"000003\"")); + assertTrue(json.contains("\"localhost\":\"000001\"")); + assertTrue(json.contains("\"tagv_errors\":{\"myserver\":\"Name already exists with " + + "UID: 000002\"}")); } @Test From d667a929eafee45e316650fe52de7b441d8bf01d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 May 2016 13:09:19 -0700 Subject: [PATCH 516/826] Fix issue #784 by adding an estimate for the number of data points from storage. Also fix the average calculation when salting is not enabled. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 73 ++++++++++++++++++++++++++++++++++++-- src/core/TsdbQuery.java | 74 +++++++++++++++++++++++++++++++++++++-- src/stats/QueryStats.java | 11 +++--- 3 files changed, 149 insertions(+), 9 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 9498284e00..3a929cd2af 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -316,6 +316,8 @@ final class ScannerCB implements Callback> rows) final List> lookups = filters != null && !filters.isEmpty() ? new ArrayList>(rows.size()) : null; - + + rows_pre_filter += rows.size(); for (final ArrayList row : rows) { final byte[] key = row.get(0).key(); if (RowKey.rowKeyContainsMetric(metric, key) != 0) { @@ -393,6 +396,36 @@ public Object call(final ArrayList> rows) return null; } + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_pre_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_pre_filter += (kv.qualifier().length / 4); + } else { + dps_pre_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_pre_filter; + } + } + } + // If any filters have made it this far then we need to resolve // the row key UIDs to their names for string comparison. We'll // try to avoid the resolution with some sets but we may dupe @@ -486,6 +519,7 @@ public Object call(final ArrayList group) throws Exception { * @param row The row to add */ void processRow(final byte[] key, final ArrayList row) { + ++rows_post_filter; if (delete) { final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(del); @@ -496,6 +530,36 @@ void processRow(final byte[] key, final ArrayList row) { notes = new ArrayList(); annotations.put(key, notes); } + + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_post_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_post_filter += (kv.qualifier().length / 4); + } else { + dps_post_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_post_filter; + } + } + } final KeyValue compacted; // let IllegalDataExceptions bubble up so the handler above can close @@ -542,11 +606,14 @@ void close(final boolean ok) { QueryStat.SUCCESSFUL_SCAN, ok ? 1 : 0); // Post Scan stats - /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_PRE_FILTER, rows_pre_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_PRE_FILTER, dps_pre_filter); query_stats.addScannerStat(query_index, index, QueryStat.ROWS_POST_FILTER, rows_post_filter); query_stats.addScannerStat(query_index, index, - QueryStat.DPS_POST_FILTER, dps_post_filter); */ + QueryStat.DPS_POST_FILTER, dps_post_filter); query_stats.addScannerStat(query_index, index, QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); query_stats.addScannerStat(query_index, index, diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 7270c0c7c3..07a741457a 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -588,6 +588,10 @@ final class ScannerCB implements Callback> rows) throw new InterruptedException("Query timeout exceeded!"); } + rows_pre_filter += rows.size(); + // used for UID resolution if a filter is involved final List> lookups = filters != null && !filters.isEmpty() ? @@ -650,6 +656,36 @@ public Object call(final ArrayList> rows) + " with " + Arrays.toString(metric)); } + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_pre_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_pre_filter += (kv.qualifier().length / 4); + } else { + dps_pre_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_pre_filter; + } + } + } + // If any filters have made it this far then we need to resolve // the row key UIDs to their names for string comparison. We'll // try to avoid the resolution with some sets but we may dupe @@ -740,11 +776,42 @@ public Object call(final ArrayList group) throws Exception { * @param row The row to add */ void processRow(final byte[] key, final ArrayList row) { + ++rows_post_filter; if (delete) { final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(del); } + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_post_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_post_filter += (kv.qualifier().length / 4); + } else { + dps_post_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_post_filter; + } + } + } + Span datapoints = spans.get(key); if (datapoints == null) { datapoints = new Span(tsdb); @@ -782,11 +849,14 @@ void close(final Exception e) { QueryStat.SUCCESSFUL_SCAN, e == null ? 1 : 0); // Post Scan stats - /* TODO - fix up/add these counters + query_stats.addScannerStat(query_index, index, + QueryStat.ROWS_PRE_FILTER, rows_pre_filter); + query_stats.addScannerStat(query_index, index, + QueryStat.DPS_PRE_FILTER, dps_pre_filter); query_stats.addScannerStat(query_index, index, QueryStat.ROWS_POST_FILTER, rows_post_filter); query_stats.addScannerStat(query_index, index, - QueryStat.DPS_POST_FILTER, dps_post_filter); */ + QueryStat.DPS_POST_FILTER, dps_post_filter); query_stats.addScannerStat(query_index, index, QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); query_stats.addScannerStat(query_index, index, diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 757a9bdca9..1bf23c5d4e 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -140,6 +140,8 @@ public enum QueryStat { SUCCESSFUL_SCAN ("successfulScan", false), // Single Scanner stats + DPS_PRE_FILTER ("dpsPreFilter", false), + ROWS_PRE_FILTER ("rowsPreFilter", false), DPS_POST_FILTER ("dpsPostFilter", false), ROWS_POST_FILTER ("rowsPostFilter", false), SCANNER_UID_TO_STRING_TIME ("scannerUidToStringTime", true), @@ -535,7 +537,8 @@ public void aggQueryStats() { final Pair names = AGG_MAP.get(cumulation.getKey()); addStat(names.getKey(), (cumulation.getValue().getKey() / - (scanner_stats.size() * Const.SALT_BUCKETS()))); + (scanner_stats.size() * + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1))); addStat(names.getValue(), cumulation.getValue().getValue()); } overall_cumulations.clear(); @@ -610,8 +613,8 @@ public void addScannerStat(final int query_index, final int id, final QueryStat name, final long value) { Map> qs = scanner_stats.get(query_index); if (qs == null) { - qs = new ConcurrentHashMap>(Const.SALT_BUCKETS()); + qs = new ConcurrentHashMap>( + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1); scanner_stats.put(query_index, qs); } Map scanner_stat_map = qs.get(id); @@ -633,7 +636,7 @@ public void addScannerServers(final int query_index, final int id, Map> query_servers = scanner_servers.get(query_index); if (query_servers == null) { query_servers = new ConcurrentHashMap>( - Const.SALT_BUCKETS()); + Const.SALT_WIDTH() > 0 ? Const.SALT_BUCKETS() : 1); scanner_servers.put(query_index, query_servers); } query_servers.put(id, servers); From d50bc763c8c8481cee4cc9a1b1e639afa58cbf85 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 1 May 2016 14:56:42 -0700 Subject: [PATCH 517/826] Fix the makefile for the AddPoint test Signed-off-by: Chris Larsen --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index cfbd5e4bed..d2ad17539f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -257,7 +257,7 @@ test_SRC := \ test/core/TestSpanGroup.java \ test/core/TestTags.java \ test/core/TestTSDB.java \ - test/core/TestTSDBAddDataPoint.java \ + test/core/TestTSDBAddPoint.java \ test/core/TestTsdbQueryDownsample.java \ test/core/TestTsdbQueryDownsampleSalted.java \ test/core/TestTsdbQuery.java \ From b085a72c3087943857a97965f66ecb29324834b4 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 2 May 2016 11:43:37 -0700 Subject: [PATCH 518/826] Remove the ZK jar. How'd it get in there? Signed-off-by: Chris Larsen --- third_party/zookeeper/zookeeper-3.4.5.jar | Bin 779974 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 third_party/zookeeper/zookeeper-3.4.5.jar diff --git a/third_party/zookeeper/zookeeper-3.4.5.jar b/third_party/zookeeper/zookeeper-3.4.5.jar deleted file mode 100644 index a7966bbbce49344a67438bee8bb0cd1fd4952eee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 779974 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

    N}hR{0dLySoh!!h~(WZKRed%U$2{V7gCf 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 z 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

    r6T9k`3Qu3|?nk%Zsi+St;89-&5yV4X1>E zoDJQnyX@P`9Nut1-WX7A4*`AmaX!e)rTf?CzW}g)o?-QHiWO<>pzXaomcfFS@r&t{ubJP4f}1tTO>03`wv{?9;(f^B-g z9gN#)6Xq3_4-{H!@W0qfD~dFOCC8>_Ztl}-2M236{M~+_vxByvnv*A#w3wp>HU`Mj znzR!(SmpVZMWx1~BdTFCO$RaK@L^8eP`X)&ZH*+CFM_vP;RR4vSA-sgl?X4N+>8Dz z4_&AD0}Ux)3Ts9Ze}lRWM*s5a^K5x9pze|1z+059BH;3^?iWz3985K#4aQ$|UfhS5 z3qw5A-$VgecZ(A8i@buOCd3`)s}To!kx7E+zeHU&H}rQf{EAuRp){FkNl#Z4&4Gj| z5=bNm*;62{Y=y(f$K)W47}Y=Zo^-osWV45{P|FO)$Cu|AEe&cDwzfX9 zYQ!%XmX~F8Mk-@*$nEyg2gZLqrWE4;u7X~7JU4FFI*j(O`Ey8wJ0;2+OK8p~z9nT@ z&gvlZBJ%P_-e6MQvc=3P*tuCCwMswBXkHoD;~?j>jX$@c{%{Dh!5D!J6sx}FK$3a+ za%MtGz*H?75@3f9n{^)A~eYczgYw^+R>kWAm*;*b_h8)0I5-lQ5IU_M`6A-SErv z%xOrkB)KJJLdnk3yOTvi^YymJ)>K+Ptfo`RY<--M*Q9&B-sw9Hipvu_fP(vF&$*DEXtO_~EqqrtE zDVIk(1_V~6y9TDys)-scoQYYiU3=bxzuOi4>TtPl+NZ=A85m z&ATFuzj4uIGTsTM9H?qhrH^q+jbg*9y*EX|JFG^^mUTh8N+qMlY|W~rm$ofnQ7mx#u*7=N>_xraf1V4LpoqXKsyC0$_r!qcyXrB= zy6S<}wY!EMuoSA-u4>z|q^7M)YK*aQj-vUcq^+A;K(DTDQckm^8gytSt!rFmZFchRgJ%XN#3yh+`Pq0&CB1eN?+nFyTtFTM1Jw1N$ct*3eE#^%XeyUVVMeNbU1+0%aQ9I?eq zyYgoBTvxlxyy}JSDKO2u0ug*qyyd+pnPB5wKJ~pZ%~=G4c}d>}yugq6{fC_aQ6UeC z%0D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 z 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≥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%0D>1+d;giZJ7s_0K3j_X9S~I1 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 HIGX Date: Sun, 1 May 2016 15:13:44 -0700 Subject: [PATCH 519/826] Cut 2.3.0-RC1. Authors, News and Thanks update. Thanks to everyone who made this release possible. This marks a feature freeze, only bug fixes and minor tweaks till the final cut. Signed-off-by: Chris Larsen --- AUTHORS | 1 + NEWS | 19 ++++++++++++++----- THANKS | 10 ++++++++++ configure.ac | 4 ++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index f52737fcdf..828a00bfd9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,5 +24,6 @@ Chris Larsen David Bainbridge Geoffrey Anderson Ion Savin +Jonathan Creasy Nicholas Whitehead Will Moss diff --git a/NEWS b/NEWS index c0950558e1..d2dabe3de2 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,6 @@ OpenTSDB - User visible changes. -* Version 2.3.0 RC1 (2016-03-28) +* Version 2.3.0 RC1 (2016-05-02) Noteworthy Changes: - Introduced option --max-connection/tsd.core.connections.limit to set the maximum number @@ -13,15 +13,24 @@ Noteworthy Changes: - Added MetaDataCache plugin api - Added timeshift() function (#175) - Now align downsampling to Gregorian Calendar (#548, #657) - - Added support for latest Java versions - - Added NONE aggregator + - Added None aggregator to fetch raw data along with first and last aggregators to + fetch only the first or last data points when downsampling. - Added script to build OpenTSDB/HBase on OSX (#674) - - Added First/Last Downsampler + - Add cross-series expressions with mathematical operators using Jexl - Added query epxressions (alias(), scale(), absolute(), movingAverage(), highestCurrent(), highestMax(), timeShift(), divide(), sum(), difference(), multiply()) (#625) + - Add a Unique ID assignment filter API for enforcing UID assignment naming conventions. + - Add a whitelist regular expression based UID assignment filter + - Add a time series storage filter plugin API that allows processing time series data + and determining if it should be stored or not. + - Allow using OpenTSDB with Google's Bigtable cloud platform or with Apache Cassandra + Bug Fixes: - Some improperly formatted timestamps were allowed (#724) - - removed stdout logging from packaged logback.xml files (#715) + - Removed stdout logging from packaged logback.xml files (#715) + - Restore the ability to create TSMeta objects via URI + - Restore raw data points (along with post-filtered data points) in query stats + - Built in UI will now properly display global annotations when the query string is passed - * Version 2.2.0 (2016-02-14) diff --git a/THANKS b/THANKS index 05e9ae58ff..08c55697a5 100644 --- a/THANKS +++ b/THANKS @@ -13,11 +13,16 @@ Adrien Mogenet Alex Ioffe Andre Pech Andrey Stepachev +Andy Flury +Anna Claiborne Aravind Gottipati Arvind Jayaprakash Berk D. Demir Bikrant Neupane Bryan Zubrod +Camden Narzt +Can Zhang +Carlos Devoto Chris McClymont Cristian Sechel Christophe Furmaniak @@ -28,7 +33,9 @@ Gabriel Nicolas Avellaneda Guenther Schmuelling Hari Krishna Dara Hong Dai Thanh +Hugo M Fernandes Hugo Trippaers +Isaiah Choe Ivan Babrou Jacek Masiulaniec Jari Takkala @@ -42,6 +49,7 @@ Johan Zeeck Johannes Meixner Jonathan Works Josh Thomas +Kevin Bowling Kieren Hynd Kimoon Kim Kris Beevers @@ -65,6 +73,7 @@ Nikhil Benesch Nitin Aggarwal Paula Keezer Peter Gotz +Ping Yong Pradeep Chhetri Rajesh G Ryan Berdeen @@ -81,5 +90,6 @@ Tristan Colgate-McFarlane Tony Landells Utkarsh Bhatnagar Vasiliy Kiryanov +Vitaliy Fuks Yulai Fu Zachary Kurey \ No newline at end of file diff --git a/configure.ac b/configure.ac index 8212e4ffe6..ea48f48ce8 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2012 The OpenTSDB Authors. +# Copyright (C) 2011-2016 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -14,7 +14,7 @@ # along with this library. If not, see . # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.3.0-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.3.0-RC1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 92762f69f27939365d3b06c56af4983b1f5f89df Mon Sep 17 00:00:00 2001 From: newpcraft Date: Tue, 24 May 2016 04:47:58 +0200 Subject: [PATCH 520/826] DumpSeries doesn't know AppendDataPoints When it migrates from a source tsdb which has `tsd.storage.enable_appends = true` in configuration, DumpSeries ignores `AppendDataPoints`. --- src/tools/DumpSeries.java | 16 +++++++++++++--- test/tools/TestDumpSeries.java | 21 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/tools/DumpSeries.java b/src/tools/DumpSeries.java index a7e9222fcc..4bb1887857 100644 --- a/src/tools/DumpSeries.java +++ b/src/tools/DumpSeries.java @@ -15,10 +15,12 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; +import net.opentsdb.core.AppendDataPoints; import org.hbase.async.DeleteRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; @@ -180,7 +182,7 @@ private static void formatKeyValue(final StringBuilder buf, final byte[] value = kv.value(); final int q_len = qualifier.length; - if (q_len % 2 != 0) { + if (q_len != 3 && q_len % 2 != 0) { if (!importformat) { // custom data object, not a data point if (kv.qualifier()[0] == Annotation.PREFIX()) { @@ -203,8 +205,16 @@ private static void formatKeyValue(final StringBuilder buf, appendImportCell(buf, cell, base_time, tags); } } else { - // compacted column - final ArrayList cells = Internal.extractDataPoints(kv); + final Collection cells; + if (q_len == 3) { + // append data points + final AppendDataPoints adps = new AppendDataPoints(); + cells = adps.parseKeyValue(tsdb, kv); + } else { + // compacted column + cells = Internal.extractDataPoints(kv); + } + if (!importformat) { buf.append(Arrays.toString(kv.qualifier())) .append('\t') diff --git a/test/tools/TestDumpSeries.java b/test/tools/TestDumpSeries.java index 375f3c4710..a589b34068 100644 --- a/test/tools/TestDumpSeries.java +++ b/test/tools/TestDumpSeries.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.util.HashMap; +import net.opentsdb.core.AppendDataPoints; import net.opentsdb.core.TSDB; import net.opentsdb.meta.Annotation; import net.opentsdb.storage.MockBase; @@ -312,7 +313,18 @@ public void dumpImportCompacted() throws Exception { assertEquals("sys.cpu.user 1356998400004 6 host=web01", log_lines[1]); assertEquals("sys.cpu.user 1356998400008 5 host=web01", log_lines[2]); } - + + @Test + public void dumpImportAppendDataPoints() throws Exception { + writeAppendDataPoints(); + doDump.invoke(null, tsdb, client, "tsdb".getBytes(MockBase.ASCII()), false, + true, new String[] { "1356998400", "1357002000", "sum", "sys.cpu.user" }); + final String[] log_lines = buffer.toString("ISO-8859-1").split("\n"); + assertNotNull(log_lines); + assertEquals("sys.cpu.user 1356998402 42 host=web01", log_lines[0]); + assertEquals("sys.cpu.user 1356998404 6 host=web01", log_lines[1]); + } + @Test public void dumpRawCompactedAndDelete() throws Exception { writeCompactedData(); @@ -397,4 +409,11 @@ private void writeCompactedData() throws Exception { // kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, ZERO))); } + + private void writeAppendDataPoints() throws Exception { + storage.addColumn(MockBase.stringToBytes("00000150E22700000001000001"), + "t".getBytes(MockBase.ASCII()), + AppendDataPoints.APPEND_COLUMN_QUALIFIER, + new byte[] { 0, 0x20, 42, 0, 0x40, 6 }); + } } From 79c54031aa4b95d2a466dba3e5c952e4cabc202e Mon Sep 17 00:00:00 2001 From: CHOE JUNGYEON Date: Tue, 24 May 2016 18:00:51 +0900 Subject: [PATCH 521/826] DumpSeries doesn't know AppendDataPoints - fixed UT failures --- src/core/AppendDataPoints.java | 5 +++++ src/tools/DumpSeries.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/AppendDataPoints.java b/src/core/AppendDataPoints.java index 8081a507c3..4647ff4b23 100644 --- a/src/core/AppendDataPoints.java +++ b/src/core/AppendDataPoints.java @@ -252,4 +252,9 @@ public byte[] value() { public Deferred repairedDeferred() { return repaired_deferred; } + + /** @return whether or not a qualifier of AppendDataPoints */ + public static boolean isAppendDataPoints(byte[] qualifier) { + return qualifier != null && qualifier.length == 3 && qualifier[0] == APPEND_COLUMN_PREFIX; + } } diff --git a/src/tools/DumpSeries.java b/src/tools/DumpSeries.java index 4bb1887857..ac710878fd 100644 --- a/src/tools/DumpSeries.java +++ b/src/tools/DumpSeries.java @@ -182,7 +182,7 @@ private static void formatKeyValue(final StringBuilder buf, final byte[] value = kv.value(); final int q_len = qualifier.length; - if (q_len != 3 && q_len % 2 != 0) { + if (!AppendDataPoints.isAppendDataPoints(qualifier) && q_len % 2 != 0) { if (!importformat) { // custom data object, not a data point if (kv.qualifier()[0] == Annotation.PREFIX()) { From 1409b550ae7f7fcf9c6cbe7e0dbf59c7363618ad Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 2 Jun 2016 11:43:48 -0700 Subject: [PATCH 522/826] Fix #794 by making sure the group bys and row key literals list is not null before checking if it's empty. Signed-off-by: Chris Larsen --- src/query/QueryUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 994ce86214..1a61c45a0a 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -200,7 +200,8 @@ public static void setDataTableScanFilter( final int end_time) { // no-op - if (group_bys.isEmpty() && row_key_literals.isEmpty()) { + if ((group_bys == null || group_bys.isEmpty()) + && (row_key_literals == null || row_key_literals.isEmpty())) { return; } From 919a1b545746e42f28be0bc609607a313f900d3d Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Wed, 22 Jun 2016 11:51:29 -0700 Subject: [PATCH 523/826] Extra space --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 639c9d8b92..d4fddaf69a 100644 --- a/README +++ b/README @@ -12,7 +12,7 @@ systems, applications) at a large scale, and make this data easily accessible and graphable. Thanks to HBase's scalability, OpenTSDB allows you to collect thousands of -metrics from tens of thousands of hosts and applications, at a high rate +metrics from tens of thousands of hosts and applications, at a high rate (every few seconds). OpenTSDB will never delete or downsample data and can easily store hundreds of billions of data points. From 6d1255f0509bce31ff468d05ae7a40f36aac4b93 Mon Sep 17 00:00:00 2001 From: lizhe Date: Wed, 29 Jun 2016 15:41:54 +0800 Subject: [PATCH 524/826] remove the miss added .swo file --- src/uid/.UniqueId.java.swo | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/uid/.UniqueId.java.swo diff --git a/src/uid/.UniqueId.java.swo b/src/uid/.UniqueId.java.swo deleted file mode 100644 index 20195dc9de84f769e58b2aed50952f2f6a08cb11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHNO^h5z74G091hDhxTP}H7BRp%?J-ZHpV8_Pe-P!fX`^U_z9V@}3n(msJ&h~V- zyQ_D1uof;zP2o7A3B9H(P#KHWF1A!yI2S5;s0}yV2&2IvP@V)NpnVs1m+ky+w zBYiX7{i>)aM|H}3v`IDT1oPnHyoPnHyoPnHyoPnHyoPqyY22^mL@j-a^2JPV!`uDbx z-`Dl0t)D+RBLAnBd;0meN93<*`6c~)N;iJS4ZT3)^c0dKXCi7{y%AX z#?R#u`5(2sq3i!(ME(aY&+Iqv7}kG%ME>lE{F;^@()zEB$o1p=CubmMAZH+FAZH+F zAZH+FAZH+FAZH+F;J=gsfdi=t+rP{QUA+I#&d0yF$1pw*d=|I_+z-5Xw_!xUTY(=R zFpQ^xcRf;^%-S@F4IToWY+5eBf=s%Q*9Y0(cU5 z2>2ax48H}w4}1>z5YPg~f&IYGz&Z2&CFcH?K14z2tO+Hl<#@uS?B5ghULZ-~>x4m6 zJaUBUo%Ejk7OhlfD3n1cBmDP5+SKfk>NpWu!a}JdMN}Eq+UzO$#QSK~@$9Yo_&(96 z|FSVbaRhZDf;vf-@5Wt^F2}-kT8<3OJv1xPbO)yL4Xt-%D5)|Nk-%@H{7~8^?WE)E z84<~8*MT!+B|n*wi9|mm<+FLIiY*>SN+`$D^hF&%RJ2ffyir@3n=9Aqm8HcRoup%r zWGWZLhSQC^{>SjSXS9OZ9qrVYyywlxnrg++w3zTAW#0Xkfscq^Z6wXZ>~R3J=4^=}NMF zTF2l*bD%&(k<<1_P$)dx@1g^#DPgCiwzKAVZHj|iv}Aduj>g}a@vv9hLE!dCcqDB{ z{kzR-0us+3zM00g;&;Xg#Zzv26L?nnZZ0sSp$-6P~$4dmY8rh&~eGWH@$Tfso^N84?)^gui6A+fJ*6xQIx{!9CK^ zyhf+dE0&~V<|OwtE7r@xP*>0Uh)N;NFU^62^k!CA(A_iJQl$?H*%D$}+>PjZZDYE? zBoF(hlG6=b*@Zn(|5*iVO4t&x=8AE;76-h}L>ENpF=>S{n-?AnG1vFT#^Ga;W@67n z28p7nM#~L0BPVrAQTP@Melq^<*b&uDm2@Zz> z5kHxPuJ5nLK{6mL%tLmSi=&)rpid10tnoVINo8jLe>A&N2@@;x0)9{ISi(XSHS4vR zGkb^=^05)vqsX-HMe0P~2tRC_k=uI5Q|8cs=qnhN4+%O=EWM_PdY)w_0VDZd5W%jc zYL@Uk8SW}yj#U+1gSB(lBIji9yl`Xe7c`G-;#NzNWxu>($$+WfyI8t!*tN`@yrDTW z(v?cy$moQfU7IN>zw1~P55t8gQ(sx;Atr(uDQr1h8DqKEjL%5K0&I)=YIiO+EnJra z)1c4!ueBTIM%Oi)*ut4J0(&5Y6O0TFaNIjt!(7sgmE)R-epZauJY}MszC4Qwv$eMe z#dm%>?`-MULE(AFTb1@M!rj{_C4sl^{XkzL*sjiVeA7zT0#gTfcp88jOx@wxh<_rV zFc6zK(md8tDmY#&uCK3~Z7()4+=>X=(u-tNJcTJ;%+}_)#TA-Iv_r(2lNjY#rkB$I zKcAMz!t7%Y)DLk8(|$V?UF=FLE%JOd!P<7I%e%cU@1$&m2*u1m zVgqVPXN~vZh@C-Co3>D{V(D9~m(En?EA@*U@MbIZ#d57ivrAPf(Q>I;uS~DZm#Vb9 zQe9rEm7%)^*LlM+aEjHfJ1+K_k{A3|>`qWKR&=3KKf8ooT50hjT_{ztXRKd5kvQo? zngn%L#A}5E+YrpLIaH~2$rK$&tn87CV@yaGfe~gc45;&&L=!}~v@v)>2Mt08_P?yM zgYbz9p~z8e9Y325VR+4NDYSz-Klbb-1W%B}$}(sHi{(_-ak_gZn3@8#_53(weW;5H ztQDqV!>#vV14?%5E1ZFnWi#r)*kpTGtzb*=O%Fg^U#F@Kj9UxBE_WRvGil# z&u+41)9BvNX?GMArpM{nv!fBrV}j^}J9}DONbq@Ul}(>1<&+WiA7O^#|T$ zB~63R|99fd{RM!}|A+KB^eWE#9|5YsQQ$S4`F{`m3iuZANnj1IfD6C^PzLS*zJh$f zGr*I;VcJ%SMS$&k0mwf&133dZ133dZ z133dZ133dZ1OI;vaB4A4FQ(tw9pMwDDHi1K{0ONm&fW6IX;DFo=%JH%f#JI2NYSxq zGkyJmJZ^fwRh#N?rrDG0RE_bLp)31|mBLs#3~@AB@9?1lSv}VmHmCTJ=o-UmtKae_ zNjKtjAh*SzRA*sS;dt^2hzsZ%Y%-23$X5wuH*s9qR%5%;;pERiLe1X8x8$x zZJ?QpqX34s6d{Iol0W8ybYXmps=k`HpG-=p{jjyMVOxgpz;))InRE(LSpkN1fmxaW zZ6h7ZhWBfX&aBM8EU7-wjSPIhR>C7P@CEMG5%sJgZMhZt-Sn+YQf#Cg4***><91=p fu}^YGPbBs9C09u Date: Thu, 7 Jul 2016 16:52:07 +0100 Subject: [PATCH 525/826] Use thread safe ConcurrentHashMap. (#3) For https://github.com/OpenTSDB/opentsdb/issues/823 Concurrent use of a HashSet can trigger race conditions and an infinite loop(s), use the thread-safe ConcurrentHashMap to avoid this. Making the change just for the thread stack seen looping. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 3a929cd2af..ce4e433c1a 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -306,8 +305,8 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); - private final Set skips = new HashSet(); - private final Set keepers = new HashSet(); + private final Set skips = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set keepers = Collections.newSetFromMap(new ConcurrentHashMap()); private long scanner_start = -1; /** nanosecond timestamps */ From d3ecb5330419598dc216b5c7d9f8e87842141bbc Mon Sep 17 00:00:00 2001 From: "Peter (Stig) Edwards" Date: Thu, 7 Jul 2016 16:52:07 +0100 Subject: [PATCH 526/826] Use thread safe ConcurrentHashMap. (#3) For https://github.com/OpenTSDB/opentsdb/issues/823 Concurrent use of a HashSet can trigger race conditions and an infinite loop(s), use the thread-safe ConcurrentHashMap to avoid this. Making the change just for the thread stack seen looping. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 3a929cd2af..ce4e433c1a 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -306,8 +305,8 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); - private final Set skips = new HashSet(); - private final Set keepers = new HashSet(); + private final Set skips = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set keepers = Collections.newSetFromMap(new ConcurrentHashMap()); private long scanner_start = -1; /** nanosecond timestamps */ From ad5b28986d4e44e8f95549ab3f3fe9c04a6ba15b Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Wed, 13 Jul 2016 18:25:38 +0000 Subject: [PATCH 527/826] Log X-Forwarded-For address when handling HTTP requests --- src/tsd/AbstractHttpQuery.java | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 967eaab09f..035404a5ad 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -467,21 +467,37 @@ protected Logger logger() { return LOG; } + protected final String logChannel() { + if (request.containsHeader("X-Forwarded-For")) { + String inetAddress; + String proxyChain = request.getHeader("X-Forwarded-For"); + int firstComma = proxyChain.indexOf(','); + if (firstComma != -1) { + inetAddress = proxyChain.substring(0, proxyChain.indexOf(',')); + } else { + inetAddress = proxyChain; + } + return "[id: 0x" + Integer.toHexString(chan.hashCode()) + ", /" + inetAddress + " => " + chan.getLocalAddress() + ']'; + } else { + return chan.toString(); + } + } + protected final void logInfo(final String msg) { if (logger().isInfoEnabled()) { - logger().info(chan.toString() + ' ' + msg); + logger().info(logChannel() + ' ' + msg); } } protected final void logWarn(final String msg) { if (logger().isWarnEnabled()) { - logger().warn(chan.toString() + ' ' + msg); + logger().warn(logChannel() + ' ' + msg); } } protected final void logError(final String msg, final Exception e) { if (logger().isErrorEnabled()) { - logger().error(chan.toString() + ' ' + msg, e); + logger().error(logChannel() + ' ' + msg, e); } } From ae67b5ed7df378d4843fb647844663c62ece7267 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Wed, 22 Jun 2016 11:51:29 -0700 Subject: [PATCH 528/826] Extra space --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 639c9d8b92..d4fddaf69a 100644 --- a/README +++ b/README @@ -12,7 +12,7 @@ systems, applications) at a large scale, and make this data easily accessible and graphable. Thanks to HBase's scalability, OpenTSDB allows you to collect thousands of -metrics from tens of thousands of hosts and applications, at a high rate +metrics from tens of thousands of hosts and applications, at a high rate (every few seconds). OpenTSDB will never delete or downsample data and can easily store hundreds of billions of data points. From 3fbb7933b4047450343f45a46112a7b4aa41818d Mon Sep 17 00:00:00 2001 From: lizhe Date: Wed, 29 Jun 2016 15:41:54 +0800 Subject: [PATCH 529/826] remove the miss added .swo file --- src/uid/.UniqueId.java.swo | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/uid/.UniqueId.java.swo diff --git a/src/uid/.UniqueId.java.swo b/src/uid/.UniqueId.java.swo deleted file mode 100644 index 20195dc9de84f769e58b2aed50952f2f6a08cb11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHNO^h5z74G091hDhxTP}H7BRp%?J-ZHpV8_Pe-P!fX`^U_z9V@}3n(msJ&h~V- zyQ_D1uof;zP2o7A3B9H(P#KHWF1A!yI2S5;s0}yV2&2IvP@V)NpnVs1m+ky+w zBYiX7{i>)aM|H}3v`IDT1oPnHyoPnHyoPnHyoPnHyoPqyY22^mL@j-a^2JPV!`uDbx z-`Dl0t)D+RBLAnBd;0meN93<*`6c~)N;iJS4ZT3)^c0dKXCi7{y%AX z#?R#u`5(2sq3i!(ME(aY&+Iqv7}kG%ME>lE{F;^@()zEB$o1p=CubmMAZH+FAZH+F zAZH+FAZH+FAZH+F;J=gsfdi=t+rP{QUA+I#&d0yF$1pw*d=|I_+z-5Xw_!xUTY(=R zFpQ^xcRf;^%-S@F4IToWY+5eBf=s%Q*9Y0(cU5 z2>2ax48H}w4}1>z5YPg~f&IYGz&Z2&CFcH?K14z2tO+Hl<#@uS?B5ghULZ-~>x4m6 zJaUBUo%Ejk7OhlfD3n1cBmDP5+SKfk>NpWu!a}JdMN}Eq+UzO$#QSK~@$9Yo_&(96 z|FSVbaRhZDf;vf-@5Wt^F2}-kT8<3OJv1xPbO)yL4Xt-%D5)|Nk-%@H{7~8^?WE)E z84<~8*MT!+B|n*wi9|mm<+FLIiY*>SN+`$D^hF&%RJ2ffyir@3n=9Aqm8HcRoup%r zWGWZLhSQC^{>SjSXS9OZ9qrVYyywlxnrg++w3zTAW#0Xkfscq^Z6wXZ>~R3J=4^=}NMF zTF2l*bD%&(k<<1_P$)dx@1g^#DPgCiwzKAVZHj|iv}Aduj>g}a@vv9hLE!dCcqDB{ z{kzR-0us+3zM00g;&;Xg#Zzv26L?nnZZ0sSp$-6P~$4dmY8rh&~eGWH@$Tfso^N84?)^gui6A+fJ*6xQIx{!9CK^ zyhf+dE0&~V<|OwtE7r@xP*>0Uh)N;NFU^62^k!CA(A_iJQl$?H*%D$}+>PjZZDYE? zBoF(hlG6=b*@Zn(|5*iVO4t&x=8AE;76-h}L>ENpF=>S{n-?AnG1vFT#^Ga;W@67n z28p7nM#~L0BPVrAQTP@Melq^<*b&uDm2@Zz> z5kHxPuJ5nLK{6mL%tLmSi=&)rpid10tnoVINo8jLe>A&N2@@;x0)9{ISi(XSHS4vR zGkb^=^05)vqsX-HMe0P~2tRC_k=uI5Q|8cs=qnhN4+%O=EWM_PdY)w_0VDZd5W%jc zYL@Uk8SW}yj#U+1gSB(lBIji9yl`Xe7c`G-;#NzNWxu>($$+WfyI8t!*tN`@yrDTW z(v?cy$moQfU7IN>zw1~P55t8gQ(sx;Atr(uDQr1h8DqKEjL%5K0&I)=YIiO+EnJra z)1c4!ueBTIM%Oi)*ut4J0(&5Y6O0TFaNIjt!(7sgmE)R-epZauJY}MszC4Qwv$eMe z#dm%>?`-MULE(AFTb1@M!rj{_C4sl^{XkzL*sjiVeA7zT0#gTfcp88jOx@wxh<_rV zFc6zK(md8tDmY#&uCK3~Z7()4+=>X=(u-tNJcTJ;%+}_)#TA-Iv_r(2lNjY#rkB$I zKcAMz!t7%Y)DLk8(|$V?UF=FLE%JOd!P<7I%e%cU@1$&m2*u1m zVgqVPXN~vZh@C-Co3>D{V(D9~m(En?EA@*U@MbIZ#d57ivrAPf(Q>I;uS~DZm#Vb9 zQe9rEm7%)^*LlM+aEjHfJ1+K_k{A3|>`qWKR&=3KKf8ooT50hjT_{ztXRKd5kvQo? zngn%L#A}5E+YrpLIaH~2$rK$&tn87CV@yaGfe~gc45;&&L=!}~v@v)>2Mt08_P?yM zgYbz9p~z8e9Y325VR+4NDYSz-Klbb-1W%B}$}(sHi{(_-ak_gZn3@8#_53(weW;5H ztQDqV!>#vV14?%5E1ZFnWi#r)*kpTGtzb*=O%Fg^U#F@Kj9UxBE_WRvGil# z&u+41)9BvNX?GMArpM{nv!fBrV}j^}J9}DONbq@Ul}(>1<&+WiA7O^#|T$ zB~63R|99fd{RM!}|A+KB^eWE#9|5YsQQ$S4`F{`m3iuZANnj1IfD6C^PzLS*zJh$f zGr*I;VcJ%SMS$&k0mwf&133dZ133dZ z133dZ133dZ1OI;vaB4A4FQ(tw9pMwDDHi1K{0ONm&fW6IX;DFo=%JH%f#JI2NYSxq zGkyJmJZ^fwRh#N?rrDG0RE_bLp)31|mBLs#3~@AB@9?1lSv}VmHmCTJ=o-UmtKae_ zNjKtjAh*SzRA*sS;dt^2hzsZ%Y%-23$X5wuH*s9qR%5%;;pERiLe1X8x8$x zZJ?QpqX34s6d{Iol0W8ybYXmps=k`HpG-=p{jjyMVOxgpz;))InRE(LSpkN1fmxaW zZ6h7ZhWBfX&aBM8EU7-wjSPIhR>C7P@CEMG5%sJgZMhZt-Sn+YQf#Cg4***><91=p fu}^YGPbBs9C09u Date: Thu, 7 Jul 2016 16:52:07 +0100 Subject: [PATCH 530/826] Use thread safe ConcurrentHashMap. (#3) For https://github.com/OpenTSDB/opentsdb/issues/823 Concurrent use of a HashSet can trigger race conditions and an infinite loop(s), use the thread-safe ConcurrentHashMap to avoid this. Making the change just for the thread stack seen looping. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 9498284e00..d4b6fa6ded 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -306,8 +305,8 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); - private final Set skips = new HashSet(); - private final Set keepers = new HashSet(); + private final Set skips = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set keepers = Collections.newSetFromMap(new ConcurrentHashMap()); private long scanner_start = -1; /** nanosecond timestamps */ From 4b906946be8b934c12d8b1cb53c91ec22f1d275e Mon Sep 17 00:00:00 2001 From: Kevin Landreth Date: Fri, 29 Jul 2016 22:50:05 -0500 Subject: [PATCH 531/826] fix ALPN version comparisons in include.mk file --- third_party/alpn-boot/include.mk | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk index 83ff6792bd..a949d1da29 100644 --- a/third_party/alpn-boot/include.mk +++ b/third_party/alpn-boot/include.mk @@ -27,25 +27,25 @@ ALPN_BOOT_VERSION = $(shell version= ;\ minor=$${BASH_REMATCH[2]}; \ sub=$${BASH_REMATCH[3]}; \ if [[ $$major = "1.7" ]]; then \ - if [[ $$sub < 71 ]]; then \ + if [[ $$sub -lt 71 ]]; then \ echo "7.1.0.v20141016"; \ - elif [[ $$sub < 75 ]]; then \ + elif [[ $$sub -lt 75 ]]; then \ echo "7.1.2.v20141202"; \ else \ echo "7.1.3.v20150130"; \ fi \ elif [[ $$major = "1.8" ]]; then \ - if [[ $$sub < 25 ]]; then \ + if [[ $$sub -lt 25 ]]; then \ echo "8.1.0.v20141016"; \ - elif [[ $$sub < 31 ]]; then \ + elif [[ $$sub -lt 31 ]]; then \ echo "8.1.2.v20141202"; \ - elif [[ $$sub < 51 ]]; then \ + elif [[ $$sub -lt 51 ]]; then \ echo "8.1.3.v20150130"; \ - elif [[ $$sub < 60 ]]; then \ + elif [[ $$sub -lt 60 ]]; then \ echo "8.1.4.v20150727"; \ - elif [[ $$sub < 65 ]]; then \ + elif [[ $$sub -lt 65 ]]; then \ echo "8.1.5.v20150921"; \ - elif [[ $$sub < 71 ]]; then \ + elif [[ $$sub -lt 71 ]]; then \ echo "8.1.6.v20151105"; \ else \ echo "8.1.7.v20160121"; \ From a80b06072cb0e0c52a27c563e3c88cabd84aa962 Mon Sep 17 00:00:00 2001 From: dfsklar Date: Thu, 1 Sep 2016 11:21:05 -0400 Subject: [PATCH 532/826] Update HttpQuery.java --- src/tsd/HttpQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index c2848ee7aa..7f704fcdad 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -1025,7 +1025,7 @@ protected Logger logger() { + "" + "

    " - + "T" - + "S" - + "D" - + "   
    "; + + "
    " + + "" + + " 
    "; private static final String PAGE_BODY_MID = "
    " + "" + "
    " - + "" + + "" + " 
    "; From 2d35e21cff82cbd75609635b2e8a8550b72461b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?maxmeng=28=E8=92=99=E5=8D=93=29?= Date: Mon, 5 Sep 2016 16:12:35 +0800 Subject: [PATCH 533/826] add bad percent fix #728 --- tools/check_tsd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/check_tsd b/tools/check_tsd index ecc42b0551..0159db4d82 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -81,6 +81,9 @@ def main(argv): parser.add_option('-N', '--now', type='int', default=None, metavar='UTC', help='Set unix timestamp for "now", for testing') + parser.add_option('-B', '--bad_percent', dest='bad_percent', default=None, + metavar='PERCENT', type='float', help='Ignore alarm if PERCENT of the data' + ' points is bad') parser.add_option('-S', '--ssl', default=False, action='store_true', help='Make queries to OpenTSDB via SSL (https)') (options, args) = parser.parse_args(args=argv[1:]) From 898c17b54d32b97c16ca0a807ebac8fa4abda29a Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Tue, 6 Sep 2016 15:01:53 -0500 Subject: [PATCH 534/826] Fixes #855, bumped Zookeeper version to 3.4.6 --- third_party/zookeeper/include.mk | 2 +- third_party/zookeeper/zookeeper-3.4.6.jar.md5 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/zookeeper/zookeeper-3.4.6.jar.md5 diff --git a/third_party/zookeeper/include.mk b/third_party/zookeeper/include.mk index 514b9dc5ed..7fc7695f4f 100644 --- a/third_party/zookeeper/include.mk +++ b/third_party/zookeeper/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ZOOKEEPER_VERSION := 3.4.5 +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) diff --git a/third_party/zookeeper/zookeeper-3.4.6.jar.md5 b/third_party/zookeeper/zookeeper-3.4.6.jar.md5 new file mode 100644 index 0000000000..ce5652c709 --- /dev/null +++ b/third_party/zookeeper/zookeeper-3.4.6.jar.md5 @@ -0,0 +1 @@ +7d01d317c717268725896cfb81b18152 \ No newline at end of file From 745d0598dadabdd3bf616669c276d6cf667e84ed Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Sep 2016 15:19:26 -0700 Subject: [PATCH 535/826] Move to AsyncHBase 1.7.2 Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.0.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.2.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.7.0.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.7.2.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0.jar.md5 b/third_party/hbase/asynchbase-1.7.0.jar.md5 deleted file mode 100644 index 5cb6466209..0000000000 --- a/third_party/hbase/asynchbase-1.7.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -f1aed41b7f16345d2f58797ffa77f36a \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.2.jar.md5 b/third_party/hbase/asynchbase-1.7.2.jar.md5 new file mode 100644 index 0000000000..df83960284 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.2.jar.md5 @@ -0,0 +1 @@ +35fdde5a8e6009553e6aab5357ed8896 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index bb25220311..7cb693bb8a 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.1 +ASYNCHBASE_VERSION := 1.7.2 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) From 15a9839407411a3fd3ac43c0a88a587c8f62bc39 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Sep 2016 15:19:26 -0700 Subject: [PATCH 536/826] Move to AsyncHBase 1.7.2 Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.0.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.2.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.7.0.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.7.2.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0.jar.md5 b/third_party/hbase/asynchbase-1.7.0.jar.md5 deleted file mode 100644 index 5cb6466209..0000000000 --- a/third_party/hbase/asynchbase-1.7.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -f1aed41b7f16345d2f58797ffa77f36a \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.2.jar.md5 b/third_party/hbase/asynchbase-1.7.2.jar.md5 new file mode 100644 index 0000000000..df83960284 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.2.jar.md5 @@ -0,0 +1 @@ +35fdde5a8e6009553e6aab5357ed8896 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index bb25220311..7cb693bb8a 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.1 +ASYNCHBASE_VERSION := 1.7.2 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) From 5b1c971dc0607f5def235b2277ef2e94efe75051 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Sep 2016 15:19:26 -0700 Subject: [PATCH 537/826] Move to AsyncHBase 1.7.2 Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.0.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.2.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.7.0.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.7.2.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.0.jar.md5 b/third_party/hbase/asynchbase-1.7.0.jar.md5 deleted file mode 100644 index 5cb6466209..0000000000 --- a/third_party/hbase/asynchbase-1.7.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -f1aed41b7f16345d2f58797ffa77f36a \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.2.jar.md5 b/third_party/hbase/asynchbase-1.7.2.jar.md5 new file mode 100644 index 0000000000..df83960284 --- /dev/null +++ b/third_party/hbase/asynchbase-1.7.2.jar.md5 @@ -0,0 +1 @@ +35fdde5a8e6009553e6aab5357ed8896 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index bb25220311..7cb693bb8a 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.1 +ASYNCHBASE_VERSION := 1.7.2 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) From dcffb36cb41f21368423661e420be8e3b4ee0f80 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 6 Jul 2016 21:31:08 -0500 Subject: [PATCH 538/826] Added better error handling and gracefully handle NPE Fixes #817 Signed-off-by: Chris Larsen --- src/tsd/QueryExecutor.java | 122 ++++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java index 22937fcb4b..c618c939e7 100644 --- a/src/tsd/QueryExecutor.java +++ b/src/tsd/QueryExecutor.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.tsd; +import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; @@ -277,72 +278,115 @@ public Object call(final ArrayList query_results) } } } - + // handle nested expressions - DirectedAcyclicGraph graph = null; + DirectedAcyclicGraph graph = new DirectedAcyclicGraph(DefaultEdge.class); + for (final Entry eii : expressions.entrySet()) { + LOG.debug(String.format("Expression entry key is %s, value is %s", eii.getKey(), eii.getValue().toString())); + LOG.debug(String.format("Time to loop through the variable names for %s", eii.getKey())); + + if (!graph.containsVertex(eii.getKey())) { + LOG.debug("Adding vertex " + eii.getKey()); + graph.addVertex(eii.getKey()); + } + for (final String var : eii.getValue().getVariableNames()) { + LOG.debug(String.format("var is %s", var)); + final ExpressionIterator ei = expressions.get(var); + if (ei != null) { + LOG.debug(String.format("The expression iterator for %s is %s", var, ei.toString())); + // TODO - really ought to calculate this earlier if (eii.getKey().equals(var)) { throw new IllegalArgumentException( "Self referencing expression found: " + eii.getKey()); } + LOG.debug("Nested expression detected. " + eii.getKey() + " depends on " + var); - if (graph == null) { - graph = new DirectedAcyclicGraph(DefaultEdge.class); - } if (!graph.containsVertex(eii.getKey())) { + LOG.debug("Added vertex " + eii.getKey()); graph.addVertex(eii.getKey()); + } else { + LOG.debug("Already contains vertex " + eii.getKey()); } + if (!graph.containsVertex(var)) { + LOG.debug("Added vertex " + var); graph.addVertex(var); + } else { + LOG.debug("Already contains vertex " + var); } + try { + LOG.debug("Added Edge " + eii.getKey() + " - " + var); graph.addDagEdge(eii.getKey(), var); } catch (CycleFoundException cfe) { throw new IllegalArgumentException("Circular reference found: " + eii.getKey(), cfe); } + } else { + LOG.debug(String.format("The expression iterator for %s is null", var)); } } } // compile all of the expressions final long intersect_start = DateTime.currentTimeMillis(); - if (graph != null) { - final ExpressionIterator[] compile_stack = - new ExpressionIterator[expressions.size()]; - final TopologicalOrderIterator it = - new TopologicalOrderIterator(graph); - int i = 0; - while (it.hasNext()) { - compile_stack[i++] = expressions.get(it.next()); + + if (graph == null) { + throw new IOException("Internal Error: graph cannot be null"); + } + + final Integer expressionLength = expressions.size(); + final ExpressionIterator[] compile_stack = new ExpressionIterator[expressionLength]; + final TopologicalOrderIterator it = new TopologicalOrderIterator(graph); + + LOG.debug(String.format("Expressions Size is %d", expressionLength)); + LOG.debug(String.format("Topology Iterator %s", it.toString())); + + int i = 0; + while (it.hasNext()) { + String next = it.next(); + LOG.debug(String.format("Expression: %s", next)); + ExpressionIterator ei = expressions.get(next); + LOG.debug(String.format("Expression Iterator: %s", ei.toString())); + if (ei == null) { + LOG.error(String.format("The expression iterator for %s is null", next)); } - for (int x = compile_stack.length - 1; x >= 0; x--) { - // look for and add expressions - for (final String var : compile_stack[x].getVariableNames()) { - ExpressionIterator source = expressions.get(var); - if (source != null) { - compile_stack[x].addResults(var, source.getCopy()); - LOG.debug("Adding expression " + source.getId() + " to " + - compile_stack[x].getId()); - } - } - - compile_stack[x].compile(); - LOG.debug("Successfully compiled " + compile_stack[x]); + compile_stack[i] = ei; + LOG.debug(String.format("Added expression %s to compile_stack[%d]", next, i)); + i++; + } + + if (i != expressionLength) { + throw new IOException(String.format(" Internal Error: Less expressions where added to the compile stack than expressions.size (%d instead of %d)", i, expressionLength)); + } + + LOG.debug(String.format("compile stack length: %d", compile_stack.length)); + + for (int x = compile_stack.length - 1; x >= 0; x--) { + if (compile_stack[x] == null) { + throw new NullPointerException(String.format("Item %d in compile_stack[] is null", x)); } - } else { - for (final ExpressionIterator ei : expressions.values()) { - ei.compile(); - LOG.debug("Successfully compiled " + ei); + // look for and add expressions + for (final String var : compile_stack[x].getVariableNames()) { + LOG.debug(String.format("Looking for variable %s for %s", var, compile_stack[x].getId())); + ExpressionIterator source = expressions.get(var); + if (source != null) { + compile_stack[x].addResults(var, source.getCopy()); + LOG.debug(String.format("Adding expression %s to %s", source.getId(), compile_stack[x].getId())); + } } + compile_stack[x].compile(); + LOG.debug(String.format("Successfully compiled %s", compile_stack[x].getId())); } - LOG.debug("Finished compilations in " + + + LOG.debug("Finished compilations in " + (DateTime.currentTimeMillis() - intersect_start) + " ms"); return serialize().addCallback(new CompleteCB()).addErrback(new ErrorCB()); @@ -498,30 +542,22 @@ public Object call(final Exception e) throws Exception { if (ex != null) { LOG.error("Unexpected exception: ", ex); // TODO - find a better way to determine the real error -// QueryExecutor.this.ts_query.getQueryStats() -// .markComplete(HttpResponseStatus.BAD_REQUEST, ex); QueryExecutor.this.http_query.badRequest(new BadRequestException(ex)); } else { LOG.error("The deferred group exception didn't have a cause???"); -// QueryExecutor.this.ts_query.getQueryStats() -// .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); } } else if (e.getClass() == QueryException.class) { -// QueryExecutor.this.ts_query.getQueryStats() -// .markComplete(HttpResponseStatus.REQUEST_TIMEOUT, e); - QueryExecutor.this.http_query.badRequest(new BadRequestException((QueryException)e)); + QueryExecutor.this.http_query.badRequest(new BadRequestException((QueryException) e)); + } else if ((e instanceof IOException) || (e instanceof NullPointerException) || (e instanceof RuntimeException)) { + QueryExecutor.this.http_query.internalError(e); } else { -// QueryExecutor.this.ts_query.getQueryStats() -// .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, e); QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); } return null; } catch (RuntimeException ex) { LOG.error("Exception thrown during exception handling", ex); -// QueryExecutor.this.ts_query.getQueryStats() -// .markComplete(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex); - QueryExecutor.this.http_query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, + QueryExecutor.this.http_query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, ex.getMessage().getBytes()); return null; } From 1a9f1fdfba13c09513e3e223aec66f18828d11ab Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Sep 2016 17:29:14 -0700 Subject: [PATCH 539/826] Properly validate expression queries before passing them to the queryExecutor. Wrap debug log lines in the query executor so they don't affect performance when not in debug mode. Filters are now optional in the expression queries. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionIterator.java | 12 +- src/query/pojo/Expression.java | 39 ++++ src/query/pojo/Query.java | 44 +++-- src/tsd/QueryExecutor.java | 183 ++++++++++++------- test/query/pojo/TestQuery.java | 4 +- test/tsd/TestQueryExecutor.java | 106 ++++++++++- 6 files changed, 299 insertions(+), 89 deletions(-) diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index 2e01585657..3b33d5678b 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -39,7 +39,7 @@ * - Instantiate with a valid expression * - Call {@link #getVariableNames()} and iterate over a set of TSSubQueries and * their results. For each query that matches a variable name, call - * {@link #addResults()} with the result set. + * {@link #addResults(String, ITimeSyncedIterator)} with the result set. * - Call {@link #compile()} to setup the meta data, fills and compute the * intersection of the series. * - Call {@link #values()} and store the reference. Results for each @@ -67,7 +67,7 @@ public class ExpressionIterator implements ITimeSyncedIterator { * as not thread safe, so I assume it's ok to instantiate one of these guys * and keep creating scripts from it. */ - private final static JexlEngine JEXL_ENGINE = new JexlEngine(); + public final static JexlEngine JEXL_ENGINE = new JexlEngine(); /** Whether or not to intersect on the query tagks instead of the result set * tagks */ @@ -114,6 +114,7 @@ public class ExpressionIterator implements ITimeSyncedIterator { // no tagk filters then we shouldn't set the II's intersect_on_query_tagks /** * Default Ctor that compiles the expression for use with this iterator. + * @param id The id of this iterator. * @param expression The expression to compile and use * @param set_operator The type of set operator to use * @param intersect_on_query_tagks Whether or not to include only the query @@ -204,7 +205,8 @@ public String toString() { /** * Adds a sub query result object to the iterator. * TODO - accept a proper object, not a map - * @param results The results to store. + * @param id The ID of source iterator. + * @param iterator The source iterator. * @throws IllegalArgumentException if the object is missing required data */ public void addResults(final String id, final ITimeSyncedIterator iterator) { @@ -235,8 +237,8 @@ public void compile() { } if (results.size() < names.size()) { throw new IllegalArgumentException("Not enough query results [" - + results.size() + "] for the expression variables [" - + names.size() + "] " + this); + + results.size() + " total results found] for the expression variables [" + + names.size() + " expected] " + this); } // don't care if we have extra results, but we had darned well better make diff --git a/src/query/pojo/Expression.java b/src/query/pojo/Expression.java index 9f5aaebdf5..0b313d9835 100644 --- a/src/query/pojo/Expression.java +++ b/src/query/pojo/Expression.java @@ -12,9 +12,17 @@ // see . package net.opentsdb.query.pojo; +import net.opentsdb.query.expression.ExpressionIterator; import net.opentsdb.query.expression.NumericFillPolicy; import net.opentsdb.query.expression.VariableIterator.SetOperator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.jexl2.Script; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -40,6 +48,12 @@ public class Expression extends Validatable { /** The fill policy to use for ? */ private NumericFillPolicy fill_policy; + /** Set of unique variables used by this expression. */ + private Set variables; + + /** The parsed expression via JEXL. */ + private Script parsed_expression; + /** * Default ctor * @param builder The builder to pull values from @@ -90,11 +104,36 @@ public void validate() { throw new IllegalArgumentException("missing or empty expr"); } + // parse it just to make sure we're happy and extract the variable names. + // Will throw JexlException + parsed_expression = ExpressionIterator.JEXL_ENGINE.createScript(expr); + variables = new HashSet(); + for (final List exp_list : + ExpressionIterator.JEXL_ENGINE.getVariables(parsed_expression)) { + for (final String variable : exp_list) { + variables.add(variable); + } + } + // others are optional if (join == null) { join = Join.Builder().setOperator(SetOperator.UNION).build(); } } + + /** @return The parsed expression. May be null if {@link validate} has not + * been called yet. */ + @JsonIgnore + public Script getParsedExpression() { + return parsed_expression; + } + + /** @return A set of unique variables for the expression. May be null if + * {@link validate} has not been called yet. */ + @JsonIgnore + public Set getVariables() { + return variables; + } @Override public boolean equals(final Object o) { diff --git a/src/query/pojo/Query.java b/src/query/pojo/Query.java index 09f62dcf78..731c449950 100644 --- a/src/query/pojo/Query.java +++ b/src/query/pojo/Query.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.common.base.Objects; +import net.opentsdb.utils.JSON; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -109,14 +111,13 @@ public void validate() { throw new IllegalArgumentException("missing or empty metrics"); } - final Set metric_ids = new HashSet(); - + final Set variable_ids = new HashSet(); for (Metric metric : metrics) { - if (metric_ids.contains(metric.getId())) { + if (variable_ids.contains(metric.getId())) { throw new IllegalArgumentException("duplicated metric id: " + metric.getId()); } - metric_ids.add(metric.getId()); + variable_ids.add(metric.getId()); } final Set filter_ids = new HashSet(); @@ -128,15 +129,13 @@ public void validate() { } filter_ids.add(filter.getId()); } - - final Set expression_ids = new HashSet(); - + for (Expression expression : expressions) { - if (expression_ids.contains(expression.getId())) { - throw new IllegalArgumentException("duplicated expression id: " + if (variable_ids.contains(expression.getId())) { + throw new IllegalArgumentException("Duplicated variable or expression id: " + expression.getId()); } - expression_ids.add(expression.getId()); + variable_ids.add(expression.getId()); } validateCollection(metrics, "metric"); @@ -150,19 +149,38 @@ public void validate() { } validateFilters(); + + if (expressions != null) { + validateCollection(expressions, "expression"); + for (final Expression exp : expressions) { + if (exp.getVariables() == null) { + throw new IllegalArgumentException("No variables found for an " + + "expression?! " + JSON.serializeToString(exp)); + } + + for (final String var : exp.getVariables()) { + if (!variable_ids.contains(var)) { + throw new IllegalArgumentException("Expression [" + exp.getExpr() + + "] was missing input " + var); + } + } + } + } } /** Validates the filters, making sure each metric has a filter * @throws IllegalArgumentException if one or more parameters were invalid */ private void validateFilters() { - final Set ids = new HashSet(); + Set ids = new HashSet(); for (Filter filter : filters) { ids.add(filter.getId()); } - for (Metric metric : metrics) { - if (!ids.contains(metric.getFilter())) { + for(Metric metric : metrics) { + if (metric.getFilter() != null && + !metric.getFilter().isEmpty() && + !ids.contains(metric.getFilter())) { throw new IllegalArgumentException( String.format("unrecognized filter id %s in metric %s", metric.getFilter(), metric.getId())); diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java index c618c939e7..b0598241e2 100644 --- a/src/tsd/QueryExecutor.java +++ b/src/tsd/QueryExecutor.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-2016 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 @@ -22,6 +22,8 @@ import java.util.Map; import java.util.Map.Entry; +import org.hbase.async.HBaseException; +import org.hbase.async.RpcTimedOutException; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferOutputStream; import org.jboss.netty.buffer.ChannelBuffers; @@ -58,6 +60,7 @@ import net.opentsdb.query.pojo.Query; import net.opentsdb.query.pojo.Timespan; import net.opentsdb.stats.QueryStats; +import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; @@ -175,14 +178,15 @@ public QueryExecutor(final TSDB tsdb, final Query query) { break; } } - sub.setRate(timespan.isRate()); - sub.setAggregator( - mq.getAggregator() != null ? mq.getAggregator() : timespan.getAggregator()); if (filters != null) { sub.setFilters(filters); sub.setExplicitTags(explicit_tags); } } + + sub.setRate(timespan.isRate()); + sub.setAggregator( + mq.getAggregator() != null ? mq.getAggregator() : timespan.getAggregator()); } final ArrayList subs = @@ -220,9 +224,7 @@ public void execute(final HttpQuery query) { final QueryStats query_stats = new QueryStats(query.getRemoteAddress(), ts_query, query.getHeaders()); ts_query.setQueryStats(query_stats); - - final long start = DateTime.currentTimeMillis(); - + /** * Sends the serialized results to the caller. This should be the very * last callback executed. @@ -271,8 +273,10 @@ public Object call(final ArrayList query_results) tsi.setFillPolicy(fill); } ei.addResults(entry.getKey(), tsi); - LOG.debug("Added results for " + entry.getKey() + - " to " + ei.getId()); + if (LOG.isDebugEnabled()) { + LOG.debug("Added results for " + entry.getKey() + + " to " + ei.getId()); + } } } } @@ -280,24 +284,36 @@ public Object call(final ArrayList query_results) } // handle nested expressions - DirectedAcyclicGraph graph = new DirectedAcyclicGraph(DefaultEdge.class); + final DirectedAcyclicGraph graph = + new DirectedAcyclicGraph(DefaultEdge.class); for (final Entry eii : expressions.entrySet()) { - LOG.debug(String.format("Expression entry key is %s, value is %s", eii.getKey(), eii.getValue().toString())); - LOG.debug(String.format("Time to loop through the variable names for %s", eii.getKey())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Expression entry key is %s, value is %s", + eii.getKey(), eii.getValue().toString())); + LOG.debug(String.format("Time to loop through the variable names " + + "for %s", eii.getKey())); + } if (!graph.containsVertex(eii.getKey())) { - LOG.debug("Adding vertex " + eii.getKey()); + if (LOG.isDebugEnabled()) { + LOG.debug("Adding vertex " + eii.getKey()); + } graph.addVertex(eii.getKey()); } for (final String var : eii.getValue().getVariableNames()) { - LOG.debug(String.format("var is %s", var)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("var is %s", var)); + } final ExpressionIterator ei = expressions.get(var); if (ei != null) { - LOG.debug(String.format("The expression iterator for %s is %s", var, ei.toString())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("The expression iterator for %s is %s", + var, ei.toString())); + } // TODO - really ought to calculate this earlier if (eii.getKey().equals(var)) { @@ -305,31 +321,39 @@ public Object call(final ArrayList query_results) "Self referencing expression found: " + eii.getKey()); } - LOG.debug("Nested expression detected. " + eii.getKey() + - " depends on " + var); + if (LOG.isDebugEnabled()) { + LOG.debug("Nested expression detected. " + eii.getKey() + + " depends on " + var); + } if (!graph.containsVertex(eii.getKey())) { - LOG.debug("Added vertex " + eii.getKey()); + if (LOG.isDebugEnabled()) { + LOG.debug("Added vertex " + eii.getKey()); + } graph.addVertex(eii.getKey()); - } else { + } else if (LOG.isDebugEnabled()) { LOG.debug("Already contains vertex " + eii.getKey()); } if (!graph.containsVertex(var)) { - LOG.debug("Added vertex " + var); + if (LOG.isDebugEnabled()) { + LOG.debug("Added vertex " + var); + } graph.addVertex(var); - } else { + } else if (LOG.isDebugEnabled()) { LOG.debug("Already contains vertex " + var); } try { - LOG.debug("Added Edge " + eii.getKey() + " - " + var); + if (LOG.isDebugEnabled()) { + LOG.debug("Added Edge " + eii.getKey() + " - " + var); + } graph.addDagEdge(eii.getKey(), var); } catch (CycleFoundException cfe) { throw new IllegalArgumentException("Circular reference found: " + eii.getKey(), cfe); } - } else { + } else if (LOG.isDebugEnabled()) { LOG.debug(String.format("The expression iterator for %s is null", var)); } } @@ -338,58 +362,83 @@ public Object call(final ArrayList query_results) // compile all of the expressions final long intersect_start = DateTime.currentTimeMillis(); - if (graph == null) { - throw new IOException("Internal Error: graph cannot be null"); - } - final Integer expressionLength = expressions.size(); - final ExpressionIterator[] compile_stack = new ExpressionIterator[expressionLength]; - final TopologicalOrderIterator it = new TopologicalOrderIterator(graph); - - LOG.debug(String.format("Expressions Size is %d", expressionLength)); - LOG.debug(String.format("Topology Iterator %s", it.toString())); + final ExpressionIterator[] compile_stack = + new ExpressionIterator[expressionLength]; + final TopologicalOrderIterator it = + new TopologicalOrderIterator(graph); + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Expressions Size is %d", expressionLength)); + LOG.debug(String.format("Topology Iterator %s", it.toString())); + } int i = 0; while (it.hasNext()) { String next = it.next(); - LOG.debug(String.format("Expression: %s", next)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Expression: %s", next)); + } ExpressionIterator ei = expressions.get(next); - LOG.debug(String.format("Expression Iterator: %s", ei.toString())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Expression Iterator: %s", ei.toString())); + } if (ei == null) { LOG.error(String.format("The expression iterator for %s is null", next)); } compile_stack[i] = ei; - LOG.debug(String.format("Added expression %s to compile_stack[%d]", next, i)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Added expression %s to compile_stack[%d]", + next, i)); + } i++; } if (i != expressionLength) { - throw new IOException(String.format(" Internal Error: Less expressions where added to the compile stack than expressions.size (%d instead of %d)", i, expressionLength)); + throw new IOException(String.format(" Internal Error: Fewer " + + "expressions where added to the compile stack than " + + "expressions.size (%d instead of %d)", i, expressionLength)); } - LOG.debug(String.format("compile stack length: %d", compile_stack.length)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("compile stack length: %d", compile_stack.length)); + } for (int x = compile_stack.length - 1; x >= 0; x--) { if (compile_stack[x] == null) { - throw new NullPointerException(String.format("Item %d in compile_stack[] is null", x)); + throw new NullPointerException(String.format("Item %d in " + + "compile_stack[] is null", x)); } // look for and add expressions for (final String var : compile_stack[x].getVariableNames()) { - LOG.debug(String.format("Looking for variable %s for %s", var, compile_stack[x].getId())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Looking for variable %s for %s", var, + compile_stack[x].getId())); + } ExpressionIterator source = expressions.get(var); if (source != null) { compile_stack[x].addResults(var, source.getCopy()); - LOG.debug(String.format("Adding expression %s to %s", source.getId(), compile_stack[x].getId())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Adding expression %s to %s", + source.getId(), compile_stack[x].getId())); + } } } compile_stack[x].compile(); - LOG.debug(String.format("Successfully compiled %s", compile_stack[x].getId())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Successfully compiled %s", + compile_stack[x].getId())); + } } - LOG.debug("Finished compilations in " + - (DateTime.currentTimeMillis() - intersect_start) + " ms"); + if (LOG.isDebugEnabled()) { + LOG.debug("Finished compilations in " + + (DateTime.currentTimeMillis() - intersect_start) + " ms"); + } - return serialize().addCallback(new CompleteCB()).addErrback(new ErrorCB()); + return serialize() + .addCallback(new CompleteCB()) + .addErrback(new ErrorCB()); } } @@ -414,7 +463,8 @@ public Deferred call(final net.opentsdb.core.Query[] queries) { // TODO - only run the ones that will be involved in an output. Folks WILL // ask for stuff they don't need.... *sigh* - ts_query.buildQueriesAsync(tsdb).addCallback(new BuildCB()) + ts_query.buildQueriesAsync(tsdb) + .addCallback(new BuildCB()) .addErrback(new ErrorCB()); } @@ -532,35 +582,42 @@ public ChannelBuffer call(final Object obj) /** This has to be attached to callbacks or we may never respond to clients */ class ErrorCB implements Callback { public Object call(final Exception e) throws Exception { + QueryRpc.query_exceptions.incrementAndGet(); + Throwable ex = e; try { LOG.error("Query exception: ", e); if (e instanceof DeferredGroupException) { - Throwable ex = e.getCause(); + ex = e.getCause(); while (ex != null && ex instanceof DeferredGroupException) { ex = ex.getCause(); } - if (ex != null) { - LOG.error("Unexpected exception: ", ex); - // TODO - find a better way to determine the real error - QueryExecutor.this.http_query.badRequest(new BadRequestException(ex)); - } else { + if (ex == null) { LOG.error("The deferred group exception didn't have a cause???"); - QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); } - } else if (e.getClass() == QueryException.class) { - QueryExecutor.this.http_query.badRequest(new BadRequestException((QueryException) e)); - } else if ((e instanceof IOException) || (e instanceof NullPointerException) || (e instanceof RuntimeException)) { - QueryExecutor.this.http_query.internalError(e); + } + if (ex instanceof RpcTimedOutException) { + QueryExecutor.this.http_query.badRequest(new BadRequestException( + HttpResponseStatus.REQUEST_TIMEOUT, ex.getMessage())); + } else if (ex instanceof HBaseException) { + QueryExecutor.this.http_query.badRequest(new BadRequestException( + HttpResponseStatus.FAILED_DEPENDENCY, ex.getMessage())); + } else if (ex instanceof QueryException) { + QueryExecutor.this.http_query.badRequest(new BadRequestException( + ((QueryException)ex).getStatus(), ex.getMessage())); + } else if (ex instanceof BadRequestException) { + QueryExecutor.this.http_query.badRequest((BadRequestException)ex); + } else if (ex instanceof NoSuchUniqueName) { + QueryExecutor.this.http_query.badRequest(new BadRequestException(ex)); } else { - QueryExecutor.this.http_query.badRequest(new BadRequestException(e)); + QueryExecutor.this.http_query.badRequest(new BadRequestException(ex)); } - return null; - } catch (RuntimeException ex) { - LOG.error("Exception thrown during exception handling", ex); - QueryExecutor.this.http_query.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, - ex.getMessage().getBytes()); - return null; + + } catch (RuntimeException ex2) { + LOG.error("Exception thrown during exception handling", ex2); + QueryExecutor.this.http_query.sendReply + (HttpResponseStatus.INTERNAL_SERVER_ERROR, ex2.getMessage().getBytes()); } + return null; } } diff --git a/test/query/pojo/TestQuery.java b/test/query/pojo/TestQuery.java index ff8585d9c8..613ff787a8 100644 --- a/test/query/pojo/TestQuery.java +++ b/test/query/pojo/TestQuery.java @@ -70,7 +70,7 @@ public class TestQuery { + " \"expressions\":[" + " {" + " \"id\":\"e1\"," - + " \"expr\":\"a + b + c\"" + + " \"expr\":\"m1 * 1024\"" + " }" + " ]," + " \"outputs\":[" @@ -96,7 +96,7 @@ public void setup() { .setId("m1").setFilter("f1").setTimeOffset("0") .setAggregator("sum").build(); expression = Expression.Builder().setId("e1") - .setExpression("a + b + c").setJoin( + .setExpression("m1 * 1024").setJoin( Join.Builder().setOperator(SetOperator.UNION).build()).build(); output = Output.Builder().setId("m1").setAlias("CPU Idle EAST DC") .build(); diff --git a/test/tsd/TestQueryExecutor.java b/test/tsd/TestQueryExecutor.java index eeabd47b3b..b5c1f5c903 100644 --- a/test/tsd/TestQueryExecutor.java +++ b/test/tsd/TestQueryExecutor.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-2016 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 @@ -19,11 +19,14 @@ import java.util.Arrays; import java.util.List; +import net.opentsdb.core.FillPolicy; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; +import net.opentsdb.query.expression.NumericFillPolicy; import net.opentsdb.query.expression.BaseTimeSyncedIteratorTest; import net.opentsdb.query.expression.VariableIterator.SetOperator; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.pojo.Downsampler; import net.opentsdb.query.pojo.Expression; import net.opentsdb.query.pojo.Filter; import net.opentsdb.query.pojo.Join; @@ -188,6 +191,101 @@ public void oneExpressionDefaultFill() throws Exception { assertTrue(response.contains("\"index\":3")); } + @Test + public void oneExpressionDownsamplingMissingTimestampNoFill() throws Exception { + threeSameEGaps(); + final Downsampler downsampler = Downsampler.Builder() + .setAggregator("sum") + .setInterval("1m") + .build(); + time = Timespan.Builder().setStart("1431561600") + .setAggregator("sum") + .setDownsampler(downsampler).build(); + String json = JSON.serializeToString(getDefaultQueryBuilder()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + query.getQueryBaseRoute(); // to the correct serializer + NettyMocks.mockChannelFuture(query); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(response.contains("\"alias\":\"A plus B\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,1.0,4.0,0.0]")); + assertTrue(response.contains("[1431561660000,0.0,20.0,8.0]")); + assertTrue(response.contains("[1431561720000,16.0,0.0,28.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":2")); + assertTrue(response.contains("\"index\":3")); + } + +// @Test +// public void oneExpressionDownsamplingMissingTimestampZeroFill() throws Exception { +// threeSameEGaps(); +// final Downsampler downsampler = Downsampler.Builder() +// .setAggregator("sum") +// .setInterval("1m") +// .setFillPolicy(new NumericFillPolicy(FillPolicy.ZERO)) +// .build(); +// time = Timespan.Builder().setStart("1431561540") +// .setEnd("1431561780") +// .setAggregator("sum") +// .setDownsampler(downsampler).build(); +// String json = JSON.serializeToString(getDefaultQueryBuilder()); +// final QueryRpc rpc = new QueryRpc(); +// final HttpQuery query = NettyMocks.postQuery(tsdb, +// "/api/query/exp", json); +// query.getQueryBaseRoute(); // to the correct serializer +// NettyMocks.mockChannelFuture(query); +// +// rpc.execute(tsdb, query); +// final String response = +// query.response().getContent().toString(Charset.forName("UTF-8")); +// assertTrue(response.contains("\"alias\":\"A plus B\"")); +// assertTrue(response.contains("\"dps\":[[1431561540000,0.0,0.0,0.0]")); +// assertTrue(response.contains("[1431561600000,1.0,4.0,0.0]")); +// assertTrue(response.contains("[1431561660000,0.0,20.0,8.0]")); +// assertTrue(response.contains("[1431561720000,16.0,0.0,28.0]")); +// assertTrue(response.contains("[1431561780000,0.0,0.0,0.0]")); +// assertTrue(response.contains("\"firstTimestamp\":1431561540000")); +// assertTrue(response.contains("\"index\":1")); +// assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); +// assertTrue(response.contains("\"index\":2")); +// assertTrue(response.contains("\"index\":3")); +// } + + @Test + public void oneExpressionNoFilter() throws Exception { + oneExtraSameE(); + final Metric metric1 = Metric.Builder().setMetric("A").setId("a") + .build(); + final Metric metric2 = Metric.Builder().setMetric("B").setId("b") + .build(); + metrics = Arrays.asList(metric1, metric2); + + final String json = JSON.serializeToString(getDefaultQueryBuilder().build()); + final QueryRpc rpc = new QueryRpc(); + final HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/query/exp", json); + NettyMocks.mockChannelFuture(query); + + rpc.execute(tsdb, query); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); +System.out.println(response); + assertTrue(response.contains("\"alias\":\"A plus B\"")); + assertTrue(response.contains("\"dps\":[[1431561600000,47.0]")); + assertTrue(response.contains("[1431561660000,52.0]")); + assertTrue(response.contains("[1431561720000,57.0]")); + assertTrue(response.contains("\"firstTimestamp\":1431561600000")); + assertTrue(response.contains("\"index\":1")); + assertTrue(response.contains("\"metrics\":[\"A\",\"B\"]")); + assertTrue(response.contains("\"index\":1")); + } + @Test public void twoExpressionsDefaultOutput() throws Exception { oneExtraSameE(); @@ -600,7 +698,7 @@ public void noIntersectionsFoundOneMetricEmpty() throws Exception { assertTrue(response.contains("\"message\":\"No intersections found")); } - @Test + @Test (expected = IllegalArgumentException.class) public void notEnoughMetrics() throws Exception { oneExtraSameE(); expressions = Arrays.asList( @@ -613,10 +711,6 @@ public void notEnoughMetrics() throws Exception { NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - final String response = - query.response().getContent().toString(Charset.forName("UTF-8")); - assertTrue(response.contains("\"code\":400")); - assertTrue(response.contains("\"message\":\"Not enough query results")); } protected Query.Builder getDefaultQueryBuilder() { From 951173b1d4e71dc0c5d260a89e66d81c2d4c0ca8 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 18 Sep 2016 15:10:29 -0700 Subject: [PATCH 540/826] Fix #708 where the sorting order of filters would change in some browsers can cause an infinite loop of loading. Now we sort after parsing and before calling the API so it's determinant. Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 106 ++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index fd51faeef2..0c2665416f 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -14,7 +14,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; + import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; @@ -160,6 +162,9 @@ private String parseWithMetric(final String metric) { filters.add(f); i++; } + if (!filters.isEmpty()) { + Collections.sort(filters); + } i = 0; for (int x = filters.size() - 1; x >= 0; x--) { @@ -418,53 +423,32 @@ public boolean buildQueryString(final StringBuilder url) { } } url.append(':').append(metric); - boolean non_groupbys = false; - int groupby_tags = 0; - { - final int ntags = getNumTags(); + List filters = getFilters(true); + if (!filters.isEmpty()) { url.append('{'); - for (int tag = 0; tag < ntags; tag++) { - final String tagname = getTagName(tag); - final String tagvalue = getTagValue(tag); - if (tagname.isEmpty() || tagvalue.isEmpty() || !isTagGroupby(tag)) { - if (!isTagGroupby(tag)) { - non_groupbys = true; - } - continue; + for (int i = 0; i < filters.size(); i++) { + if (i > 0) { + url.append(","); } - url.append(tagname).append('=').append(tagvalue) - .append(','); - ++groupby_tags; - } - final int last = url.length() - 1; - if (url.charAt(last) == '{') { // There was no tag. - url.setLength(last); // So remove the `{'. - } else { // Need to replace the last `,' with a `}'. - url.setCharAt(url.length() - 1, '}'); + url.append(filters.get(i).tagk) + .append("=") + .append(filters.get(i).tagv); } + url.append('}'); } - if (non_groupbys) { - if (groupby_tags == 0) { - // need this to shift group by to non-group by - url.append("{}"); - } - final int ntags = getNumTags(); + // now the non-group bys + filters = getFilters(false); + if (!filters.isEmpty()) { url.append('{'); - for (int tag = 0; tag < ntags; tag++) { - final String tagname = getTagName(tag); - final String tagvalue = getTagValue(tag); - if (tagname.isEmpty() || tagvalue.isEmpty() || isTagGroupby(tag)) { - continue; + for (int i = 0; i < filters.size(); i++) { + if (i > 0) { + url.append(","); } - url.append(tagname).append('=').append(tagvalue) - .append(','); - } - final int last = url.length() - 1; - if (url.charAt(last) == '{') { // There was no tag. - url.setLength(last); // So remove the `{'. - } else { // Need to replace the last `,' with a `}'. - url.setCharAt(url.length() - 1, '}'); + url.append(filters.get(i).tagk) + .append("=") + .append(filters.get(i).tagv); } + url.append('}'); } url.append("&o="); if (x1y2.getValue()) { @@ -472,7 +456,34 @@ public boolean buildQueryString(final StringBuilder url) { } return true; } - + + /** + * Helper method to extract the tags from the row set and sort them before + * sending to the API so that we avoid a bug wherein the sort order changes + * on reload. + * @param group_by Whether or not to fetch group by or non-group by filters. + * @return A non-null list of filters. May be empty. + */ + private List getFilters(final boolean group_by) { + final int ntags = getNumTags(); + final List filters = new ArrayList(ntags); + for (int tag = 0; tag < ntags; tag++) { + final Filter filter = new Filter(); + filter.tagk = getTagName(tag); + filter.tagv = getTagValue(tag); + filter.is_groupby = isTagGroupby(tag); + if (filter.tagk.isEmpty() || filter.tagv.isEmpty()) { + continue; + } + if (filter.is_groupby = group_by) { + filters.add(filter); + } + } + // sort on the tagk + Collections.sort(filters); + return filters; + } + private int getNumTags() { return tagtable.getRowCount() - 1; } @@ -527,7 +538,7 @@ private void addTag(final String default_tagname, final String default_value, final boolean is_groupby) { final int row = tagtable.getRowCount(); - + final ValidatedTextBox tagname = new ValidatedTextBox(); final SuggestBox suggesttagk = RemoteOracle.newSuggestBox("tagk", tagname); final ValidatedTextBox tagvalue = new ValidatedTextBox(); @@ -731,10 +742,19 @@ static final public LocalRateOptions parseRateOptions(boolean rate, String spec) } } - private static class Filter { + private static class Filter implements Comparable { String tagk; String tagv; boolean is_groupby; + + @Override + public int compareTo(final Filter filter) { + if (filter == this) { + return 0; + } + return tagk.compareTo(filter.tagk) + + tagv.compareTo(filter.tagv); + } } // ------------------- // From ca9bf6ccc5977b6252fd0856df5b5b5423d90ae2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 18 Sep 2016 15:10:29 -0700 Subject: [PATCH 541/826] Fix #708 where the sorting order of filters would change in some browsers can cause an infinite loop of loading. Now we sort after parsing and before calling the API so it's determinant. Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 106 ++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index fd51faeef2..0c2665416f 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -14,7 +14,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; + import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; @@ -160,6 +162,9 @@ private String parseWithMetric(final String metric) { filters.add(f); i++; } + if (!filters.isEmpty()) { + Collections.sort(filters); + } i = 0; for (int x = filters.size() - 1; x >= 0; x--) { @@ -418,53 +423,32 @@ public boolean buildQueryString(final StringBuilder url) { } } url.append(':').append(metric); - boolean non_groupbys = false; - int groupby_tags = 0; - { - final int ntags = getNumTags(); + List filters = getFilters(true); + if (!filters.isEmpty()) { url.append('{'); - for (int tag = 0; tag < ntags; tag++) { - final String tagname = getTagName(tag); - final String tagvalue = getTagValue(tag); - if (tagname.isEmpty() || tagvalue.isEmpty() || !isTagGroupby(tag)) { - if (!isTagGroupby(tag)) { - non_groupbys = true; - } - continue; + for (int i = 0; i < filters.size(); i++) { + if (i > 0) { + url.append(","); } - url.append(tagname).append('=').append(tagvalue) - .append(','); - ++groupby_tags; - } - final int last = url.length() - 1; - if (url.charAt(last) == '{') { // There was no tag. - url.setLength(last); // So remove the `{'. - } else { // Need to replace the last `,' with a `}'. - url.setCharAt(url.length() - 1, '}'); + url.append(filters.get(i).tagk) + .append("=") + .append(filters.get(i).tagv); } + url.append('}'); } - if (non_groupbys) { - if (groupby_tags == 0) { - // need this to shift group by to non-group by - url.append("{}"); - } - final int ntags = getNumTags(); + // now the non-group bys + filters = getFilters(false); + if (!filters.isEmpty()) { url.append('{'); - for (int tag = 0; tag < ntags; tag++) { - final String tagname = getTagName(tag); - final String tagvalue = getTagValue(tag); - if (tagname.isEmpty() || tagvalue.isEmpty() || isTagGroupby(tag)) { - continue; + for (int i = 0; i < filters.size(); i++) { + if (i > 0) { + url.append(","); } - url.append(tagname).append('=').append(tagvalue) - .append(','); - } - final int last = url.length() - 1; - if (url.charAt(last) == '{') { // There was no tag. - url.setLength(last); // So remove the `{'. - } else { // Need to replace the last `,' with a `}'. - url.setCharAt(url.length() - 1, '}'); + url.append(filters.get(i).tagk) + .append("=") + .append(filters.get(i).tagv); } + url.append('}'); } url.append("&o="); if (x1y2.getValue()) { @@ -472,7 +456,34 @@ public boolean buildQueryString(final StringBuilder url) { } return true; } - + + /** + * Helper method to extract the tags from the row set and sort them before + * sending to the API so that we avoid a bug wherein the sort order changes + * on reload. + * @param group_by Whether or not to fetch group by or non-group by filters. + * @return A non-null list of filters. May be empty. + */ + private List getFilters(final boolean group_by) { + final int ntags = getNumTags(); + final List filters = new ArrayList(ntags); + for (int tag = 0; tag < ntags; tag++) { + final Filter filter = new Filter(); + filter.tagk = getTagName(tag); + filter.tagv = getTagValue(tag); + filter.is_groupby = isTagGroupby(tag); + if (filter.tagk.isEmpty() || filter.tagv.isEmpty()) { + continue; + } + if (filter.is_groupby = group_by) { + filters.add(filter); + } + } + // sort on the tagk + Collections.sort(filters); + return filters; + } + private int getNumTags() { return tagtable.getRowCount() - 1; } @@ -527,7 +538,7 @@ private void addTag(final String default_tagname, final String default_value, final boolean is_groupby) { final int row = tagtable.getRowCount(); - + final ValidatedTextBox tagname = new ValidatedTextBox(); final SuggestBox suggesttagk = RemoteOracle.newSuggestBox("tagk", tagname); final ValidatedTextBox tagvalue = new ValidatedTextBox(); @@ -731,10 +742,19 @@ static final public LocalRateOptions parseRateOptions(boolean rate, String spec) } } - private static class Filter { + private static class Filter implements Comparable { String tagk; String tagv; boolean is_groupby; + + @Override + public int compareTo(final Filter filter) { + if (filter == this) { + return 0; + } + return tagk.compareTo(filter.tagk) + + tagv.compareTo(filter.tagv); + } } // ------------------- // From 6bba88cb48a7e45b61a438c1e6df8d5ffa9d5b93 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Mon, 19 Sep 2016 18:08:46 -0500 Subject: [PATCH 542/826] This is the fix for #793 and 3781 Fixes #781 Fixes #793 --- src/tsd/HttpQuery.java | 49 ------------------------------------------ 1 file changed, 49 deletions(-) diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index c2848ee7aa..d56d0fe74e 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -372,8 +372,6 @@ public void internalError(final Exception cause) { HttpQuery.escapeJson(pretty_exc, buf); buf.append("\"}"); sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, buf); - } else if (hasQueryStringParam("png")) { - sendAsPNG(HttpResponseStatus.INTERNAL_SERVER_ERROR, pretty_exc, 30); } else { sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, makePage("Internal Server Error", "Houston, we have a problem", @@ -421,8 +419,6 @@ public void badRequest(final BadRequestException exception) { HttpQuery.escapeJson(exception.getMessage(), buf); buf.append("\"}"); sendReply(HttpResponseStatus.BAD_REQUEST, buf); - } else if (hasQueryStringParam("png")) { - sendAsPNG(HttpResponseStatus.BAD_REQUEST, exception.getMessage(), 3600); } else { sendReply(HttpResponseStatus.BAD_REQUEST, makePage("Bad Request", "Looks like it's your fault this time", @@ -456,8 +452,6 @@ public void notFound() { if (hasQueryStringParam("json")) { sendReply(HttpResponseStatus.NOT_FOUND, new StringBuilder("{\"err\":\"Page Not Found\"}")); - } else if (hasQueryStringParam("png")) { - sendAsPNG(HttpResponseStatus.NOT_FOUND, "Page Not Found", 3600); } else { sendReply(HttpResponseStatus.NOT_FOUND, PAGE_NOT_FOUND); } @@ -604,49 +598,6 @@ public void sendReply(final HttpResponseStatus status, sendBuffer(status, buf); } - /** - * Sends the given message as a PNG image. - * This method will block while image is being generated. - * It's only recommended for cases where we want to report an error back to - * the user and the user's browser expects a PNG image. Don't abuse it. - * @param status The status of the request (e.g. 200 OK or 404 Not Found). - * @param msg The message to send as an image. - * @param max_age The expiration time of this entity, in seconds. This is - * not a timestamp, it's how old the resource is allowed to be in the client - * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable - * caching. - */ - public void sendAsPNG(final HttpResponseStatus status, - final String msg, - final int max_age) { - try { - final long now = System.currentTimeMillis() / 1000; - Plot plot = new Plot(now - 1, now); - HashMap params = new HashMap(1); - StringBuilder buf = new StringBuilder(1 + msg.length() + 18); - - buf.append('"'); - escapeJson(msg, buf); - buf.append("\" at graph 0.02,0.97"); - params.put("label", buf.toString()); - buf = null; - plot.setParams(params); - params = null; - final String basepath = - tsdb.getConfig().getDirectoryName("tsd.http.cachedir") - + Integer.toHexString(msg.hashCode()); - GraphHandler.runGnuplot(this, basepath, plot); - plot = null; - sendFile(status, basepath + ".png", max_age); - } catch (Exception e) { - getQueryString().remove("png"); // Avoid recursion. - this.sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, - serializer.formatErrorV1(new RuntimeException( - "Failed to generate a PNG with the" - + " following message: " + msg, e))); - } - } - /** * Send a file (with zero-copy) to the client with a 200 OK status. * This method doesn't provide any security guarantee. The caller is From 910b433b351575e54d90185bc6613049292cf6d0 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Mon, 19 Sep 2016 23:11:44 -0500 Subject: [PATCH 543/826] RPCManager, plugin loader refactor (#870) * Refactored RPCManager, allowed for disabling the built-in UI or API via configuration options Fix #830 * Pass mode in for tests * Added Logging to identify if UI and API are enabled * Added CLI Options to disable UI or API RPC endpoints * Missed a few lines with the new option --- Makefile.am | 4 +- src/opentsdb.conf | 8 + src/tools/CliOptions.java | 4 + src/tools/TSDMain.java | 4 + src/tsd/DropCachesRpc.java | 88 ++++++++++ src/tsd/RpcManager.java | 313 +++++++++++++++-------------------- src/tsd/RpcUtil.java | 37 +++++ src/utils/Config.java | 2 + test/tsd/TestRpcManager.java | 29 +++- 9 files changed, 303 insertions(+), 186 deletions(-) create mode 100644 src/tsd/DropCachesRpc.java create mode 100644 src/tsd/RpcUtil.java diff --git a/Makefile.am b/Makefile.am index d2ad17539f..73ba335146 100644 --- a/Makefile.am +++ b/Makefile.am @@ -98,7 +98,7 @@ tsdb_SRC := \ src/query/expression/PostAggregatedDataPoints.java \ src/query/expression/Scale.java \ src/query/expression/SumSeries.java \ - src/query/expression/TimeShift.java \ + src/query/expression/TimeShift.java \ src/query/expression/TimeSyncedIterator.java \ src/query/expression/UnionIterator.java \ src/query/expression/VariableIterator.java \ @@ -147,6 +147,7 @@ tsdb_SRC := \ src/tsd/AnnotationRpc.java \ src/tsd/BadRequestException.java \ src/tsd/ConnectionManager.java \ + src/tsd/DropCachesRpc.java \ src/tsd/GnuplotException.java \ src/tsd/GraphHandler.java \ src/tsd/HttpJsonSerializer.java \ @@ -164,6 +165,7 @@ tsdb_SRC := \ src/tsd/RpcHandler.java \ src/tsd/RpcPlugin.java \ src/tsd/RpcManager.java \ + src/tsd/RpcUtil.java \ src/tsd/RTPublisher.java \ src/tsd/SearchRpc.java \ src/tsd/StaticFileRpc.java \ diff --git a/src/opentsdb.conf b/src/opentsdb.conf index ba977a7b41..8ba7028a52 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -37,6 +37,14 @@ tsd.http.cachedir = # is False #tsd.core.auto_create_metrics = false +# Whether or not to enable the built-in UI Rpc Plugins, default +# is True +#tsd.core.enable_ui = true + +# Whether or not to enable the built-in API Rpc Plugins, default +# is True +#tsd.core.enable_api = true + # --------- STORAGE ---------- # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index cf5d58b4e0..a87c45a19f 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -120,6 +120,10 @@ static void overloadConfig(final ArgP argp, final Config config) { // map the overrides if (entry.getKey().toLowerCase().equals("--auto-metric")) { config.overrideConfig("tsd.core.auto_create_metrics", "true"); + } else if (entry.getKey().toLowerCase().equals("--disable-ui")) { + config.overrideConfig("tsd.core.enable_ui", "false"); + } else if (entry.getKey().toLowerCase().equals("--disable-api")) { + config.overrideConfig("tsd.core.enable_api", "false"); } else if (entry.getKey().toLowerCase().equals("--table")) { config.overrideConfig("tsd.storage.hbase.data_table", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--uidtable")) { diff --git a/src/tools/TSDMain.java b/src/tools/TSDMain.java index f26a895419..a5bda860a0 100644 --- a/src/tools/TSDMain.java +++ b/src/tools/TSDMain.java @@ -93,6 +93,10 @@ public static void main(String[] args) throws IOException { "Use async NIO (default true) or traditional blocking io"); argp.addOption("--read-only", "true|false", "Set tsd.mode to ro (default false)"); + argp.addOption("--disable-ui", "true|false", + "Set tsd.core.enable_ui to false (default true)"); + argp.addOption("--disable-api", "true|false", + "Set tsd.core.enable_api to false (default true)"); argp.addOption("--backlog", "NUM", "Size of connection attempt queue (default: 3072 or kernel" + " somaxconn."); diff --git a/src/tsd/DropCachesRpc.java b/src/tsd/DropCachesRpc.java new file mode 100644 index 0000000000..9cd59d15e9 --- /dev/null +++ b/src/tsd/DropCachesRpc.java @@ -0,0 +1,88 @@ +// 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.tsd; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Atomics; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +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 net.opentsdb.tools.BuildData; +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.TSDB; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.JSON; +import net.opentsdb.utils.PluginLoader; + +import java.io.IOException; + +/** The "dropcaches" command. */ +public final class DropCachesRpc implements TelnetRpc, HttpRpc { + private static final Logger LOG = LoggerFactory.getLogger(DropCachesRpc.class); + + public Deferred execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + dropCaches(tsdb, chan); + chan.write("Caches dropped.\n"); + return Deferred.fromResult(null); + } + + public void execute(final TSDB tsdb, final HttpQuery query) + throws IOException { + + // only accept GET/DELETE + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.DELETE.getName()); + + dropCaches(tsdb, query.channel()); + + if (query.apiVersion() > 0) { + final HashMap response = new HashMap(); + response.put("status", "200"); + response.put("message", "Caches dropped"); + query.sendReply(query.serializer().formatDropCachesV1(response)); + } else { // deprecated API + query.sendReply("Caches dropped.\n"); + } + } + + /** Drops in memory caches. */ + private void dropCaches(final TSDB tsdb, final Channel chan) { + LOG.warn(chan + " Dropping all in-memory caches."); + tsdb.dropCaches(); + } +} \ No newline at end of file diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 1814326c34..86638c1a12 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -52,36 +52,36 @@ /** * Manager for the lifecycle of HttpRpcs, TelnetRpcs, * RpcPlugins, and HttpRpcPlugin. This is a - * singleton. Its lifecycle must be managed by the "container". If you are - * launching via {@code TSDMain} then shutdown (and non-lazy initialization) + * singleton. Its lifecycle must be managed by the "container". If you are + * launching via {@code TSDMain} then shutdown (and non-lazy initialization) * is taken care of. Outside of the use of {@code TSDMain}, you are responsible * for shutdown, at least. - * + * *

    Here's an example of how to correctly handle shutdown manually: - * + * *

      * // Startup our TSDB instance...
      * TSDB tsdb_instance = ...;
    - * 
    + *
      * // ... later, during shtudown ..
    - * 
    + *
      * if (RpcManager.isInitialized()) {
      *   // Check that its actually been initialized.  We don't want to
      *   // create a new instance only to shutdown!
      *   RpcManager.instance(tsdb_instance).shutdown().join();
      * }
      * 
    - * + * * @since 2.2 */ public final class RpcManager { private static final Logger LOG = LoggerFactory.getLogger(RpcManager.class); - + /** This is base path where {@link HttpRpcPlugin}s are rooted. It's used * to match incoming requests. */ @VisibleForTesting protected static final String PLUGIN_BASE_WEBPATH = "plugin"; - + /** Splitter for web paths. Removes empty strings to handle trailing or * leading slashes. For instance, all of /plugin/mytest, * plugin/mytest/, and plugin/mytest will be @@ -89,13 +89,13 @@ public final class RpcManager { private static final Splitter WEBPATH_SPLITTER = Splitter.on('/') .trimResults() .omitEmptyStrings(); - + /** Matches paths declared by {@link HttpRpcPlugin}s that are rooted in * the system's plugins path. */ private static final Pattern HAS_PLUGIN_BASE_WEBPATH = Pattern.compile( - "^/?" + PLUGIN_BASE_WEBPATH + "/?.*", + "^/?" + PLUGIN_BASE_WEBPATH + "/?.*", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - + /** Reference to our singleton instance. Set in {@link #initialize}. */ private static final AtomicReference INSTANCE = Atomics.newReference(); @@ -107,10 +107,10 @@ public final class RpcManager { private ImmutableMap http_plugin_commands; /** List of activated RPC plugins */ private ImmutableList rpc_plugins; - + /** The TSDB that owns us. */ private TSDB tsdb; - + /** * Constructor used by singleton factory method. * @param tsdb the owning TSDB instance. @@ -118,7 +118,7 @@ public final class RpcManager { private RpcManager(final TSDB tsdb) { this.tsdb = tsdb; } - + /** * Get or create the singleton instance of the manager, loading all the * plugins enabled in the given TSDB's {@link Config}. @@ -133,9 +133,9 @@ public static synchronized RpcManager instance(final TSDB tsdb) { final RpcManager manager = new RpcManager(tsdb); final String mode = Strings.nullToEmpty(tsdb.getConfig().getString("tsd.mode")); - + // Load any plugins that are enabled via Config. Fail if any plugin cannot be loaded. - + final ImmutableList.Builder rpcBuilder = ImmutableList.builder(); if (tsdb.getConfig().hasProperty("tsd.rpc.plugins")) { final String[] plugins = tsdb.getConfig().getString("tsd.rpc.plugins").split(","); @@ -155,13 +155,13 @@ public static synchronized RpcManager instance(final TSDB tsdb) { manager.initializeHttpRpcPlugins(mode, plugins, httpPluginsBuilder); } manager.http_plugin_commands = httpPluginsBuilder.build(); - + INSTANCE.set(manager); return manager; } - + /** - * @return {@code true} if the shared instance has been initialized; + * @return {@code true} if the shared instance has been initialized; * {@code false} otherwise. */ public static synchronized boolean isInitialized() { @@ -169,17 +169,17 @@ public static synchronized boolean isInitialized() { } /** - * @return list of loaded {@link RpcPlugin}s. Possibly empty but + * @return list of loaded {@link RpcPlugin}s. Possibly empty but * never {@code null}. */ @VisibleForTesting protected ImmutableList getRpcPlugins() { return rpc_plugins; } - + /** * Lookup a {@link TelnetRpc} based on given command name. Note that this - * lookup is case sensitive in that the {@code command} passed in must + * lookup is case sensitive in that the {@code command} passed in must * match a registered RPC command exactly. * @param command a telnet API command name. * @return the {@link TelnetRpc} for the given {@code command} or {@code null} @@ -188,36 +188,36 @@ protected ImmutableList getRpcPlugins() { TelnetRpc lookupTelnetRpc(final String command) { return telnet_commands.get(command); } - + /** * Lookup a built-in {@link HttpRpc} based on the given {@code queryBaseRoute}. * The lookup is based on exact match of the input parameter and the registered * {@link HttpRpc}s. - * @param queryBaseRoute the HTTP query's base route, with no trailing or + * @param queryBaseRoute the HTTP query's base route, with no trailing or * leading slashes. For example: {@code api/query} - * @return the {@link HttpRpc} for the given {@code queryBaseRoute} or + * @return the {@link HttpRpc} for the given {@code queryBaseRoute} or * {@code null} if not found. */ HttpRpc lookupHttpRpc(final String queryBaseRoute) { return http_commands.get(queryBaseRoute); } - + /** - * Lookup a user-supplied {@link HttpRpcPlugin} for the given - * {@code queryBaseRoute}. The lookup is based on exact match of the input + * Lookup a user-supplied {@link HttpRpcPlugin} for the given + * {@code queryBaseRoute}. The lookup is based on exact match of the input * parameter and the registered {@link HttpRpcPlugin}s. * @param queryBaseRoute the value of {@link HttpRpcPlugin#getPath()} with no * trailing or leading slashes. - * @return the {@link HttpRpcPlugin} for the given {@code queryBaseRoute} or + * @return the {@link HttpRpcPlugin} for the given {@code queryBaseRoute} or * {@code null} if not found. */ HttpRpcPlugin lookupHttpRpcPlugin(final String queryBaseRoute) { return http_plugin_commands.get(queryBaseRoute); } - + /** * @param uri HTTP request URI, with or without query parameters. - * @return {@code true} if the URI represents a request for a + * @return {@code true} if the URI represents a request for a * {@link HttpRpcPlugin}; {@code false} otherwise. Note that this * method returning true says nothing about * whether or not there is a {@link HttpRpcPlugin} registered @@ -233,12 +233,12 @@ boolean isHttpRpcPluginPath(final String uri) { if (qmark != -1) { path = uri.substring(0, qmark); } - + final List parts = WEBPATH_SPLITTER.splitToList(path); return (parts.size() > 1 && parts.get(0).equals(PLUGIN_BASE_WEBPATH)); } } - + /** * Load and init instances of {@link TelnetRpc}s and {@link HttpRpc}s. * These are not generally configurable via TSDB config. @@ -248,67 +248,75 @@ boolean isHttpRpcPluginPath(final String uri) { * instances. * @param http a map of API endpoints to {@link HttpRpc} instances. */ - private void initializeBuiltinRpcs(final String mode, + private void initializeBuiltinRpcs(final String mode, final ImmutableMap.Builder telnet, final ImmutableMap.Builder http) { + + final Boolean enableApi = tsdb.getConfig().getString("tsd.core.enable_api").equals("true"); + final Boolean enableUi = tsdb.getConfig().getString("tsd.core.enable_ui").equals("true"); + final Boolean enableDieDieDie = tsdb.getConfig().getString("tsd.no_diediedie").equals("false"); + + LOG.info("Mode: {}, HTTP UI Enabled: {}, HTTP API Enabled: {}", mode, enableUi, enableApi); + if (mode.equals("rw") || mode.equals("wo")) { final PutDataPointRpc put = new PutDataPointRpc(); telnet.put("put", put); - http.put("api/put", put); + if (enableApi) { + http.put("api/put", put); + } } - + if (mode.equals("rw") || mode.equals("ro")) { - http.put("", new HomePage()); final StaticFileRpc staticfile = new StaticFileRpc(); - http.put("favicon.ico", staticfile); - http.put("s", staticfile); - final StatsRpc stats = new StatsRpc(); - telnet.put("stats", stats); - http.put("stats", stats); - http.put("api/stats", stats); + final DropCachesRpc dropcaches = new DropCachesRpc(); + final ListAggregators aggregators = new ListAggregators(); + final SuggestRpc suggest_rpc = new SuggestRpc(); + final AnnotationRpc annotation_rpc = new AnnotationRpc(); + final Version version = new Version(); - final DropCaches dropcaches = new DropCaches(); + telnet.put("stats", stats); telnet.put("dropcaches", dropcaches); - http.put("dropcaches", dropcaches); - http.put("api/dropcaches", dropcaches); + telnet.put("version", version); + telnet.put("exit", new Exit()); + telnet.put("help", new Help()); - final ListAggregators aggregators = new ListAggregators(); - http.put("aggregators", aggregators); - http.put("api/aggregators", aggregators); + if (enableUi) { + http.put("", new HomePage()); + http.put("aggregators", aggregators); + http.put("dropcaches", dropcaches); + http.put("favicon.ico", staticfile); + http.put("logs", new LogsRpc()); + http.put("q", new GraphHandler()); + http.put("s", staticfile); + http.put("stats", stats); + http.put("suggest", suggest_rpc); + http.put("version", version); + } - final SuggestRpc suggest_rpc = new SuggestRpc(); - http.put("suggest", suggest_rpc); - http.put("api/suggest", suggest_rpc); - - http.put("logs", new LogsRpc()); - http.put("q", new GraphHandler()); - http.put("api/serializers", new Serializers()); - http.put("api/uid", new UniqueIdRpc()); - http.put("api/query", new QueryRpc()); - http.put("api/tree", new TreeRpc()); - { - final AnnotationRpc annotation_rpc = new AnnotationRpc(); + if (enableApi) { + http.put("api/aggregators", aggregators); http.put("api/annotation", annotation_rpc); http.put("api/annotations", annotation_rpc); - } - http.put("api/search", new SearchRpc()); - http.put("api/config", new ShowConfig()); - - if (tsdb.getConfig().getString("tsd.no_diediedie").equals("false")) { - final DieDieDie diediedie = new DieDieDie(); - telnet.put("diediedie", diediedie); - http.put("diediedie", diediedie); - } - { - final Version version = new Version(); - telnet.put("version", version); - http.put("version", version); + http.put("api/config", new ShowConfig()); + http.put("api/dropcaches", dropcaches); + http.put("api/query", new QueryRpc()); + http.put("api/search", new SearchRpc()); + http.put("api/serializers", new Serializers()); + http.put("api/stats", stats); + http.put("api/suggest", suggest_rpc); + http.put("api/tree", new TreeRpc()); + http.put("api/uid", new UniqueIdRpc()); http.put("api/version", version); } + } - telnet.put("exit", new Exit()); - telnet.put("help", new Help()); + if (enableDieDieDie) { + final DieDieDie diediedie = new DieDieDie(); + telnet.put("diediedie", diediedie); + if (enableUi) { + http.put("diediedie", diediedie); + } } } @@ -317,10 +325,10 @@ private void initializeBuiltinRpcs(final String mode, * {@code pluginClassNames}. * @param mode is this TSD in read/write ("rw") or read-only ("ro") * mode? - * @param pluginClassNames fully-qualified class names that are + * @param pluginClassNames fully-qualified class names that are * instances of {@link HttpRpcPlugin}s - * @param http a map of canonicalized paths - * (obtained via {@link #canonicalizePluginPath(String)}) + * @param http a map of canonicalized paths + * (obtained via {@link #canonicalizePluginPath(String)}) * to {@link HttpRpcPlugin} instance. */ @VisibleForTesting @@ -338,7 +346,7 @@ protected void initializeHttpRpcPlugins(final String mode, } /** - * Ensure that the given path for an {@link HttpRpcPlugin} is valid. This + * Ensure that the given path for an {@link HttpRpcPlugin} is valid. This * method simply returns for valid inputs; throws and exception otherwise. * @param path a request path, no query parameters, etc. * @throws IllegalArgumentException on invalid paths. @@ -349,9 +357,9 @@ protected void validateHttpRpcPluginPath(final String path) { "Invalid HttpRpcPlugin path. Path is null or empty."); final String testPath = path.trim(); Preconditions.checkArgument(!HAS_PLUGIN_BASE_WEBPATH.matcher(path).matches(), - "Invalid HttpRpcPlugin path %s. Path contains system's plugin base path.", + "Invalid HttpRpcPlugin path %s. Path contains system's plugin base path.", testPath); - + URI uri = URI.create(testPath); Preconditions.checkArgument(!Strings.isNullOrEmpty(uri.getPath()), "Invalid HttpRpcPlugin path %s. Parsed path is null or empty.", testPath); @@ -384,18 +392,18 @@ protected String canonicalizePluginPath(final String origPath) { /** * Load and init the {@link RpcPlugin}s provided as an array of * {@code pluginClassNames}. - * @param pluginClassNames fully-qualified class names that are + * @param pluginClassNames fully-qualified class names that are * instances of {@link RpcPlugin}s * @param rpcs a list of loaded and initialized plugins */ - private void initializeRpcPlugins(final String[] pluginClassNames, + private void initializeRpcPlugins(final String[] pluginClassNames, final ImmutableList.Builder rpcs) { for (final String plugin : pluginClassNames) { final RpcPlugin rpc = createAndInitialize(plugin, RpcPlugin.class); rpcs.add(rpc); } } - + /** * Helper method to load and initialize a given plugin class. This uses reflection * because plugins share no common interfaces. (They could though!) @@ -406,7 +414,7 @@ private void initializeRpcPlugins(final String[] pluginClassNames, @VisibleForTesting protected T createAndInitialize(final String pluginClassName, final Class pluginClass) { final T instance = PluginLoader.loadSpecificPlugin(pluginClassName, pluginClass); - Preconditions.checkState(instance != null, + Preconditions.checkState(instance != null, "Unable to locate %s using name '%s", pluginClass, pluginClassName); try { final Method initMeth = instance.getClass().getMethod("initialize", TSDB.class); @@ -421,9 +429,9 @@ protected T createAndInitialize(final String pluginClassName, final Class throw new RuntimeException("Failed to initialize " + instance.getClass(), e); } } - + /** - * Called to gracefully shutdown the plugin. Implementations should close + * Called to gracefully shutdown the plugin. Implementations should close * any IO they have open * @return A deferred object that indicates the completion of the request. * The {@link Object} has not special meaning and can be {@code null} @@ -434,22 +442,22 @@ public Deferred> shutdown() { INSTANCE.set(null); final Collection> deferreds = Lists.newArrayList(); - + if (http_plugin_commands != null) { for (final Map.Entry entry : http_plugin_commands.entrySet()) { deferreds.add(entry.getValue().shutdown()); } } - + if (rpc_plugins != null) { for (final RpcPlugin rpc : rpc_plugins) { deferreds.add(rpc.shutdown()); } } - + return Deferred.groupInOrder(deferreds); } - + /** * Collect stats on the shared instance of {@link RpcManager}. */ @@ -470,7 +478,7 @@ static void collectStats(final StatsCollector collector) { if (manager.http_plugin_commands != null) { try { collector.addExtraTag("plugin", "httprpc"); - for (final Map.Entry entry + for (final Map.Entry entry : manager.http_plugin_commands.entrySet()) { entry.getValue().collectStats(collector); } @@ -480,7 +488,7 @@ static void collectStats(final StatsCollector collector) { } } } - + // ---------------------------- // // Individual command handlers. // // ---------------------------- // @@ -557,7 +565,7 @@ public Deferred execute(final TSDB tsdb, final Channel chan, /** The home page ("GET /"). */ private static final class HomePage implements HttpRpc { - public void execute(final TSDB tsdb, final HttpQuery query) + public void execute(final TSDB tsdb, final HttpQuery query) throws IOException { final StringBuilder buf = new StringBuilder(2048); buf.append("
    " @@ -571,19 +579,15 @@ public void execute(final TSDB tsdb, final HttpQuery query) "OpenTSDB", "", buf.toString())); } } - + /** The "/aggregators" endpoint. */ private static final class ListAggregators implements HttpRpc { - public void execute(final TSDB tsdb, final HttpQuery query) + 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"); - } - + + // only accept GET / POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + if (query.apiVersion() > 0) { query.sendReply( query.serializer().formatAggregatorsV1(Aggregators.set())); @@ -604,16 +608,12 @@ public Deferred execute(final TSDB tsdb, final Channel chan, return Deferred.fromResult(null); } - public void execute(final TSDB tsdb, final HttpQuery query) throws + 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"); - } - + + // only accept GET / POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + final HashMap version = new HashMap(); version.put("version", BuildData.version); version.put("short_revision", BuildData.short_revision); @@ -628,7 +628,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) throws if (query.apiVersion() > 0) { query.sendReply(query.serializer().formatVersionV1(version)); } else { - final boolean json = query.request().getUri().endsWith("json"); + final boolean json = query.request().getUri().endsWith("json"); if (json) { query.sendReply(JSON.serializeToBytes(version)); } else { @@ -643,82 +643,37 @@ public void execute(final TSDB tsdb, final HttpQuery query) throws } } } - - /** The "dropcaches" command. */ - private static final class DropCaches implements TelnetRpc, HttpRpc { - public Deferred execute(final TSDB tsdb, final Channel chan, - final String[] cmd) { - dropCaches(tsdb, chan); - chan.write("Caches dropped.\n"); - return Deferred.fromResult(null); - } - - public void execute(final TSDB tsdb, final HttpQuery query) - throws IOException { - dropCaches(tsdb, query.channel()); - - // 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"); - } - - if (query.apiVersion() > 0) { - final HashMap response = new HashMap(); - response.put("status", "200"); - response.put("message", "Caches dropped"); - query.sendReply(query.serializer().formatDropCachesV1(response)); - } else { // deprecated API - query.sendReply("Caches dropped.\n"); - } - } - - /** Drops in memory caches. */ - private void dropCaches(final TSDB tsdb, final Channel chan) { - LOG.warn(chan + " Dropping all in-memory caches."); - tsdb.dropCaches(); - } - } - /** The /api/formatters endpoint + /** The /api/formatters endpoint * @since 2.0 */ private static final class Serializers implements HttpRpc { - public void execute(final TSDB tsdb, final HttpQuery query) + 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"); - } - + // only accept GET / POST + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); + switch (query.apiVersion()) { case 0: case 1: query.sendReply(query.serializer().formatSerializersV1()); break; - default: - throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, - "Requested API version not implemented", "Version " + + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); } } } - + private static final class ShowConfig implements HttpRpc { @Override public void execute(TSDB tsdb, 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[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1].toLowerCase() : ""; - + if (endpoint.equals("filters")) { switch (query.apiVersion()) { case 0: @@ -726,9 +681,9 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { query.sendReply(query.serializer().formatFilterConfigV1( TagVFilter.loadedFilters())); break; - default: - throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, - "Requested API version not implemented", "Version " + + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); } } else { @@ -737,9 +692,9 @@ public void execute(TSDB tsdb, HttpQuery query) throws IOException { case 1: query.sendReply(query.serializer().formatConfigV1(tsdb.getConfig())); break; - default: - throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, - "Requested API version not implemented", "Version " + + default: + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "Requested API version not implemented", "Version " + query.apiVersion() + " is not implemented"); } } diff --git a/src/tsd/RpcUtil.java b/src/tsd/RpcUtil.java new file mode 100644 index 0000000000..07660e208f --- /dev/null +++ b/src/tsd/RpcUtil.java @@ -0,0 +1,37 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.tsd; + +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 java.util.List; + +public class RpcUtil { + + private static final Logger LOG = LoggerFactory.getLogger(RpcUtil.class); + + public static void allowedMethods(HttpMethod requestMethod, String... allowedMethods) { + for(String method : allowedMethods) { + LOG.debug(String.format("Trying Method: %s", method)); + if (requestMethod.getName() == method) { + LOG.debug(String.format("Method Allowed: %s", method)); + return; + } + } + throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, + "Method not allowed", "The HTTP method [" + requestMethod.getName() + "] is not permitted for this endpoint"); + } +} diff --git a/src/utils/Config.java b/src/utils/Config.java index 4927d43fbb..782bf36430 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -488,6 +488,8 @@ protected void setDefaults() { default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); default_map.put("tsd.core.connections.limit", "0"); + default_map.put("tsd.core.enable_api", "true"); + default_map.put("tsd.core.enable_ui", "true"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); diff --git a/test/tsd/TestRpcManager.java b/test/tsd/TestRpcManager.java index 58a8193487..fc062bc506 100644 --- a/test/tsd/TestRpcManager.java +++ b/test/tsd/TestRpcManager.java @@ -46,6 +46,12 @@ public class TestRpcManager { @Before public void before() { Config config = mock(Config.class); + when(config.getString("tsd.core.enable_api")) + .thenReturn("true"); + when(config.getString("tsd.core.enable_ui")) + .thenReturn("true"); + when(config.getString("tsd.no_diediedie")) + .thenReturn("false"); TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); mock_tsdb_no_plugins = tsdb; @@ -62,10 +68,16 @@ public void after() throws Exception { public void loadHttpRpcPlugins() throws Exception { Config config = mock(Config.class); when(config.hasProperty("tsd.http.rpc.plugins")) - .thenReturn(true); + .thenReturn(true); when(config.getString("tsd.http.rpc.plugins")) .thenReturn("net.opentsdb.tsd.DummyHttpRpcPlugin"); - + when(config.getString("tsd.core.enable_api")) + .thenReturn("true"); + when(config.getString("tsd.core.enable_ui")) + .thenReturn("true"); + when(config.getString("tsd.no_diediedie")) + .thenReturn("false"); + TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); @@ -80,17 +92,22 @@ public void loadHttpRpcPlugins() throws Exception { public void loadRpcPlugin() throws Exception { Config config = mock(Config.class); when(config.hasProperty("tsd.rpc.plugins")) - .thenReturn(true); + .thenReturn(true); when(config.getString("tsd.rpc.plugins")) .thenReturn("net.opentsdb.tsd.DummyRpcPlugin"); - when(config.hasProperty("tsd.rpcplugin.DummyRPCPlugin.hosts")) - .thenReturn(true); + .thenReturn(true); when(config.getString("tsd.rpcplugin.DummyRPCPlugin.hosts")) .thenReturn("blah"); when(config.getInt("tsd.rpcplugin.DummyRPCPlugin.port")) .thenReturn(1000); - + when(config.getString("tsd.core.enable_api")) + .thenReturn("true"); + when(config.getString("tsd.core.enable_ui")) + .thenReturn("true"); + when(config.getString("tsd.no_diediedie")) + .thenReturn("false"); + TSDB tsdb = mock(TSDB.class); when(tsdb.getConfig()).thenReturn(config); From 6cb553a98142d5b88a2049f3b4a371f3957c35c2 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Mon, 19 Sep 2016 23:20:55 -0500 Subject: [PATCH 544/826] Added Dockerfile and tool to build docker root (#871) --- .gitignore | 6 ++++++ tools/docker/Dockerfile | 43 +++++++++++++++++++++++++++++++++++++++++ tools/docker/docker.sh | 16 +++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 tools/docker/Dockerfile create mode 100755 tools/docker/docker.sh diff --git a/.gitignore b/.gitignore index 41729cc410..60fb346c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ src-main src-test plugin_test.jar bin/ + +#Docker +tools/docker/libs +tools/docker/*.jar +tools/docker/logback.xml +tools/docker/opentsdb.conf diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile new file mode 100644 index 0000000000..c9410133e1 --- /dev/null +++ b/tools/docker/Dockerfile @@ -0,0 +1,43 @@ +FROM java:openjdk-8-alpine + +MAINTAINER jonathan.creasy@gmail.com + +ENV VERSION 2.3.0-RC1 +ENV WORKDIR /usr/share/opentsdb +ENV LOGDIR /var/log/opentsdb +ENV DATADIR /data/opentsdb +ENV ETCDIR /etc/opentsdb + +RUN mkdir -p $WORKDIR/static +RUN mkdir -p $WORKDIR/libs +RUN mkdir -p $WORKDIR/third_party +RUN mkdir -p $WORKDIR/resources +RUN mkdir -p $DATADIR/cache +RUN mkdir -p $LOGDIR +RUN mkdir -p $ETCDIR + +ENV CONFIG $ETCDIR/opentsdb.conf +ENV STATICROOT $WORKDIR/static +ENV CACHEDIR $DATADIR/cache + +ENV CLASSPATH $WORKDIR:$WORKDIR/tsdb-$VERSION.jar:$WORKDIR/libs/*:$WORKDIR/logback.xml +ENV CLASS net.opentsdb.tools.TSDMain + +# It is expected these might need to be passed in with the -e flag +ENV JAVA_OPTS="-Xms512m -Xmx2048m" +ENV ZKQUORUM zookeeper:2181 +ENV ZKBASEDIR /hbase +ENV TSDB_OPTS "--read-only --disable-ui" +ENV TSDB_PORT 4244 + +WORKDIR $WORKDIR + +ADD libs $WORKDIR/libs +ADD logback.xml $WORKDIR +ADD tsdb-$VERSION.jar $WORKDIR +ADD opentsdb.conf $ETCDIR/opentsdb.conf + +VOLUME ["/etc/openstsdb"] +VOLUME ["/data/opentsdb"] + +ENTRYPOINT java -enableassertions -enablesystemassertions -classpath ${CLASSPATH} ${CLASS} --config=${CONFIG} --staticroot=${STATICROOT} --cachedir=${CACHEDIR} --port=${TSDB_PORT} --zkquorum=${ZKQUORUM} --zkbasedir=${ZKBASEDIR} ${TSDB_OPTS} diff --git a/tools/docker/docker.sh b/tools/docker/docker.sh new file mode 100755 index 0000000000..17686a0e61 --- /dev/null +++ b/tools/docker/docker.sh @@ -0,0 +1,16 @@ +#!/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 From 9e2774c916a26c6049e201df0c64a8f355489566 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 26 Sep 2016 22:12:50 -0700 Subject: [PATCH 545/826] Fix #837 by making sure the fuzzy filter is created only when fuzzy filtering is enabled and explicit tags are enabled. Also add some unit tests for the above. Signed-off-by: Chris Larsen --- src/query/QueryUtil.java | 2 +- test/query/TestQueryUtil.java | 148 ++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 test/query/TestQueryUtil.java diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 1a61c45a0a..c7b9782b2a 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -231,7 +231,7 @@ public static void setDataTableScanFilter( byteRegexToString(regex)); } - if (!explicit_tags || !enable_fuzzy_filter) { + if (!(explicit_tags && enable_fuzzy_filter)) { scanner.setFilter(regex_filter); return; } diff --git a/test/query/TestQueryUtil.java b/test/query/TestQueryUtil.java new file mode 100644 index 0000000000..03760e8499 --- /dev/null +++ b/test/query/TestQueryUtil.java @@ -0,0 +1,148 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.FilterList; +import org.hbase.async.KeyRegexpFilter; +import org.hbase.async.ScanFilter; +import org.hbase.async.Scanner; +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; + +import com.google.common.collect.Lists; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Scanner.class }) +public class TestQueryUtil { + private Scanner scanner; + + @Before + public void before() throws Exception { + scanner = mock(Scanner.class); + } + + @Test + public void setDataTableScanFilterNoOp() throws Exception { + QueryUtil.setDataTableScanFilter( + scanner, + Lists.newArrayList(), + new ByteMap(), + false, + false, + 0); + verify(scanner, never()).getCurrentKey(); + verify(scanner, never()).setFilter(any(ScanFilter.class)); + verify(scanner, never()).setStartKey(any(byte[].class)); + verify(scanner, never()).setStopKey(any(byte[].class)); + } + + @Test + public void setDataTableScanFilterGroupBy() throws Exception { + QueryUtil.setDataTableScanFilter( + scanner, + Lists.newArrayList(new byte[] { 0, 0, 1 }), + new ByteMap(), + false, + false, + 0); + verify(scanner, never()).getCurrentKey(); + // TODO - validate the regex + verify(scanner, times(1)).setFilter(any(KeyRegexpFilter.class)); + verify(scanner, never()).setStartKey(any(byte[].class)); + verify(scanner, never()).setStopKey(any(byte[].class)); + } + + @Test + public void setDataTableScanFilterTags() throws Exception { + final ByteMap tags = new ByteMap(); + tags.put(new byte[] { 0, 0, 1 }, new byte[][] { new byte[] {0, 0, 1} }); + QueryUtil.setDataTableScanFilter( + scanner, + Lists.newArrayList(), + tags, + false, + false, + 0); + verify(scanner, never()).getCurrentKey(); + // TODO - validate the regex + verify(scanner, times(1)).setFilter(any(KeyRegexpFilter.class)); + verify(scanner, never()).setStartKey(any(byte[].class)); + verify(scanner, never()).setStopKey(any(byte[].class)); + } + + @Test + public void setDataTableScanFilterEnableFuzzy() throws Exception { + final ByteMap tags = new ByteMap(); + tags.put(new byte[] { 0, 0, 1 }, new byte[][] { new byte[] {0, 0, 1} }); + QueryUtil.setDataTableScanFilter( + scanner, + Lists.newArrayList(), + tags, + false, + true, + 0); + verify(scanner, never()).getCurrentKey(); + // TODO - validate the regex + verify(scanner, times(1)).setFilter(any(KeyRegexpFilter.class)); + verify(scanner, never()).setStartKey(any(byte[].class)); + verify(scanner, never()).setStopKey(any(byte[].class)); + } + + @Test + public void setDataTableScanFilterEnableExplicit() throws Exception { + final ByteMap tags = new ByteMap(); + tags.put(new byte[] { 0, 0, 1 }, new byte[][] { new byte[] {0, 0, 1} }); + QueryUtil.setDataTableScanFilter( + scanner, + Lists.newArrayList(), + tags, + true, + false, + 0); + verify(scanner, never()).getCurrentKey(); + // TODO - validate the regex + verify(scanner, times(1)).setFilter(any(KeyRegexpFilter.class)); + verify(scanner, never()).setStartKey(any(byte[].class)); + verify(scanner, never()).setStopKey(any(byte[].class)); + } + + @Test + public void setDataTableScanFilterEnableBoth() throws Exception { + when(scanner.getCurrentKey()).thenReturn(new byte[] { 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( + scanner, + Lists.newArrayList(), + tags, + true, + true, + 0); + verify(scanner, times(2)).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)); + } +} From e7aaa5fa208f87a15aafd9d268d68e3bc718ebbf Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 10:30:55 -0700 Subject: [PATCH 546/826] Fix a directory redirection error in the makefile when building an RPM. --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 73ba335146..0c010a7911 100644 --- a/Makefile.am +++ b/Makefile.am @@ -568,7 +568,7 @@ install-data-local: staticroot install-data-lib install-data-tools \ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ dstdir=`dirname "$(DESTDIR)$(staticdir)/$$p"`; \ if test -d "$$dstdir"; then :; else \ - echo " $(mkdir_p) '$$dstdir'"; ../$(mkdir_p) "$$dstdir"; fi; \ + echo " $(mkdir_p) '$$dstdir'"; $(mkdir_p) "$$dstdir"; fi; \ echo " $(INSTALL_DATA) '$$d$$p' '$(DESTDIR)$(staticdir)/$$p'"; \ $(INSTALL_DATA) "$$d$$p" "$(DESTDIR)$(staticdir)/$$p"; \ done From 8129e3a9faeec72927c51ddba048b323f96f308d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 10:35:12 -0700 Subject: [PATCH 547/826] Bump to version 2.3.0-RC2 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index ea48f48ce8..b55a385569 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.3.0-RC1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.3.0-RC2], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 24ed9953c3534165bcbec730f920388d47fde707 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Wed, 23 Mar 2016 14:01:07 -0500 Subject: [PATCH 548/826] Updated regex to match dotted timestamp 1234567890.2.3 - no match 1234.56789.1234 - no match 1234567890.1234 - matches 1234.567890.2..3 - no match 1234567890.2..3 - no match Fixes #724 --- src/utils/DateTime.java | 12 +++++++++--- test/utils/TestDateTime.java | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 5ea80c6742..cdce972271 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -122,9 +122,14 @@ public static final long parseDateTimeString(final String datetime, try { long time; Boolean containsDot = datetime.contains("."); - Boolean containsTwoDots = datetime.matches(".*\\..*\\..*"); + // [0-9]{10} ten digits + // \\. a dot + // [0-9]{1,3} one to three digits + Boolean isValidDottedMillesecond = datetime.matches("^[0-9]{10}\\.[0-9]{1,3}$"); + // one to ten digits (0-9) + Boolean isValidSeconds = datetime.matches("^[0-9]{1,10}$"); if (containsDot) { - if (datetime.charAt(10) != '.' || datetime.length() != 14 || containsTwoDots) { + if (!isValidDottedMillesecond) { throw new IllegalArgumentException("Invalid time: " + datetime + ". Millisecond timestamps must be in the format " + ". where the milliseconds are limited to 3 digits"); @@ -139,8 +144,9 @@ public static final long parseDateTimeString(final String datetime, } // this is a nasty hack to determine if the incoming request is // in seconds or milliseconds. This will work until November 2286 - if (datetime.length() <= 10) + if (datetime.length() <= 10) { time *= 1000; + } return time; } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid time: " + datetime diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index eef640dd00..f5d75f6c25 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -124,10 +124,28 @@ public void parseDateTimeStringUnixSecondsNegative() { DateTime.parseDateTimeString("-135596160", null); } + /* + 1234567890.418 - match + 1234567890.1235 - no match + 1234567890.12 - matches + 1234.56789.003 - no match + 1234567890.3 - match + */ + @Test(expected = IllegalArgumentException.class) public void parseDateTimeStringMultipleDots() { DateTime.parseDateTimeString("1234567890.2.4", null); } + + @Test(expected = IllegalArgumentException.class) + public void parseDateTimeStringMultipleDotsEarlyDot() { + DateTime.parseDateTimeString("1234.56789.123", null); + } + + @Test(expected = IllegalArgumentException.class) + public void parseDateTimeStringEarlyandExtraDots() { + DateTime.parseDateTimeString("1234.56789.0.3", null); + } @Test public void parseDateTimeStringUnixSecondsInvalidLong() { @@ -147,6 +165,18 @@ public void parseDateTimeStringUnixMSDot() { long t = DateTime.parseDateTimeString("1355961603.418", null); assertEquals(1355961603418L, t); } + + @Test + public void parseDateTimeStringUnixMSDotShorter() { + long t = DateTime.parseDateTimeString("1355961603.41", null); + assertEquals(135596160341L, t); + } + + @Test + public void parseDateTimeStringUnixMSDotShortest() { + long t = DateTime.parseDateTimeString("1355961603.4", null); + assertEquals(13559616034L, t); + } @Test (expected = IllegalArgumentException.class) public void parseDateTimeStringUnixMSDotInvalid() { From 30910799a97c9909f8a7140b6e8c40d512b76afd Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 18 Apr 2016 18:26:42 -0700 Subject: [PATCH 549/826] Little bit of cleanup in DateTime.parseDateTimeString() by removing a regex that isn't used and formatting the variable names per TSD spec. Signed-off-by: Chris Larsen --- src/utils/DateTime.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index cdce972271..793b97b369 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -121,15 +121,14 @@ public static final long parseDateTimeString(final String datetime, } else { try { long time; - Boolean containsDot = datetime.contains("."); + final boolean contains_dot = datetime.contains("."); // [0-9]{10} ten digits // \\. a dot // [0-9]{1,3} one to three digits - Boolean isValidDottedMillesecond = datetime.matches("^[0-9]{10}\\.[0-9]{1,3}$"); - // one to ten digits (0-9) - Boolean isValidSeconds = datetime.matches("^[0-9]{1,10}$"); - if (containsDot) { - if (!isValidDottedMillesecond) { + final boolean valid_dotted_ms = + datetime.matches("^[0-9]{10}\\.[0-9]{1,3}$"); + if (contains_dot) { + if (!valid_dotted_ms) { throw new IllegalArgumentException("Invalid time: " + datetime + ". Millisecond timestamps must be in the format " + ". where the milliseconds are limited to 3 digits"); From 5e649f2f1a80cb8446a3e5152defb24c09cf47b8 Mon Sep 17 00:00:00 2001 From: dfsklar Date: Thu, 1 Sep 2016 11:21:05 -0400 Subject: [PATCH 550/826] Update HttpQuery.java --- src/tsd/HttpQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index d56d0fe74e..49c30268d6 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -976,7 +976,7 @@ protected Logger logger() { + "" + "" + "" + "
    " - + "" + + "" + " 
    "; From dc9f0cfb2751a18c31432315a43ec716e634a173 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 12:06:31 -0700 Subject: [PATCH 551/826] Cut Release 2.2.1 --- NEWS | 20 +++++++++++++++++++- THANKS | 7 +++++++ configure.ac | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 14bac38ef2..b3cdd4cddb 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,6 @@ OpenTSDB - User visible changes. -* Version 2.2.1 (2015-?-?) +* Version 2.2.1 (2016-10-08) Noteworthy Changes - Generate an incrementing TSMeta request only if both enable_tsuid_incrementing and @@ -8,6 +8,24 @@ Noteworthy Changes regardless of whether or not the real time ts setting was enabled. If tsuid incrementing is disabled then a get and optional put is executed each time without modifying the meta counter field. + - Improve metadata storage performance by removing an extra getFromStorage() call. + - Add global Annotations to the gnuplot graphs (#773) + - Allow creation of a TSMeta object without a TSUID (#778) + - Move to AsyncHBase 1.7.2 + +Bug Fixes: + - Fix Python scripts to use the environment directory. + - Fix config name for "tsd.network.keep_alive" in included config files. + - Fix an issue with the filter metric and tag resolution chain during queries. + - Fix an issue with malformed, double dotted timestamps (#724). + - Fix an issue with tag filters where we need a copy before modifying the list. + - Fix comments in the config file around TCP no delay settings. + - Fix some query stats calculations around averaging and estimating the number + of data points (#784). + - Clean out old .SWO files (#821) + - Fix a live-lock situation when performing regular expression or wildcard queries (#823). + - Change the static file path for the HTTP API to be relative (#857). + - Fix an issue where the GUI could flicker when two or more tag filters were set (#708). * Version 2.2.0 (2016-02-14) diff --git a/THANKS b/THANKS index 05e9ae58ff..9493ba58e1 100644 --- a/THANKS +++ b/THANKS @@ -23,11 +23,14 @@ Cristian Sechel Christophe Furmaniak Dave Barr Davide D Amico +Dfsklar +Ethan Wang Filippo Giunchedi Gabriel Nicolas Avellaneda Guenther Schmuelling Hari Krishna Dara Hong Dai Thanh +Hugo M Fernandes Hugo Trippaers Ivan Babrou Jacek Masiulaniec @@ -42,11 +45,13 @@ Johan Zeeck Johannes Meixner Jonathan Works Josh Thomas +Kevin Bowling Kieren Hynd Kimoon Kim Kris Beevers Kyle Brandt Lex Herbert +Li Zhe Liangliang He Liu Yubao Loïs Burg @@ -64,6 +69,7 @@ Nicole Nagele Nikhil Benesch Nitin Aggarwal Paula Keezer +Peter Edwards Peter Gotz Pradeep Chhetri Rajesh G @@ -81,5 +87,6 @@ Tristan Colgate-McFarlane Tony Landells Utkarsh Bhatnagar Vasiliy Kiryanov +Vitaliy Fuks Yulai Fu Zachary Kurey \ No newline at end of file diff --git a/configure.ac b/configure.ac index 0ca046cf10..c422ff8d93 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.2.1-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From c4b467959918da1b946bf605952e24d370bcb7e0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 12:27:03 -0700 Subject: [PATCH 552/826] Copy the full contents of the tools directory to prevent errors in building the Debian package. --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 0c010a7911..ab2a3fa505 100644 --- a/Makefile.am +++ b/Makefile.am @@ -861,7 +861,7 @@ debian: dist staticroot cp -r gwt/queryui/* $(distdir)/debian/usr/share/opentsdb/static `for dep_jar in $(tsdb_DEPS); do cp $$dep_jar \ $(distdir)/debian/usr/share/opentsdb/lib; done;` - cp $(top_srcdir)/tools/* $(distdir)/debian/usr/share/opentsdb/tools + cp -r $(top_srcdir)/tools/* $(distdir)/debian/usr/share/opentsdb/tools dpkg -b $(distdir)/debian $(distdir)/opentsdb-$(PACKAGE_VERSION)_all.deb .PHONY: jar doc check gwtc gwtdev printdeps staticroot gwttsd rpm From 2ec0b33f25624331eed31c53fe3f4582cb39dab6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 12:27:37 -0700 Subject: [PATCH 553/826] Cut release 2.3.0RC2 --- NEWS | 43 ++++++++++++++++++++++++++++++++++++++++++- THANKS | 6 ++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index d2dabe3de2..335ee67c42 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,26 @@ OpenTSDB - User visible changes. +* Version 2.3.0 RC2 (2016-10-08) + +Noteworthy Changes: + - Added a docker file and tool to build TSD docker containers (#871). + - Log X-Forwarded-For addresses when handling HTTP requests. + - Expand aggregator options in the Nagios check script. + - Allow enabling or disabling the HTTP API or UI. + - TSD will now exit when an unrecognized CLI param is passed. + +Bug Fixes: + - Improved ALPN version detection when using Google Bigtable. + - Fix the DumpSeries class to support appended data point types. + - Fix queries where groupby is set to false on all filters. + - Fix a missing attribute in the Nagios check script (#728). + - Fix a major security bug where requesting a PNG with certain URI params could execute code + on the host (#793, #781). + - Return a proper error code when dropping caches with the DELETE HTTP verb (#830). + - Fix backwards compatibility with HBase 0.94 when using explicit tags by removing the + fuzzy filter (#837). + - Fix an RPM build issue when creating the GWT directory. + * Version 2.3.0 RC1 (2016-05-02) Noteworthy Changes: @@ -31,7 +52,27 @@ Bug Fixes: - Restore the ability to create TSMeta objects via URI - Restore raw data points (along with post-filtered data points) in query stats - Built in UI will now properly display global annotations when the query string is passed - - + +Noteworthy Changes: + - Improve metadata storage performance by removing an extra getFromStorage() call. + - Add global Annotations to the gnuplot graphs (#773) + - Allow creation of a TSMeta object without a TSUID (#778) + - Move to AsyncHBase 1.7.2 + +Bug Fixes: + - Fix Python scripts to use the environment directory. + - Fix config name for "tsd.network.keep_alive" in included config files. + - Fix an issue with the filter metric and tag resolution chain during queries. + - Fix an issue with malformed, double dotted timestamps (#724). + - Fix an issue with tag filters where we need a copy before modifying the list. + - Fix comments in the config file around TCP no delay settings. + - Fix some query stats calculations around averaging and estimating the number + of data points (#784). + - Clean out old .SWO files (#821) + - Fix a live-lock situation when performing regular expression or wildcard queries (#823). + - Change the static file path for the HTTP API to be relative (#857). + - Fix an issue where the GUI could flicker when two or more tag filters were set (#708). + * Version 2.2.0 (2016-02-14) Noteworthy Changes diff --git a/THANKS b/THANKS index 08c55697a5..f31c0c88db 100644 --- a/THANKS +++ b/THANKS @@ -28,6 +28,8 @@ Cristian Sechel Christophe Furmaniak Dave Barr Davide D Amico +Dfsklar +Ethan Wang Filippo Giunchedi Gabriel Nicolas Avellaneda Guenther Schmuelling @@ -50,11 +52,13 @@ Johannes Meixner Jonathan Works Josh Thomas Kevin Bowling +Kevin Landreth Kieren Hynd Kimoon Kim Kris Beevers Kyle Brandt Lex Herbert +Li Zhe Liangliang He Liu Yubao Loïs Burg @@ -64,6 +68,7 @@ Matt Schallert Marc Tamsky Mark Smith Martin Jansen +Max Meng Michal Kimle Mike Bryant Mike Kobyakov @@ -73,6 +78,7 @@ Nikhil Benesch Nitin Aggarwal Paula Keezer Peter Gotz +Peter Edwards Ping Yong Pradeep Chhetri Rajesh G From 70fdc78cfff65d793e9728c150d939b6c7bfa739 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 16:49:04 -0700 Subject: [PATCH 554/826] Set version to 2.4.0-SNAPSHOT --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index b55a385569..e5389aaab2 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.3.0-RC2], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.4.0-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 220e3797bd74b0da9bad8568f00f74dc5c7c9fe3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 8 Oct 2016 19:00:00 -0700 Subject: [PATCH 555/826] Add the Median aggregator. Signed-off-by: Chris Larsen --- src/core/Aggregators.java | 46 +++++++++++++++++++++++++++++++++- test/core/TestAggregators.java | 36 +++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index ef1371bc68..18995d7952 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-2016 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 @@ -12,17 +12,20 @@ // see . package net.opentsdb.core; +import java.util.Collections; import java.util.HashMap; import java.util.NoSuchElementException; import java.util.Set; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import org.apache.commons.math3.stat.descriptive.rank.Percentile; import org.apache.commons.math3.stat.descriptive.rank.Percentile.EstimationType; import org.apache.commons.math3.util.ResizableDoubleArray; import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; /** * Utility class that provides common, generally useful aggregators. @@ -55,6 +58,10 @@ public enum Interpolation { public static final Aggregator AVG = new Avg( Interpolation.LERP, "avg"); + /** Aggregator that returns the emedian of the data points. */ + public static final Aggregator MEDIAN = new Median(Interpolation.LERP, + "median"); + /** Aggregator that skips aggregation/interpolation and/or downsampling. */ public static final Aggregator NONE = new None(Interpolation.ZIM, "raw"); @@ -156,6 +163,7 @@ public enum Interpolation { aggregators.put("max", MAX); aggregators.put("avg", AVG); aggregators.put("none", NONE); + aggregators.put("median", MEDIAN); aggregators.put("mult", MULTIPLY); aggregators.put("dev", DEV); aggregators.put("count", COUNT); @@ -333,6 +341,42 @@ public double runDouble(final Doubles values) { } + private static final class Median extends Aggregator { + public Median(final Interpolation method, final String name) { + super(method, name); + } + + @Override + public long runLong(final Longs values) { + final List collection = Lists.newArrayList(); + while (values.hasNextValue()) { + collection.add(values.nextLongValue()); + } + if (collection.isEmpty()) { + throw new IllegalStateException("Shouldn't be here without any data"); + } + Collections.sort(collection); + return collection.get(collection.size() / 2); + } + + @Override + public double runDouble(final Doubles values) { + final List collection = Lists.newArrayList(); + while (values.hasNextValue()) { + final double val = values.nextDoubleValue(); + if (!Double.isNaN(val)) { + collection.add(val); + } + } + if (collection.isEmpty()) { + // in this case we may have had lots of NaNs so just drop em. + return Double.NaN; + } + Collections.sort(collection); + return collection.get(collection.size() / 2); + } + } + /** * An aggregator that isn't meant for aggregation. Paradoxical!! * Really it's used as a flag to indicate that, during sorting and iteration, diff --git a/test/core/TestAggregators.java b/test/core/TestAggregators.java index 7f4f843517..bba62f2ec7 100644 --- a/test/core/TestAggregators.java +++ b/test/core/TestAggregators.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2012 The OpenTSDB Authors. +// Copyright (C) 2012-2016 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 @@ -13,6 +13,8 @@ package net.opentsdb.core; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Random; @@ -215,6 +217,38 @@ public void testLast() { assertEquals(9.5, agg.runDouble(numbers), EPSILON_PERCENTAGE); } + @Test + public void testMedian() { + final Aggregator agg = Aggregators.get("median"); + Numbers numbers = new Numbers(new long[] { 5, 2, -1, 400, 3 }); + assertEquals(3, agg.runLong(numbers)); + + numbers = new Numbers(new long[] { 5, 2, -1, 400, 3, -42 }); + assertEquals(3, agg.runLong(numbers)); + + numbers = new Numbers(new long[] { 42 }); + assertEquals(42, agg.runLong(numbers)); + + numbers = new Numbers(new long[] { }); + try { + assertEquals(42, agg.runLong(numbers)); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + + numbers = new Numbers(new double[] { 5.1, 2.434, -1.99, 400.69487, 3.15168 }); + assertEquals(3.15168, agg.runDouble(numbers), 0.0001); + + numbers = new Numbers(new double[] { 5.1, 2.434, -1.99, 400.69487, + 3.15168, -42 }); + assertEquals(3.15168, agg.runDouble(numbers), 0.0001); + + numbers = new Numbers(new double[] { 42.5 }); + assertEquals(42.5, agg.runDouble(numbers), 0.0001); + + numbers = new Numbers(new double[] { }); + assertTrue(Double.isNaN(agg.runDouble(numbers))); + } + private void assertAggregatorEquals(long value, Aggregator agg, Numbers numbers) { if (numbers.isInteger()) { Assert.assertEquals(value, agg.runLong(numbers)); From 7f4f98dfe21248203c65fcbf1ec0790c7dec6ed8 Mon Sep 17 00:00:00 2001 From: murphy66 Date: Wed, 12 Oct 2016 18:33:50 +0200 Subject: [PATCH 556/826] Fix empty array check. (#876) --- src/core/RowKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/RowKey.java b/src/core/RowKey.java index 694ac119c5..3563d8e566 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -55,7 +55,7 @@ static String metricName(final TSDB tsdb, final byte[] row) { */ public static Deferred metricNameAsync(final TSDB tsdb, final byte[] row) { - if (row == null || row.length < 0) { + if (row == null || row.length < 1) { throw new IllegalArgumentException("Row key cannot be null or empty"); } if (row.length < Const.SALT_WIDTH() + tsdb.metrics.width()) { From b39763e70f66c98d9bbe7afd9aa75d1f3f96d0bc Mon Sep 17 00:00:00 2001 From: murphy66 Date: Wed, 12 Oct 2016 18:33:50 +0200 Subject: [PATCH 557/826] Fix empty array check. (#876) --- src/core/RowKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/RowKey.java b/src/core/RowKey.java index 694ac119c5..3563d8e566 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -55,7 +55,7 @@ static String metricName(final TSDB tsdb, final byte[] row) { */ public static Deferred metricNameAsync(final TSDB tsdb, final byte[] row) { - if (row == null || row.length < 0) { + if (row == null || row.length < 1) { throw new IllegalArgumentException("Row key cannot be null or empty"); } if (row.length < Const.SALT_WIDTH() + tsdb.metrics.width()) { From 45727c6f351160257f1b72bba356a8abaedaf369 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 12 Oct 2016 21:25:33 -0700 Subject: [PATCH 558/826] Add helper methods to DateTime for parsing out the duration interval and units from a duration specifier. Signed-off-by: Chris Larsen --- src/utils/DateTime.java | 61 +++++++++++++++++++++++++ test/utils/TestDateTime.java | 86 ++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 41c10ed805..3d0e1eed52 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -67,6 +67,7 @@ public class DateTime { *
  • 1355961600.000
  • * * @param datetime The string to parse a value for + * @param tz The timezone to use for parsing. * @return A Unix epoch timestamp in milliseconds * @throws NullPointerException if the timestamp is null * @throws IllegalArgumentException if the request was malformed @@ -224,6 +225,66 @@ public static final long parseDuration(final String duration) { return interval * multiplier; } + /** + * Returns the suffix or "units" of the duration as a string. The result will + * be ms, s, m, h, d, w, n or y. + * @param duration The duration in the format #units, e.g. 1d or 6h + * @return Just the suffix, e.g. 'd' or 'h' + * @throws IllegalArgumentException if the duration is null, empty or if + * the units are invalid. + * @since 2.4 + */ + public static final String getDurationUnits(final String duration) { + if (duration == null || duration.isEmpty()) { + throw new IllegalArgumentException("Duration cannot be null or empty"); + } + int unit = 0; + while (unit < duration.length() && + Character.isDigit(duration.charAt(unit))) { + unit++; + } + final String units = duration.substring(unit).toLowerCase(); + if (units.equals("ms") || units.equals("s") || units.equals("m") || + units.equals("h") || units.equals("d") || units.equals("w") || + units.equals("n") || units.equals("y")) { + return units; + } + throw new IllegalArgumentException("Invalid units in the duration: " + units); + } + + /** + * Parses the prefix of the duration, the interval and returns it as a number. + * E.g. if you supply "1d" it will return "1". If you supply "60m" it will + * return "60". + * @param duration The duration to parse in the format #units, e.g. "1d" or "60m" + * @return The interval as an integer, regardless of units. + * @throws IllegalArgumentException if the duration is null, empty or parsing + * of the integer failed. + * @since 2.4 + */ + public static final int getDurationInterval(final String duration) { + if (duration == null || duration.isEmpty()) { + throw new IllegalArgumentException("Duration cannot be null or empty"); + } + if (duration.contains(".")) { + throw new IllegalArgumentException("Floating point intervals are not supported"); + } + int unit = 0; + while (Character.isDigit(duration.charAt(unit))) { + unit++; + } + int interval; + try { + interval = Integer.parseInt(duration.substring(0, unit)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid duration (number): " + duration); + } + if (interval <= 0) { + throw new IllegalArgumentException("Zero or negative duration: " + duration); + } + return interval; + } + /** * Returns whether or not a date is specified in a relative fashion. *

    diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index b21bb25866..1b54e602ca 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -370,6 +370,92 @@ public void parseDurationTooBig() { DateTime.parseDuration("6393590450230209347573980s"); } + @Test + public void getDurationUnits() { + assertEquals("ms", DateTime.getDurationUnits("5ms")); + assertEquals("s", DateTime.getDurationUnits("30s")); + assertEquals("m", DateTime.getDurationUnits("60m")); + assertEquals("h", DateTime.getDurationUnits("42h")); + assertEquals("d", DateTime.getDurationUnits("9d")); + assertEquals("w", DateTime.getDurationUnits("4w")); + assertEquals("n", DateTime.getDurationUnits("12n")); + assertEquals("y", DateTime.getDurationUnits("4y")); + + // zeros ok + assertEquals("d", DateTime.getDurationUnits("0d")); + + // biggies + assertEquals("s", DateTime.getDurationUnits("86400s")); + assertEquals("s", DateTime.getDurationUnits("8641816584387483775236188168735700s")); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationUnitsNegative() { + DateTime.getDurationUnits("-1d"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationUnitsFloat() { + DateTime.getDurationUnits("0.42d"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationUnitsNoUnits() { + DateTime.getDurationUnits("42"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationUnitsNull() { + DateTime.getDurationUnits(null); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationUnitsEmpty() { + DateTime.getDurationUnits(""); + } + + @Test + public void getDurationInterval() { + assertEquals(1, DateTime.getDurationInterval("1s")); + assertEquals(1, DateTime.getDurationInterval("1ms")); + assertEquals(42, DateTime.getDurationInterval("42d")); + assertEquals(86400, DateTime.getDurationInterval("86400s")); + assertEquals(Integer.MAX_VALUE, DateTime.getDurationInterval( + Integer.MAX_VALUE + "s")); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIntervalTooBig() { + long value = Integer.MAX_VALUE; + value++; + DateTime.getDurationInterval(Long.toString(value) + "s"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIntervalNegative() { + DateTime.getDurationInterval("-1s"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIntervalFloat() { + DateTime.getDurationInterval("1.42s"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIntervalNoInt() { + DateTime.getDurationInterval("s"); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIntervalNull() { + DateTime.getDurationInterval(null); + } + + @Test (expected = IllegalArgumentException.class) + public void getDurationIntervalEmpty() { + DateTime.getDurationInterval(""); + } + @Test public void setTimeZone() { SimpleDateFormat fmt = new SimpleDateFormat("yyyy/MM/dd"); From ff5a1c0692c37db691b4699afc01c4dd02aa4733 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 12 Oct 2016 21:58:02 -0700 Subject: [PATCH 559/826] Add initial classes to support time rolled-up and pre-aggregated data points. These include a config class (currently hard-coded), some utility methods, exceptions and a config object class. Signed-off-by: Chris Larsen --- .../NoSuchRollupForIntervalException.java | 34 + src/rollup/NoSuchRollupForTableException.java | 34 + src/rollup/RollupConfig.java | 243 +++++ src/rollup/RollupInterval.java | 309 ++++++ src/rollup/RollupUtils.java | 258 +++++ test/rollup/TestRollupConfig.java | 135 +++ test/rollup/TestRollupInterval.java | 404 ++++++++ test/rollup/TestRollupUtils.java | 974 ++++++++++++++++++ 8 files changed, 2391 insertions(+) create mode 100644 src/rollup/NoSuchRollupForIntervalException.java create mode 100644 src/rollup/NoSuchRollupForTableException.java create mode 100644 src/rollup/RollupConfig.java create mode 100644 src/rollup/RollupInterval.java create mode 100644 src/rollup/RollupUtils.java create mode 100644 test/rollup/TestRollupConfig.java create mode 100644 test/rollup/TestRollupInterval.java create mode 100644 test/rollup/TestRollupUtils.java diff --git a/src/rollup/NoSuchRollupForIntervalException.java b/src/rollup/NoSuchRollupForIntervalException.java new file mode 100644 index 0000000000..d698ebec99 --- /dev/null +++ b/src/rollup/NoSuchRollupForIntervalException.java @@ -0,0 +1,34 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import java.util.NoSuchElementException; + +/** + * Exception thrown when a rollup couldn't be found in the interval to rollup + * interval map + * @since 2.4 + */ +public class NoSuchRollupForIntervalException extends NoSuchElementException { + + /** + * Ctor that builds the message based on a string interval lookup + * @param interval The interval, e.g. "1m" + */ + public NoSuchRollupForIntervalException(final String interval) { + super("No rollups configured for the interval: " + interval); + } + + private static final long serialVersionUID = -8225702079229243161L; + +} diff --git a/src/rollup/NoSuchRollupForTableException.java b/src/rollup/NoSuchRollupForTableException.java new file mode 100644 index 0000000000..ee2b6148a3 --- /dev/null +++ b/src/rollup/NoSuchRollupForTableException.java @@ -0,0 +1,34 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import java.util.NoSuchElementException; + +/** + * Exception thrown when a rollup couldn't be found in the table name to rollup + * interval map + * @since 2.4 + */ +public class NoSuchRollupForTableException extends NoSuchElementException { + + /** + * Ctor that builds the message based on a string table lookup + * @param table The table name + */ + public NoSuchRollupForTableException(final String table) { + super("No rollups configured for the table: " + table); + } + + private static final long serialVersionUID = 6620255176637863260L; + +} diff --git a/src/rollup/RollupConfig.java b/src/rollup/RollupConfig.java new file mode 100644 index 0000000000..2f7d60acc8 --- /dev/null +++ b/src/rollup/RollupConfig.java @@ -0,0 +1,243 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.opentsdb.core.TSDB; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.stumbleupon.async.Deferred; +import com.stumbleupon.async.DeferredGroupException; +import java.util.TreeMap; + +/** + * A hard-coded rollup configuration class that stores the lookup map, config + * and other bits surrounding rollups and pre-aggregates. + * + * Each rollup requires two table names for writing, an interval and a span. + * temporal_table - The table name for raw, temporal only rollup data + * groupby_table - The table name for pre-aggregated and rolled up data + * interval - A interval that the rollup is aligned on, e.g. 10 minute intervals + * would be denoted as "10m" and hourly would be "1h". + * span - A time unit describing the width of the row, or how many data points + * could be in it. E.g 'h' would mean the row holds an hour of data while + * 'y' holds a full year. Possible values are: + * 'h' = hour + * 'd' = day + * 'm' = month + * 'y' = year + * @since 2.4 + */ +public class RollupConfig { + private static final Logger LOG = LoggerFactory.getLogger(RollupConfig.class); + + /** The interval to interval map where keys are things like "10m" or "1d"*/ + final Map forward_intervals = + new HashMap(); + + /** The table name to interval map for queries */ + final Map reverse_intervals = + new HashMap(); + + /** + * Ctor that contains the hard coded intervals. + * TODO - now that we're not writing to a single table, we can load + * this from a config file + */ + public RollupConfig() { + final List config = new ArrayList(); + + /** ---------------- CONFIG --------------------- + * WARNING: Do NOT change these maps after you start pushing data or you + * will invalidate anything you've written to the database. You can always + * add intervals and delete them to stop accepting data, but never remove + */ + config.add(new RollupInterval("tsdb", + "tsdb-agg", "1m", "1h", true)); + config.add(new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d")); + + // don't remove this + validateAndCompileIntervals(config); + } + + /** + * Ctor for unit testing or loading intervals from an alternate source + * @param config The list of rollup intervals to store in the config + */ + public RollupConfig(final List config) { + validateAndCompileIntervals(config); + } + + /** + * Fetches the RollupInterval corresponding to the forward interval string map + * @param interval The interval to lookup + * @return The RollupInterval object configured for the given interval + * @throws IllegalArgumentException if the interval is null or empty + * @throws NoSuchRollupForIntervalException if the interval was not configured + */ + public RollupInterval getRollupInterval(final String interval) { + if (interval == null || interval.isEmpty()) { + throw new IllegalArgumentException("Interval cannot be null or empty"); + } + final RollupInterval rollup = forward_intervals.get(interval); + if (rollup == null) { + throw new NoSuchRollupForIntervalException(interval); + } + return rollup; + } + + /** + * Fetches the RollupInterval corresponding to the integer interval in seconds. + * It returns a list of matching RollupInterval and best next matches in the + * order. It will help to search on the next best rollup tables. + * It is guaranteed that it return a non-empty list + * For example if the interval is 1 day + * then it may return RollupInterval objects in the order + * 1 day, 1 hour, 10 minutes, 1 minute + * @param interval The interval in seconds to lookup + * @param str_interval String representation of the interval, for logging + * @return The RollupInterval object configured for the given interval + * @throws IllegalArgumentException if the interval is null or empty + * @throws NoSuchRollupForIntervalException if the interval was not configured + */ + public List getRollupInterval(final long interval, + final String str_interval) { + + if (interval <= 0) { + throw new IllegalArgumentException("Interval cannot be null or empty"); + } + + Map rollups = new TreeMap(Collections.reverseOrder()); + boolean right_match = false; + + for (RollupInterval rollup: forward_intervals.values()) { + if (rollup.getInterval() == interval) { + rollups.put(new Long(rollup.getInterval()), rollup); + right_match = true; + } + else if (interval % rollup.getInterval() == 0) { + rollups.put(new Long(rollup.getInterval()), rollup); + } + } + + if (rollups.isEmpty()) { + throw new NoSuchRollupForIntervalException(Long.toString(interval)); + } + + List best_matches = + new ArrayList(rollups.values()); + + if (!right_match) { + LOG.warn("No such rollup interval found, " + str_interval + ". So falling " + + "back to the next best match " + best_matches.get(0). + getStringInterval()); + } + + return best_matches; + } + + /** + * Fetches the RollupInterval corresponding to the rollup or pre-agg table + * name. + * @param table The name of the table to fetch + * @return The RollupInterval object matching the table + * @throws IllegalArgumentException if the table is null or empty + * @throws NoSuchRollupForTableException if the interval was not configured + * for the given table + */ + public RollupInterval getRollupIntervalForTable(final String table) { + if (table == null || table.isEmpty()) { + throw new IllegalArgumentException("The table name cannot be null or empty"); + } + final RollupInterval rollup = reverse_intervals.get(table); + if (rollup == null) { + throw new NoSuchRollupForTableException(table); + } + return rollup; + } + + /** + * Makes sure each of the rollup tables exists + * @param tsdb The TSDB to use for fetching the HBase client + */ + public void ensureTablesExist(final TSDB tsdb) { + + final List> deferreds = + new ArrayList>(forward_intervals.size() * 2); + + for (RollupInterval interval : forward_intervals.values()) { + deferreds.add(tsdb.getClient() + .ensureTableExists(interval.getTemporalTable())); + deferreds.add(tsdb.getClient() + .ensureTableExists(interval.getGroupbyTable())); + } + + try { + Deferred.group(deferreds).joinUninterruptibly(); + } catch (DeferredGroupException e) { + throw new RuntimeException(e.getCause()); + } catch (InterruptedException e) { + LOG.warn("Interrupted", e); + Thread.currentThread().interrupt(); + } catch (Exception e) { + throw new RuntimeException("Unexpected exception", e); + } + } + + /** @return an unmodifiable map of the rollups for printing and debugging */ + public Map getRollups() { + return Collections.unmodifiableMap(forward_intervals); + } + + /** + * Determines if the config supplied in the ctor is valid. This will throw + * exceptions if: + * 1) One of the strings is bad when passed to {@link getIntervals} above + * 2) A table name is missing + * 3) If more than one interval (e.g. "1m") is configured. These must + * be unique. + * @param config The list of RollupIntervals to process + * @throws IllegalArgumentException if something is invalid + */ + void validateAndCompileIntervals(final List config) { + if (config.isEmpty()) { + LOG.info("No intervals configured for this TSD"); + return; + } + + for (final RollupInterval config_interval : config) { + + if (forward_intervals.containsKey(config_interval.getStringInterval())) { + throw new IllegalArgumentException( + "Only one interval of each type can be configured: " + + config_interval); + } + + forward_intervals.put(config_interval.getStringInterval(), config_interval); + reverse_intervals.put(config_interval.getTemporalTableName(), config_interval); + reverse_intervals.put(config_interval.getGroupbyTableName(), config_interval); + LOG.debug("Configured rollup: " + config_interval); + } + + LOG.info("Configured [" + forward_intervals.size() + "] rollup intervals"); + } + +} diff --git a/src/rollup/RollupInterval.java b/src/rollup/RollupInterval.java new file mode 100644 index 0000000000..7a9a8a2b4c --- /dev/null +++ b/src/rollup/RollupInterval.java @@ -0,0 +1,309 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Objects; + +import net.opentsdb.core.Const; +import net.opentsdb.utils.DateTime; + +/** + * Holds information about a rollup interval. During construction the inputs + * are validated. + * @since 2.4 + */ +public class RollupInterval { + /** Static intervals */ + private static final int MAX_SECONDS_IN_HOUR = 60 * 60; + private static final int MAX_SECONDS_IN_DAY = 60 * 60 * 24; + // account for leap years, etc + private static final int MAX_SECONDS_IN_MONTH = 60 * 60 * 24 * 32; + private static final int MAX_SECONDS_IN_YEAR = 60 * 60 * 24 * 366; + + /** Based on a 2 bytes with 4 bits reserved for data value size and type */ + public static int MAX_INTERVALS = 7774; + + /** The minimum # of intervals in a span */ + public static int MIN_INTERVALS = 12; + + /** User assigned name for the temporal only table */ + private final String temporal_table_name; + private byte[] temporal_table; + + /** User assigned name for the group by table for this interval */ + private final String groupby_table_name; + private byte[] groupby_table; + + /** User given interval as a string in the format <#> similar to a + * downsampling query + */ + private final String string_interval; + + /** Width of the row as a time unit, e.g. 'h' for hour, 'd' for day, 'm' for + * month and 'y' for year + */ + private final char units; + + /** How many of the units we want in our span */ + private final int unit_multiplier; + + /** Parsed interval unit from the user supplied string */ + private char interval_units; + + /** The interval in seconds */ + private int interval; + + /** The number of intervals in this span */ + private int intervals; + + /** Tells whether it is the default rollup interval + * Default interval is of 1m interval, and will be stored in normal + * tsdb table/s. So if true, which means the raw cell column qualifier format + * also it might be compacted. + * TODO. This will be changed when the spatial aggregation logic is in place. + * Here it is added to handle the pre-aggregated data on raw data + */ + private final boolean default_interval; + + /** + * Default Ctor used when configuring rollups + * @param temporal_table_name The rollup only table name + * @param groupby_table_name The pre-agg rollup table name + * @param interval The rollup interval, e.g. 10m or 15m or 1h + * @param span The row span, e.g. 1h, 6h, 1d, 1m, 1y. Values greater than 1 + * are only allowed with the 'h' unit. + * @throws IllegalArgumentException if milliseconds were passed in the interval + * or the interval couldn't be parsed, the tables are missing, or if the + * duration is too large, too large for the span or the interval is too + * large or small for the span or if the span is invalid. + * @throws NullPointerException if the interval is empty or null + */ + public RollupInterval(final String temporal_table_name, + final String groupby_table_name, final String interval, + final String span) { + this(temporal_table_name, groupby_table_name, interval, span, false); + } + + /** + * Default Ctor used when configuring rollups + * @param temporal_table_name The rollup only table name + * @param groupby_table_name The pre-agg rollup table name + * @param interval The rollup interval, e.g. 10m or 15m or 1h + * @param span The row span, e.g. 1h, 6h, 1d, 1m, 1y. Values greater than 1 + * are only allowed with the 'h' unit. + * @param default_interval Tells whether it is the default rollup interval + * that needs to be written into default tsdb table + * @throws IllegalArgumentException if milliseconds were passed in the interval + * or the interval couldn't be parsed, the tables are missing, or if the + * duration is too large, too large for the span or the interval is too + * large or small for the span or if the span is invalid. + * @throws NullPointerException if the interval is empty or null + */ + public RollupInterval(final String temporal_table_name, + final String groupby_table_name, final String interval, + final String span, boolean default_interval) { + this.temporal_table_name = temporal_table_name; + this.groupby_table_name = groupby_table_name; + this.string_interval = interval; + this.default_interval = default_interval; + + final String parsed_units = DateTime.getDurationUnits(span); + if (parsed_units.length() > 1) { + throw new IllegalArgumentException("Milliseconds are not supported"); + } + units = parsed_units.charAt(0); + this.unit_multiplier = DateTime.getDurationInterval(span); + + validateAndCompile(); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("table=").append(temporal_table_name) + .append(", agg_table=").append(groupby_table_name) + .append(", interval=").append(string_interval) + .append(", units=").append(units) + .append(", unit_multipier=").append(unit_multiplier) + .append(", intervals=").append(intervals) + .append(", interval=").append(interval) + .append(", interval_units=").append(interval_units); + return buf.toString(); + } + + @Override + public int hashCode() { + return Objects.hashCode(temporal_table_name, groupby_table_name, units, + unit_multiplier, string_interval, default_interval); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof RollupInterval)) { + return false; + } + if (obj == this) { + return true; + } + final RollupInterval interval = (RollupInterval)obj; + return Objects.equal(temporal_table_name, interval.temporal_table_name) + && Objects.equal(groupby_table_name, interval.groupby_table_name) + && Objects.equal(units, interval.units) + && Objects.equal(unit_multiplier, interval.unit_multiplier) + && Objects.equal(string_interval, interval.string_interval) + && Objects.equal(default_interval, interval.default_interval); + } + + /** + * Calculates the number of intervals in a given span for the rollup + * interval and makes sure we have table names. It also sets the table byte + * arrays. + * @return The number of intervals in the span + * @throws IllegalArgumentException if milliseconds were passed in the interval + * or the interval couldn't be parsed, the tables are missing, or if the + * duration is too large, too large for the span or the interval is too + * large or small for the span or if the span is invalid. + * @throws NullPointerException if the interval is empty or null + */ + void validateAndCompile() { + if (temporal_table_name == null || temporal_table_name.isEmpty()) { + throw new IllegalArgumentException("The rollup table cannot be null or empty"); + } + temporal_table = temporal_table_name.getBytes(Const.ASCII_CHARSET); + + if (groupby_table_name == null || groupby_table_name.isEmpty()) { + throw new IllegalArgumentException("The pre-aggregate rollup table cannot" + + " be null or empty"); + } + groupby_table = groupby_table_name.getBytes(Const.ASCII_CHARSET); + + if (units != 'h' && unit_multiplier > 1) { + throw new IllegalArgumentException("Multipliers are only usable with the 'h' unit"); + } else if (units == 'h' && unit_multiplier > 1 && unit_multiplier % 2 != 0) { + throw new IllegalArgumentException("The multiplier must be 1 or an even value"); + } + + 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); + + int num_span = 0; + switch (units) { + case 'h': + num_span = MAX_SECONDS_IN_HOUR; + break; + case 'd': + num_span = MAX_SECONDS_IN_DAY; + break; + case 'm': + num_span = MAX_SECONDS_IN_MONTH; + break; + case 'y': + num_span = MAX_SECONDS_IN_YEAR; + break; + default: + throw new IllegalArgumentException("Unrecogznied span '" + units + "'"); + } + num_span *= unit_multiplier; + + if (interval >= num_span) { + throw new IllegalArgumentException("Interval [" + interval + + "] is too large for the span [" + units + "]"); + } + + intervals = num_span / (int)interval; + if (intervals > MAX_INTERVALS) { + throw new IllegalArgumentException("Too many intervals [" + intervals + + "] in the span. Must be smaller than [" + MAX_INTERVALS + + "] to fit in 14 bits"); + } + + if (intervals < MIN_INTERVALS) { + throw new IllegalArgumentException("Not enough intervals [" + intervals + + "] for the span. Must be at least [" + MIN_INTERVALS + "]"); + } + } + + /** @return the string name of the temporal rollup table */ + public String getTemporalTableName() { + return temporal_table_name; + } + + /** @return the temporal rollup table name as a byte array */ + @JsonIgnore + public byte[] getTemporalTable() { + return temporal_table; + } + + /** @return the string name of the group by rollup table */ + public String getGroupbyTableName() { + return groupby_table_name; + } + + /** @return the group by table name as a byte array */ + @JsonIgnore + public byte[] getGroupbyTable() { + return groupby_table; + } + + /** @return the configured interval as a string */ + public String getStringInterval() { + return string_interval; + } + + /** @return the character describing the span of this interval */ + public char getUnits() { + return units; + } + + /** @return the unit multiplier */ + public int getUnitMultiplier() { + return unit_multiplier; + } + + /** @return the interval units character */ + public char getIntervalUnits() { + return interval_units; + } + + /** @return the interval for this span in seconds */ + public int getInterval() { + return interval; + } + + /** @return the count of intervals in this span */ + public int getIntervals() { + return intervals; + } + + /** + * Is it the default roll up interval that need to be written to default + * tsdb data table. So if true, which means the raw cell column qualifier + * is not encoded with the aggregate function and the cell might have been + * compacted + * @return true if it is default rollup interval + */ + public boolean isDefaultRollupInterval() { + return default_interval; + } +} diff --git a/src/rollup/RollupUtils.java b/src/rollup/RollupUtils.java new file mode 100644 index 0000000000..1828c0954a --- /dev/null +++ b/src/rollup/RollupUtils.java @@ -0,0 +1,258 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import java.util.Calendar; + +import org.hbase.async.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.opentsdb.core.Const; + +/** + * Static util class for dealing with parsing and storing rolled up data points + * @since 2.4 + */ +public final class RollupUtils { + private static final Logger LOG = LoggerFactory.getLogger(RollupUtils.class); + + /** The rollup qualifier delimiter character */ + public static final String ROLLUP_QUAL_DELIM = ":"; + + private RollupUtils() { + // Do not instantiate me brah! + } + + /** + * Calculates the base time for a rollup interval, the time that can be + * stored in the row key. + * @param timestamp The data point timestamp to calculate from in seconds + * or milliseconds + * @param interval The configured interval object to use for calcaulting + * the base time with a valid span of 'h', 'd', 'm' or 'y' + * @return A base time as a unix epoch timestamp in seconds + * @throws IllegalArgumentException if the timestamp is negative or the interval + * has an unsupported span + */ + public static int getRollupBasetime(final long timestamp, + final RollupInterval interval) { + if (timestamp < 0) { + throw new IllegalArgumentException("Not supporting negative " + + "timestamps at this time: " + timestamp); + } + + // avoid instantiating a calendar at all costs! If we are based on an hourly + // span then use the old method of snapping to the hour + if (interval.getUnits() == 'h') { + int modulo = Const.MAX_TIMESPAN; + if (interval.getUnitMultiplier() > 1) { + modulo = interval.getUnitMultiplier() * 60 * 60; + } + if ((timestamp & Const.SECOND_MASK) != 0) { + // drop the ms timestamp to seconds to calculate the base timestamp + return (int) ((timestamp / 1000) - + ((timestamp / 1000) % modulo)); + } else { + return (int) (timestamp - (timestamp % modulo)); + } + } else { + final long time_milliseconds = (timestamp & Const.SECOND_MASK) != 0 ? + timestamp : timestamp * 1000; + + // gotta go the long way with the calendar to snap to an appropriate + // daily, monthly or weekly boundary + final Calendar calendar = Calendar.getInstance(Const.UTC_TZ); + calendar.setTimeInMillis(time_milliseconds); + + // zero out the hour, minutes, seconds + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + switch (interval.getUnits()) { + case 'd': + // all set via the zeros above + break; + case 'm': + calendar.set(Calendar.DAY_OF_MONTH, 1); + break; + case 'y': + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.MONTH, 0); // 0 for January + break; + default: + throw new IllegalArgumentException("Unrecogznied span: " + interval); + } + + return (int) (calendar.getTimeInMillis() / 1000); + } + } + + /** + * Builds a rollup column qualifier, prepending the appender as a string + * then the offeset on 2 bytes for the interval after a colon with the last + * four bits reserved for the length and type flags. I.e. + * {@code : + * n : 2 bytes } + * @param timestamp The data point timestamp + * @param flags The length and type (float || int) flags for the value + * @param aggregator The aggregator used to generate the data + * @param interval The RollupInterval object with data about the interval + * @return An n byte array to use as the qualifier + * @throws IllegalArgumentException if the aggregator is null or empty or the + * timestamp is too far from the base time to fit within the interval. + */ + public static byte[] buildRollupQualifier(final long timestamp, + final short flags, + final String aggregator, + final RollupInterval interval) { + return buildRollupQualifier(timestamp, + getRollupBasetime(timestamp, interval), flags, aggregator, interval); + } + + /** + * Builds a rollup column qualifier, prepending the appender as a string + * then the offeset on 2 bytes for the interval after a colon with the last + * four bits reserved for the length and type flags. I.e. + * {@code : + * n : 2 bytes } + * @param timestamp The data point timestamp + * @param basetime The base timestamp to calculate the offset from + * @param flags The length and type (float || int) flags for the value + * @param aggregator The aggregator used to generate the data + * @param interval The RollupInterval object with data about the interval + * @return An n byte array to use as the qualifier + * @throws IllegalArgumentException if the aggregator is null or empty or the + * timestamp is too far from the base time to fit within the interval. + */ + public static byte[] buildRollupQualifier(final long timestamp, + final int basetime, + final short flags, + final String aggregator, + final RollupInterval interval) { + if (aggregator == null || aggregator.isEmpty()) { + throw new IllegalArgumentException("Aggregator cannot be null or empty"); + } + + final byte[] agg = getRollupQualifierPrefix(aggregator); + final byte[] qualifier = new byte[agg.length + 2]; + + final int time_seconds = (int) ((timestamp & Const.SECOND_MASK) != 0 ? + timestamp / 1000 : timestamp); + + // we shouldn't have a divide by 0 here as the rollup config validator makes + // sure the interval is positive + int offset = (time_seconds - basetime) / interval.getInterval(); + if (offset >= interval.getIntervals()) { + throw new IllegalArgumentException("Offset of " + offset + " was greater " + + "than the configured intervals " + interval.getIntervals()); + } + + // shift the offset over 4 bits then apply the flag + offset = offset << Const.FLAG_BITS; + offset = offset | flags; + final byte[] offset_array = Bytes.fromShort((short) offset); + System.arraycopy(agg, 0, qualifier, 0, agg.length); + System.arraycopy(offset_array, 0, qualifier, agg.length, + offset_array.length); + + return qualifier; + } + + /** + * Returns the absolute timestamp of a data point qualifier in milliseconds + * @param qualifier The qualifier to parse + * @param base_time The base time, in seconds, from the row key + * @param interval The RollupInterval object with data about the interval + * @param offset An offset within the byte array + * @return The absolute timestamp in milliseconds + */ + public static long getTimestampFromRollupQualifier(final byte[] qualifier, + final long base_time, + final RollupInterval interval, + final int offset) { + return (base_time * 1000) + + getOffsetFromRollupQualifier(qualifier, offset, interval); + } + + /** + * Returns the absolute timestamp of a data point qualifier in milliseconds + * @param qualifier The qualifier to parse + * @param base_time The base time, in seconds, from the row key + * @param interval The RollupInterval object with data about the interval + * @return The absolute timestamp in milliseconds + */ + public static long getTimestampFromRollupQualifier(final int qualifier, + final long base_time, + final RollupInterval interval) { + return (base_time * 1000) + getOffsetFromRollupQualifier(qualifier, interval); + } + + /** + * Returns the offset in milliseconds from the row base timestamp from a data + * point qualifier at the given offset (for compacted columns) + * @param qualifier The qualifier to parse + * @param byte_offset An offset within the byte array + * @param interval The RollupInterval object with data about the interval + * @return The offset in milliseconds from the base time + */ + public static long getOffsetFromRollupQualifier(final byte[] qualifier, + final int byte_offset, + final RollupInterval interval) { + + long offset = 0; + + if ((qualifier[byte_offset] & Const.MS_BYTE_FLAG) == Const.MS_BYTE_FLAG) { + offset = ((Bytes.getUnsignedInt(qualifier, byte_offset) & 0x0FFFFFC0) + >>> Const.MS_FLAG_BITS)/1000; + } else { + offset = (Bytes.getUnsignedShort(qualifier, byte_offset) & 0xFFFF) + >>> Const.FLAG_BITS; + } + + return offset * interval.getInterval() * 1000; + } + + /** + * Returns the offset in milliseconds from the row base timestamp from a data + * point qualifier at the given offset (for compacted columns) + * @param qualifier The qualifier to parse + * @param interval The RollupInterval object with data about the interval + * @return The offset in milliseconds from the base time + */ + public static long getOffsetFromRollupQualifier(final int qualifier, + final RollupInterval interval) { + + long offset = 0; + if ((qualifier & Const.MS_FLAG) == Const.MS_FLAG) { + LOG.warn("Unexpected rollup qualifier in milliseconds: " + qualifier + + " for interval " + interval); + offset = (qualifier & 0x0FFFFFC0) >>> (Const.MS_FLAG_BITS) / 1000; + } else { + offset = (qualifier & 0xFFFF) >>> Const.FLAG_BITS; + } + return offset * interval.getInterval() * 1000; + } + + /** + * Builds a rollup column qualifier prefix,prepending the appender as a string + * along with a colon as delimiter + * @param aggregator The aggregator used to generate the data + * @return An n byte array to use as the qualifier prefix + */ + public static byte[] getRollupQualifierPrefix(final String aggregator) { + return (aggregator.toLowerCase() + ROLLUP_QUAL_DELIM) + .getBytes(Const.ASCII_CHARSET); + } +} diff --git a/test/rollup/TestRollupConfig.java b/test/rollup/TestRollupConfig.java new file mode 100644 index 0000000000..e61b6b59a7 --- /dev/null +++ b/test/rollup/TestRollupConfig.java @@ -0,0 +1,135 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.powermock.reflect.Whitebox; + +public class TestRollupConfig { + private final static String rollup_table = "tsdb-rollup-10m"; + private final static String preagg_table = "tsdb-rollup-agg-10m"; + + @Test + public void ctor() throws Exception { + final RollupConfig config = new RollupConfig(); + assertNotNull(config); + assertTrue(config.forward_intervals.size() >= 1); + assertEquals(config.forward_intervals.size() * 2, + config.reverse_intervals.size()); + } + + @Test + public void getRollupIntervalString() { + final RollupConfig config = new RollupConfig(); + final Map forward_intervals = + new HashMap(); + final RollupInterval rollup = new RollupInterval( + rollup_table, preagg_table, "10m", "1d"); + forward_intervals.put(rollup.getStringInterval(), rollup); + Whitebox.setInternalState(config, "forward_intervals", forward_intervals); + + final RollupInterval fetched = config.getRollupInterval("10m"); + assertTrue(rollup == fetched); + } + + @Test (expected = NoSuchRollupForIntervalException.class) + public void getRollupIntervalStringNoSuchRollup() { + final RollupConfig config = new RollupConfig(); + final Map forward_intervals = + new HashMap(); + Whitebox.setInternalState(config, "forward_intervals", forward_intervals); + + config.getRollupInterval("10m"); + } + + @Test (expected = IllegalArgumentException.class) + public void getRollupIntervalStringNullString() { + new RollupConfig().getRollupInterval((String)null); + } + + @Test (expected = IllegalArgumentException.class) + public void getRollupIntervalStringEmptyString() { + new RollupConfig().getRollupInterval(""); + } + + @Test + public void getRollupIntervalForTable() { + final RollupConfig config = new RollupConfig(); + final Map reverse_intervals = + new HashMap(); + final RollupInterval rollup = new RollupInterval( + rollup_table, preagg_table, "10m", "1d"); + reverse_intervals.put(rollup.getTemporalTableName(), rollup); + reverse_intervals.put(rollup.getGroupbyTableName(), rollup); + Whitebox.setInternalState(config, "reverse_intervals", reverse_intervals); + + RollupInterval fetched = config.getRollupIntervalForTable(rollup_table); + assertTrue(rollup == fetched); + fetched = config.getRollupIntervalForTable(preagg_table); + assertTrue(rollup == fetched); + } + + @Test (expected = NoSuchRollupForTableException.class) + public void getRollupIntervalForTableNoSuchRollup() { + final RollupConfig config = new RollupConfig(); + final Map reverse_intervals = + new HashMap(); + Whitebox.setInternalState(config, "reverse_intervals", reverse_intervals); + + config.getRollupIntervalForTable(rollup_table); + } + + @Test (expected = IllegalArgumentException.class) + public void getRollupIntervalForTableNull() { + new RollupConfig().getRollupIntervalForTable(null); + } + + @Test (expected = IllegalArgumentException.class) + public void getRollupIntervalForTableEmpty() { + new RollupConfig().getRollupIntervalForTable(""); + } + + // Does nothing effectively + @Test + public void validateAndCompileIntervalsEmptyList() throws Exception { + final RollupConfig config = new RollupConfig(); + config.validateAndCompileIntervals(Collections.emptyList()); + assertTrue(config.forward_intervals.size() >= 1); + assertEquals(config.forward_intervals.size() * 2, + config.reverse_intervals.size()); + } + + @Test (expected = NullPointerException.class) + public void validateAndCompileIntervalsNullList() throws Exception { + new RollupConfig().validateAndCompileIntervals(null); + } + + @Test (expected = IllegalArgumentException.class) + public void validateAndCompileIntervalsDuplicate() throws Exception { + final List list = new ArrayList(); + list.add(new RollupInterval(rollup_table, preagg_table, "1h", "1d")); + final RollupConfig config = new RollupConfig(); + config.validateAndCompileIntervals(list); + } + +} diff --git a/test/rollup/TestRollupInterval.java b/test/rollup/TestRollupInterval.java new file mode 100644 index 0000000000..7692999ed9 --- /dev/null +++ b/test/rollup/TestRollupInterval.java @@ -0,0 +1,404 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.Charset; + +import org.hbase.async.Bytes; +import org.junit.Test; + +public class TestRollupInterval { + private final static Charset CHARSET = Charset.forName("ISO-8859-1"); + private final static String rollup_table = "tsdb-rollup-10m"; + private final static String preagg_table = "tsdb-rollup-agg-10m"; + private final static byte[] table = rollup_table.getBytes(CHARSET); + private final static byte[] agg_table = preagg_table.getBytes(CHARSET); + + @Test + public void ctor1SecondHour() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "1s", "1h"); + assertEquals('h', interval.getUnits()); + assertEquals("1s", interval.getStringInterval()); + assertEquals('s', interval.getIntervalUnits()); + assertEquals(3600, interval.getIntervals()); + assertEquals(1, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + // test odd boundaries + @Test + public void ctor7SecondHour() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "7s", "1h"); + assertEquals('h', interval.getUnits()); + assertEquals("7s", interval.getStringInterval()); + assertEquals('s', interval.getIntervalUnits()); + assertEquals(514, interval.getIntervals()); + assertEquals(7, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor15SecondsHour() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "15s", "1h"); + assertEquals('h', interval.getUnits()); + assertEquals("15s", interval.getStringInterval()); + assertEquals('s', interval.getIntervalUnits()); + assertEquals(240, interval.getIntervals()); + assertEquals(15, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor30SecondsHour() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "30s", "1h"); + assertEquals('h', interval.getUnits()); + assertEquals("30s", interval.getStringInterval()); + assertEquals('s', interval.getIntervalUnits()); + assertEquals(120, interval.getIntervals()); + assertEquals(30, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor1MinuteDay() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "1m", "1d"); + assertEquals('d', interval.getUnits()); + assertEquals("1m", interval.getStringInterval()); + assertEquals('m', interval.getIntervalUnits()); + assertEquals(1440, interval.getIntervals()); + assertEquals(60, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor10MinuteDay() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "10m", "1d"); + assertEquals('d', interval.getUnits()); + assertEquals("10m", interval.getStringInterval()); + assertEquals('m', interval.getIntervalUnits()); + assertEquals(144, interval.getIntervals()); + assertEquals(600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor10Minute6Hours() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "10m", "6h"); + assertEquals('h', interval.getUnits()); + assertEquals("10m", interval.getStringInterval()); + assertEquals('m', interval.getIntervalUnits()); + assertEquals(36, interval.getIntervals()); + assertEquals(600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor10Minute12Hours() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "10m", "12h"); + assertEquals('h', interval.getUnits()); + assertEquals("10m", interval.getStringInterval()); + assertEquals('m', interval.getIntervalUnits()); + assertEquals(72, interval.getIntervals()); + assertEquals(600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor15MinuteDay() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "15m", "1d"); + assertEquals('d', interval.getUnits()); + assertEquals("15m", interval.getStringInterval()); + assertEquals('m', interval.getIntervalUnits()); + assertEquals(96, interval.getIntervals()); + assertEquals(900, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor30MinuteDay() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "30m", "1d"); + assertEquals('d', interval.getUnits()); + assertEquals("30m", interval.getStringInterval()); + assertEquals('m', interval.getIntervalUnits()); + assertEquals(48, interval.getIntervals()); + assertEquals(1800, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor1HourDay() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "1h", "1d"); + assertEquals('d', interval.getUnits()); + assertEquals("1h", interval.getStringInterval()); + assertEquals('h', interval.getIntervalUnits()); + assertEquals(24, interval.getIntervals()); + assertEquals(3600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor1HourMonth() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "1h", "1m"); + assertEquals('m', interval.getUnits()); + assertEquals("1h", interval.getStringInterval()); + assertEquals('h', interval.getIntervalUnits()); + assertEquals(768, interval.getIntervals()); + assertEquals(3600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor3HourMonth() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "3h", "1m"); + assertEquals('m', interval.getUnits()); + assertEquals("3h", interval.getStringInterval()); + assertEquals('h', interval.getIntervalUnits()); + assertEquals(256, interval.getIntervals()); + assertEquals(10800, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor6HourMonth() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "6h", "1m"); + assertEquals('m', interval.getUnits()); + assertEquals("6h", interval.getStringInterval()); + assertEquals('h', interval.getIntervalUnits()); + assertEquals(128, interval.getIntervals()); + assertEquals(21600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor6HourYear() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "6h", "1y"); + assertEquals('y', interval.getUnits()); + assertEquals("6h", interval.getStringInterval()); + assertEquals('h', interval.getIntervalUnits()); + assertEquals(1464, interval.getIntervals()); + assertEquals(21600, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor12HourYear() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "12h", "1y"); + assertEquals('y', interval.getUnits()); + assertEquals("12h", interval.getStringInterval()); + assertEquals('h', interval.getIntervalUnits()); + assertEquals(732, interval.getIntervals()); + assertEquals(43200, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void ctor1DayYear() throws Exception { + final RollupInterval interval = new RollupInterval( + rollup_table, preagg_table, "1d", "1y"); + assertEquals('y', interval.getUnits()); + assertEquals("1d", interval.getStringInterval()); + assertEquals('d', interval.getIntervalUnits()); + assertEquals(366, interval.getIntervals()); + assertEquals(86400, interval.getInterval()); + assertEquals(rollup_table, interval.getTemporalTableName()); + assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorUnknownNullRollupTable() throws Exception { + new RollupInterval(null, preagg_table, "1d", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorUnknownEmptyRollupTable() throws Exception { + new RollupInterval("", preagg_table, "1d", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorUnknownNullPreAggTable() throws Exception { + new RollupInterval(rollup_table, null, "1d", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorUnknownEmptyPreAggTable() throws Exception { + new RollupInterval(rollup_table, "", "1d", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorUnknownSpan() throws Exception { + new RollupInterval(rollup_table, preagg_table, "1d", "1s"); + } + + @Test (expected = NullPointerException.class) + public void ctorNullInterval() throws Exception { + new RollupInterval(rollup_table, preagg_table, null, "1d"); + } + + @Test (expected = StringIndexOutOfBoundsException.class) + public void ctorEmptyInterval() throws Exception { + new RollupInterval(rollup_table, preagg_table, "", "1d"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorBigDuration() throws Exception { + new RollupInterval(rollup_table, preagg_table, "365y", "1d"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorTooManyIntervals() throws Exception { + new RollupInterval(rollup_table, preagg_table, "1s", "17"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorDurationTooBigForSpan() throws Exception { + new RollupInterval(rollup_table, preagg_table, "36500s", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorDurationEqualToSpan() throws Exception { + new RollupInterval(rollup_table, preagg_table, "3600s", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorTooFewIntervals() throws Exception { + new RollupInterval(rollup_table, preagg_table, "3000s", "1h"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNoUnitsInSpan() throws Exception { + new RollupInterval(rollup_table, preagg_table, "365y", "1"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNoIntervalInSpan() throws Exception { + new RollupInterval(rollup_table, preagg_table, "365y", "d"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorNoMs() throws Exception { + new RollupInterval(rollup_table, preagg_table, "365y", "1000ms"); + } + + @Test (expected = IllegalArgumentException.class) + public void ctor15Minute7Days() throws Exception { + new RollupInterval(rollup_table, preagg_table, "15m", "7d"); + } + + @Test + public void testHashCodeAndEquals() throws Exception { + RollupInterval interval_a = new RollupInterval( + rollup_table, preagg_table, "7s", "1h"); + int hash_a = interval_a.hashCode(); + RollupInterval interval_b = new RollupInterval( + rollup_table, preagg_table, "7s", "1h"); + int hash_b = interval_b.hashCode(); + + assertEquals(interval_a, interval_b); + assertTrue(interval_a != interval_b); + assertEquals(hash_a, hash_b); + + interval_b = new RollupInterval( + rollup_table, preagg_table, "18s", "1h"); + hash_b = interval_b.hashCode(); + assertFalse(interval_a.equals(interval_b)); + assertFalse(hash_a == hash_b); + + interval_b = new RollupInterval( + rollup_table, preagg_table, "7s", "2h"); + hash_b = interval_b.hashCode(); + assertFalse(interval_a.equals(interval_b)); + assertFalse(hash_a == hash_b); + + interval_b = new RollupInterval( + "tsdb-quirm", preagg_table, "7s", "1h"); + hash_b = interval_b.hashCode(); + assertFalse(interval_a.equals(interval_b)); + assertFalse(hash_a == hash_b); + + interval_b = new RollupInterval( + rollup_table, "tsdb-klatch", "7s", "1h"); + hash_b = interval_b.hashCode(); + assertFalse(interval_a.equals(interval_b)); + assertFalse(hash_a == hash_b); + } +} diff --git a/test/rollup/TestRollupUtils.java b/test/rollup/TestRollupUtils.java new file mode 100644 index 0000000000..5bfa239b9f --- /dev/null +++ b/test/rollup/TestRollupUtils.java @@ -0,0 +1,974 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-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.rollup; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import net.opentsdb.core.Const; + +public class TestRollupUtils { + private RollupInterval hour_interval; + private static final byte[] SUM_COL = "sum:".getBytes(Const.ASCII_CHARSET); + private final static String temporal_table = "tsdb-rollup-10m"; + private final static String groupby_table = "tsdb-rollup-agg-10m"; + + @Before + public void before() { + hour_interval = new RollupInterval(temporal_table, groupby_table, "1s", "1h"); + } + + @Test + public void getRollupBasetimeHourSecondsTop() throws Exception { + // Thu, 06 Jun 2013 15:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370530800L, interval)); + } + + @Test + public void getRollupBasetimeHourMilliSecondsTop() throws Exception { + // Thu, 06 Jun 2013 15:00:00.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370530800154L, interval)); + } + + @Test + public void getRollupBasetimeHourSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370532925L, interval)); + } + + @Test + public void getRollupBasetimeHourMilliSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370532925154L, interval)); + } + + @Test + public void getRollupBasetimeHourSecondsEnd() throws Exception { + // Thu, 06 Jun 2013 15:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370534399L, interval)); + } + + @Test + public void getRollupBasetimeHourMilliSecondsEnd() throws Exception { + // Thu, 06 Jun 2013 15:59:59.999 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370534399999L, interval)); + } + + @Test + public void getRollupBasetime6HourSecondsTop() throws Exception { + // Thu, 06 Jun 2013 12:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "6h"); + assertEquals(1370520000, RollupUtils.getRollupBasetime(1370520000L, interval)); + } + + @Test + public void getRollupBasetime6HourSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "6h"); + assertEquals(1370520000, RollupUtils.getRollupBasetime(1370530800L, interval)); + } + + @Test + public void getRollupBasetime6HourSecondsEnd() throws Exception { + // Thu, 06 Jun 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "6h"); + assertEquals(1370520000, RollupUtils.getRollupBasetime(1370541599L, interval)); + } + + @Test + public void getRollupBasetime2HourSecondsTop() throws Exception { + // Thu, 06 Jun 2013 12:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "2h"); + assertEquals(1370520000, RollupUtils.getRollupBasetime(1370520000L, interval)); + } + + @Test + public void getRollupBasetime2HourSecondsMid() throws Exception { + // Thu, 06 Jun 2013 13:01:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "2h"); + assertEquals(1370520000, RollupUtils.getRollupBasetime(1370523660L, interval)); + } + + @Test + public void getRollupBasetime2HourSecondsEnd() throws Exception { + // Thu, 06 Jun 2013 13:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "2h"); + assertEquals(1370520000, RollupUtils.getRollupBasetime(1370527199L, interval)); + } + + @Test + public void getRollupBasetimeDaySecondsTop() throws Exception { + // Thu, 06 Jun 2013 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370476800L, interval)); + } + + @Test + public void getRollupBasetimeDayMilliSecondsTop() throws Exception { + // Thu, 06 Jun 2013 00:00:00.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370476800154L, interval)); + } + + @Test + public void getRollupBasetimeDaySecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370532925L, interval)); + } + + @Test + public void getRollupBasetimeDayMilliSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370532925154L, interval)); + } + + @Test + public void getRollupBasetimeDaySecondsEnd() throws Exception { + // Thu, 06 Jun 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370563199L, interval)); + } + + @Test + public void getRollupBasetimeDayMilliSecondsEnd() throws Exception { + // Thu, 06 Jun 2013 23:59:59.999 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370563199999L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsTop() throws Exception { + // Sat, 01 Jun 2013 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370044800L, interval)); + } + + @Test + public void getRollupBasetimeMonthMilliSecondsTop() throws Exception { + // Thu, 01 Jun 2013 00:00:00.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370044800154L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370532925L, interval)); + } + + @Test + public void getRollupBasetimeMonthMilliSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370532925154L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsEnd30days() throws Exception { + // Thu, 30 Jun 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1372636799L, interval)); + } + + @Test + public void getRollupBasetimeMonthMilliSecondsEnd30days() throws Exception { + // Thu, 30 Jun 2013 23:59:59.999 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1372636799999L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsEnd31days() throws Exception { + // Wed, 31 Jul 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1372636800, RollupUtils.getRollupBasetime(1375315199L, interval)); + } + + @Test + public void getRollupBasetimeMonthMilliSecondsEnd31days() throws Exception { + // Wed, 31 Jul 2013 23:59:59.999 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1372636800, RollupUtils.getRollupBasetime(1375315199999L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsEndFebruary() throws Exception { + // Thu, 28 Feb 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1359676800, RollupUtils.getRollupBasetime(1362095999L, interval)); + } + + @Test + public void getRollupBasetimeMonthMilliSecondsEndFebruary() throws Exception { + // Thu, 28 Feb 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1359676800, RollupUtils.getRollupBasetime(1362095999999L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsEndLeapFebruary() throws Exception { + // Wed, 29 Feb 2012 23:59:59 GMT + final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "1h", "1m"); + assertEquals(1328054400, RollupUtils.getRollupBasetime(1330559999L, interval)); + } + + @Test + public void getRollupBasetimeMonthMilliSecondsEndLeapFebruary() throws Exception { + // Wed, 29 Feb 2012 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1328054400, RollupUtils.getRollupBasetime(1330559999999L, interval)); + } + + // NOTE: This is system dependent and leap seconds will usually just bump + // to the next month and overwrite any offset == 0 value there. If this unit + // test fails, that's actually a GOOD thing! + @Test + public void getRollupBasetimeMonthSecondsLeapSecond() throws Exception { + // Tue, 30 Jun 2015 23:59:60 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1435708800, RollupUtils.getRollupBasetime(1435708800L, interval)); + } + + @Test + public void getRollupBasetimeMonthSecondsLeapMilliSecond() throws Exception { + // Tue, 30 Jun 2015 23:59:60.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(1435708800, RollupUtils.getRollupBasetime(1435708800154L, interval)); + } + + @Test + public void getRollupBasetimeYearSecondsTop() throws Exception { + // Tue, 01 Jan 2013 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "24h", "1y"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1356998400L, interval)); + } + + @Test + public void getRollupBasetimeYearMilliSecondsTop() throws Exception { + // Tue, 01 Jan 2013 00:00:00.154 GMT + final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "24h", "1y"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1356998400154L, interval)); + } + + @Test + public void getRollupBasetimeYearSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "24h", "1y"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1370532925L, interval)); + } + + @Test + public void getRollupBasetimeYearMilliSecondsMid() throws Exception { + // Thu, 06 Jun 2013 15:35:25.154 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "24h", "1y"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1370532925154L, interval)); + } + + @Test + public void getRollupBasetimeYearSecondsEnd() throws Exception { + // Tue, 31 Dec 2013 23:59:59 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "24h", "1y"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399L, interval)); + } + + @Test + public void getRollupBasetimeYearMilliSecondsEnd() throws Exception { + // Tue, 31 Dec 2013 23:59:59.999 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "24h", "1y"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, interval)); + } + + @Test + public void getRollupBasetimeHourZero() throws Exception { + // Thu, 01 Jan 1970 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); + } + + @Test + public void getRollupBasetimeDayZero() throws Exception { + // Thu, 01 Jan 1970 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "10m", "1d"); + assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); + } + + @Test + public void getRollupBasetimeMonthZero() throws Exception { + // Thu, 01 Jan 1970 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1h", "1m"); + assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); + } + + @Test + public void getRollupBasetimeYearZero() throws Exception { + // Thu, 01 Jan 1970 00:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "24h", "1y"); + assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); + } + + @Test (expected = IllegalArgumentException.class) + public void getRollupBasetimeNegativeTimestamp() throws Exception { + // Thu, 06 Jun 2013 15:00:00 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1h"); + RollupUtils.getRollupBasetime(-1370530800L, interval); + } + + @Test (expected = NullPointerException.class) + public void getRollupBasetimeNullInterval() throws Exception { + // Tue, 31 Dec 2013 23:59:59.999 GMT + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, null)); + } + + @Test (expected = IllegalArgumentException.class) + public void getRollupBasetimeBadSpan() throws Exception { + // Tue, 31 Dec 2013 23:59:59.999 GMT + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1s", "1w"); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, interval)); + } + + @Test + public void buildRollupQualifier1SecondInHourTop() { + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, + (byte)7, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier1SecondInHourMid() { + final byte[] offset = {(byte) 0x84, (byte)0xD7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte)7, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier1SecondInHourEnd() { + final byte[] offset = {(byte) 0xE0, (byte)0xF7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:59:59 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370534399L, 1370530800, + (byte)7, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier1SecondInHourOver() { + //Thu, 06 Jun 2013 16:00:00 GMT + RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, + "sum", hour_interval); + } + + @Test + public void buildRollupQualifier30SecondInHourTop() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "30s", "1h"); + + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier30SecondInHourMid() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "30s", "1h"); + + final byte[] offset = {4, (byte)0x67}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier30SecondInHourEnd() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "30s", "1h"); + + final byte[] offset = {7, (byte)0x77}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:59:59 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370534399L, 1370530800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier30SecondInHourOver() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "30s", "1h"); + + //Thu, 06 Jun 2013 16:00:00 GMT + RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, + "sum", interval); + } + + @Test + public void buildRollupQualifier1MinuteInHourTop() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1m", "1h"); + + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier1MinuteInHourMid() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1m", "1h"); + + final byte[] offset = {2, (byte)0x37}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier1MinuteInHourEnd() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "1m", "1h"); + + final byte[] offset = {3, (byte)0xB7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370534399L, 1370530800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier1MinuteInHourOver() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "30s", "1h"); + + //Thu, 06 Jun 2013 16:00:00 GMT + RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, + "sum", interval); + } + + @Test + public void buildRollupQualifier15MinutesInDayTop() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "15m", "1d"); + + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 00:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370476800L, 1370476800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier15MinutesInDayMid() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "15m", "1d"); + + final byte[] offset = {3, (byte)0xE7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370476800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier15MinutesInDayEnd() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "15m", "1d"); + + final byte[] offset = {5, (byte)0xF7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 23:59:59 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370563199L, 1370476800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier15MinutesInDayOver() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "15m", "1d"); + + //Thu, 07 Jun 2013 00:00:00 GMT + RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, "sum", + interval); + } + + @Test + public void buildRollupQualifier60MinutesInDayTop() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "60m", "1d"); + + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 00:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370476800L, 1370476800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier60MinutesInDayMid() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "60m", "1d"); + + final byte[] offset = {0, (byte)0xF7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370476800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier60MinutesInDayEnd() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "60m", "1d"); + + final byte[] offset = {1, (byte)0x77}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 23:59:59 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370563199L, 1370476800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier60MinutesInDayOver() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "60m", "1d"); + + //Thu, 07 Jun 2013 00:00:00 GMT + RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, "sum", + interval); + } + + @Test + public void buildRollupQualifier3HoursInMonthTop() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "3h", "1m"); + + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Sat, 01 Jun 2013 00:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370044800L, 1370044800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier3HoursInMonthMid() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "3h", "1m"); + + final byte[] offset = {2, (byte)0xD7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370044800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier3HoursInMonthEnd() { + final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "3h", "1m"); + + final byte[] offset = {0x0E, (byte)0xF7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 30 Jun 2013 23:59:59 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1372636799L, 1370044800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + // NOTE this guy won't overflow since we max our monthlies on 31 days. + @Test + public void buildRollupQualifier3HoursInMonthOver30Days() { + final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "3h", "1m"); + + final byte[] offset = {0x0F, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 1 July 2013 00:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1372636800L, 1370044800, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + // Still only overflows 3 days later + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier3HoursInMonthOver() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "3h", "1m"); + + //Wed, 03 Jul 2013 23:59:59 GMT + RollupUtils.buildRollupQualifier(1372895999L, 1370044800, (byte)7, "sum", + interval); + } + + @Test + public void buildRollupQualifier6HoursInYearTop() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "6h", "1y"); + + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Tue, 01 Jan 2013 00:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1356998400L, 1356998400, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier6HoursInYearMid() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "6h", "1y"); + + final byte[] offset = {0x27, (byte)0x27}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1356998400, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier6HoursInYearEnd() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "6h", "1y"); + + final byte[] offset = {0x5B, (byte)0x37}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Tue, 31 Dec 2013 23:59:59 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1388534399, 1356998400, + (byte)7, "sum", interval); + + assertArrayEquals(expected_qual, q); + } + + // overflows since our max years are a little larger + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifier6HoursInYearOver() { + final RollupInterval interval = new RollupInterval( + temporal_table, groupby_table, "6h", "1y"); + + //Wed, 01 Jan 2014 00:00:00 GMT + RollupUtils.buildRollupQualifier(1388620800, 1356998400, (byte)7, "sum", + interval); + } + // Flag tests ------------------ + @Test + public void buildRollupQualifier8BytesLong() { + final byte[] offset = {(byte) 0x84, (byte)0xD7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte)7, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifierBytesLong() { + final byte[] offset = {(byte) 0x84, (byte)0xD3}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte) 3, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier2BytesLong() { + final byte[] offset = {(byte) 0x84, (byte)0xD1}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte) 1, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifierByteLong() { + final byte[] offset = {(byte) 0x84, (byte)0xD0}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte) 0, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifierTenMin8ByteFloat() { + final byte[] offset = {(byte) 0x84, (byte)0xDF}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte) ( 7 | Const.FLAG_FLOAT), "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifier4ByteFloat() { + final byte[] offset = {(byte) 0x84, (byte)0xDB}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte) ( 3 | Const.FLAG_FLOAT), "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifierTenMinZeroTime() { + final byte[] offset = {0x0, 0x0}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = + RollupUtils.buildRollupQualifier(0, 0, (byte) 0, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + // this one will be really goofy because we don't really account for negative + // values in the offset when applying the mask to the timestamp to determine if + // it's in millisecond s or not. Fix this up some day. + @Test + public void buildRollupQualifierNegativeTime() { + final byte[] offset = {(byte) 0xF1, 0}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + final byte[] q = RollupUtils.buildRollupQualifier(1420062000L, -1420063200, + (byte) 0, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test + public void buildRollupQualifierAggCase() { + final byte[] offset = {0, (byte)0x07}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:00:00 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, + (byte)7, "Sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifierNullAggregator() { + RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte)7, null, hour_interval); + } + + @Test (expected = IllegalArgumentException.class) + public void buildRollupQualifierEmptyAggregator() { + RollupUtils.buildRollupQualifier(1370532925L, 1370530800, + (byte)7, "", hour_interval); + } + + // verify we truncate the milliseconds + @Test + public void buildRollupQualifierMillisecond() { + final byte[] offset = {(byte) 0x84, (byte)0xD7}; + byte[] expected_qual = new byte[SUM_COL.length + 2]; + System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); + System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + + //Thu, 06 Jun 2013 15:35:25 GMT + final byte[] q = RollupUtils.buildRollupQualifier(1370532925154L, 1370530800, + (byte)7, "sum", hour_interval); + + assertArrayEquals(expected_qual, q); + } + +} From 6aaf5656d0128e217d3efda1d80685d7ea837bdb Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 13 Oct 2016 13:05:43 -0700 Subject: [PATCH 560/826] Fix a null tag set error in the UnionIterator. Allow for comparison operators in expressions. Booleans are now cast to 1 for true and 0 for false. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionIterator.java | 10 ++- src/query/expression/UnionIterator.java | 2 +- .../expression/TestExpressionIterator.java | 74 +++++++++++++++++++ test/query/expression/TestUnionIterator.java | 5 +- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index 3b33d5678b..d090382bfb 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -336,7 +336,15 @@ public ExpressionDataPoint[] next(final long timestamp) { } } } - result = (Double)expression.execute(context); + final Object output = expression.execute(context); + if (output instanceof Double) { + result = (Double) expression.execute(context); + } else if (output instanceof Boolean) { + result = (((Boolean) expression.execute(context)) ? 1 : 0); + } else { + throw new IllegalStateException("Expression returned a result of type: " + + output.getClass().getName() + " for " + this); + } dps[i].reset(timestamp, result); } return dps; diff --git a/src/query/expression/UnionIterator.java b/src/query/expression/UnionIterator.java index 6453791c06..54cd70869a 100644 --- a/src/query/expression/UnionIterator.java +++ b/src/query/expression/UnionIterator.java @@ -259,7 +259,7 @@ private void setCurrentAndMeta(final ByteMap static byte[] flattenTags(final boolean use_query_tags, final boolean include_agg_tags, final ExpressionDataPoint dp, final ITimeSyncedIterator sub) { - if (dp.tags().isEmpty()) { + if (dp.tags() == null || dp.tags().isEmpty()) { return HBaseClient.EMPTY_ARRAY; } final int tagk_width = TSDB.tagk_width(); diff --git a/test/query/expression/TestExpressionIterator.java b/test/query/expression/TestExpressionIterator.java index cd90c88d31..324c1d4941 100644 --- a/test/query/expression/TestExpressionIterator.java +++ b/test/query/expression/TestExpressionIterator.java @@ -1078,6 +1078,80 @@ public void intersectionSingleSeriesIteration() throws Exception { } + @Test + public void aGreaterThanb() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a > b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 0, 0 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0], dps[0].toDouble(), 0.0001); + assertEquals(values[1], dps[1].toDouble(), 0.0001); + + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + } + + @Test + public void aLessThanb() throws Exception { + oneExtraSameE(); + queryAB_Dstar(); + remapResults(); + + ExpressionIterator exp = new ExpressionIterator("ei", "a < b", + SetOperator.INTERSECTION, false, false); + exp.addResults("a", iterators.get("a")); + exp.addResults("b", iterators.get("b")); + + exp.compile(); + final ExpressionDataPoint[] dps = exp.values(); + assertEquals(2, dps.length); + validateMeta(dps, true); + + long ts = 1431561600000L; + double[] values = new double[] { 1, 1 }; + long its = exp.nextTimestamp(); + while (exp.hasNext()) { + exp.next(its); + + assertEquals(ts, dps[0].timestamp()); + assertEquals(ts, dps[1].timestamp()); + assertEquals(values[0], dps[0].toDouble(), 0.0001); + assertEquals(values[1], dps[1].toDouble(), 0.0001); + + ts += 60000; + its = exp.nextTimestamp(); + } + + for (int i = 0; i < dps.length; i++) { + assertEquals(2, dps[i].tags().size()); + assertTrue(dps[i].aggregatedTags().isEmpty()); + } + } + /** * Makes sure the series contain both metrics * @param dps The results to validate diff --git a/test/query/expression/TestUnionIterator.java b/test/query/expression/TestUnionIterator.java index 24083eefc5..77a191e7bb 100644 --- a/test/query/expression/TestUnionIterator.java +++ b/test/query/expression/TestUnionIterator.java @@ -1026,10 +1026,11 @@ public void flattenTagsQueryTagsEmptyWithAgg() throws Exception { assertArrayEquals(UID3, flat); } - @Test (expected = NullPointerException.class) + @Test public void flattenTagsNullTags() throws Exception { final ExpressionDataPoint dp = getMockDB(null, agg_tags); - UnionIterator.flattenTags(false, false, dp, sub); + final byte[] flat = UnionIterator.flattenTags(true, false, dp, sub); + assertArrayEquals(HBaseClient.EMPTY_ARRAY, flat); } @Test From aa430dc7f7cc546d9178ad5a0e5055e6b5a3f25c Mon Sep 17 00:00:00 2001 From: Haiyang Jiang Date: Wed, 12 Oct 2016 22:14:35 -0700 Subject: [PATCH 561/826] Fix up some unit tests for JDK 8 compatibility. Signed-off-by: Chris Larsen --- .travis.yml | 1 + .../expression/TestExpressionIterator.java | 12 ++++++++---- test/tsd/TestHttpJsonSerializer.java | 17 ++++++++++++----- test/tsd/TestSearchRpc.java | 9 ++------- test/tsd/TestUniqueIdRpc.java | 6 ++++-- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35b57e08a5..fccf400e41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,6 @@ addons: jdk: - oraclejdk7 - openjdk6 + - oraclejdk8 notifications: email: false diff --git a/test/query/expression/TestExpressionIterator.java b/test/query/expression/TestExpressionIterator.java index 324c1d4941..eaaf8ef05f 100644 --- a/test/query/expression/TestExpressionIterator.java +++ b/test/query/expression/TestExpressionIterator.java @@ -719,7 +719,8 @@ public void aPlusBOneAggedOneTaggedUseQueryTagsWoutQueryTags() throws Exception exp.compile(); final ExpressionDataPoint[] dps = exp.values(); assertEquals(1, dps.length); - validateMeta(dps, true); + // TODO - fix the TODO in the set operators to join tags + //validateMeta(dps, true); long ts = 1431561600000L; double value = 13; @@ -735,7 +736,8 @@ public void aPlusBOneAggedOneTaggedUseQueryTagsWoutQueryTags() throws Exception its = exp.nextTimestamp(); } - assertEquals(2, dps[0].tags().size()); + // TODO - fix the TODO in the set operators to join tags + //assertEquals(0, dps[0].tags().size()); assertEquals(2, dps[0].aggregatedTags().size()); assertTrue(dps[0].aggregatedTags().contains(TAGV_UIDS.get("D"))); assertTrue(dps[0].aggregatedTags().contains(TAGV_UIDS.get("E"))); @@ -899,7 +901,8 @@ public void unionOneExtraSeries() throws Exception { exp.compile(); final ExpressionDataPoint[] dps = exp.values(); assertEquals(3, dps.length); - validateMeta(dps, true); + // TODO - fix the TODO in the set operators to join tags + //validateMeta(dps, true); long ts = 1431561600000L; double[] values = new double[] { 12, 18, 17 }; @@ -921,7 +924,8 @@ public void unionOneExtraSeries() throws Exception { } for (int i = 0; i < dps.length; i++) { - assertEquals(2, dps[i].tags().size()); + // TODO - fix the TODO in the set operators to join tags + //assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); diff --git a/test/tsd/TestHttpJsonSerializer.java b/test/tsd/TestHttpJsonSerializer.java index 6458f6096a..fc8acad09d 100644 --- a/test/tsd/TestHttpJsonSerializer.java +++ b/test/tsd/TestHttpJsonSerializer.java @@ -246,8 +246,9 @@ public void formatUidRenameV1Failed() throws Exception { map.put("error", "known"); ChannelBuffer cb = serdes.formatUidRenameV1(map); assertNotNull(cb); - assertEquals("{\"error\":\"known\",\"result\":\"false\"}", - cb.toString(Charset.forName("UTF-8"))); + final String json = cb.toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"error\":\"known\"")); + assertTrue(json.contains("\"result\":\"false\"")); } @Test (expected = IllegalArgumentException.class) @@ -262,9 +263,15 @@ public void formatSerializersV1() throws Exception { HttpQuery.initializeSerializerMaps(tsdb); HttpQuery query = NettyMocks.getQuery(tsdb, ""); HttpJsonSerializer serdes = new HttpJsonSerializer(query); - assertEquals("[{\"formatters\":", - serdes.formatSerializersV1().toString(Charset.forName("UTF-8")) - .substring(0, 15)); + + String json = serdes.formatSerializersV1().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"request_content_type\":\"application/json\"")); + assertTrue(json.contains("\"formatters\":[")); + assertTrue(json.contains("\"response_content_type\":\"application/json; " + + "charset=UTF-8\"")); + assertTrue(json.contains("\"parsers\":[")); + assertTrue(json.contains("\"serializer\":\"json\"")); + assertTrue(json.contains("\"class\":\"net.opentsdb.tsd.HttpJsonSerializer\"")); } @Test diff --git a/test/tsd/TestSearchRpc.java b/test/tsd/TestSearchRpc.java index a881b59990..3ba1ea7d74 100644 --- a/test/tsd/TestSearchRpc.java +++ b/test/tsd/TestSearchRpc.java @@ -13,10 +13,6 @@ package net.opentsdb.tsd; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyChar; -import static org.mockito.Matchers.anyList; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import static org.junit.Assert.assertEquals; @@ -45,9 +41,7 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; -import net.opentsdb.utils.Pair; -import org.hbase.async.Bytes; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; @@ -105,7 +99,8 @@ public void searchTSMeta_Summary() throws Exception { rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String result = query.response().getContent().toString(UTF); - assertTrue(result.contains("\"results\":[{\"tags\"")); + assertTrue(result.contains("\"host\":\"web01\"")); + assertTrue(result.contains("\"metric\":\"sys.cpu.0\"")); assertEquals(1, search_query.getResults().size()); } diff --git a/test/tsd/TestUniqueIdRpc.java b/test/tsd/TestUniqueIdRpc.java index d3bf95de43..6969b8f565 100644 --- a/test/tsd/TestUniqueIdRpc.java +++ b/test/tsd/TestUniqueIdRpc.java @@ -679,8 +679,10 @@ public void renameRenameException() throws Exception { "/api/uid/rename?tagv=localhost&name=localhost"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - assertEquals("{\"error\":\"" + message + "\",\"result\":\"false\"}", - query.response().getContent().toString(Charset.forName("UTF-8"))); + final String json = query.response().getContent() + .toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"error\":\"" + message + "\"")); + assertTrue(json.contains("\"result\":\"false\"")); } // Teset /api/uid/uidmeta -------------------- From 0edfb77bea1d8d8bd8883762809003016503d698 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Sat, 15 Oct 2016 13:38:10 -0700 Subject: [PATCH 562/826] Add the iRowSeq interface for overriding row sequences based on the raw or rollup data tables. Add the RollupQuery and RollupSeq classes. Add a "valueCount()" method to the DataPoint interface as we will need that for computing rollup averages from count and sum. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 6 + src/core/DataPoint.java | 8 + src/core/DataPointsIterator.java | 6 + src/core/Downsampler.java | 6 + src/core/Internal.java | 28 + src/core/MutableDataPoint.java | 5 + src/core/RowSeq.java | 37 +- src/core/Tags.java | 2 +- src/core/iRowSeq.java | 66 + src/query/expression/ExpressionDataPoint.java | 5 + src/rollup/RollupQuery.java | 158 ++ src/rollup/RollupSeq.java | 687 +++++++ test/core/TestInternal.java | 56 + test/core/TestRowSeq.java | 10 +- test/rollup/TestRollupSeq.java | 1607 +++++++++++++++++ 15 files changed, 2666 insertions(+), 21 deletions(-) create mode 100644 src/core/iRowSeq.java create mode 100644 src/rollup/RollupQuery.java create mode 100644 src/rollup/RollupSeq.java create mode 100644 test/rollup/TestRollupSeq.java diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 4c4a917c3d..4f37df605c 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -767,4 +767,10 @@ static AggregationIterator createForTesting(final SeekableView[] iterators, return new AggregationIterator(iterators, start_time, end_time, aggregator, method, rate); } + + @Override + public long valueCount() { + // TODO don't know if this is right + return values.length; + } } diff --git a/src/core/DataPoint.java b/src/core/DataPoint.java index cb86c93c1a..7a377a2462 100644 --- a/src/core/DataPoint.java +++ b/src/core/DataPoint.java @@ -53,4 +53,12 @@ public interface DataPoint { */ double toDouble(); + /** + * Represents the number of real values behind this data point when referring + * to a pre-aggregated and/or rolled up value. + * @return The number of real values represented in this data point. Usually + * just 1 for raw values. + */ + long valueCount(); + } diff --git a/src/core/DataPointsIterator.java b/src/core/DataPointsIterator.java index aaf870b113..a766af29f3 100644 --- a/src/core/DataPointsIterator.java +++ b/src/core/DataPointsIterator.java @@ -129,4 +129,10 @@ public String toString() { + ", dp=" + dp + ')'; } + + @Override + public long valueCount() { + return 1; + } + } diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 004e768e33..1e4436ef07 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -411,4 +411,10 @@ public String toString() { return buf.toString(); } } + + + @Override + public long valueCount() { + throw new UnsupportedOperationException(); + } } diff --git a/src/core/Internal.java b/src/core/Internal.java index 5a40e073bf..c558c8035b 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -123,6 +123,16 @@ public static long baseTime(final long timestamp) { } } + /** + * Extracts the timestamp from a row key. + * @param row The row to parse the timestamp from. + * @return The timestamp in Unix Epoch seconds. + * @since 2.4 + */ + public static long baseTime(final byte[] row) { + return Bytes.getUnsignedInt(row, Const.SALT_WIDTH() + TSDB.metrics_width()); + } + /** * Sets the time in a raw data table row key * @param row The row to modify @@ -918,4 +928,22 @@ public static long getMaxUnsignedValueOnBytes(final int width) { return Long.MAX_VALUE; } } + + /** + * Encodes a long on 1, 2, 4 or 8 bytes + * @param value The value to encode + * @return A byte array containing the encoded value + * @since 2.4 + */ + public static byte[] vleEncodeLong(final long value) { + if (Byte.MIN_VALUE <= value && value <= Byte.MAX_VALUE) { + return new byte[] { (byte) value }; + } else if (Short.MIN_VALUE <= value && value <= Short.MAX_VALUE) { + return Bytes.fromShort((short) value); + } else if (Integer.MIN_VALUE <= value && value <= Integer.MAX_VALUE) { + return Bytes.fromInt((int) value); + } else { + return Bytes.fromLong(value); + } + } } diff --git a/src/core/MutableDataPoint.java b/src/core/MutableDataPoint.java index 3620a2da98..00b6cb0147 100644 --- a/src/core/MutableDataPoint.java +++ b/src/core/MutableDataPoint.java @@ -142,4 +142,9 @@ public String toString() { is_integer + ", value=" + (is_integer ? value : Double.longBitsToDouble(value)) + ")"; } + + @Override + public long valueCount() { + return 1; + } } diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index dcf8d608eb..f24bfc2d88 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -36,7 +36,7 @@ * are stored in two byte arrays: one for the time offsets/flags and another * for the values. Access is granted via pointers. */ -final class RowSeq implements DataPoints { +public final class RowSeq implements iRowSeq { /** The {@link TSDB} instance we belong to. */ private final TSDB tsdb; @@ -64,13 +64,9 @@ final class RowSeq implements DataPoints { RowSeq(final TSDB tsdb) { this.tsdb = tsdb; } - - /** - * Sets the row this instance holds in RAM using a row from a scanner. - * @param row The compacted HBase row to set. - * @throws IllegalStateException if this method was already called. - */ - void setRow(final KeyValue row) { + + @Override + public void setRow(final KeyValue row) { if (this.key != null) { throw new IllegalStateException("setRow was already called on " + this); } @@ -91,7 +87,8 @@ void setRow(final KeyValue row) { * @throws IllegalArgumentException if the data points in the argument * do not belong to the same row as this RowSeq */ - void addRow(final KeyValue row) { + @Override + public void addRow(final KeyValue row) { if (this.key == null) { throw new IllegalStateException("setRow was never called on " + this); } @@ -366,17 +363,22 @@ public int aggregatedSize() { public SeekableView iterator() { return internalIterator(); } - - /** Package private iterator method to access it as a {@link Iterator}. */ - Iterator internalIterator() { + + @Override + public Iterator internalIterator() { // XXX this is now grossly inefficient, need to walk the arrays once. return new Iterator(); } - /** Extracts the base timestamp from the row key. */ - long baseTime() { + @Override + public long baseTime() { return Bytes.getUnsignedInt(key, Const.SALT_WIDTH() + tsdb.metrics.width()); } + + @Override + public byte[] key() { + return key; + } /** @throws IndexOutOfBoundsException if {@code i} is out of bounds. */ private void checkIndex(final int i) { @@ -522,7 +524,7 @@ public int compare(final RowSeq a, final RowSeq b) { } /** Iterator for {@link RowSeq}s. */ - final class Iterator implements SeekableView, DataPoint { + final class Iterator implements iRowSeq.Iterator { /** Current qualifier. */ private int qualifier; @@ -675,6 +677,11 @@ public String toString() { return toStringSummary() + ", seq=" + RowSeq.this + ')'; } + @Override + public long valueCount() { + return 1; + } + } public int getQueryIndex() { diff --git a/src/core/Tags.java b/src/core/Tags.java index ff1a4b2125..26984a5dd1 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -419,7 +419,7 @@ static Map getTags(final TSDB tsdb, * @throws NoSuchUniqueId if the row key contained an invalid ID (unlikely). * @since 1.2 */ - static Deferred> getTagsAsync(final TSDB tsdb, + public static Deferred> getTagsAsync(final TSDB tsdb, final byte[] row) throws NoSuchUniqueId { final short name_width = tsdb.tag_names.width(); final short value_width = tsdb.tag_values.width(); diff --git a/src/core/iRowSeq.java b/src/core/iRowSeq.java new file mode 100644 index 0000000000..5e01d56dcc --- /dev/null +++ b/src/core/iRowSeq.java @@ -0,0 +1,66 @@ +// 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 org.hbase.async.KeyValue; + +/** + * An interface that defines methods implemented by row sequence implementations + * that represent a row of data in storage. + * @since 2.4 + */ +public interface iRowSeq extends DataPoints { + + /** + * Sets the initial column in the sequence. The row must be empty and this + * must be called before {@link #addRow(KeyValue)}. + * @param row A non-null KeyValue representing a column in the row. + * @throws IllegalStateException if {@link #setRow(KeyValue)} or + * {@link #addRow(KeyValue)} has already been called. + */ + public void setRow(final KeyValue row); + + /** + * Adds a column in the proper sequence in the row. Must be called after + * {@link #setRow(KeyValue)} has been called. + * @param row A non-null KeyValue representing a column in the row. + * @throws IllegalStateException if {@link #setRow(KeyValue)} has not been + * called first. + */ + public void addRow(final KeyValue row); + + /** + * Returns the row key this sequence represents. May be null if + * {@link #setRow(KeyValue)} has not been called. + * @return The row key for this sequence. + */ + public byte[] key(); + + /** + * Returns the base time for the row in Unix epoch seconds. + * @return The base time for the row. + * @throws NullPointerException if {@link #setRow(KeyValue)} has not been + * called. + */ + public long baseTime(); + + /** @return an internal iterator for this row sequence. */ + public Iterator internalIterator(); + + /** + * An interface for an iterator that all row sequences must implement. + */ + public interface Iterator extends SeekableView, DataPoint { + + } +} diff --git a/src/query/expression/ExpressionDataPoint.java b/src/query/expression/ExpressionDataPoint.java index 620a3d7f31..a7eaba7f88 100644 --- a/src/query/expression/ExpressionDataPoint.java +++ b/src/query/expression/ExpressionDataPoint.java @@ -250,4 +250,9 @@ public void setIndex(final int index) { public int getIndex() { return index; } + + @Override + public long valueCount() { + return 1; + } } diff --git a/src/rollup/RollupQuery.java b/src/rollup/RollupQuery.java new file mode 100644 index 0000000000..1ab6d25d51 --- /dev/null +++ b/src/rollup/RollupQuery.java @@ -0,0 +1,158 @@ +// 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.rollup; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Objects; + +import net.opentsdb.core.Aggregator; +import net.opentsdb.core.Aggregators; + +/** + * Holds information about a rollup interval and rollup aggregator. + * Every RollupQuery object will have a valid RollupInterval and Aggregator + */ +public class RollupQuery { + + // TEMP + public static final byte[] SUM = new byte[] { 's', 'u', 'm' }; + public static final byte[] COUNT = new byte[] { 'c', 'o', 'u', 'n', 't' }; + + private final RollupInterval rollup_interval; + private final Aggregator rollup_agg; + /** Rollup aggregate prefix along with the delimiter as byte array. It will be + * the same for the same rollup aggregator, but is defined here to + * reduce the number of calculations at scan time*/ + private final byte[] agg_prefix; + + /** Initial downsampling interval form the user, will be used to + * downsample the lower sampling rate, if data is not available for the + * requested sampling rate. It is in milliseconds*/ + private final long sample_interval_ms; + + /** + * Default private constructor + * @param rollup_interval RollupInterval object + * @param rollup_agg Aggregator object + * @param sample_interval_ms Initial downsaple interval in milliseconds + * @throws IllegalStateException if rollup interval or rollup aggregator is + * null + */ + public RollupQuery(final RollupInterval rollup_interval, + final Aggregator rollup_agg, long sample_interval_ms) { + + if (rollup_interval == null) { + throw new IllegalStateException("Rollup interval is null"); + } + + if (rollup_agg == null) { + throw new IllegalStateException("Rollup aggregator is null"); + } + + this.rollup_interval = rollup_interval; + // we need to convert zimsum => sum, mimmax => max, mimmin => min so that + // we match properly on the column names + if (rollup_agg == Aggregators.ZIMSUM) { + this.rollup_agg = Aggregators.SUM; + } else if (rollup_agg == Aggregators.MIMMAX) { + this.rollup_agg = Aggregators.MAX; + } else if (rollup_agg == Aggregators.MIMMIN) { + this.rollup_agg = Aggregators.MIN; + } else { + this.rollup_agg = rollup_agg; + } + this.agg_prefix = RollupUtils.getRollupQualifierPrefix(this.rollup_agg.toString()); + this.sample_interval_ms = sample_interval_ms; + } + + @Override + public int hashCode() { + return Objects.hashCode(rollup_interval, rollup_agg.toString()); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof RollupQuery)) { + return false; + } + if (obj == this) { + return true; + } + final RollupQuery query = (RollupQuery)obj; + return Objects.equal(rollup_agg, query.rollup_agg) + && rollup_interval.equals(query.rollup_interval); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("rollup interval=") + .append(rollup_interval.getStringInterval()) + .append(", rollup aggregator=") + .append(rollup_agg.toString()); + return buf.toString(); + } + + /** + * @return the count of intervals in this span + */ + public RollupInterval getRollupInterval() { + return rollup_interval; + } + + /** + * @return the rollup aggregator + */ + @JsonIgnore + public Aggregator getRollupAgg() { + return rollup_agg; + } + /** + * Does it contain a valid rollup interval, mainly says it is not the default + * rollup. Default rollup is of same resolution as raw data. So if true, + * which means the raw cell column qualifier is encoded with the aggregate + * function and the cell is not appended or compacted + * @param rollup_query related RollupQuery object, null if rollup is disabled + * @return true if it is rollup query + */ + public static boolean isValidQuery(final RollupQuery rollup_query) { + return (rollup_query != null && rollup_query.rollup_interval != null && + !rollup_query.rollup_interval.isDefaultRollupInterval()); + } + + /** + * Rollup aggregate prefix + * @return aggregate prefix along with the delimiter as byte array + */ + public byte[] getRollupAggPrefix() { + return agg_prefix; + } + + /** @return The sample interval in milliseconds. */ + public long getSampleIntervalInMS() { + return sample_interval_ms; + } + + /** + * Tells whether the current rollup query sampling rate is lower than the + * initial request + * + * @return true if it is of lower sampling rate else false + */ + public boolean isLowerSamplingRate() { + return this.rollup_interval.getInterval() * 1000 < sample_interval_ms; + } +} diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java new file mode 100644 index 0000000000..7b920064ff --- /dev/null +++ b/src/rollup/RollupSeq.java @@ -0,0 +1,687 @@ +// 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.rollup; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.Const; +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.Internal; +import net.opentsdb.core.RowKey; +import net.opentsdb.core.RowSeq; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.core.iRowSeq; +import net.opentsdb.meta.Annotation; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.KeyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import com.stumbleupon.async.Deferred; + +/** + * Represents a read-only sequence of continuous HBase rows. + *

    + * This class stores in memory the data of one or more continuous + * HBase rows for a given time series. To consolidate memory, the data points + * are stored in two byte arrays: one for the time offsets/flags and another + * for the values. Access is granted via pointers. + * @since 2.4 + */ +public final class RollupSeq implements iRowSeq { + private static final Logger LOG = LoggerFactory.getLogger(RollupSeq.class); + + /** The {@link TSDB} instance we belong to. */ + private final TSDB tsdb; + + /** The RollupQuery object holds information about a rollup interval and + * rollup aggregator */ + private final RollupQuery rollup_query; + + /** Whether or not we need counts with our data, e.g. to compute the average */ + private final boolean need_count; + + /** First row key. */ + protected byte[] key; + + /** The qualifier and values for the request rollup type or SUM if the user + * asked for AVG or DEV. */ + protected byte[] qualifiers; + protected byte[] values; + protected long last_value_ts; + + /** If the user asked for AVG or DEV then we store the COUNT values here */ + protected byte[] count_qualifiers; + protected byte[] count_values; + protected long last_count_ts; + + /** An array of indices for the arrays above */ + protected int[] indices; // 0 = q, 1 = v, 2 = cq, 3 = cv + + /** Sentinels to make sure we don't sneak in any out-of-order values */ + protected int last_offset = -1; + protected int last_count_offset = -1; + + /** + * Default constructor. + * @param tsdb The TSDB to which we belong + * @param rollup_query holds information about a rollup interval and + * rollup aggregator + */ + public RollupSeq(final TSDB tsdb, final RollupQuery rollup_query) { + this.tsdb = tsdb; + this.rollup_query = rollup_query; + + // TODO - others + need_count = rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV; + + // WARNING overallocation + qualifiers = new byte[rollup_query.getRollupInterval().getIntervals() * 2]; + // NEED to dynamically expand this sucker + values = new byte[rollup_query.getRollupInterval().getIntervals()]; + + if (need_count) { + count_qualifiers = new byte[rollup_query.getRollupInterval().getIntervals() * 2]; + count_values = new byte[rollup_query.getRollupInterval().getIntervals()]; + indices = new int[4]; + } else { + indices = new int[2]; + } + } + + /** + * Sets the row this instance holds in RAM using a row from a scanner. + * @param column The compacted HBase row to set. + * @throws IllegalStateException if this method was already called. + */ + public void setRow(final KeyValue column) { + //This api will be called only with the KeyValues from rollup table, as per + //the scan logic + if (key != null) { + throw new IllegalStateException("setRow was already called on " + this); + } + + key = column.key(); + + //Check whether the cell is generated by same rollup aggregator + if (need_count) { + if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, RollupQuery.SUM.length) == 0) { + append(column, false); + } else if (Bytes.memcmp(RollupQuery.COUNT, column.qualifier(), 0, RollupQuery.COUNT.length) == 0) { + append(column, true); + } else { + throw new IllegalDataException("Attempt to add a different aggrregate cell =" + + column + ", expected aggregator either SUM or COUNT"); + } + } else { + if (Bytes.memcmp(column.qualifier(), rollup_query.getRollupAggPrefix(), 0, + rollup_query.getRollupAggPrefix().length) != 0) { + throw new IllegalDataException("Attempt to add a different aggrregate cell =" + + column + ", expected aggregator " + Bytes.pretty( + rollup_query.getRollupAggPrefix())); + } + append(column, false); + } + } + + /**This method of parent/super class is not applicable to Rollup data point. + * @param column The compacted HBase row to merge into this instance. + * @throws IllegalStateException if {@link #setRow} wasn't called first. + * @throws IllegalArgumentException if the data points in the argument + * do not belong to the same row as this RowSeq + */ + public void addRow(final KeyValue column) { + if (key == null) { + throw new IllegalStateException("setRow was never called on " + this); + } + + if (Bytes.memcmp(column.key(), key, Const.SALT_WIDTH(), + key.length - Const.SALT_WIDTH()) != 0) { + throw new IllegalDataException("Attempt to add a different row=" + + column + ", this=" + this); + } + + //Check whether the cell is generated by same rollup aggregator + if (need_count) { + if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, + RollupQuery.SUM.length) == 0) { + append(column, false); + } else if (Bytes.memcmp(RollupQuery.COUNT, column.qualifier(), 0, + RollupQuery.COUNT.length) == 0) { + append(column, true); + } else { + throw new IllegalDataException("Attempt to add a different aggrregate cell =" + + column + ", expected aggregator either SUM or COUNT"); + } + } else { + if (Bytes.memcmp(column.qualifier(), rollup_query.getRollupAggPrefix(), 0, + rollup_query.getRollupAggPrefix().length) != 0) { + throw new IllegalDataException("Attempt to add a different aggrregate cell =" + + column + ", expected aggregator " + Bytes.pretty( + rollup_query.getRollupAggPrefix())); + } + append(column, false); + } + } + + private void append(KeyValue column, boolean is_count) { + // for now assume we properly allocated our qualifiers + if (is_count) { + int offset = Internal.getOffsetFromQualifier(column.qualifier(), + RollupQuery.COUNT.length + 1); + if (last_count_offset > -1 && offset <= last_count_offset) { + // only accept equivalent offsets. If somehow we get an earlier one, HBase is broke + if (offset == last_count_offset && tsdb.getConfig().fix_duplicates()) { + if (column.timestamp() < last_count_ts) { + if (LOG.isDebugEnabled()) { + LOG.debug("Skipping older duplicate count value for " + column + + " of " + offset + " which is = the last offset " + last_count_offset + + " for " + this); + } + return; + } else { // if it's equal, just use the one we got first + // roll back the indices + indices[2] -= 2; + indices[3] -= Internal.getValueLengthFromQualifier( + count_qualifiers, indices[2]); + if (LOG.isDebugEnabled()) { + LOG.debug("Replacing older duplicate count with " + column + + " of " + offset + " which is = the last offset " + last_count_offset + + " for " + this); + } + } + } else { + throw new IllegalArgumentException("The count offset for " + column + + " of " + offset + " is <= the last offset " + last_count_offset + + " for " + this); + } + } + last_count_offset = offset; + last_count_ts = column.timestamp(); + System.arraycopy(column.qualifier(), RollupQuery.COUNT.length + 1, + count_qualifiers, indices[2], 2); + indices[2] += 2; + + if (indices[3] + column.value().length > count_values.length) { + byte[] buf = new byte[count_values.length * 2]; + System.arraycopy(count_values, 0, buf, 0, count_values.length); + count_values = buf; + } + System.arraycopy(column.value(), 0, count_values, indices[3], + column.value().length); + indices[3] += column.value().length; + } else { + int offset = Internal.getOffsetFromQualifier(column.qualifier(), + rollup_query.getRollupAggPrefix().length); + if (last_offset > -1 && offset <= last_offset) { + // only accept equivalent offsets. If somehow we get an earlier one, HBase is broke + if (offset == last_offset && tsdb.getConfig().fix_duplicates()) { + if (column.timestamp() < last_value_ts) { + if (LOG.isDebugEnabled()) { + LOG.debug("Skipping older duplicate value for " + column + + " of " + offset + " which is = the last offset " + last_count_offset + + " for " + this); + } + return; + } else { // if it's equal, just use the one we got first + // roll back the indices + indices[0] -= 2; + indices[1] -= Internal.getValueLengthFromQualifier(qualifiers, indices[0]); + if (LOG.isDebugEnabled()) { + LOG.debug("Replacing older duplicate value with " + column + + " of " + offset + " is = the last offset " + last_count_offset + + " for " + this); + } + } + } else { + throw new IllegalArgumentException("The offset for " + column + + " of " + offset + " is <= the last offset " + last_offset + + " for " + this); + } + } + last_offset = offset; + last_value_ts = column.timestamp(); + System.arraycopy(column.qualifier(), + rollup_query.getRollupAggPrefix().length, qualifiers, indices[0], 2); + indices[0] += 2; + + if (indices[1] + column.value().length > values.length) { + byte[] buf = new byte[values.length * 2]; + System.arraycopy(values, 0, buf, 0, values.length); + values = buf; + } + System.arraycopy(column.value(), 0, values, indices[1], + column.value().length); + indices[1] += column.value().length; + } + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(80 + + + (key == null ? 0 : key.length * 4) + + indices[0] * 8); + buf.append("RollupSeq(key=") + .append(key == null ? "" : Arrays.toString(key)) + .append(" base_time=") + .append(key == null ? "" : baseTime()) + .append(", basetime=") + .append(key == null ? "no data" : new Date(baseTime() * 1000)) + .append(", "); + buf.append("datapoints=").append(indices[0] / 2) + .append(", counts=").append(indices.length > 2 ? indices[2] / 2 : "0"); + buf.append(",\n (qualifier=[").append(Arrays.toString(qualifiers)); + buf.append("]),\n (values=[").append(Arrays.toString(values)); + buf.append("],\n (count_qualifier=[").append(Arrays.toString(count_qualifiers)); + buf.append("],\n (count_values=[").append(Arrays.toString(count_values)); + buf.append("])"); + return buf.toString(); + } + + public String metricName() { + try { + return metricNameAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the metric name call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred metricNameAsync() { + if (key == null) { + throw new IllegalStateException("the row key is null!"); + } + return RowKey.metricNameAsync(tsdb, key); + } + + public byte[] metricUID() { + return Arrays.copyOfRange(key, Const.SALT_WIDTH(), + Const.SALT_WIDTH() + TSDB.metrics_width()); + } + + public Map getTags() { + try { + return getTagsAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the tags call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred> getTagsAsync() { + return Tags.getTagsAsync(tsdb, key); + } + + @Override + public ByteMap getTagUids() { + return Tags.getTagUids(key); + } + + /** @return an empty list since aggregated tags cannot exist on a single row */ + public List getAggregatedTags() { + return Collections.emptyList(); + } + + public Deferred> getAggregatedTagsAsync() { + final List empty = Collections.emptyList(); + return Deferred.fromResult(empty); + } + + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } + + public List getTSUIDs() { + return Collections.emptyList(); + } + + /** @return null since annotations are stored at the SpanGroup level. They + * are filtered when a row is compacted */ + public List getAnnotations() { + return Collections.emptyList(); + } + + @Override + public int size() { + if (need_count) { + int count = 0; + final Iterator it = internalIterator(); + while (it.hasNext()) { + it.next(); + ++count; + } + return count; + } else { + return indices[0] / 2; + } + } + + @Override + public int aggregatedSize() { + return 0; + } + + @Override + public long timestamp(int i) { + if (i < 0) { + throw new IndexOutOfBoundsException("index " + i + + " must be positive for this=" + this); + } + if (need_count) { + final Iterator it = internalIterator(); + int count = 0; + while (it.hasNext()) { + final DataPoint dp = it.next(); + if (count == i) { + return dp.timestamp(); + } + ++count; + } + throw new IndexOutOfBoundsException("index " + i + " >= " + size() + + " for this=" + this); + } else { + if (i * 2 >= indices[0]) { + throw new IndexOutOfBoundsException("index " + i + " >= " + size() + + " for this=" + this); + } + return RollupUtils.getTimestampFromRollupQualifier(qualifiers, baseTime(), + rollup_query.getRollupInterval(), i * 2); + } + } + + @Override + public boolean isInteger(int i) { + throw new NotImplementedException(); + } + + @Override + public long longValue(int i) { + throw new NotImplementedException(); + } + + @Override + public double doubleValue(int i) { + throw new NotImplementedException(); + } + + @Override + public int getQueryIndex() { + return 0; + } + + @Override + public byte[] key() { + return key; + } + + @Override + public long baseTime() { + return Internal.baseTime(key); + } + + @Override + public SeekableView iterator() { + return internalIterator(); + } + + @Override + public Iterator internalIterator() { + return new RollupIterator(); + } + + /** Iterator for {@link RowSeq}s. */ + public final class RollupIterator implements iRowSeq.Iterator { + + /** Current qualifier. */ + private int qualifier; + + /** Next index in {@link #qualifiers}. */ + private int qual_index; + + /** Next index in {@link #values}. */ + private int value_index; + + /** Current qualifier for the counts */ + private int count_qualifier; + + /** Next index in {@link #count_qualifier}. */ + private int count_qual_index; + + /** Next index in {@link #count_values}. */ + private int count_value_index; + + /** Pre-extracted base time of this row sequence. */ + private final long base_time = baseTime(); + + RollupIterator() { + if (need_count) { + sync(); + } + } + + // ------------------ // + // Iterator interface // + // ------------------ // + + public boolean hasNext() { + if (need_count) { + sync(); + return qual_index < indices[0] && + count_qual_index < indices[2]; + } + return qual_index < indices[0]; + } + + void sync() { + if (qual_index >= indices[0] || count_qual_index >= indices[2]) { + return; + } + long q_ts = Internal.getOffsetFromQualifier(qualifiers, qual_index); + long c_ts = Internal.getOffsetFromQualifier(count_qualifiers, count_qual_index); + if (q_ts == c_ts) { + return; + } + LOG.warn("Different agg [" + q_ts + "] and count [" + c_ts + + "] offsets for " + this); + if (q_ts > c_ts) { + count_value_index += Internal.getValueLengthFromQualifier( + count_qualifiers, count_qual_index); + count_qual_index += 2; + if (count_qual_index >= count_qualifiers.length) { + LOG.warn("Ran out of counts: " + this); + return; + } + sync(); + } else { + value_index += Internal.getValueLengthFromQualifier(qualifiers, qual_index); + qual_index += 2; + if (qual_index >= qualifiers.length) { + LOG.warn("Ran out of qualifiers: " + this); + return; + } + sync(); + } + } + + public DataPoint next() { + if (!hasNext()) { + throw new NoSuchElementException("no more elements"); + } + + value_index += Internal.getValueLengthFromQualifier(qualifiers, qual_index); + qualifier = Bytes.getUnsignedShort(qualifiers, qual_index); + qual_index += 2; + if (need_count) { + count_value_index += Internal.getValueLengthFromQualifier( + count_qualifiers, count_qual_index); + count_qualifier = Bytes.getUnsignedShort(count_qualifiers, count_qual_index); + count_qual_index += 2; + } + return this; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + // ---------------------- // + // SeekableView interface // + // ---------------------- // + + @Override + public void seek(final long timestamp) { + final long ts; + if ((timestamp & Const.SECOND_MASK) == 0) { + ts = timestamp * 1000; + } else { + ts = timestamp; + } + // reset all + qual_index = value_index = count_qual_index = count_value_index = 0; + if (!hasNext()) { + return; + } + + qualifier = Bytes.getUnsignedShort(qualifiers, qual_index); + if (need_count) { + count_qualifier = Bytes.getUnsignedShort(count_qualifiers, count_qual_index); + } + while (qual_index < indices[0] && + RollupUtils.getTimestampFromRollupQualifier(qualifiers, base_time, + rollup_query.getRollupInterval(), qual_index) < ts) { + value_index += Internal.getValueLengthFromQualifier(qualifiers, qual_index); + qualifier = Bytes.getUnsignedShort(qualifiers, qual_index); + qual_index += 2; + if (need_count) { + count_value_index += Internal.getValueLengthFromQualifier( + count_qualifiers, count_qual_index); + count_qualifier = Bytes.getUnsignedShort(count_qualifiers, count_qual_index); + count_qual_index += 2; + } + } + } + + // ------------------- // + // DataPoint interface // + // ------------------- // + + public long timestamp() { + return RollupUtils.getTimestampFromRollupQualifier(qualifier, base_time, + rollup_query.getRollupInterval()); + } + + public boolean isInteger() { + assert qual_index > 0: "not initialized: " + this; + return (qualifier & Const.FLAG_FLOAT) == 0x0; + } + + @Override + public long valueCount() { + if (count_values == null) { + return -1; + } + final byte flags = (byte) count_qualifier; + final byte vlen = (byte) ((flags & Const.LENGTH_MASK) + 1); + if ((count_qualifier & Const.FLAG_FLOAT) == 0x0) { + return Internal.extractIntegerValue(count_values, count_value_index - vlen, flags); + } else { + return (long)Internal.extractFloatingPointValue(count_values, count_value_index - vlen, flags); + } + } + + public long longValue() { + if (!isInteger()) { + throw new ClassCastException("value @" + + qual_index + " is not a long in " + this); + } + final byte flags = (byte) qualifier; + final byte vlen = (byte) ((flags & Const.LENGTH_MASK) + 1); + return Internal.extractIntegerValue(values, value_index - vlen, flags); + //return extractIntegerValue(values, value_index - vlen, flags); + } + + public double doubleValue() { + if (isInteger()) { + throw new ClassCastException("value @" + + qual_index + " is not a float in " + this); + } + final byte flags = (byte) qualifier; + final byte vlen = (byte) ((flags & Const.LENGTH_MASK) + 1); + return Internal.extractFloatingPointValue(values, value_index - vlen, flags); + //return extractFloatingPointValue(values, value_index - vlen, flags); + } + + public double toDouble() { + return isInteger() ? longValue() : doubleValue(); + } + + // ---------------- // + // Helpers for Span // + // ---------------- // + + /** Helper to take a snapshot of the state of this iterator. */ + long saveState() { + return ((long)qual_index << 32) | ((long)value_index & 0xFFFFFFFF); + } + + /** Helper to restore a snapshot of the state of this iterator. */ + void restoreState(long state) { + value_index = (int) state & 0xFFFFFFFF; + state >>>= 32; + qual_index = (int) state; + qualifier = 0; + } + + /** + * Look a head to see the next timestamp. + * @throws IndexOutOfBoundsException if we reached the end already. + */ + long peekNextTimestamp() { + return RollupUtils.getTimestampFromRollupQualifier(qualifiers, base_time, + rollup_query.getRollupInterval(), qual_index); + } + + /** Only returns internal state for the iterator itself. */ + String toStringSummary() { + return "RowSeq.Iterator(qual_index=" + qual_index + + ", value_index=" + value_index + ", cq_idx=" + count_qual_index + + ", cv_idx=" + count_value_index; + } + + public String toString() { + return toStringSummary() + ", seq=" + RollupSeq.this + ')'; + } + + } +} diff --git a/test/core/TestInternal.java b/test/core/TestInternal.java index c654074078..991d5b2484 100644 --- a/test/core/TestInternal.java +++ b/test/core/TestInternal.java @@ -838,6 +838,62 @@ public void getMaxUnsignedValueOnBytes() throws Exception { assertNotNull(e); } } + + @Test + public void vleEncodeLong0() throws Exception { + final byte[] expected = new byte[1]; + assertArrayEquals(expected, Internal.vleEncodeLong(0)); + } + + @Test + public void vleEncodeLong1byte() throws Exception { + final byte[] expected = new byte[] { 42 }; + assertArrayEquals(expected, Internal.vleEncodeLong(42)); + } + + @Test + public void vleEncodeLong1byteNegative() throws Exception { + final byte[] expected = new byte[] { -42 }; + assertArrayEquals(expected, Internal.vleEncodeLong(-42)); + } + + @Test + public void vleEncodeLong2bytes() throws Exception { + final byte[] expected = new byte[] { 1, 1 }; + assertArrayEquals(expected, Internal.vleEncodeLong(257)); + } + + @Test + public void vleEncodeLong2bytesNegative() throws Exception { + final byte[] expected = new byte[] { (byte) 0xFE, (byte) 0xFF }; + assertArrayEquals(expected, Internal.vleEncodeLong(-257)); + } + + @Test + public void vleEncodeLong4bytes() throws Exception { + final byte[] expected = new byte[] { 0, 1, 0, 1 }; + assertArrayEquals(expected, Internal.vleEncodeLong(65537)); + } + + @Test + public void vleEncodeLong4bytesNegative() throws Exception { + final byte[] expected = + new byte[] { (byte) 0xFF, (byte) 0xFE, (byte) 0xFF, (byte) 0xFF }; + assertArrayEquals(expected, Internal.vleEncodeLong(-65537)); + } + + @Test + public void vleEncodeLong8bytes() throws Exception { + final byte[] expected = new byte[] { 0, 0, 0, 1, 0, 0, 0, 0 }; + assertArrayEquals(expected, Internal.vleEncodeLong(4294967296L)); + } + + @Test + public void vleEncodeLong8bytesNegative() throws Exception { + final byte[] expected = new byte[] { + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0, 0, 0, 0 }; + assertArrayEquals(expected, Internal.vleEncodeLong(-4294967296L)); + } /** Shorthand to create a {@link KeyValue}. */ private static KeyValue makekv(final byte[] qualifier, final byte[] value) { diff --git a/test/core/TestRowSeq.java b/test/core/TestRowSeq.java index 1a3792759b..fe8d1a932e 100644 --- a/test/core/TestRowSeq.java +++ b/test/core/TestRowSeq.java @@ -52,13 +52,13 @@ public final class TestRowSeq { private TSDB tsdb = mock(TSDB.class); private Config config = mock(Config.class); private UniqueId metrics = mock(UniqueId.class); - private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; - private static final byte[] KEY = + public static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; + public static final byte[] KEY = { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; - private static final byte[] SALTED_KEY = + public static final byte[] SALTED_KEY = { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; - private static final byte[] FAMILY = { 't' }; - private static final byte[] ZERO = { 0 }; + public static final byte[] FAMILY = { 't' }; + public static final byte[] ZERO = { 0 }; @Before public void before() throws Exception { diff --git a/test/rollup/TestRollupSeq.java b/test/rollup/TestRollupSeq.java new file mode 100644 index 0000000000..5e6918b1f5 --- /dev/null +++ b/test/rollup/TestRollupSeq.java @@ -0,0 +1,1607 @@ +// 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.rollup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.Const; +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.IllegalDataException; +import net.opentsdb.core.Internal; +import net.opentsdb.core.RowKey; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TestRowSeq; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +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 org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +import java.util.Arrays; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ RollupSeq.class, TSDB.class, UniqueId.class, KeyValue.class, + Config.class, RowKey.class, Const.class }) +public final class TestRollupSeq { + private TSDB tsdb = mock(TSDB.class); + private Config config = mock(Config.class); + private UniqueId metrics = mock(UniqueId.class); + private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; + private static final byte[] KEY = + new byte[] { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + public static final byte[] FAMILY = { 't' }; + private static final RollupQuery rollup_query_sum = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), + Aggregators.SUM, 1000); + private static final RollupQuery rollup_query_avg = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), + Aggregators.AVG, 1000); + private static final RollupQuery rollup_query_sum_mimmax = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), + Aggregators.MIMMAX, 1000); + private static final RollupQuery rollup_query_10m_sum = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), + Aggregators.SUM, 600000); + private static final RollupQuery rollup_query_10m_avg = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), + Aggregators.AVG, 600000); + private static final RollupQuery rollup_query_10m_count = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), + Aggregators.COUNT, 600000); + private static final RollupQuery rollup_query_1h_sum = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), + Aggregators.SUM, 3600000); + private static final RollupQuery rollup_query_1h_avg = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), + Aggregators.AVG, 3600000); + private static final RollupQuery rollup_query_1h_count = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), + Aggregators.COUNT, 3600000); + + @Before + public void before() throws Exception { + // Inject the attributes we need into the "tsdb" object. + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "table", TABLE); + Whitebox.setInternalState(tsdb, "config", config); + when(tsdb.getConfig()).thenReturn(config); + when(RowKey.metricNameAsync(tsdb, TestRowSeq.KEY)) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + } + + @Test + public void setRow() throws Exception { + final KeyValue kv = getRollupKeyValue(1356998400000L, 4L, rollup_query_sum); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(kv); + assertEquals(1, rs.size()); + final SeekableView it = rs.iterator(); + assertTrue(it.hasNext()); + DataPoint dp = it.next(); + assertTrue(dp.isInteger()); + assertEquals(4, dp.longValue()); + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(-1, dp.valueCount()); + assertFalse(it.hasNext()); + } + + @Test (expected = IllegalStateException.class) + public void setRowAlreadySet() throws Exception { + final KeyValue kv = getRollupKeyValue(1356998400000L, 4L, rollup_query_sum); + + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(kv); + assertEquals(1, rs.size()); + //Expects an IllegalStateException + final KeyValue kv1 = getRollupKeyValue(1356998500000L, 5L, rollup_query_sum); + rs.setRow(kv1); + } + + @Test + public void addRow() throws Exception { + final KeyValue kv1 = getRollupKeyValue(1356998400000L, 4L, rollup_query_sum); + final KeyValue kv2 = getRollupKeyValue(1356998500000L, 5L, rollup_query_sum); + + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(kv1); + assertEquals(1, rs.size()); + + rs.addRow(kv2); + assertEquals(2, rs.size()); + + final SeekableView it = rs.iterator(); + assertTrue(it.hasNext()); + DataPoint dp = it.next(); + assertTrue(dp.isInteger()); + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(4, dp.longValue()); + assertEquals(-1, dp.valueCount()); + + assertTrue(it.hasNext()); + dp = it.next(); + assertTrue(dp.isInteger()); + assertEquals(1356998500000L, dp.timestamp()); + assertEquals(5, dp.longValue()); + assertEquals(-1, dp.valueCount()); + assertFalse(it.hasNext()); + } + + // This should never happen + @Test + public void addRowMergeDifferentSalt() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); + + final byte[] key = new byte[TestRowSeq.KEY.length + 1]; + key[0] = 1; + System.arraycopy(TestRowSeq.KEY, 0, key, 1, TestRowSeq.KEY.length); + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] key2 = Arrays.copyOf(key, key.length); + key2[0] = 2; + rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); + rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); + + assertEquals(4, rs.size()); + + final SeekableView it = rs.iterator(); + long value = 4; + long ts = 1356998400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(-1, dp.valueCount()); + ++value; + ts += 1000; + } + } + + @Test + public void addRowMergeLater() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val4 = Bytes.fromLong(7L); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.addRow(TestRowSeq.makekv(qual4, val4)); + + assertEquals(4, rs.size()); + final SeekableView it = rs.iterator(); + long value = 4; + long ts = 1356998400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(-1, dp.valueCount()); + ++value; + ts += 1000; + } + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMergeEarlier() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val1 = Bytes.fromLong(6L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val2 = Bytes.fromLong(7L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val3 = Bytes.fromLong(4L); + rs.addRow(TestRowSeq.makekv( qual3, val3)); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMergeMiddle() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; + final byte[] val3 = Bytes.fromLong(8L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x57 }; + final byte[] val4 = Bytes.fromLong(9L); + rs.addRow(TestRowSeq.makekv( qual3, val3)); + rs.addRow(TestRowSeq.makekv(qual4, val4)); + assertEquals(4, rs.size()); + + final byte[] qual5 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val5 = Bytes.fromLong(6L); + rs.addRow(TestRowSeq.makekv( qual5, val5)); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMergeDuplicateLater() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + assertEquals(3, rs.size()); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + } + + @Test + public void addRowMergeDuplicateLaterRepair() throws Exception { + when(config.fix_duplicates()).thenReturn(true); + + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + assertEquals(3, rs.size()); + SeekableView it = rs.iterator(); + long ts = 1356998400000L; + double value = 4.0; + while(it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.toDouble(), 0.0001); + ts += 1000; + ++value; + } + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 8, Bytes.fromLong(7L))); + assertEquals(3, rs.size()); + + it = rs.iterator(); + ts = 1356998400000L; + value = 4.0; + while(it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.toDouble(), 0.0001); + ts += 1000; + if (value >= 5) { + value = 7.0; + } else { + ++value; + } + } + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMergeDuplicateEarlier() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val4 = Bytes.fromLong(5L); + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val1 = Bytes.fromLong(6L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val2 = Bytes.fromLong(7L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual4, val4)); + rs.addRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(3, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val3 = Bytes.fromLong(4L); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + } + + @Test + public void addRowMergeDuplicateEarlierRepair() throws Exception { + when(config.fix_duplicates()).thenReturn(true); + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + assertEquals(3, rs.size()); + SeekableView it = rs.iterator(); + long ts = 1356998400000L; + double value = 4.0; + while(it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.toDouble(), 0.0001); + ts += 1000; + ++value; + } + + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 1, Bytes.fromLong(7L))); + assertEquals(3, rs.size()); + + it = rs.iterator(); + ts = 1356998400000L; + value = 4.0; + while(it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.toDouble(), 0.0001); + ts += 1000; + ++value; + } + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMergeDuplicateCountLater() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + assertEquals(0, rs.size()); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + } + + @Test + public void addRowMergeDuplicateCountLaterRepair() throws Exception { + when(config.fix_duplicates()).thenReturn(true); + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); + rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + assertEquals(6, rs.count_values[23]); + assertEquals(0, rs.size()); + + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 8, Bytes.fromLong(7L))); + assertEquals(7, rs.count_values[23]); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMergeDuplicateCountEarlier() throws Exception { + // this happens if the same row key is used for the addRow call + final byte[] qual4 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x17 }; + final byte[] val4 = Bytes.fromLong(5L); + final byte[] qual1 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; + final byte[] val1 = Bytes.fromLong(6L); + final byte[] qual2 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x37 }; + final byte[] val2 = Bytes.fromLong(7L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); + rs.setRow(TestRowSeq.makekv(qual4, val4)); + rs.addRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(0, rs.size()); + + final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x07 }; + final byte[] val3 = Bytes.fromLong(4L); + rs.addRow(TestRowSeq.makekv(qual3, val3)); + } + + @Test + public void addRowMergeDuplicateCountEarlierRepair() throws Exception { + when(config.fix_duplicates()).thenReturn(true); + // this happens if the same row key is used for the addRow call + final byte[] qual1 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x17 }; + final byte[] val2 = Bytes.fromLong(5L); + final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; + final byte[] val3 = Bytes.fromLong(6L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); + rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + assertEquals(0, rs.size()); + assertEquals(6, rs.count_values[23]); + + rs.addRow(new KeyValue(KEY, FAMILY, qual3, 1, Bytes.fromLong(7L))); + assertEquals(6, rs.count_values[23]); + } + + @Test (expected = IllegalDataException.class) + public void addRowDiffBaseTime() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] row2 = { 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 1, 0, 0, 2 }; + rs.addRow(new KeyValue(row2, TestRowSeq.FAMILY, qual3, val3)); + rs.addRow(new KeyValue(row2, TestRowSeq.FAMILY, qual4, val4)); + } + + @Test (expected = IllegalStateException.class) + public void addRowNotSet() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.addRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + + } + + @Test (expected = IllegalDataException.class) + public void addRowMergeDifferentKey() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] key2 = Arrays.copyOf(TestRowSeq.KEY, TestRowSeq.KEY.length); + key2[key2.length - 1] = 3; + rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); + rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); + } + + @Test (expected = IllegalDataException.class) + public void addRowMergeDifferentKeyAndSalt() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] key2 = Arrays.copyOf(TestRowSeq.KEY, TestRowSeq.KEY.length); + key2[0] = 2; + key2[key2.length - 1] = 3; + rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); + rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); + } + + @Test (expected = IllegalDataException.class) + public void addRowMergeDifferentTime() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); + rs.setRow(TestRowSeq.makekv(qual1, val1)); + rs.addRow(TestRowSeq.makekv(qual2, val2)); + assertEquals(2, rs.size()); + + final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; + final byte[] val3 = Bytes.fromLong(6L); + final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; + final byte[] val4 = Bytes.fromLong(7L); + final byte[] key2 = Arrays.copyOf(TestRowSeq.KEY, TestRowSeq.KEY.length); + key2[7] = 3; + rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); + rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); + } + + @Test + public void timestamp() throws Exception { + final KeyValue kv = getRollupKeyValue(1356998400000L, 7L, rollup_query_sum_mimmax); + + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum_mimmax); + rs.setRow(kv); + + assertEquals(1, rs.size()); + final SeekableView it = rs.iterator(); + assertTrue(it.hasNext()); + DataPoint dp = it.next(); + assertTrue(dp.isInteger()); + assertEquals(7, dp.longValue()); + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(-1, dp.valueCount()); + assertFalse(it.hasNext()); + } + // NOTE: many of the tests below also test RollupSeq.size() + @Test + public void rollup10m() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + 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(-1, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollup10mDouble() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 0.50, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 0.75, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 1.00, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 1.25, rollup_query_10m_sum)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + double value = 0.25; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(-1, dp.valueCount()); + value += 0.25; + ts += 600000; + } + } + + @Test + public void rollup10mFloat() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 10.25F, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 10.75F, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 11.25F, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 11.75F, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 12.25F, rollup_query_10m_sum)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + double value = 10.25; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(-1, dp.valueCount()); + value += 0.50; + ts += 600000; + } + } + + @Test + public void rollup10mMixFloatAndLong() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 10.50F, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 11.50F, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 12.50F, rollup_query_10m_sum)); + + assertEquals(6, rs.size()); + final SeekableView it = rs.iterator(); + double dvalue = 10.50; + long lvalue = 20; + long ts = 1420070400000L; + boolean toggle = false; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + if (dp.isInteger()) { + assertEquals(lvalue, dp.longValue()); + } else { + assertEquals(dvalue, dp.doubleValue(), 0.0001); + } + assertEquals(-1, dp.valueCount()); + if (toggle) { + dvalue += 1; + } else { + ++lvalue; + } + toggle = !toggle; + ts += 600000; + } + } + + @Test + public void rollupAvg10mWithCount() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(4, rs.size()); + final SeekableView it = rs.iterator(); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollupAvg10mWithCountFirst() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + + assertEquals(4, rs.size()); + final SeekableView it = rs.iterator(); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollupAvg10mMissingCount() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + + assertEquals(0, rs.size()); + final SeekableView it = rs.iterator(); + assertFalse(it.hasNext()); + } + + @Test + public void rollupAvg10mSkipFirstCount() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + long value = 21; + long ts = 1420071000000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollupAvg10mSkipLastCount() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollupAvg10mSkipMiddleCount() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + if (ts == 1420070400000L) { + ts = 1420071600000L; + value += 2; + } else { + ts += 600000; + ++value; + } + } + } + + @Test + public void rollupAvg10mMissingSum() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(0, rs.size()); + final SeekableView it = rs.iterator(); + assertFalse(it.hasNext()); + } + + public void rollupAvg10mSkipFirstSum() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + //rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + long value = 21; + long ts = 1420071000000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollupAvg10mSkipLastSum() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollupAvg10mSkipMiddleSum() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + if (ts == 1420070400000L) { + ts = 1420071600000L; + value += 2; + } else { + ts += 600000; + ++value; + } + } + } + + @Test + public void rollupAvg10mUnaligned() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(0, rs.size()); + final SeekableView it = rs.iterator(); + assertFalse(it.hasNext()); + } + + @Test + public void endOfArrayDivergence() throws Exception { + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + + int[]indices = new int[4]; + byte[] b = new byte[] { 0, 11, 0, 27, 0, 43, 0, 59, 0, 75, 0, 91, 0, 107, 0, 123, 0, -117, 0, -101, 0, -85, 0, -69, 0, -53, 0, -37, 0, -21, 0, -5, 1, 11, 1, 27, 1, 43, 1, 59, 1, 75, 1, 91, 1, 123, 0, 0 }; + Whitebox.setInternalState(rs, "key", key); + Whitebox.setInternalState(rs, "qualifiers", b); + Whitebox.setInternalState(rs, "indices", indices); + indices[0] = b.length; + Whitebox.setInternalState(rs, "values", new byte[] { 67, 10, 71, -82, 66, -10, -108, 123, 66, -52, -52, -51, 66, -56, 97, 72, 66, -78, 5, 31, 66, -79, -31, 72, 66, 101, 0, 0, 67, 1, 99, -41, 66, -17, -77, 51, 66, -48, 10, 61, 66, -48, -118, 61, 66, -77, -47, -20, 66, -79, 81, -20, 66, -33, -118, 61, 67, 6, -31, 72, 66, -22, -26, 102, 66, -51, -103, -102, 66, -65, 51, 51, 66, -73, 112, -92, 66, -71, -47, -20, 66, -25, -6, -31, 67, 10, 10, 61, 65, -115, 92, 41, 0, 0, 0, 0 }); + b = new byte[] { 0, 11, 0, 27, 0, 43, 0, 59, 0, 75, 0, 91, 0, 107, 0, 123, 0, -117, 0, -101, 0, -85, 0, -69, 0, -53, 0, -37, 0, -21, 0, -5, 1, 11, 1, 27, 1, 43, 1, 59, 1, 75, 1, 91, 1, 107, 0, 0 }; + indices[2] = b.length; + Whitebox.setInternalState(rs, "count_qualifiers", b); + Whitebox.setInternalState(rs, "count_values", new byte[] { 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 65, -16, 0, 0, 66, 100, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 66, 112, 0, 0, 65, 32, 0, 0, 0, 0, 0, 0 }); + + final SeekableView it = rs.iterator(); + int count = 0; + while (it.hasNext()) { + ++count; + it.next(); + } + assertEquals(22, count); + } + + @Test + public void arrayOverflowAvoidance() throws Exception { + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + + final int[]indices = new int[4]; + byte[] b = new byte[] { 0, 11, 0, 27, 0, 43, 0, 59, 0, 75, 0, 91, 0, 107, 0, 123, 0, -117, 0, -101, 0, -85, 0, -69, 0, -53, 0, -37, 0, -21, 0, -5, 1, 11, 1, 27, 1, 43, 1, 59, 1, 75, 1, 91, 1, 107, 1, 123 }; + Whitebox.setInternalState(rs, "key", key); + Whitebox.setInternalState(rs, "qualifiers", b); + Whitebox.setInternalState(rs, "indices", indices); + indices[0] = b.length; + Whitebox.setInternalState(rs, "values", new byte[] { 75, 29, -83, 11, 75, 28, -40, -13, 75, 23, -115, 8, 75, 9, -84, 94, 75, 11, 55, -77, 75, 12, -115, 111, 75, 8, -21, -48, 75, 12, 66, 76, 75, 8, 118, -48, 75, 11, -65, 121, 75, 26, 64, -100, 75, 69, -15, -114, 75, 95, 85, -64, 75, 109, 28, 40, 75, 118, 100, -32, 75, 110, -119, 20, 75, 100, -71, -94, 75, 100, -94, 50, 75, 90, -53, 44, 75, 90, 47, 70, 75, 82, 80, 40, 75, 66, 106, 121, 75, 57, -102, -34, 75, 53, 69, 4 }); + indices[2] = b.length; + Whitebox.setInternalState(rs, "count_qualifiers", b); + Whitebox.setInternalState(rs, "count_values", new byte[] { 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 48, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0, 65, 64, 0, 0 }); + + final SeekableView it = rs.iterator(); + int count = 0; + while (it.hasNext()) { + ++count; + it.next(); + } + assertEquals(24, count); + } + + @Test + public void rollup1hLong() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 4L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 5L, rollup_query_1h_sum)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + 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(-1, dp.valueCount()); + ++value; + ts += 3600000; + } + } + + @Test + public void rollup1hFloat() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 0.5, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 0.75, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 1.0, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 1.25, rollup_query_1h_sum)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + double value = 0.25; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertFalse(dp.isInteger()); + assertEquals(value, dp.doubleValue(), 0.0001); + assertEquals(-1, dp.valueCount()); + value += 0.25; + ts += 3600000; + } + } + + @Test + public void rollup1hLongWithCount() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2L, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 2L, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420081200, 4L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 2L, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420084800, 5L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 2L, rollup_query_1h_count)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + 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(2, dp.valueCount()); + ++value; + ts += 3600000; + } + } + + @Test + public void rollup1hLongWithCountWithDoubles() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2L, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2D, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 2D, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420081200, 4L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 2D, rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420084800, 5L, rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 2L, rollup_query_1h_count)); + + assertEquals(5, rs.size()); + final SeekableView it = rs.iterator(); + 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(2, dp.valueCount()); + ++value; + ts += 3600000; + } + } + + @Test + public void rollup10mSeekTop() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + + assertEquals(8, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420070400000L); + 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(-1, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollup10mSeek() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + + assertEquals(8, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420072200000L); + long value = 4; + long ts = 1420072200000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(-1, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollup10mSeekOOB() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + + assertEquals(8, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420075200000L); + assertFalse(it.hasNext()); + } + + @Test + public void rollup10mSeekSeconds() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + + assertEquals(8, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420072200L); + long value = 4; + long ts = 1420072200000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(-1, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollup10mSeekUnaligned() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + + assertEquals(8, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420072123456L); + long value = 4; + long ts = 1420072200000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(-1, dp.valueCount()); + ++value; + ts += 600000; + } + } + + @Test + public void rollup10mAvgSeekTopAligned() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(4, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420070400000L); + long value = 20; + long ts = 1420070400000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + assertEquals(24, value); + } + + @Test + public void rollup10mAvgSeekTopUnaligned() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(2, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420070400000L); + DataPoint dp = it.next(); + assertEquals(1420070400000L, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(20, dp.longValue()); + assertEquals(2, dp.valueCount()); + dp = it.next(); + assertEquals(1420072200000L, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(23, dp.longValue()); + assertEquals(2, dp.valueCount()); + assertFalse(it.hasNext()); + } + + @Test + public void rollup10mAvgSeekTopUnalignedMissingTop() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(3, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420070400000L); + long value = 21; + long ts = 1420071000000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + assertEquals(24, value); + } + + @Test + public void rollup10mAvgSeekAligned() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(4, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420071600); + long value = 22; + long ts = 1420071600000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + assertEquals(24, value); + } + + @Test + public void rollup10mAvgSeekUnaligned() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(2, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420071600); + long value = 23; + long ts = 1420072200000L; + while (it.hasNext()) { + final DataPoint dp = it.next(); + assertEquals(ts, dp.timestamp()); + assertTrue(dp.isInteger()); + assertEquals(value, dp.longValue()); + assertEquals(2, dp.valueCount()); + ++value; + ts += 600000; + } + assertEquals(24, value); + } + + @Test + public void rollup10mAvgSeekUnalignedEmpty() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(0, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420071600); + assertFalse(it.hasNext()); + } + + @Test + public void rollup10mAvgSeekTopAlignedOOB() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(4, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420075200000L); + assertFalse(it.hasNext()); + } + + @Test + public void rollup10mAvgSeekTopUnalignedOOB() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(2, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420075200000L); + assertFalse(it.hasNext()); + } + + @Test + public void rollup10mAvgSeekTopUnalignedEmptyOOB() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + + assertEquals(0, rs.size()); + final SeekableView it = rs.iterator(); + it.seek(1420075200000L); + assertFalse(it.hasNext()); + } + + @Test + public void rollup10mTimestamp() throws Exception { + byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); + final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + + assertEquals(1420070400000L, rs.timestamp(0)); + assertEquals(1420071000000L, rs.timestamp(1)); + assertEquals(1420071600000L, rs.timestamp(2)); + + try { + assertEquals(1420071600000L, rs.timestamp(3)); + fail("Excpected an IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { } + + try { + assertEquals(1420071600000L, rs.timestamp(-1)); + fail("Excpected an IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { } + } + + private static KeyValue getRollupKeyValue(final long timestamp, + final long value, + final RollupQuery rollup_query) { + return getRollupKeyValue(TestRowSeq.KEY, timestamp, value, rollup_query); + } + + private static KeyValue getRollupKeyValue(final byte[] key, + final long timestamp, + final long value, + final RollupQuery rollup_query) { + + final byte[] val = Internal.vleEncodeLong(value); + final short flags = (short) (val.length - 1); // Just the length. + return new KeyValue(key, TestRowSeq.FAMILY, getQualifier(timestamp, flags, + rollup_query), val); + } + + private static KeyValue getRollupKeyValue(final byte[] key, + final long timestamp, + final float value, + final RollupQuery rollup_query) { + + final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. + final byte[] val = Bytes.fromInt(Float.floatToRawIntBits(value)); + return new KeyValue(key, TestRowSeq.FAMILY, getQualifier(timestamp, flags, + rollup_query), val); + } + + private static KeyValue getRollupKeyValue(final byte[] key, + final long timestamp, + final double value, + final RollupQuery rollup_query) { + + final short flags = Const.FLAG_FLOAT | 0x7; // A double stored on 8 bytes. + final byte[] val = Bytes.fromLong(Double.doubleToRawLongBits(value)); + return new KeyValue(key, TestRowSeq.FAMILY, getQualifier(timestamp, flags, + rollup_query), val); + } + + private static byte[] getQualifier(final long timestamp, + final short flags, RollupQuery rollup_query) { + + final int base_time = RollupUtils.getRollupBasetime(timestamp, + rollup_query.getRollupInterval()); + return RollupUtils.buildRollupQualifier(timestamp, base_time, flags, + rollup_query.getRollupAgg().toString(), + rollup_query.getRollupInterval()); + } +} From 71f35ca4a4cc4763110ac10d8e9ff265e7d6a750 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 15 Oct 2016 14:46:47 -0700 Subject: [PATCH 563/826] Fix #572 and #877 by allowing for a config flag that allows for importing out of order timestamps. Any timestamp that was out of order will simply be redirected to the standard TSDB call. Signed-off-by: Chris Larsen --- src/core/IncomingDataPoints.java | 25 ++++++++++++++++--------- src/core/TSDB.java | 2 +- src/utils/Config.java | 1 + 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 6f6245b780..4789662a7e 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -47,6 +47,9 @@ final class IncomingDataPoints implements WritableDataPoints { /** The {@code TSDB} instance we belong to. */ private final TSDB tsdb; + + /** Whether or not to allow out of order data. */ + private final boolean allow_out_of_order_data; /** * The row key. Optional salt + 3 bytes for the metric name, 4 bytes for @@ -88,11 +91,8 @@ final class IncomingDataPoints implements WritableDataPoints { */ IncomingDataPoints(final TSDB tsdb) { this.tsdb = tsdb; - // the qualifiers and values were meant for pre-compacting the rows. We - // could implement this later, but for now we don't need to track the values - // as they'll just consume space during an import - // this.qualifiers = new short[3]; - // this.values = new long[3]; + allow_out_of_order_data = tsdb.getConfig() + .getBoolean("tsd.core.bulk.allow_out_of_order_timestamps"); } /** @@ -284,10 +284,17 @@ private Deferred addPointInternal(final long timestamp, // always maintain last_ts in milliseconds if ((ms_timestamp ? timestamp : timestamp * 1000) <= last_ts) { - throw new IllegalArgumentException("New timestamp=" + timestamp - + " is less than or equal to previous=" + last_ts - + " when trying to add value=" + Arrays.toString(value) + " to " - + this); + if (allow_out_of_order_data) { + // as we don't want to perform any funky calculations to find out if + // we're still in the same time range, just pass it off to the regular + // TSDB add function. + return tsdb.addPointInternal(metric, timestamp, value, tags, flags); + } else { + throw new IllegalArgumentException("New timestamp=" + timestamp + + " is less than or equal to previous=" + last_ts + + " when trying to add value=" + Arrays.toString(value) + " to " + + this); + } } /** Callback executed for chaining filter calls to see if the value diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 57e591ef07..5463aee8fc 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -946,7 +946,7 @@ public Deferred addPoint(final String metric, tags, flags); } - private Deferred addPointInternal(final String metric, + Deferred addPointInternal(final String metric, final long timestamp, final byte[] value, final Map tags, diff --git a/src/utils/Config.java b/src/utils/Config.java index 782bf36430..5929a3d3cd 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -502,6 +502,7 @@ protected void setDefaults() { default_map.put("tsd.core.preload_uid_cache.max_entries", "300000"); default_map.put("tsd.core.storage_exception_handler.enable", "false"); default_map.put("tsd.core.uid.random_metrics", "false"); + default_map.put("tsd.core.bulk.allow_out_of_order_timestamps", "false"); default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); From 63529b723fd54b7970e8933da92d5755af5e98c6 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Tue, 18 Oct 2016 10:34:03 -0700 Subject: [PATCH 564/826] Add the RollupSpan class for dealing with rolled up data. Modify the Span class to be extensible and use the iRowSeq interface. Also add a method to instantiate a regular RowSeq to Span. Modify RowSeq to compare on iRowSeqs. Signed-off-by: Chris Larsen --- src/core/RowSeq.java | 4 +- src/core/Span.java | 51 ++++++++++++++--------- src/rollup/RollupSpan.java | 84 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/rollup/RollupSpan.java diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index f24bfc2d88..0434db0618 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -514,8 +514,8 @@ public String toString() { * on the {@code RowSeq#baseTime()} * @since 2.0 */ - public static final class RowSeqComparator implements Comparator { - public int compare(final RowSeq a, final RowSeq b) { + public static final class RowSeqComparator implements Comparator { + public int compare(final iRowSeq a, final iRowSeq b) { if (a.baseTime() == b.baseTime()) { return 0; } diff --git a/src/core/Span.java b/src/core/Span.java index 15338956ae..e0e5ca731f 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-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 @@ -33,17 +33,17 @@ *

    * This class stores a continuous sequence of {@link RowSeq}s in memory. */ -final class Span implements DataPoints { +public class Span implements DataPoints { /** The {@link TSDB} instance we belong to. */ - private final TSDB tsdb; + protected final TSDB tsdb; /** All the rows in this span. */ - private final ArrayList rows = new ArrayList(); + protected final ArrayList rows = new ArrayList(); /** A list of annotations for this span. We can't lazily initialize since we * have to pass a collection to the compaction queue */ - private final ArrayList annotations = new ArrayList(0); + protected final ArrayList annotations = new ArrayList(0); /** * Whether or not the rows have been sorted. This should be toggled by the @@ -55,7 +55,7 @@ final class Span implements DataPoints { * Default constructor. * @param tsdb The TSDB to which we belong */ - Span(final TSDB tsdb) { + protected Span(final TSDB tsdb) { this.tsdb = tsdb; } @@ -138,7 +138,7 @@ public List getAggregatedTagUids() { * mix of second and millisecond timestamps */ public int size() { int size = 0; - for (final RowSeq row : rows) { + for (final iRowSeq row : rows) { size += row.size(); } return size; @@ -153,7 +153,7 @@ public List getTSUIDs() { if (rows.size() < 1) { return null; } - final byte[] tsuid = UniqueId.getTSUIDFromKey(rows.get(0).key, + final byte[] tsuid = UniqueId.getTSUIDFromKey(rows.get(0).key(), TSDB.metrics_width(), Const.TIMESTAMP_BYTES); final List tsuids = new ArrayList(1); tsuids.add(UniqueId.uidToString(tsuid)); @@ -172,28 +172,28 @@ public List getAnnotations() { * @throws IllegalArgumentException if the argument and this span are for * two different time series. */ - void addRow(final KeyValue row) { + protected void addRow(final KeyValue row) { long last_ts = 0; if (rows.size() != 0) { // Verify that we have the same metric id and tags. final byte[] key = row.key(); - final RowSeq last = rows.get(rows.size() - 1); + final iRowSeq last = rows.get(rows.size() - 1); final short metric_width = tsdb.metrics.width(); final short tags_offset = (short) (Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES); final short tags_bytes = (short) (key.length - tags_offset); String error = null; - if (key.length != last.key.length) { + if (key.length != last.key().length) { error = "row key length mismatch"; } else if ( - Bytes.memcmp(key, last.key, Const.SALT_WIDTH(), metric_width) != 0) { + Bytes.memcmp(key, last.key(), Const.SALT_WIDTH(), metric_width) != 0) { error = "metric ID mismatch"; - } else if (Bytes.memcmp(key, last.key, tags_offset, tags_bytes) != 0) { + } else if (Bytes.memcmp(key, last.key(), tags_offset, tags_bytes) != 0) { error = "tags mismatch"; } if (error != null) { throw new IllegalArgumentException(error + ". " - + "This Span's last row key is " + Arrays.toString(last.key) + + "This Span's last row key is " + Arrays.toString(last.key()) + " whereas the row key being added is " + Arrays.toString(key) + " and metric_width=" + metric_width); } @@ -205,9 +205,9 @@ void addRow(final KeyValue row) { sorted = false; if (last_ts >= rowseq.timestamp(0)) { // scan to see if we need to merge into an existing row - for (final RowSeq rs : rows) { - if (Bytes.memcmp(rs.key, row.key(), Const.SALT_WIDTH(), - (rs.key.length - Const.SALT_WIDTH())) == 0) { + for (final iRowSeq rs : rows) { + if (Bytes.memcmp(rs.key(), row.key(), Const.SALT_WIDTH(), + (rs.key().length - Const.SALT_WIDTH())) == 0) { rs.addRow(row); return; } @@ -253,7 +253,7 @@ private long getIdxOffsetFor(final int i) { checkRowOrder(); int idx = 0; int offset = 0; - for (final RowSeq row : rows) { + for (final iRowSeq row : rows) { final int sz = row.size(); if (offset + sz > i) { break; @@ -357,7 +357,7 @@ public String toString() { private int seekRow(final long timestamp) { checkRowOrder(); int row_index = 0; - RowSeq row = null; + iRowSeq row = null; final int nrows = rows.size(); for (int i = 0; i < nrows; i++) { row = rows.get(i); @@ -402,7 +402,7 @@ final class Iterator implements SeekableView { private int row_index; /** Iterator on the current row. */ - private RowSeq.Iterator current_row; + private iRowSeq.Iterator current_row; Iterator() { current_row = rows.get(0).internalIterator(); @@ -511,6 +511,17 @@ Downsampler downsampler(final long start_time, downsampler, query_start, query_end); } + /** + * RowSeq abstract factory API implementation + * @param tsdb The TSDB to which we belong + * @return RowSeq object which stores read-only sequence of continuous + * HBase rows + * @since 2.4 + */ + protected iRowSeq createRowSequence(TSDB tsdb) { + return new RowSeq(tsdb); + } + public int getQueryIndex() { throw new UnsupportedOperationException("Not mapped to a query"); } diff --git a/src/rollup/RollupSpan.java b/src/rollup/RollupSpan.java new file mode 100644 index 0000000000..86d581392e --- /dev/null +++ b/src/rollup/RollupSpan.java @@ -0,0 +1,84 @@ +// 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.rollup; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; + +import net.opentsdb.core.RowSeq; +import net.opentsdb.core.Span; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.iRowSeq; + +/** + * Represents a read-only sequence of continuous data points. + *

    + * This class stores a continuous sequence of {@link RowSeq}s in memory. + * @since 2.4 + */ +public final class RollupSpan extends Span { + + private RollupQuery rollup_query; + + private byte[] tsuid; + + /** + * Default constructor. + * @param tsdb The TSDB to which we belong + * @param rollup_query holds information about a rollup interval and + * rollup aggregator + * @throws IllegalStateException if it is default rollup interval + */ + public RollupSpan(final TSDB tsdb, RollupQuery rollup_query) { + super(tsdb); + + if (rollup_query.getRollupInterval().isDefaultRollupInterval()) { + throw new IllegalStateException("Rolup Span is not applicable to default " + + "rollup interval. Default rollup interval is encoded in the same way" + + " as the raw data."); + } + + this.rollup_query = rollup_query; + } + + @Override + protected void addRow(final KeyValue row) { + if (rows.size() > 0) { + final byte[] key = row.key(); + final iRowSeq last = rows.get(rows.size() - 1); + String error = null; + if (key.length != last.key().length) { + error = "row key length mismatch"; + } + + if (Bytes.memcmp(last.key(), key) == 0) { + last.addRow(row); + return; + } + } + final iRowSeq rowseq = createRowSequence(tsdb); + rowseq.setRow(row); + rows.add(rowseq); + } + + /** + * RowSeq abstract factory API implementation + * @param tsdb The TSDB to which we belong + * @return RollupSeq object which stores read-only sequence + * of continuous HBase rows - rollup data + */ + @Override + protected iRowSeq createRowSequence(final TSDB tsdb) { + return new RollupSeq(tsdb, this.rollup_query); + } +} From 1871fa2b9d8aa63270a2c46d1352da9585170f84 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Tue, 18 Oct 2016 11:15:38 -0700 Subject: [PATCH 565/826] Add the RollupDataPoint class that extends the IncomingDataPoint class so we can perform serdes on rollup data. Signed-off-by: Chris Larsen --- src/core/IncomingDataPoint.java | 86 +++++++++++++++++++++--- src/rollup/RollUpDataPoint.java | 114 ++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/rollup/RollUpDataPoint.java diff --git a/src/core/IncomingDataPoint.java b/src/core/IncomingDataPoint.java index dced8077ef..ca17f01721 100644 --- a/src/core/IncomingDataPoint.java +++ b/src/core/IncomingDataPoint.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2013-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 @@ -13,11 +13,16 @@ package net.opentsdb.core; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Bridging class that stores a normalized data point parsed from the "put" * RPC methods and gets it ready for storage. Also has some helper methods that @@ -32,22 +37,25 @@ * @since 2.0 */ @JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) public class IncomingDataPoint { + private static final Logger LOG = LoggerFactory.getLogger(IncomingDataPoint.class); + /** The incoming metric name */ - private String metric; + protected String metric; /** The incoming timestamp in Unix epoch seconds or milliseconds */ - private long timestamp; + protected long timestamp; /** The incoming value as a string, we'll parse it to float or int later */ - private String value; + protected String value; /** A hash map of tag name/values */ - private HashMap tags; + protected Map tags; /** TSUID for the data point */ - private String tsuid; - + protected String tsuid; + /** * Empty constructor necessary for some de/serializers */ @@ -65,7 +73,7 @@ public IncomingDataPoint() { public IncomingDataPoint(final String metric, final long timestamp, final String value, - final HashMap tags) { + final Map tags) { this.metric = metric; this.timestamp = timestamp; this.value = value; @@ -94,10 +102,10 @@ public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("metric=").append(this.metric); buf.append(" ts=").append(this.timestamp); - buf.append(" value=").append(this.value); + buf.append(" value=").append(this.value).append(" "); if (this.tags != null) { for (Map.Entry entry : this.tags.entrySet()) { - buf.append(" ").append(entry.getKey()).append("=").append(entry.getValue()); + buf.append(entry.getKey()).append("=").append(entry.getValue()); } } return buf.toString(); @@ -119,7 +127,7 @@ public final String getValue() { } /** @return the tags */ - public final HashMap getTags() { + public final Map getTags() { return tags; } @@ -152,4 +160,60 @@ public final void setTags(HashMap tags) { public final void setTSUID(String tsuid) { this.tsuid = tsuid; } + + /** + * Pre-validation of the various fields to make sure they're valid + * @param details a map to hold detailed error message. If null then + * the errors will be logged + * @return true if data point is valid, otherwise false + */ + public boolean validate(final List> details) { + if (this.getMetric() == null || this.getMetric().isEmpty()) { + if (details != null) { + details.add(getHttpDetails("Metric name was empty")); + } + LOG.warn("Metric name was empty: " + this); + return false; + } + + //TODO add blacklisted metric validatin here too + + if (this.getTimestamp() <= 0) { + if (details != null) { + details.add(getHttpDetails("Invalid timestamp")); + } + LOG.warn("Invalid timestamp: " + this); + return false; + } + + if (this.getValue() == null || this.getValue().isEmpty()) { + if (details != null) { + details.add(getHttpDetails("Empty value")); + } + LOG.warn("Empty value: " + this); + return false; + } + + if (this.getTags() == null || this.getTags().size() < 1) { + if (details != null) { + details.add(getHttpDetails("Missing tags")); + } + LOG.warn("Missing tags: " + this); + return false; + } + return true; + } + + /** + * Creates a map with an error message and this data point to return + * to the HTTP put data point RPC handler + * @param message The message to log + * @return A map to append to the HTTP response + */ + protected final Map getHttpDetails(final String message) { + final Map map = new HashMap(); + map.put("error", message); + map.put("datapoint", this); + return map; + } } diff --git a/src/rollup/RollUpDataPoint.java b/src/rollup/RollUpDataPoint.java new file mode 100644 index 0000000000..cb3669d24b --- /dev/null +++ b/src/rollup/RollUpDataPoint.java @@ -0,0 +1,114 @@ +// 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.rollup; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import net.opentsdb.core.Aggregators; +import net.opentsdb.core.IncomingDataPoint; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; + +/** + * Represents a single rolled up data point. It overrides the + * {@link IncomingDataPoint} class. + * @since 2.4 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RollUpDataPoint extends IncomingDataPoint { + private static final Logger LOG = LoggerFactory.getLogger(RollUpDataPoint.class); + + /** The interval in the format <#> such as 1m or 2h */ + private String interval; + + /** The name of the aggregator that created this data point */ + private String aggregator; + + /** + * Default Ctor necessary for de/serialization + */ + public RollUpDataPoint() { + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("metric=").append(metric) + .append(" ts=").append(this.timestamp) + .append(" value=").append(this.value).append(" "); + if (this.tags != null) { + for (Map.Entry entry : this.tags.entrySet()) { + buf.append(entry.getKey()).append("=").append(entry.getValue()); + } + } + buf.append(" interval=").append(interval) + .append(" aggregator=").append(aggregator); + return buf.toString(); + } + + /** @return the interval as a string */ + public String getInterval() { + return interval; + } + + /** @param interval The interval for this data point such as "1m" */ + public void setInterval(final String interval) { + this.interval = interval; + } + + /** @return the name of the aggregator used to generate this data point */ + public String getAggregator() { + return aggregator; + } + + /** @param aggregator The name of the aggregator used to generate this dp */ + public void setAggregator(final String aggregator) { + this.aggregator = aggregator; + } + + @Override + public boolean validate(final List> details) { + if (!super.validate(details)) + return false; + + if (this.getInterval() == null || this.getInterval().isEmpty()) { + if (details != null) { + details.add(getHttpDetails("Missing interval")); + } + LOG.warn("Missing interval: " + this); + return false; + } + + if (this.getAggregator() == null || this.getAggregator().isEmpty()) { + if (details != null) { + details.add(getHttpDetails("Missing aggregator")); + } + LOG.warn("Missing aggregator: " + this); + return false; + } + + if (!Aggregators.set().contains(this.getAggregator().toLowerCase())) { + if (details != null) { + details.add(getHttpDetails("Invalid aggregator")); + } + LOG.warn("Invalid aggregator: " + this); + return false; + } + + return true; + } +} From 7387673231fe5feddfd04ef6b6959266a1ef42d9 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 19 Oct 2016 11:28:10 -0700 Subject: [PATCH 566/826] Add functions to store aggregate and/or rolled up data points in TSDB. Modify the base TSDB test code to clean up a little bit. Signed-off-by: Chris Larsen --- src/core/Query.java | 7 + src/core/TSDB.java | 218 +++- src/core/TsdbQuery.java | 5 + src/utils/Config.java | 2 + test/core/BaseTsdbTest.java | 377 +++--- test/core/TestTSDB.java | 54 + test/core/TestTSDBAddAggregatePoint.java | 1030 +++++++++++++++++ test/meta/TestTSUIDQuery.java | 19 +- .../BaseTimeSyncedIteratorTest.java | 2 +- .../expression/TestExpressionIterator.java | 86 +- 10 files changed, 1536 insertions(+), 264 deletions(-) create mode 100644 test/core/TestTSDBAddAggregatePoint.java diff --git a/src/core/Query.java b/src/core/Query.java index f4ea1bce4f..32e94efd66 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -223,4 +223,11 @@ public Deferred configureFromQuery(final TSQuery query, * @since 1.2 */ public Deferred runAsync() throws HBaseException; + + /** + * Returns an index for this sub-query in the original set of queries. + * @return A zero based index. + * @since 2.4 + */ + public int getQueryIdx(); } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 5463aee8fc..f55598e24f 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -60,6 +60,9 @@ import net.opentsdb.meta.UIDMeta; import net.opentsdb.query.expression.ExpressionFactory; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupUtils; import net.opentsdb.search.SearchPlugin; import net.opentsdb.search.SearchQuery; import net.opentsdb.tools.StartupPlugin; @@ -112,6 +115,9 @@ public final class TSDB { /** Timer used for various tasks such as idle timeouts or query timeouts */ private final HashedWheelTimer timer; + /** Name of the tag we use to determine aggregates */ + private final String agg_tag; + /** * Row keys that need to be compacted. * Whenever we write a new data point to a row, we add the row key to this @@ -141,6 +147,12 @@ public final class TSDB { /** A filter plugin for allowing or blocking UIDs */ private UniqueIdFilterPlugin uid_filter; + /** The rollup config object for storing and querying rollups */ + private final RollupConfig rollup_config; + + /** The default rollup interval. */ + private final RollupInterval default_interval; + /** Writes rejected by the filter */ private final AtomicLong rejected_dps = new AtomicLong(); private final AtomicLong rejected_aggregate_dps = new AtomicLong(); @@ -203,7 +215,8 @@ public TSDB(final HBaseClient client, final Config config) { uidtable = config.getString("tsd.storage.hbase.uid_table").getBytes(CHARSET); treetable = config.getString("tsd.storage.hbase.tree_table").getBytes(CHARSET); meta_table = config.getString("tsd.storage.hbase.meta_table").getBytes(CHARSET); - + agg_tag = config.getString("tsd.core.agg_tag"); + if (config.getBoolean("tsd.core.uid.random_metrics")) { metrics = new UniqueId(this, uidtable, METRICS_QUAL, METRICS_WIDTH, true); } else { @@ -219,6 +232,27 @@ public TSDB(final HBaseClient client, final Config config) { timer = Threads.newTimer("TSDB Timer"); + if (config.getBoolean("tsd.rollups.enable")) { + rollup_config = new RollupConfig(); + RollupInterval config_default = null; + for (final RollupInterval interval: rollup_config.getRollups().values()) { + if (interval.isDefaultRollupInterval()) { + config_default = interval; + System.out.println("Found default: " + interval); + break; + } + } + + if (config_default == null) { + throw new IllegalArgumentException("None of the rollup intervals were " + + "marked as the \"default\"."); + } + default_interval = config_default; + } else { + rollup_config = null; + default_interval = null; + } + QueryStats.setEnableDuplicates( config.getBoolean("tsd.query.allow_simultaneous_duplicates")); @@ -1049,6 +1083,188 @@ public String toString() { return Deferred.fromResult(true).addCallbackDeferring(new WriteCB()); } + /** + * Adds a rolled up and/or groupby/pre-agged data point to the proper table. + * If {@code interval} is null then the value will be directed to the + * pre-agg table. + * If the {@code is_groupby} flag is set, then the aggregate tag, defined in + * "tsd.core.agg_tag", will be added or overwritten with the {@code aggregator} + * value in uppercase as the value. + * @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. + * @param is_groupby Whether or not the value is a pre-aggregate + * @param interval The interval the data reflects (may be null) + * @param aggregator The aggregator used to generate the data + * @return A deferred to optionally wait on to be sure the value was stored + * @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 tags list is empty or one of the + * elements contains illegal characters. + * @throws HBaseException (deferred) if there was a problem while persisting + * data. + * @since 2.4 + */ + public Deferred addAggregatePoint(final String metric, + final long timestamp, + final long value, + final Map tags, + final boolean is_groupby, + final String interval, + final String aggregator) { + final byte[] val = Internal.vleEncodeLong(value); + + final short flags = (short) (val.length - 1); // Just the length. + + return addAggregatePointInternal(metric, timestamp, + val, tags, flags, is_groupby, interval, aggregator); + } + + /** + * Adds a rolled up and/or groupby/pre-agged data point to the proper table. + * If {@code interval} is null then the value will be directed to the + * pre-agg table. + * If the {@code is_groupby} flag is set, then the aggregate tag, defined in + * "tsd.core.agg_tag", will be added or overwritten with the {@code aggregator} + * value in uppercase as the value. + * @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. + * @param is_groupby Whether or not the value is a pre-aggregate + * @param interval The interval the data reflects (may be null) + * @param aggregator The aggregator used to generate the data + * @return A deferred to optionally wait on to be sure the value was stored + * @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 tags list is empty or one of the + * elements contains illegal characters. + * @throws HBaseException (deferred) if there was a problem while persisting + * data. + * @since 2.4 + */ + public Deferred addAggregatePoint(final String metric, + final long timestamp, + final float value, + final Map tags, + final boolean is_groupby, + final String interval, + final String aggregator) { + if (Float.isNaN(value) || Float.isInfinite(value)) { + throw new IllegalArgumentException("value is NaN or Infinite: " + value + + " for metric=" + metric + + " timestamp=" + timestamp); + } + + final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. + + final byte[] val = Bytes.fromInt(Float.floatToRawIntBits(value)); + + return addAggregatePointInternal(metric, timestamp, + val, tags, flags, is_groupby, interval, aggregator); + } + + Deferred addAggregatePointInternal(final String metric, + final long timestamp, + final byte[] value, + final Map tags, + final short flags, + final boolean is_groupby, + final String interval, + final String aggregator) { + + if (rollup_config == null) { + throw new IllegalArgumentException( + "No rollup or aggregations were configured"); + } + + // we only accept positive unix epoch timestamps in seconds for rollups + // and allow milliseconds for pre-aggregates + if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0)) { + throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + + " timestamp=" + timestamp + + " when trying to add value=" + Arrays.toString(value) + '/' + flags + + " to metric=" + metric + ", tags=" + tags); + } + + // enforce the aggregate tag and bump to upper case + if (is_groupby) { + tags.put(agg_tag, aggregator.toUpperCase()); + } + + IncomingDataPoints.checkMetricAndTags(metric, tags); + + final RollupInterval rollup_interval = (interval == null ? null : + rollup_config.getRollupInterval(interval)); + + final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); + + final int base_time = interval == null ? + (int)(timestamp - (timestamp % Const.MAX_TIMESPAN)) + : RollupUtils.getRollupBasetime(timestamp, rollup_interval); + final byte[] qualifier = interval == null ? + Internal.buildQualifier(timestamp, flags) + : RollupUtils.buildRollupQualifier( + timestamp, base_time, flags, aggregator, rollup_interval); + + /** Callback executed for chaining filter calls to see if the value + * should be written or not. */ + final class WriteCB implements Callback, Boolean> { + @Override + public Deferred call(final Boolean allowed) throws Exception { + if (!allowed) { + rejected_aggregate_dps.incrementAndGet(); + return Deferred.fromResult(null); + } + Internal.setBaseTime(row, base_time); + // NOTE: Do not modify the row key after calculating and applying the salt + RowKey.prefixKeyWithSalt(row); + + Deferred result; + + final PutRequest point; + if (interval == null) { + if (!is_groupby) { + throw new IllegalArgumentException("Interval cannot be null " + + "for a non-group by point"); + } + point = new PutRequest(default_interval.getGroupbyTable(), row, + FAMILY, qualifier, value); + } else { + point = new PutRequest( + is_groupby ? rollup_interval.getGroupbyTable() : + rollup_interval.getTemporalTable(), + row, FAMILY, qualifier, value); + } + + // TODO: Add a callback to time the latency of HBase and store the + // timing in a moving Histogram (once we have a class for this). + result = client.put(point); + + // TODO - figure out what we want to do with the real time publisher and + // the meta tracking. + return result; + } + } + + if (ts_filter != null) { + return ts_filter.allowDataPoint(metric, timestamp, value, tags, flags) + .addCallbackDeferring(new WriteCB()); + } + try { + return new WriteCB().call(true); + } catch (Exception e) { + return Deferred.fromError(e); + } + } + /** * Forces a flush of any un-committed in memory data including left over * compactions. diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 07a741457a..29d91a4ce5 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1166,6 +1166,11 @@ private void createAndSetTSUIDFilter(final Scanner scanner) { } scanner.setKeyRegexp(regex, CHARSET); } + + @Override + public int getQueryIdx() { + return query_index; + } @Override public String toString() { diff --git a/src/utils/Config.java b/src/utils/Config.java index 5929a3d3cd..27f859fb1e 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -484,6 +484,7 @@ protected void setDefaults() { default_map.put("tsd.network.tcp_no_delay", "true"); default_map.put("tsd.network.keep_alive", "true"); default_map.put("tsd.network.reuse_address", "true"); + default_map.put("tsd.core.agg_tag", "_aggregate"); default_map.put("tsd.core.auto_create_metrics", "false"); default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); @@ -507,6 +508,7 @@ protected void setDefaults() { default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.query.enable_fuzzy_filter", "true"); + default_map.put("tsd.rollups.enable", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 7c5f209abb..eeda244510 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -16,9 +16,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.Matchers.anyInt; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -29,7 +33,9 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; import org.hbase.async.HBaseClient; import org.hbase.async.Scanner; @@ -57,27 +63,8 @@ "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - HashedWheelTimer.class, Scanner.class, Const.class }) + HashedWheelTimer.class, Scanner.class, Const.class, Threads.class }) public class BaseTsdbTest { - /** A list of UIDs from A to Z for unit testing UIDs values */ - public static final Map METRIC_UIDS = - new HashMap(26); - public static final Map TAGK_UIDS = - new HashMap(26); - public static final Map TAGV_UIDS = - new HashMap(26); - static { - char letter = 'A'; - int uid = 10; - for (int i = 0; i < 26; i++) { - METRIC_UIDS.put(Character.toString(letter), - UniqueId.longToUID(uid, TSDB.metrics_width())); - TAGK_UIDS.put(Character.toString(letter), - UniqueId.longToUID(uid, TSDB.tagk_width())); - TAGV_UIDS.put(Character.toString(letter++), - UniqueId.longToUID(uid++, TSDB.tagv_width())); - } - } public static final String METRIC_STRING = "sys.cpu.user"; public static final byte[] METRIC_BYTES = new byte[] { 0, 0, 1 }; @@ -103,6 +90,16 @@ public class BaseTsdbTest { static final String NOTE_DESCRIPTION = "Hello DiscWorld!"; static final String NOTE_NOTES = "Millenium hand and shrimp"; + public static final Map UIDS = new HashMap(26); + static { + char letter = 'A'; + byte[] uid = new byte[] { 0, 0, 10 }; + for (int i = 0; i < 26; i++) { + UIDS.put(Character.toString(letter++), Arrays.copyOf(uid, uid.length)); + uid[2]++; + } + } + protected HashedWheelTimer timer; protected Config config; protected TSDB tsdb; @@ -110,13 +107,16 @@ public class BaseTsdbTest { protected UniqueId metrics = mock(UniqueId.class); protected UniqueId tag_names = mock(UniqueId.class); protected UniqueId tag_values = mock(UniqueId.class); - protected Map tags = new HashMap(1); + protected Map tags; protected MockBase storage; @Before public void before() throws Exception { + PowerMockito.mockStatic(Threads.class); timer = mock(HashedWheelTimer.class); - + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() .thenReturn(timer); PowerMockito.whenNew(HBaseClient.class).withAnyArguments() @@ -140,225 +140,72 @@ public void before() throws Exception { when(tag_names.width()).thenReturn((short)3); when(tag_values.width()).thenReturn((short)3); + tags = new HashMap(1); tags.put(TAGK_STRING, TAGV_STRING); } /** Adds the static UIDs to the metrics UID mock object */ void setupMetricMaps() { - when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); - when(metrics.getIdAsync(METRIC_STRING)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(METRIC_BYTES); - } - }); - when(metrics.getOrCreateId(METRIC_STRING)) - .thenReturn(METRIC_BYTES); - - when(metrics.getId(METRIC_B_STRING)).thenReturn(METRIC_B_BYTES); - when(metrics.getIdAsync(METRIC_B_STRING)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(METRIC_B_BYTES); - } - }); - when(metrics.getOrCreateId(METRIC_B_STRING)) - .thenReturn(METRIC_B_BYTES); - - when(metrics.getNameAsync(METRIC_BYTES)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(METRIC_STRING); - } - }); - when(metrics.getNameAsync(METRIC_B_BYTES)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(METRIC_B_STRING); - } - }); - when(metrics.getNameAsync(NSUI_METRIC)) - .thenThrow(new NoSuchUniqueId("metrics", NSUI_METRIC)); - - final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_METRIC, "metrics"); - + mockUID(UniqueIdType.METRIC, METRIC_STRING, METRIC_BYTES); + mockUID(UniqueIdType.METRIC, METRIC_B_STRING, METRIC_B_BYTES); + + final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_METRIC, "metric"); + when(metrics.getId(NSUN_METRIC)).thenThrow(nsun); when(metrics.getIdAsync(NSUN_METRIC)) - .thenReturn(Deferred.fromError(nsun)); + .thenReturn(Deferred. fromError(nsun)); when(metrics.getOrCreateId(NSUN_METRIC)).thenThrow(nsun); + final NoSuchUniqueName nsunic = new NoSuchUniqueName(NSUN_METRIC, "metric"); + when(metrics.getOrCreateIdAsync(eq(NSUN_METRIC))).thenThrow(nsunic); + when(metrics.getNameAsync(NSUI_METRIC)).thenReturn( + Deferred.fromError(new NoSuchUniqueId("metrics", NSUI_METRIC))); - // Iterate over the metric UIDs and handle both forward and reverse - for (final Map.Entry uid : METRIC_UIDS.entrySet()) { - when(metrics.getId(uid.getKey())).thenReturn(uid.getValue()); - when(metrics.getIdAsync(uid.getKey())) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(uid.getValue()); - } - }); - when(metrics.getOrCreateId(uid.getKey())) - .thenReturn(uid.getValue()); - when(metrics.getNameAsync(uid.getValue())) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(uid.getKey()); - } - }); + for (final Map.Entry uid : UIDS.entrySet()) { + mockUID(UniqueIdType.METRIC, uid.getKey(), uid.getValue()); } } /** Adds the static UIDs to the tag keys UID mock object */ void setupTagkMaps() { - when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); - when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); - when(tag_names.getIdAsync(TAGK_STRING)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGK_BYTES); - } - }); - when(tag_names.getOrCreateIdAsync(TAGK_STRING)) - .thenReturn(Deferred.fromResult(TAGK_BYTES)); - - when(tag_names.getId(TAGK_B_STRING)).thenReturn(TAGK_B_BYTES); - when(tag_names.getOrCreateId(TAGK_B_STRING)).thenReturn(TAGK_B_BYTES); - when(tag_names.getIdAsync(TAGK_B_STRING)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGK_B_BYTES); - } - }); - when(tag_names.getOrCreateIdAsync(TAGK_B_STRING)) - .thenReturn(Deferred.fromResult(TAGK_B_BYTES)); - - when(tag_names.getNameAsync(TAGK_BYTES)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGK_STRING); - } - }); - when(tag_names.getNameAsync(TAGK_B_BYTES)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGK_B_STRING); - } - }); - when(tag_names.getNameAsync(NSUI_TAGK)) + mockUID(UniqueIdType.TAGK, TAGK_STRING, TAGK_BYTES); + mockUID(UniqueIdType.TAGK, TAGK_B_STRING, TAGK_B_BYTES); + + final NoSuchUniqueName nsunic = new NoSuchUniqueName(NSUN_TAGK, "tagk"); + when(tag_names.getIdAsync(NSUN_TAGK)).thenReturn( + Deferred. fromError(nsunic)); + when(tag_names.getOrCreateId(eq(NSUN_TAGK))).thenThrow(nsunic); + when(tag_names.getOrCreateIdAsync(eq(NSUN_TAGK))).thenReturn( + Deferred. fromError(nsunic)); + when(tag_names.getName(NSUI_TAGK)) .thenThrow(new NoSuchUniqueId("tagk", NSUI_TAGK)); + when(tag_names.getNameAsync(NSUI_TAGK)).thenReturn( + Deferred.fromError(new NoSuchUniqueId("tagk", NSUI_TAGK))); - final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_TAGK, "tagk"); - - when(tag_names.getId(NSUN_TAGK)) - .thenThrow(nsun); - when(tag_names.getIdAsync(NSUN_TAGK)) - .thenReturn(Deferred.fromError(nsun)); - - // Iterate over the tagk UIDs and handle both forward and reverse - for (final Map.Entry uid : TAGK_UIDS.entrySet()) { - when(tag_names.getId(uid.getKey())).thenReturn(uid.getValue()); - when(tag_names.getIdAsync(uid.getKey())) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(uid.getValue()); - } - }); - when(tag_names.getOrCreateId(uid.getKey())) - .thenReturn(uid.getValue()); - when(tag_names.getNameAsync(uid.getValue())) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(uid.getKey()); - } - }); + for (final Map.Entry uid : UIDS.entrySet()) { + mockUID(UniqueIdType.TAGK, uid.getKey(), uid.getValue()); } } /** Adds the static UIDs to the tag values UID mock object */ void setupTagvMaps() { - when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); - when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); - when(tag_values.getIdAsync(TAGV_STRING)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGV_BYTES); - } - }); - when(tag_values.getOrCreateIdAsync(TAGV_STRING)) - .thenReturn(Deferred.fromResult(TAGV_BYTES)); - - when(tag_values.getId(TAGV_B_STRING)).thenReturn(TAGV_B_BYTES); - when(tag_values.getOrCreateId(TAGV_B_STRING)).thenReturn(TAGV_B_BYTES); - when(tag_values.getIdAsync(TAGV_B_STRING)) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGV_B_BYTES); - } - }); - when(tag_values.getOrCreateIdAsync(TAGV_B_STRING)) - .thenReturn(Deferred.fromResult(TAGV_B_BYTES)); - - when(tag_values.getNameAsync(TAGV_BYTES)) - .thenReturn(Deferred.fromResult(TAGV_STRING)); - when(tag_values.getNameAsync(TAGV_B_BYTES)) - .thenReturn(Deferred.fromResult(TAGV_B_STRING)); - when(tag_values.getNameAsync(NSUI_TAGV)) - .thenThrow(new NoSuchUniqueId("tagv", NSUI_TAGV)); + mockUID(UniqueIdType.TAGV, TAGV_STRING, TAGV_BYTES); + mockUID(UniqueIdType.TAGV, TAGV_B_STRING, TAGV_B_BYTES); final NoSuchUniqueName nsun = new NoSuchUniqueName(NSUN_TAGV, "tagv"); - + final NoSuchUniqueId nsui = new NoSuchUniqueId("tagv", NSUI_TAGV); when(tag_values.getId(NSUN_TAGV)).thenThrow(nsun); when(tag_values.getIdAsync(NSUN_TAGV)) - .thenReturn(Deferred.fromError(nsun)); + .thenReturn(Deferred. fromError(nsun)); + when(tag_values.getName(NSUI_TAGV)).thenThrow(nsui); + when(tag_values.getNameAsync(NSUI_TAGV)) + .thenReturn(Deferred.fromError(nsui)); + final NoSuchUniqueName nsunic = new NoSuchUniqueName(NSUN_TAGV, "tagv"); + when(tag_values.getOrCreateId(eq(NSUN_TAGV))).thenThrow(nsunic); + when(tag_values.getOrCreateIdAsync(eq(NSUN_TAGV))).thenReturn( + Deferred. fromError(nsunic)); - // Iterate over the tagv UIDs and handle both forward and reverse - for (final Map.Entry uid : TAGV_UIDS.entrySet()) { - when(tag_values.getId(uid.getKey())).thenReturn(uid.getValue()); - when(tag_values.getIdAsync(uid.getKey())) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(uid.getValue()); - } - }); - when(tag_values.getOrCreateId(uid.getKey())) - .thenReturn(uid.getValue()); - when(tag_values.getNameAsync(uid.getValue())) - .thenAnswer(new Answer>() { - @Override - public Deferred answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(uid.getKey()); - } - }); + for (final Map.Entry uid : UIDS.entrySet()) { + mockUID(UniqueIdType.TAGV, uid.getKey(), uid.getValue()); } } @@ -366,6 +213,106 @@ public Deferred answer(InvocationOnMock invocation) // Helper functions. // // ----------------- // + /** + * Mocks out the UID calls to match keys and values + * + * @param type + * The type of UID to deal with + * @param key + * The String name of the UID + * @param uid + * The byte array UID to pair up with + */ + protected void mockUID(final UniqueIdType type, final String key, + final byte[] uid) { + switch (type) { + case METRIC: + when(metrics.getId(key)).thenReturn(uid); + when(metrics.getIdAsync(key)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid); + } + }); + when(metrics.getOrCreateId(key)).thenReturn(uid); + when(metrics.getOrCreateIdAsync(key)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid); + } + }); + when(metrics.getName(uid)).thenReturn(key); + when(metrics.getNameAsync(uid)).thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(key); + } + }); + break; + case TAGK: + when(tag_names.getId(key)).thenReturn(uid); + when(tag_names.getIdAsync(key)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid); + } + }); + when(tag_names.getOrCreateId(key)).thenReturn(uid); + when(tag_names.getOrCreateIdAsync(key)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid); + } + }); + when(tag_names.getName(uid)).thenReturn(key); + when(tag_names.getNameAsync(uid)).thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(key); + } + }); + break; + case TAGV: + when(tag_values.getId(key)).thenReturn(uid); + when(tag_values.getIdAsync(key)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid); + } + }); + when(tag_values.getOrCreateId(key)).thenReturn(uid); + when(tag_values.getOrCreateIdAsync(key)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(uid); + } + }); + when(tag_values.getName(uid)).thenReturn(key); + when(tag_values.getNameAsync(uid)).thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(key); + } + }); + break; + } + } + /** @return a row key template with the default metric and tags */ protected byte[] getRowKeyTemplate() { return IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 64c4e64738..92ef3366a1 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -15,11 +15,17 @@ 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.fail; import static org.mockito.Mockito.when; import java.lang.reflect.Field; import java.util.HashMap; +import java.util.List; +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; @@ -36,10 +42,13 @@ 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 org.powermock.reflect.Whitebox; +import com.google.common.collect.Lists; import com.stumbleupon.async.Deferred; @RunWith(PowerMockRunner.class) @@ -67,6 +76,51 @@ public void ctorNullConfig() throws Exception { new TSDB(client, null); } + @Test + public void ctorRollups() throws Exception { + + TSDB tsdb = new TSDB(client, config); + assertNull(Whitebox.getInternalState(tsdb, "rollup_config")); + assertNull(Whitebox.getInternalState(tsdb, "default_interval")); + + List intervals = Lists.newArrayList( + new RollupInterval("tsdb", "tsdb-agg", "1m", "1h", true)); + RollupConfig rollups = new RollupConfig(intervals); + PowerMockito.whenNew(RollupConfig.class).withAnyArguments() + .thenReturn(rollups); + + config.overrideConfig("tsd.rollups.enable", "true"); + tsdb = new TSDB(client, config); + assertSame(rollups, Whitebox.getInternalState(tsdb, "rollup_config")); + assertSame(intervals.get(0), Whitebox.getInternalState(tsdb, + "default_interval")); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorRollupsNoDefault() throws Exception { + // no default + List intervals = Lists.newArrayList( + new RollupInterval("tsdb", "tsdb-agg", "1m", "1h")); + RollupConfig rollups = new RollupConfig(intervals); + PowerMockito.whenNew(RollupConfig.class).withAnyArguments() + .thenReturn(rollups); + + config.overrideConfig("tsd.rollups.enable", "true"); + new TSDB(client, config); + } + + @Test (expected = IllegalArgumentException.class) + public void ctorRollupsEmpty() throws Exception { + // no default + List intervals = Lists.newArrayList(); + RollupConfig rollups = new RollupConfig(intervals); + PowerMockito.whenNew(RollupConfig.class).withAnyArguments() + .thenReturn(rollups); + + config.overrideConfig("tsd.rollups.enable", "true"); + new TSDB(client, config); + } + @Test public void ctorOverrideUIDWidths() throws Exception { // assert defaults diff --git a/test/core/TestTSDBAddAggregatePoint.java b/test/core/TestTSDBAddAggregatePoint.java new file mode 100644 index 0000000000..8e958c65fb --- /dev/null +++ b/test/core/TestTSDBAddAggregatePoint.java @@ -0,0 +1,1030 @@ +// 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyShort; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import net.opentsdb.rollup.NoSuchRollupForIntervalException; +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupUtils; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId.UniqueIdType; + +import org.hbase.async.Bytes; +import org.junit.Before; +import org.junit.Test; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +public class TestTSDBAddAggregatePoint extends BaseTsdbTest { + private final static byte[] TSDB_TABLE = "tsdb".getBytes(MockBase.ASCII()); + private final static byte[] AGG_TABLE = "tsdb-agg".getBytes(MockBase.ASCII()); + private final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); + private HashMap tags; + private RollupConfig rollup_config; + + @Before + public void beforeLocal() { + tags = new HashMap(1); + tags.put(TAGK_STRING, TAGV_STRING); + + storage = new MockBase(tsdb, client, true, true, true, true); + final List families = new ArrayList(); + families.add(FAMILY); + + storage.addTable("tsdb-rollup-10m".getBytes(), families); + storage.addTable("tsdb-rollup-agg-10m".getBytes(), families); + storage.addTable("tsdb-rollup-1h".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1h".getBytes(), families); + storage.addTable("tsdb-rollup-1d".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1d".getBytes(), families); + storage.addTable(AGG_TABLE, families); + + final List rollups = new ArrayList(); + rollups.add(new RollupInterval( + "tsdb", "tsdb-agg", "1m", "1h", true)); + rollups.add(new RollupInterval( + "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "1d")); + rollups.add(new RollupInterval( + "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1m")); + rollups.add(new RollupInterval( + "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1y")); + + rollup_config = new RollupConfig(rollups); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + + mockUID(UniqueIdType.TAGK, "_aggregate", new byte[] { 0, 0, 42 }); + mockUID(UniqueIdType.TAGV, "SUM", new byte[] { 0, 0, 42 }); + mockUID(UniqueIdType.TAGV, "MAX", new byte[] { 0, 0, 43 }); + mockUID(UniqueIdType.TAGV, "MIN", new byte[] { 0, 0, 44 }); + mockUID(UniqueIdType.TAGV, "COUNT", new byte[] { 0, 0, 45 }); + mockUID(UniqueIdType.TAGV, "AVG", new byte[] { 0, 0, 46 }); + mockUID(UniqueIdType.TAGV, "NOSUCHAGG", new byte[] { 0, 0, 47 }); + } + + @Test + public void addAggregatePointLong1Byte() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong1ByteNegative() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {(byte) 0xD6}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong2Bytes() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 1}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 257, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {1, 1}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong2BytesNegative() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 1}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -257, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {(byte) 0xFE, (byte) 0xFF}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong4Bytes() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 3}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 65537, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0, 1, 0, 1}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong4BytesNegative() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 3}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -65537, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = + {(byte) 0xFF, (byte) 0xFE, (byte) 0xFF, (byte) 0xFF}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong8Bytes() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 7}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 4294967296L, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0, 0, 0, 1, 0, 0, 0, 0}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointLong8BytesNegative() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 7}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -4294967296L, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = + {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0, 0, 0, 0}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePointFloat4Bytes() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42.5F, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final float read = + Float.intBitsToFloat(Bytes.getInt(Arrays.copyOf(value, 4))); + assertEquals(42.5F, read, 0.0000001); + } + + @Test + public void addAggregatePointFloat4BytesNegative() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42.5F, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final float read = + Float.intBitsToFloat(Bytes.getInt(Arrays.copyOf(value, 4))); + assertEquals(-42.5F, read, 0.0000001); + } + + @Test + public void addAggregatePointFloat4BytesPrecision() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42.5123459999F, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final float read = + Float.intBitsToFloat(Bytes.getInt(Arrays.copyOf(value, 4))); + assertEquals(42.5123459999F, read, 0.0000001); + } + + @Test + public void addAggregatePointFloat4BytesPrecisionNegative() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42.5123459999F, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final float read = + Float.intBitsToFloat(Bytes.getInt(Arrays.copyOf(value, 4))); + assertEquals(-42.5123459999F, read, 0.0000001); + } + + // not allowing rollups with millisecond precision + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointMilliseconds() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1419992400000L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + } + + @Test (expected = NoSuchRollupForIntervalException.class) + public void addAggregatePointNoSuchRollup() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, + "11m", "sum").joinUninterruptibly(); + } + + @Test + public void addAggregatePoint10mInDayTop() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xAF, (byte) 0xD1, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1370476800, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint10mInDayMid() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xAF, (byte) 0xD1, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 5, (byte) 0xD0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1370532925L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint10mInDayEnd() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xAF, (byte) 0xD1, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 5, (byte) 0xF0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1370534399L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint10mInDayOver() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xB1, (byte) 0x22, (byte) 0x80, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1370563200L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1hInMonthTop() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xA9, (byte) 0x39, (byte) 0x80, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1370044800L, 42, tags, false, + "1h", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1hInMonthMid() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xA9, (byte) 0x39, (byte) 0x80, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0x2C, (byte) 0xF0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1372636799L, 42, tags, false, + "1h", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1hInMonthOver() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x51, (byte) 0xD0, (byte) 0xC6, (byte) 0x80, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1372636800L, 42, tags, false, + "1h", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1dInYearTop() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, (byte) 0x27, (byte) 0x00, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1d").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1dInYearMid() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, (byte) 0x27, (byte) 0x00, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 9, (byte) 0xC0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1370532925L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1d").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1dInYearEnd() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, (byte) 0x27, (byte) 0x00, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0x16, (byte) 0xC0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1388534399L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1d").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void addAggregatePoint1dInYearOver() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, + 0x52, (byte) 0xC3, (byte) 0x5A, (byte) 0x80, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + + tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1d").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test (expected = NoSuchUniqueName.class) + public void addAggregatePointNSUNMetric() throws Exception { + tsdb.addAggregatePoint(NSUN_METRIC, 1388534400L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + } + + @Test (expected = NoSuchUniqueName.class) + public void addAggregatePointNSUNTagK() throws Exception { + tags.clear(); + tags.put(NSUN_TAGK, TAGV_STRING); + tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + } + + @Test (expected = NoSuchUniqueName.class) + public void addAggregatePointNSUNTagV() throws Exception { + tags.put(TAGK_STRING, NSUN_TAGV); + tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, + "1d", "sum").joinUninterruptibly(); + } + + // This is allowed, we don't check the aggregation function in this method. + // It's up to the RPC level to check + @Test + public void addAggregatePointRollupNoSuchAgg() throws Exception { + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + "nosuchagg").joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1 }; + RowKey.prefixKeyWithSalt(row); + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, + "nosuchagg", interval))[0]); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointRollupsNotConfigured() throws Exception { + Whitebox.setInternalState(tsdb, "rollup_config", (RollupConfig)null); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + "nosuchagg").joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointNegativeTimestamp() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, -1356998400, 42, tags, false, "10m", + "sum").joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointEmptyTags() throws Exception { + tags.put(TAGK_STRING, ""); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + "nosuchagg").joinUninterruptibly(); + } + + // not allowed at this time + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointMS() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400000L, 42, tags, false, "10m", + "nosuchagg").joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointNullInterval() throws Exception { + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, null, + "sum").joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointEmptyInterval() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "", + "sum").joinUninterruptibly(); + } + + @Test (expected = NoSuchRollupForIntervalException.class) + public void addAggregatePointIntervalNotConfigured() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "6h", + "sum").joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointNullAggregator() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + null).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointEmptyAggregator() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + "").joinUninterruptibly(); + } + + @Test + public void addAggregatePointRollupRouting() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1 }; + RowKey.prefixKeyWithSalt(row); + + RollupInterval interval = rollup_config.getRollupInterval("10m"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + // make sure it didn't get into the tsdb table + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + + storage.flushStorage(); + + interval = rollup_config.getRollupInterval("1h"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", + "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + rollup_config.getRollupInterval("10m")))); + + storage.flushStorage(); + + interval = rollup_config.getRollupInterval("1d"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1d", + "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + rollup_config.getRollupInterval("1h")))); + + storage.flushStorage(); + // other aggs + interval = rollup_config.getRollupInterval("1h"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", + "max").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + interval))[0]); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", + "min").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + interval))[0]); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", + "count").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + interval))[0]); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", + "avg").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))[0]); + } + + @Test + public void addAggregatePointLongs() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1 }; + RowKey.prefixKeyWithSalt(row); + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + // 1 byte + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 0, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(0, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -42, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(-42, storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + + // 2 bytes + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 257, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(257, Bytes.getShort(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, "sum", + interval)))); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -257, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(-257, Bytes.getShort(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, "sum", + interval)))); + + // 4 bytes + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 65537, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(65537, Bytes.getInt(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, "sum", + interval)))); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -65537, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(-65537, Bytes.getInt(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, "sum", + interval)))); + + // 8 bytes + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 4294967296L, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(4294967296L, Bytes.getLong(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, "sum", + interval)))); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -4294967296L, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(-4294967296L, Bytes.getLong(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, "sum", + interval)))); + } + + @Test + public void addAggregatePointFloats() throws Exception { + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1 }; + RowKey.prefixKeyWithSalt(row); + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 0.0F, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(0.0, Float.intBitsToFloat(Bytes.getInt( + storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + interval)))), 0.0001); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42.5F, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(42.5, Float.intBitsToFloat(Bytes.getInt( + storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + interval)))), 0.0001); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -42.5F, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(-42.5, Float.intBitsToFloat(Bytes.getInt( + storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + interval)))), 0.0001); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42.5123459999F, tags, false, "10m", + "sum").joinUninterruptibly(); + assertEquals(42.5123459999F, Float.intBitsToFloat(Bytes.getInt( + storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + interval)))), 0.0000001); + } + + @Test + public void addAggregatePointGroupByRollupRouting() throws Exception { + byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2A }; + RowKey.prefixKeyWithSalt(row); + + RollupInterval interval = rollup_config.getRollupInterval("10m"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "10m", + "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + // make sure it didn't get into the tsdb table OR rollup table + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + + storage.flushStorage(); + + interval = rollup_config.getRollupInterval("1h"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", + "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + + storage.flushStorage(); + + interval = rollup_config.getRollupInterval("1d"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1d", + "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + + storage.flushStorage(); + // other aggs + row[row.length-1] = 0x2B; + RowKey.prefixKeyWithSalt(row); + interval = rollup_config.getRollupInterval("1h"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", + "max").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + interval))[0]); + + row[row.length-1] = 0x2C; + RowKey.prefixKeyWithSalt(row); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", + "min").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + interval))[0]); + + row[row.length-1] = 0x2D; + RowKey.prefixKeyWithSalt(row); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", + "count").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + interval))[0]); + + row[row.length-1] = 0x2E; + RowKey.prefixKeyWithSalt(row); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", + "avg").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))[0]); + } + + //This is allowed, we don't check the aggregation function in this method. + // It's up to the RPC level to check + @Test + public void addAggregatePointGroupByRollupNoSuchAgg() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "10m", + "nosuchagg").joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2F }; + RowKey.prefixKeyWithSalt(row); + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, + "nosuchagg", interval))[0]); + } + + //This is allowed, we don't check the aggregation function in this method. + // It's up to the RPC level to check + @Test + public void addAggregatePointGroupByNoSuchAgg() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + "nosuchagg").joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2F }; + RowKey.prefixKeyWithSalt(row); + assertEquals(42, storage.getColumn(AGG_TABLE, row, FAMILY, + new byte[] { 0, 0 })[0]); + } + + @Test + public void addAggregatePointGroupBy() throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + "sum").joinUninterruptibly(); + final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, + 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2A }; + RowKey.prefixKeyWithSalt(row); + assertEquals(42, storage.getColumn(AGG_TABLE, row, FAMILY, + new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, row, FAMILY, + new byte[] { 0, 0 })); + assertNull(storage.getColumn( + rollup_config.getRollupInterval("10m").getGroupbyTable(), row, FAMILY, + new byte[] { 0, 0 })); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 24, tags, true, null, + "count").joinUninterruptibly(); + row[row.length-1] = 0x2D; + RowKey.prefixKeyWithSalt(row); + assertEquals(24, storage.getColumn(AGG_TABLE, row, FAMILY, + new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, row, FAMILY, + new byte[] { 0, 0 })); + assertNull(storage.getColumn( + rollup_config.getRollupInterval("10m").getGroupbyTable(), row, FAMILY, + new byte[] { 0, 0 })); + } + + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointGroupByRollupsDisabled() throws Exception { + Whitebox.setInternalState(tsdb, "rollup_config", (Object) null); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + } + + @Test + public void dpFilterOK() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenReturn(Deferred.fromResult(true)); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void uidFilterBlocked() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenReturn(Deferred.fromResult(false)); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, + "10m", "sum").joinUninterruptibly(); + + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + assertNull(value); + } + + @Test + public void dpFilterReturnsException() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenReturn(Deferred.fromError(new UnitTestException("Boo!"))); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + final Deferred deferred = tsdb.addAggregatePoint(METRIC_STRING, + 1356998400L, 42, tags, false, "10m", "sum"); + + try { + deferred.join(); + fail("Expected an UnitTestException"); + } catch (UnitTestException e) { }; + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + assertNull(value); + } + + @Test + public void dpFilterThrowsException() throws Exception { + final WriteableDataPointFilterPlugin filter = + mock(WriteableDataPointFilterPlugin.class); + when(filter.filterDataPoints()).thenReturn(true); + when(filter.allowDataPoint(eq(METRIC_STRING), anyLong(), + any(byte[].class), eq(tags), anyShort())) + .thenThrow(new UnitTestException("Boo!")); + Whitebox.setInternalState(tsdb, "ts_filter", filter); + + try { + tsdb.addAggregatePoint(METRIC_STRING, + 1356998400L, 42, tags, false, "10m", "sum"); + fail("Expected an UnitTestException"); + } catch (UnitTestException e) { }; + final byte[] row = new byte[] { 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("10m").getTemporalTable(), + row, FAMILY, qualifier); + assertNull(value); + } +} diff --git a/test/meta/TestTSUIDQuery.java b/test/meta/TestTSUIDQuery.java index 39923cc412..b92b01bcf4 100644 --- a/test/meta/TestTSUIDQuery.java +++ b/test/meta/TestTSUIDQuery.java @@ -18,6 +18,7 @@ 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.List; @@ -675,7 +676,7 @@ public void getLastPointTSUIDTagkNSUINotResolved() throws Exception { assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); } - @Test (expected = NoSuchUniqueId.class) + @Test public void getLastPointTSUIDTagkNSUI() throws Exception { Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); Whitebox.setInternalState(config, "enable_realtime_ts", false); @@ -686,7 +687,12 @@ public void getLastPointTSUIDTagkNSUI() throws Exception { PowerMockito.mockStatic(DateTime.class); PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); query = new TSUIDQuery(tsdb, tsuid); - query.getLastPoint(true, 0).join(); + try { + query.getLastPoint(true, 0).join(); + fail("Expected DeferredGroupException"); + } catch (DeferredGroupException e) { + assertTrue(e.getCause() instanceof NoSuchUniqueId); + } } @Test @@ -708,7 +714,7 @@ public void getLastPointTSUIDTagvNSUINotResolved() throws Exception { assertEquals(UniqueId.uidToString(tsuid), dp.getTSUID()); } - @Test (expected = NoSuchUniqueId.class) + @Test public void getLastPoitTSUIDTagvNSUI() throws Exception { Whitebox.setInternalState(config, "enable_tsuid_incrementing", false); Whitebox.setInternalState(config, "enable_realtime_ts", false); @@ -719,7 +725,12 @@ public void getLastPoitTSUIDTagvNSUI() throws Exception { PowerMockito.mockStatic(DateTime.class); PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1356998400000L); query = new TSUIDQuery(tsdb, tsuid); - query.getLastPoint(true, 0).join(); + try { + query.getLastPoint(true, 0).join(); + fail("Expected DeferredGroupException"); + } catch (DeferredGroupException e) { + assertTrue(e.getCause() instanceof NoSuchUniqueId); + } } @Test diff --git a/test/query/expression/BaseTimeSyncedIteratorTest.java b/test/query/expression/BaseTimeSyncedIteratorTest.java index 252fbb8796..1e6be27dd4 100644 --- a/test/query/expression/BaseTimeSyncedIteratorTest.java +++ b/test/query/expression/BaseTimeSyncedIteratorTest.java @@ -113,7 +113,7 @@ protected void queryA_DD() throws Exception { /** * Executes the queries against MockBase through the regular pipeline and stores - * the results in {@linke #results} + * the results in {@link #results} * @param subs The queries to execute */ protected void runQueries(final ArrayList subs) throws Exception { diff --git a/test/query/expression/TestExpressionIterator.java b/test/query/expression/TestExpressionIterator.java index cd90c88d31..1bc059907a 100644 --- a/test/query/expression/TestExpressionIterator.java +++ b/test/query/expression/TestExpressionIterator.java @@ -69,7 +69,7 @@ public void aPlusBWithTwoSeries() throws Exception { oneExtraSameE(); queryAB_Dstar(); remapResults(); - + storage.dumpToSystemOut(); ExpressionIterator exp = new ExpressionIterator("ei", "a + b", SetOperator.INTERSECTION, false, false); exp.addResults("a", iterators.get("a")); @@ -101,8 +101,8 @@ public void aPlusBWithTwoSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -138,8 +138,8 @@ public void aMinusBWithTwoSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -188,8 +188,8 @@ public void aTimesBWithTwoSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -238,8 +238,8 @@ public void aDivideBWithTwoSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -276,8 +276,8 @@ public void aModBWithTwoSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -314,8 +314,8 @@ public void aDivideByZeroWithTwoSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -364,8 +364,8 @@ public void doubleVariableAndPrecedence() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -414,8 +414,8 @@ public void doubleVariableAndPrecedenceChanged() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -452,8 +452,8 @@ public void aPlusScalarDropB() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test (expected = IllegalArgumentException.class) @@ -522,9 +522,9 @@ public void aPlusBMissingPointsDefaultFillZero() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("G"), dps[2].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("G"), dps[2].tags().get(UIDS.get("D"))); } @Test @@ -581,9 +581,9 @@ public void aPlusBMissingPointsFillOne() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("G"), dps[2].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("G"), dps[2].tags().get(UIDS.get("D"))); } @Test @@ -642,9 +642,9 @@ public void aPlusBMissingPointsFillInfectiousNaN() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("G"), dps[2].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("G"), dps[2].tags().get(UIDS.get("D"))); } @Test @@ -701,8 +701,8 @@ public void aPlusBResultsOffsetDefaultFill() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -737,8 +737,8 @@ public void aPlusBOneAggedOneTaggedUseQueryTagsWoutQueryTags() throws Exception assertEquals(2, dps[0].tags().size()); assertEquals(2, dps[0].aggregatedTags().size()); - assertTrue(dps[0].aggregatedTags().contains(TAGV_UIDS.get("D"))); - assertTrue(dps[0].aggregatedTags().contains(TAGV_UIDS.get("E"))); + assertTrue(dps[0].aggregatedTags().contains(UIDS.get("D"))); + assertTrue(dps[0].aggregatedTags().contains(UIDS.get("E"))); // TODO - make sure the tags are empty once the expression data does it's // thing //assertTrue(dps[0].tags().isEmpty()); @@ -786,8 +786,8 @@ public void singleNestedExpression() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -837,8 +837,8 @@ public void doubleNestedExpression() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test (expected = IllegalDataException.class) @@ -924,8 +924,8 @@ public void unionOneExtraSeries() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -982,8 +982,8 @@ public void unionOffset() throws Exception { assertEquals(2, dps[i].tags().size()); assertTrue(dps[i].aggregatedTags().isEmpty()); } - assertArrayEquals(TAGV_UIDS.get("D"), dps[0].tags().get(TAGV_UIDS.get("D"))); - assertArrayEquals(TAGV_UIDS.get("F"), dps[1].tags().get(TAGV_UIDS.get("D"))); + assertArrayEquals(UIDS.get("D"), dps[0].tags().get(UIDS.get("D"))); + assertArrayEquals(UIDS.get("F"), dps[1].tags().get(UIDS.get("D"))); } @Test @@ -1091,9 +1091,9 @@ private void validateMeta(final ExpressionDataPoint[] dps, // arrays boolean found = false; for (final byte[] metric : dps[i].metricUIDs()) { - if (Bytes.memcmp(TAGV_UIDS.get("A"), metric) == 0) { + if (Bytes.memcmp(UIDS.get("A"), metric) == 0) { found = true; - } else if (Bytes.memcmp(TAGV_UIDS.get("B"), metric) == 0) { + } else if (Bytes.memcmp(UIDS.get("B"), metric) == 0) { found = true; break; } @@ -1103,7 +1103,7 @@ private void validateMeta(final ExpressionDataPoint[] dps, } if (common_e) { - assertArrayEquals(TAGV_UIDS.get("E"), dps[i].tags().get(TAGV_UIDS.get("E"))); + assertArrayEquals(UIDS.get("E"), dps[i].tags().get(UIDS.get("E"))); } } } From 6500afbeef0ada3decbb7a6f0de7d9a1255ec934 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Sat, 22 Oct 2016 13:32:35 -0700 Subject: [PATCH 567/826] Modify PutDataPointRpc to allow for overrides when the Rollup datapoint RPC is added. Also add some unit tests and change the base TSDB test a bit. Signed-off-by: Chris Larsen --- src/tsd/HttpJsonSerializer.java | 42 +- src/tsd/HttpSerializer.java | 19 + src/tsd/PutDataPointRpc.java | 396 +++++++--- src/tsd/RollupDataPointRpc.java | 223 ++++++ src/tsd/RpcManager.java | 2 +- src/utils/Config.java | 1 + test/core/BaseTsdbTest.java | 4 +- test/tsd/BaseTestPutRpc.java | 161 ++++ test/tsd/NettyMocks.java | 10 +- test/tsd/TestPutRpc.java | 1211 ++++++++++++++----------------- test/tsd/TestRollupRpc.java | 663 +++++++++++++++++ test/tsd/TestSuggestRpc.java | 2 +- 12 files changed, 1968 insertions(+), 766 deletions(-) create mode 100644 src/tsd/RollupDataPointRpc.java create mode 100644 test/tsd/BaseTestPutRpc.java create mode 100644 test/tsd/TestRollupRpc.java diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index b29a834bab..3a4a48daf6 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -43,6 +43,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; +import net.opentsdb.rollup.RollUpDataPoint; import net.opentsdb.search.SearchQuery; import net.opentsdb.stats.QueryStats; import net.opentsdb.stats.QueryStats.QueryStat; @@ -66,9 +67,13 @@ class HttpJsonSerializer extends HttpSerializer { /** Type reference for incoming data points */ - private static TypeReference> TR_INCOMING = + static TypeReference> TR_INCOMING = new TypeReference>() {}; + /** Type reference for rollup data points */ + public static TypeReference> TR_ROLLUP = + new TypeReference>() {}; + /** Type reference for uid assignments */ private static TypeReference>> UID_ASSIGN = new TypeReference>>() {}; @@ -151,6 +156,41 @@ public List parsePutV1() { } } + /** + * Parses one or more data points for storage + * @return an array of data points to process for storage + * @throws JSONException if parsing failed + * @throws BadRequestException if the content was missing or parsing failed + * @since 2.4 + */ + @Override + public List parsePutV1( + final Class type, final TypeReference> typeReference) { + if (!query.hasContent()) { + throw new BadRequestException("Missing request content"); + } + + // convert to a string so we can handle character encoding properly + final String content = query.getContent().trim(); + final int firstbyte = content.charAt(0); + try { + if (firstbyte == '{') { + final T dp = + JSON.parseToObject(content, type); + final ArrayList dps = + new ArrayList(1); + dps.add(dp); + return dps; + } else if (firstbyte == '[') { + return JSON.parseToObject(content, typeReference); + } else { + throw new BadRequestException("The JSON must start as an object or an array"); + } + } catch (IllegalArgumentException iae) { + throw new BadRequestException("Unable to parse the given JSON", iae); + } + } + /** * Parses a suggestion query * @return a hash map of key/value pairs diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 1022177224..8b9a343c9b 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -13,6 +13,7 @@ package net.opentsdb.tsd; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,6 +27,7 @@ import ch.qos.logback.classic.spi.ThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxyUtil; +import com.fasterxml.jackson.core.type.TypeReference; import com.stumbleupon.async.Deferred; import net.opentsdb.core.DataPoints; @@ -174,6 +176,23 @@ public List parsePutV1() { " has not implemented parsePutV1"); } + /** + * Parses one or more data points for storage + * @param The type of incoming data points to parse. + * @param type The type of the class to parse. + * @param typeReference The reference to use for parsing. + * @return an array of data points to process for storage + * @throws BadRequestException if the plugin has not implemented this method + * @since 2.4 + */ + public List parsePutV1(final Class type, + final TypeReference> typeReference) { + throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED, + "The requested API endpoint has not been implemented", + this.getClass().getCanonicalName() + + " has not implemented parsePutV1"); + } + /** * Parses a suggestion query * @return a hash map of key/value pairs diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 71cd8ecc5d..e13d199e7e 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-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 @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -24,6 +25,8 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.TimeoutException; +import org.hbase.async.HBaseException; +import org.hbase.async.PleaseThrottleException; import org.jboss.netty.channel.Channel; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; @@ -35,65 +38,194 @@ import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.rollup.NoSuchRollupForIntervalException; +import net.opentsdb.rollup.RollUpDataPoint; import net.opentsdb.stats.StatsCollector; import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.utils.Config; -/** Implements the "put" telnet-style command. */ -final class PutDataPointRpc implements TelnetRpc, HttpRpc { - private static final Logger LOG = LoggerFactory.getLogger(PutDataPointRpc.class); - private static final ArrayList EMPTY_DEFERREDS = +/** + * This class handles the parsing and writing of data points over any of the + * implemented RPCs. Each put method should track the deferred of the + * {@code tsdb.addPoint()} method so that we know whether or not the write was + * actually successful. Note that if HBase is backed up, then the arguments + * will hang around until the deferred is called back. This could take many + * seconds, during which time the heap will keep growing. + *

    + * Each execute method also checks the tsd's {@code StorageExceptionHandler} + * object on failure so that we can do something else with the data point such + * as spool it to disk or send it back to an external queue. We do that in the + * error callback here instead of in the TSDB {@code addPoint()} because we + * want to avoid adding yet another callback and chewing up more heap + * unnecessarily. + *

    + * Note that this class can be subclassed to handle different types of + * data points such as Rollups or Pre-Aggregates + */ +class PutDataPointRpc implements TelnetRpc, HttpRpc { + protected static final Logger LOG = LoggerFactory.getLogger(PutDataPointRpc.class); + protected static final ArrayList EMPTY_DEFERREDS = new ArrayList(0); - 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(); - private static final AtomicLong writes_blocked = new AtomicLong(); - private static final AtomicLong writes_timedout = new AtomicLong(); + protected static final AtomicLong telnet_requests = new AtomicLong(); + protected static final AtomicLong http_requests = new AtomicLong(); + protected static final AtomicLong raw_dps = new AtomicLong(); + protected static final AtomicLong rollup_dps = new AtomicLong(); + protected static final AtomicLong raw_stored = new AtomicLong(); + protected static final AtomicLong rollup_stored = new AtomicLong(); + protected static final AtomicLong hbase_errors = new AtomicLong(); + protected static final AtomicLong unknown_errors = new AtomicLong(); + protected static final AtomicLong invalid_values = new AtomicLong(); + protected static final AtomicLong illegal_arguments = new AtomicLong(); + protected static final AtomicLong unknown_metrics = new AtomicLong(); + protected static final AtomicLong inflight_exceeded = new AtomicLong(); + protected static final AtomicLong writes_blocked = new AtomicLong(); + protected static final AtomicLong writes_timedout = new AtomicLong(); + protected static final AtomicLong requests_timedout = new AtomicLong(); + /** Whether or not to send error messages back over telnet */ + private final boolean send_telnet_errors; + + /** The type of data point we're writing. + * @since 2.4 */ + public enum DataPointType { + PUT("put"), + ROLLUP("rollup"); + + private final String name; + DataPointType(final String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + /** + * Default Ctor + * @param config The TSDB config to pull from + */ + public PutDataPointRpc(final Config config) { + send_telnet_errors = config.getBoolean("tsd.rpc.telnet.return_errors"); + } + + @Override public Deferred execute(final TSDB tsdb, final Channel chan, final String[] cmd) { - requests.incrementAndGet(); + telnet_requests.incrementAndGet(); + final DataPointType type; + final String command = cmd[0].toLowerCase(); + if (command.equals("put")) { + type = DataPointType.PUT; + raw_dps.incrementAndGet(); + } else if (command.equals("rollup")) { + type = DataPointType.ROLLUP; + rollup_dps.incrementAndGet(); + } else { + throw new IllegalArgumentException("Unrecognized command: " + cmd[0]); + } + String errmsg = null; try { - final class PutErrback implements Callback { - public Exception call(final Exception arg) { + + /** + * Error callback that handles passing a data point to the storage + * exception handler as well as responding to the client when HBase + * is unable to write the data. + */ + final class PutErrback implements Callback { + @Override + public Object call(final Exception arg) { + String errmsg = null; + if (arg instanceof PleaseThrottleException) { + if (send_telnet_errors) { + errmsg = type + ": Please throttle writes: " + arg.getMessage() + '\n'; + } + inflight_exceeded.incrementAndGet(); + } else { + if (send_telnet_errors) { + errmsg = type + ": HBase error: " + arg.getMessage()+ '\n'; + } + if (arg instanceof HBaseException) { + hbase_errors.incrementAndGet(); + } + } + // we handle the storage exceptions here so as to avoid creating yet // another callback object on every data point. handleStorageException(tsdb, getDataPointFromString(cmd), arg); - if (chan.isConnected()) { - if (chan.isWritable()) { - chan.write("put: HBase error: " + arg.getMessage() + '\n'); - } else { - writes_blocked.incrementAndGet(); + + if (send_telnet_errors) { + if (chan.isConnected()) { + if (chan.isWritable()) { + chan.write(errmsg); + } else { + writes_blocked.incrementAndGet(); + } } } - hbase_errors.incrementAndGet(); + return null; } public String toString() { return "report error to channel"; } } - return importDataPoint(tsdb, cmd).addErrback(new PutErrback()); + + /** + * Simply called to increment the success counter and log hearbeats + */ + final class SuccessCB implements Callback { + @Override + public Object call(final Object obj) { + if (type == DataPointType.PUT) { + raw_stored.incrementAndGet(); + } else { + rollup_stored.incrementAndGet(); + } + return true; + } + } + + // Rollups override this method in their implementation so that it will + // route properly. + return importDataPoint(tsdb, cmd) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); } catch (NumberFormatException x) { - errmsg = "put: invalid value: " + x.getMessage() + '\n'; + errmsg = type + ": invalid value: " + x.getMessage() + '\n'; invalid_values.incrementAndGet(); + } catch (NoSuchRollupForIntervalException x) { + errmsg = type + ": No such rollup: " + x.getMessage() + '\n'; + illegal_arguments.incrementAndGet(); } catch (IllegalArgumentException x) { - errmsg = "put: illegal argument: " + x.getMessage() + '\n'; + errmsg = type + ": illegal argument: " + x.getMessage() + '\n'; illegal_arguments.incrementAndGet(); } catch (NoSuchUniqueName x) { - errmsg = "put: unknown metric: " + x.getMessage() + '\n'; + errmsg = type + ": unknown metric: " + x.getMessage() + '\n'; unknown_metrics.incrementAndGet(); + /*} catch (NoSuchUniqueNameInCache x) { + errmsg = "put: waiting for cache: " + x.getMessage() + '\n'; + handleStorageException(tsdb, getDataPointFromString(cmd), x); */ + } catch (PleaseThrottleException x) { + errmsg = type + ": Throttling exception: " + x.getMessage() + '\n'; + inflight_exceeded.incrementAndGet(); + handleStorageException(tsdb, getDataPointFromString(cmd), x); + } catch (TimeoutException tex) { + errmsg = type + ": Request timed out: " + tex.getMessage() + '\n'; + handleStorageException(tsdb, getDataPointFromString(cmd), tex); } - if (errmsg != null) { - LOG.debug(errmsg); - if (chan.isConnected()) { - if (chan.isWritable()) { - chan.write(errmsg); - } else { - writes_blocked.incrementAndGet(); - } + catch (RuntimeException rex) { + errmsg = type + ": Unexpected runtime exception: " + rex.getMessage() + '\n'; + throw rex; + } + + if (errmsg != null && chan.isConnected()) { + if (chan.isWritable()) { + chan.write(errmsg); + } else { + writes_blocked.incrementAndGet(); } } return Deferred.fromResult(null); @@ -108,9 +240,10 @@ public String toString() { * @throws BadRequestException if the user supplied bad data * @since 2.0 */ + @Override public void execute(final TSDB tsdb, final HttpQuery query) throws IOException { - requests.incrementAndGet(); + http_requests.incrementAndGet(); // only accept POST if (query.method() != HttpMethod.POST) { @@ -118,12 +251,33 @@ public void execute(final TSDB tsdb, final HttpQuery query) "Method not allowed", "The HTTP method [" + query.method().getName() + "] is not permitted for this endpoint"); } - - final List dps = query.serializer().parsePutV1(); + final List dps; + try { + dps = query.serializer() + .parsePutV1(IncomingDataPoint.class, HttpJsonSerializer.TR_INCOMING); + } catch (BadRequestException e) { + illegal_arguments.incrementAndGet(); + throw e; + } + processDataPoint(tsdb, query, dps); + } + + /** + * Handles one or more incoming data point types for the HTTP endpoint + * to put raw, rolled up or aggregated data points + * @param An {@link IncomingDataPoint} class. + * @param tsdb The TSDB to which we belong + * @param query The query to respond to + * @param dps The de-serialized data points + * @throws BadRequestException if the data is invalid in some way + * @since 2.4 + */ + public void processDataPoint(final TSDB tsdb, + final HttpQuery query, final List dps) { 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 boolean synchronous = query.hasQueryStringParam("sync"); @@ -132,111 +286,153 @@ public void execute(final TSDB tsdb, final HttpQuery query) // this is used to coordinate timeouts final AtomicBoolean sending_response = new AtomicBoolean(); sending_response.set(false); - - final ArrayList> details = show_details - ? new ArrayList>() : null; + + final List> details = show_details + ? new ArrayList>() : null; int queued = 0; final List> deferreds = synchronous ? new ArrayList>(dps.size()) : null; + for (final IncomingDataPoint dp : dps) { + final DataPointType type; + if (dp instanceof RollUpDataPoint) { + type = DataPointType.ROLLUP; + rollup_dps.incrementAndGet(); + } else { + type = DataPointType.PUT; + raw_dps.incrementAndGet(); + } - /** Handles passing a data point to the storage exception handler if - * we were unable to store it for any reason */ + /** + * Error back callback to handle storage failures + */ final class PutErrback implements Callback { public Boolean call(final Exception arg) { - handleStorageException(tsdb, dp, arg); - hbase_errors.incrementAndGet(); + if (arg instanceof PleaseThrottleException) { + inflight_exceeded.incrementAndGet(); + } else { + hbase_errors.incrementAndGet(); + } if (show_details) { details.add(getHttpDetails("Storage exception: " + arg.getMessage(), dp)); } + + // we handle the storage exceptions here so as to avoid creating yet + // another callback object on every data point. + handleStorageException(tsdb, dp, arg); return false; } public String toString() { - return "HTTP Put Exception CB"; + return "HTTP Put exception"; } } - - /** Simply marks the put as successful */ + final class SuccessCB implements Callback { @Override public Boolean call(final Object obj) { + switch (type) { + case PUT: + raw_stored.incrementAndGet(); + break; + case ROLLUP: + rollup_stored.incrementAndGet(); + break; + default: + // don't care + } return true; } - public String toString() { - return "HTTP Put success CB"; - } } 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); - illegal_arguments.incrementAndGet(); - continue; - } - if (dp.getTimestamp() <= 0) { - if (show_details) { - details.add(this.getHttpDetails("Invalid timestamp", dp)); - } - LOG.warn("Invalid timestamp: " + dp); - illegal_arguments.incrementAndGet(); - continue; - } - if (dp.getValue() == null || dp.getValue().isEmpty()) { - if (show_details) { - details.add(this.getHttpDetails("Empty value", dp)); - } - LOG.warn("Empty value: " + dp); - invalid_values.incrementAndGet(); - continue; - } - if (dp.getTags() == null || dp.getTags().size() < 1) { - if (show_details) { - details.add(this.getHttpDetails("Missing tags", dp)); - } - LOG.warn("Missing tags: " + dp); + if (!dp.validate(details)) { illegal_arguments.incrementAndGet(); continue; } - final Deferred deferred; + // TODO - refactor the add calls someday or move some of this into the + // actual data point class. + final Deferred deferred; if (Tags.looksLikeInteger(dp.getValue())) { - deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Tags.parseLong(dp.getValue()), dp.getTags()); + if (dp instanceof RollUpDataPoint) { + final RollUpDataPoint rdp = (RollUpDataPoint)dp; + deferred = tsdb.addAggregatePoint(rdp.getMetric(), rdp.getTimestamp(), + Tags.parseLong(rdp.getValue()), dp.getTags(), false, + rdp.getInterval(), rdp.getAggregator()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + } else { + deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), + Tags.parseLong(dp.getValue()), dp.getTags()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + } } else { - deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Float.parseFloat(dp.getValue()), dp.getTags()); + if (dp instanceof RollUpDataPoint) { + final RollUpDataPoint rdp = (RollUpDataPoint)dp; + deferred = tsdb.addAggregatePoint(rdp.getMetric(), rdp.getTimestamp(), + Float.parseFloat(rdp.getValue()), dp.getTags(), false, + rdp.getInterval(), rdp.getAggregator()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + } else { + deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), + Float.parseFloat(dp.getValue()), dp.getTags()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + } } + ++queued; if (synchronous) { - deferreds.add(deferred.addCallback(new SuccessCB())); + deferreds.add(deferred); } - deferred.addErrback(new PutErrback()); - ++queued; + } catch (NumberFormatException x) { if (show_details) { - details.add(this.getHttpDetails("Unable to parse value to a number", + details.add(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)); + details.add(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)); + details.add(getHttpDetails("Unknown metric", dp)); } LOG.warn("Unknown metric: " + dp); unknown_metrics.incrementAndGet(); + } catch (PleaseThrottleException x) { + handleStorageException(tsdb, dp, x); + if (show_details) { + details.add(getHttpDetails("Please throttle", dp)); + } + inflight_exceeded.incrementAndGet(); + } catch (TimeoutException tex) { + handleStorageException(tsdb, dp, tex); + if (show_details) { + details.add(getHttpDetails("Timeout exception", dp)); + } + requests_timedout.incrementAndGet(); + /*} catch (NoSuchUniqueNameInCache x) { + handleStorageException(tsdb, dp, x); + if (show_details) { + details.add(getHttpDetails("Not cached yet", dp)); + } */ + } catch (RuntimeException e) { + if (show_details) { + details.add(getHttpDetails("Unexpected exception", dp)); + } + LOG.warn("Unexpected exception: " + dp); + unknown_errors.incrementAndGet(); } } - + /** A timer task that will respond to the user with the number of timeouts * for synchronous writes. */ class PutTimeout implements TimerTask { @@ -377,7 +573,7 @@ class ErrCB implements Callback { public Object call(final Exception e) throws Exception { if (sending_response.get()) { if (LOG.isDebugEnabled()) { - LOG.debug("Put data point call " + query + " was marked as timedout"); + LOG.debug("ERROR point call " + query + " was marked as timedout", e); } return null; } else { @@ -396,7 +592,8 @@ public String toString() { } if (synchronous) { - Deferred.groupInOrder(deferreds).addCallback(new GroupCB(queued)) + Deferred.groupInOrder(deferreds) + .addCallback(new GroupCB(queued)) .addErrback(new ErrCB()); } else { new GroupCB(queued).call(EMPTY_DEFERREDS); @@ -408,7 +605,7 @@ public String toString() { * @param collector The collector to use. */ public static void collectStats(final StatsCollector collector) { - collector.record("rpc.received", requests, "type=put"); + collector.record("rpc.received", http_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"); @@ -426,12 +623,14 @@ public static void collectStats(final StatsCollector collector) { * @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) { + protected 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) + ')'); + + " (need least 4, got " + + (words.length - 1) + ')'); } final String metric = words[1]; if (metric.length() <= 0) { @@ -462,8 +661,7 @@ private Deferred importDataPoint(final TSDB tsdb, final String[] words) return tsdb.addPoint(metric, timestamp, Float.parseFloat(value), tags); } } - - + /** * Converts the string array to an IncomingDataPoint. WARNING: This method * does not perform validation. It should only be used by the Telnet style @@ -472,7 +670,7 @@ private Deferred importDataPoint(final TSDB tsdb, final String[] words) * @param words The array of strings representing a data point * @return An incoming data point object. */ - final private IncomingDataPoint getDataPointFromString(final String[] words) { + protected IncomingDataPoint getDataPointFromString(final String[] words) { final IncomingDataPoint dp = new IncomingDataPoint(); dp.setMetric(words[1]); diff --git a/src/tsd/RollupDataPointRpc.java b/src/tsd/RollupDataPointRpc.java new file mode 100644 index 0000000000..32202321db --- /dev/null +++ b/src/tsd/RollupDataPointRpc.java @@ -0,0 +1,223 @@ +// 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.IncomingDataPoint; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.rollup.RollUpDataPoint; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.utils.Config; + +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 java.io.IOException; +import java.util.HashMap; +import java.util.List; + +/** + * A class that handles overriding parsing calls when writing a rolled up data + * point to storage. + * @since 2.4 + */ +class RollupDataPointRpc extends PutDataPointRpc + implements TelnetRpc, HttpRpc { + private static final Logger LOG = LoggerFactory.getLogger(RollupDataPointRpc.class); + + private enum TelnetIndex { + COMMAND, + INTERVAL_AGG, + METRIC, + TIMESTAMP, + VALUE, + TAGS + } + + /** + * Default Ctor + * @param config The TSDB config to pull from + */ + public RollupDataPointRpc(final Config config) { + super(config); + } + + /** + * 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 + */ + @Override + public void execute(final TSDB tsdb, final HttpQuery query) + throws IOException { + 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"); + } + + final List dps = query.serializer() + .parsePutV1(RollUpDataPoint.class, HttpJsonSerializer.TR_ROLLUP); + processDataPoint(tsdb, query, dps); + } + + /** + * Imports a single rolled up 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 rollup interval:[aggregator] metric timestamp value ..tags..} + * @return A deferred object that indicates the completion of the request. + * @throws NumberFormatException if the timestamp, value or count is invalid. + * @throws IllegalArgumentException if any other argument is invalid. + * @throws NoSuchUniqueName if the metric isn't registered. + */ + @Override + protected Deferred importDataPoint(final TSDB tsdb, + final String[] words) { + words[TelnetIndex.COMMAND.ordinal()] = null; // Ditch the "rollup string". + if (words.length < TelnetIndex.TAGS.ordinal() + 1) { + throw new IllegalArgumentException("not enough arguments" + + " (need least 7, got " + (words.length - 1) + ')'); + } + + final String interval_agg = words[TelnetIndex.INTERVAL_AGG.ordinal()]; + if (interval_agg.isEmpty()) { + throw new IllegalArgumentException("Missing interval or aggregator"); + } + + String interval = null; + String temporal_agg = null; + String spatial_agg = null; + // if the interval_agg has a - in it, then it's an interval. If there's a : + // then it is both. If no dash or colon then it's just a spatial agg. + final String[] interval_parts = interval_agg.split(":"); + final int dash = interval_parts[0].indexOf("-"); + if (dash > -1) { + interval = interval_parts[0].substring(0,dash); + temporal_agg = interval_parts[0].substring(dash + 1); + + } else if (interval_parts.length == 1) { + spatial_agg = interval_parts[0]; + } + if (interval_parts.length > 1) { + spatial_agg = interval_parts[1]; + } + + final String metric = words[TelnetIndex.METRIC.ordinal()]; + if (metric.length() <= 0) { + throw new IllegalArgumentException("empty metric name"); + } + + final long timestamp; + if (words[TelnetIndex.TIMESTAMP.ordinal()].contains(".")) { + timestamp = Tags.parseLong(words[TelnetIndex.TIMESTAMP.ordinal()] + .replace(".", "")); + } else { + timestamp = Tags.parseLong(words[TelnetIndex.TIMESTAMP.ordinal()]); + } + if (timestamp <= 0) { + throw new IllegalArgumentException("invalid timestamp: " + timestamp); + } + + final String value = words[TelnetIndex.VALUE.ordinal()]; + if (value.length() <= 0) { + throw new IllegalArgumentException("empty value"); + } + + final HashMap tags = new HashMap(); + for (int i = TelnetIndex.TAGS.ordinal(); i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + + if (spatial_agg != null && temporal_agg == null) { + temporal_agg = spatial_agg; + } + + if (Tags.looksLikeInteger(value)) { + return tsdb.addAggregatePoint(metric, timestamp, Tags.parseLong(value), + tags, spatial_agg != null ? true : false, interval, temporal_agg); + } else { // floating point value + return tsdb.addAggregatePoint(metric, timestamp, Float.parseFloat(value), + tags, spatial_agg != null ? true : false, interval, temporal_agg); + } + } + + /** + * Converts the string array to an IncomingDataPoint. WARNING: This method + * does not perform validation. It should only be used by the Telnet style + * {@code execute} above within the error callback. At that point it means + * the array parsed correctly as per {@code importDataPoint}. + * @param words The array of strings representing a data point + * @return An incoming data point object. + */ + @Override + protected IncomingDataPoint getDataPointFromString(final String[] words) { + final RollUpDataPoint dp = new RollUpDataPoint(); + + final String interval_agg = words[TelnetIndex.INTERVAL_AGG.ordinal()]; + String interval = null; + String temporal_agg = null; + String spatial_agg = null; + // if the interval_agg has a - in it, then it's an interval. If there's a : + // then it is both. If no dash or colon then it's just a spatial agg. + final String[] interval_parts = interval_agg.split(":"); + final int dash = interval_parts[0].indexOf("-"); + if (dash > -1) { + interval = interval_parts[0].substring(0,dash); + temporal_agg = interval_parts[0].substring(dash + 1); + + } else if (interval_parts.length == 1) { + spatial_agg = interval_parts[0]; + } + if (interval_parts.length > 1) { + spatial_agg = interval_parts[1]; + } + dp.setInterval(interval); + dp.setAggregator(temporal_agg); + // TODO - spatial agg + + dp.setMetric(words[TelnetIndex.METRIC.ordinal()]); + + if (words[TelnetIndex.TIMESTAMP.ordinal()].contains(".")) { + dp.setTimestamp(Tags.parseLong(words[TelnetIndex.TIMESTAMP.ordinal()] + .replace(".", ""))); + } else { + dp.setTimestamp(Tags.parseLong(words[TelnetIndex.TIMESTAMP.ordinal()])); + } + + dp.setValue(words[TelnetIndex.VALUE.ordinal()]); + + final HashMap tags = new HashMap(); + for (int i = TelnetIndex.TAGS.ordinal(); i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + dp.setTags(tags); + return dp; + } +} diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 86638c1a12..f3af3f271f 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -259,7 +259,7 @@ private void initializeBuiltinRpcs(final String mode, LOG.info("Mode: {}, HTTP UI Enabled: {}, HTTP API Enabled: {}", mode, enableUi, enableApi); if (mode.equals("rw") || mode.equals("wo")) { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); telnet.put("put", put); if (enableApi) { http.put("api/put", put); diff --git a/src/utils/Config.java b/src/utils/Config.java index 27f859fb1e..f51a1e98e7 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -508,6 +508,7 @@ protected void setDefaults() { default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.query.enable_fuzzy_filter", "true"); + default_map.put("tsd.rpc.telnet.return_errors", "true"); default_map.put("tsd.rollups.enable", "false"); default_map.put("tsd.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index eeda244510..ede3c260a2 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -100,7 +100,7 @@ public class BaseTsdbTest { } } - protected HashedWheelTimer timer; + protected FakeTaskTimer timer; protected Config config; protected TSDB tsdb; protected HBaseClient client = mock(HBaseClient.class); @@ -113,7 +113,7 @@ public class BaseTsdbTest { @Before public void before() throws Exception { PowerMockito.mockStatic(Threads.class); - timer = mock(HashedWheelTimer.class); + timer = new FakeTaskTimer(); PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); diff --git a/test/tsd/BaseTestPutRpc.java b/test/tsd/BaseTestPutRpc.java new file mode 100644 index 0000000000..b6bc2eb514 --- /dev/null +++ b/test/tsd/BaseTestPutRpc.java @@ -0,0 +1,161 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.atomic.AtomicLong; + +import org.hbase.async.HBaseClient; +import org.hbase.async.PleaseThrottleException; +import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; +import org.junit.Before; +import org.junit.Ignore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.TimeoutException; + +import net.opentsdb.core.TSDB; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.core.BaseTsdbTest; +import net.opentsdb.core.Const; +import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; + +@Ignore +@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, UniqueId.class, + HBaseClient.class, HashedWheelTimer.class, Scanner.class, Const.class, Threads.class, + TimeoutException.class, StorageExceptionHandler.class, PutDataPointRpc.class, + PleaseThrottleException.class, RollupDataPointRpc.class }) +public class BaseTestPutRpc extends BaseTsdbTest { + protected AtomicLong telnet_requests = new AtomicLong(); + protected AtomicLong http_requests = new AtomicLong(); + protected AtomicLong raw_dps = new AtomicLong(); + protected AtomicLong rollup_dps = new AtomicLong(); + protected AtomicLong raw_stored = new AtomicLong(); + protected AtomicLong rollup_stored = new AtomicLong(); + protected AtomicLong hbase_errors = new AtomicLong(); + protected AtomicLong unknown_errors = new AtomicLong(); + protected AtomicLong invalid_values = new AtomicLong(); + protected AtomicLong illegal_arguments = new AtomicLong(); + protected AtomicLong unknown_metrics = new AtomicLong(); + protected AtomicLong inflight_exceeded = new AtomicLong(); + protected AtomicLong writes_blocked = new AtomicLong(); + protected AtomicLong writes_timedout = new AtomicLong(); + protected AtomicLong requests_timedout = new AtomicLong(); + protected StorageExceptionHandler handler; + + @Before + public void beforeCounters() throws Exception { + telnet_requests = Whitebox.getInternalState(PutDataPointRpc.class, "telnet_requests"); + telnet_requests.set(0); + http_requests = Whitebox.getInternalState(PutDataPointRpc.class, "http_requests"); + http_requests.set(0); + raw_dps = Whitebox.getInternalState(PutDataPointRpc.class, "raw_dps"); + raw_dps.set(0); + rollup_dps = Whitebox.getInternalState(PutDataPointRpc.class, "rollup_dps"); + rollup_dps.set(0); + hbase_errors = Whitebox.getInternalState(PutDataPointRpc.class, "hbase_errors"); + hbase_errors.set(0); + unknown_errors = Whitebox.getInternalState(PutDataPointRpc.class, "unknown_errors"); + unknown_errors.set(0); + raw_stored = Whitebox.getInternalState(PutDataPointRpc.class, "raw_stored"); + raw_stored.set(0); + rollup_stored = Whitebox.getInternalState(PutDataPointRpc.class, "rollup_stored"); + rollup_stored.set(0); + invalid_values = Whitebox.getInternalState(PutDataPointRpc.class, "invalid_values"); + invalid_values.set(0); + illegal_arguments = Whitebox.getInternalState(PutDataPointRpc.class, "illegal_arguments"); + illegal_arguments.set(0); + unknown_metrics = Whitebox.getInternalState(PutDataPointRpc.class, "unknown_metrics"); + unknown_metrics.set(0); + inflight_exceeded = Whitebox.getInternalState(PutDataPointRpc.class, "inflight_exceeded"); + inflight_exceeded.set(0); + writes_blocked = Whitebox.getInternalState(PutDataPointRpc.class, "writes_blocked"); + writes_blocked.set(0); + writes_timedout = Whitebox.getInternalState(PutDataPointRpc.class, "writes_timedout"); + writes_timedout.set(0); + requests_timedout = Whitebox.getInternalState(PutDataPointRpc.class, "requests_timedout"); + requests_timedout.set(0); + } + + /** + * Helper to set the storage exception handler in the TSDB under test. + */ + protected void setStorageExceptionHandler() { + handler = mock(StorageExceptionHandler.class); + Whitebox.setInternalState(tsdb, "storage_exception_handler", handler); + } + + /** + * Helper to validate calls to the storage exception handler. + * @param called Whether or not SEH should have been called. + */ + protected void validateSEH(final boolean called) { + if (called) { + verify(tsdb, times(1)).getStorageExceptionHandler(); + if (handler != null) { + verify(handler, times(1)).handleError((IncomingDataPoint)any(), + (Exception)any()); + } + } else { + verify(tsdb, never()).getStorageExceptionHandler(); + if (handler != null) { + verify(handler, never()).handleError((IncomingDataPoint)any(), + (Exception)any()); + } + } + } + + // Helper to validate all the counters per call. + protected void validateCounters( + final long telnet_requests, + final long http_requests, + final long raw_dps, + final long rollup_dps, + final long raw_stored, + final long rollup_stored, + final long hbase_errors, + final long unknown_errors, + final long invalid_values, + final long illegal_arguments, + final long unknown_metrics, + final long inflight_exceeded, + final long writes_blocked, + final long writes_timedout, + final long requests_timedout) { + assertEquals(telnet_requests, this.telnet_requests.get()); + assertEquals(http_requests, this.http_requests.get()); + assertEquals(raw_dps, this.raw_dps.get()); + assertEquals(rollup_dps, this.rollup_dps.get()); + assertEquals(raw_stored, this.raw_stored.get()); + assertEquals(rollup_stored, this.rollup_stored.get()); + assertEquals(hbase_errors, this.hbase_errors.get()); + assertEquals(unknown_errors, this.unknown_errors.get()); + assertEquals(invalid_values, this.invalid_values.get()); + assertEquals(illegal_arguments, this.illegal_arguments.get()); + assertEquals(unknown_metrics, this.unknown_metrics.get()); + assertEquals(inflight_exceeded, this.inflight_exceeded.get()); + assertEquals(writes_blocked, this.writes_blocked.get()); + assertEquals(writes_timedout, this.writes_timedout.get()); + assertEquals(requests_timedout, this.requests_timedout.get()); + } +} diff --git a/test/tsd/NettyMocks.java b/test/tsd/NettyMocks.java index f641ea9550..3c4fe35342 100644 --- a/test/tsd/NettyMocks.java +++ b/test/tsd/NettyMocks.java @@ -18,7 +18,6 @@ import java.net.SocketAddress; import java.nio.charset.Charset; -import java.util.HashMap; import net.opentsdb.core.TSDB; import net.opentsdb.utils.Config; @@ -36,7 +35,6 @@ import org.jboss.netty.handler.codec.http.HttpResponseEncoder; import org.jboss.netty.handler.codec.http.HttpVersion; import org.junit.Ignore; -import org.powermock.reflect.Whitebox; /** * Helper class that provides mockups for testing any OpenTSDB processes that @@ -49,12 +47,10 @@ public final class NettyMocks { * Sets up a TSDB object for HTTP RPC tests that has a Config object * @return A TSDB mock */ - public static TSDB getMockedHTTPTSDB() { + public static TSDB getMockedHTTPTSDB() throws Exception { final TSDB tsdb = mock(TSDB.class); - final Config config = mock(Config.class); - HashMap properties = new HashMap(); - properties.put("tsd.http.show_stack_trace", "true"); - Whitebox.setInternalState(config, "properties", properties); + final Config config = new Config(false); + config.overrideConfig("tsd.http.show_stack_trace", "true"); when(tsdb.getConfig()).thenReturn(config); return tsdb; } diff --git a/test/tsd/TestPutRpc.java b/test/tsd/TestPutRpc.java index 7dab72e5a7..4995a02cfe 100644 --- a/test/tsd/TestPutRpc.java +++ b/test/tsd/TestPutRpc.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2013-2016 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 @@ -23,29 +23,21 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.nio.charset.Charset; import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; - -import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; -import net.opentsdb.core.IncomingDataPoint; -import net.opentsdb.core.TSDB; -import net.opentsdb.uid.NoSuchUniqueName; -import net.opentsdb.utils.Config; +import org.hbase.async.HBaseException; +import org.hbase.async.PleaseThrottleException; +import org.hbase.async.PutRequest; import org.jboss.netty.channel.Channel; 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.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @@ -55,147 +47,79 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, - StorageExceptionHandler.class }) -public final class TestPutRpc { - private static final Map TAGS = new HashMap(1); - static { - TAGS.put("host", "web01"); - } - private TSDB tsdb = null; - private AtomicLong requests = new AtomicLong(); - private AtomicLong hbase_errors = new AtomicLong(); - private AtomicLong invalid_values = new AtomicLong(); - private AtomicLong illegal_arguments = new AtomicLong(); - private AtomicLong unknown_metrics = new AtomicLong(); - private AtomicLong writes_blocked = new AtomicLong(); - private StorageExceptionHandler handler; - private FakeTaskTimer timer; - - @Before - public void before() throws Exception { - tsdb = NettyMocks.getMockedHTTPTSDB(); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, 42, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -42, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, 42.2f, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -42.2f, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, 4220.0f, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -4220.0f, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, .0042f, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.nice", 1365465600, -0.0042f, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - when(tsdb.addPoint("sys.cpu.system", 1365465600, 24, TAGS)) - .thenReturn(Deferred.fromResult(new Object())); - // errors - when(tsdb.addPoint("doesnotexist", 1365465600, 42, TAGS)) - .thenThrow(new NoSuchUniqueName("metric", "doesnotexist")); - when(tsdb.addPoint("sys.cpu.system", 1365465600, 1, TAGS)) - .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); - when(tsdb.addPoint("sys.cpu.system", 1365465600, 2, TAGS)) - .thenReturn(new Deferred()); - - requests = Whitebox.getInternalState(PutDataPointRpc.class, "requests"); - requests.set(0); - hbase_errors = Whitebox.getInternalState(PutDataPointRpc.class, "hbase_errors"); - hbase_errors.set(0); - invalid_values = Whitebox.getInternalState(PutDataPointRpc.class, "invalid_values"); - invalid_values.set(0); - illegal_arguments = Whitebox.getInternalState(PutDataPointRpc.class, "illegal_arguments"); - illegal_arguments.set(0); - unknown_metrics = Whitebox.getInternalState(PutDataPointRpc.class, "unknown_metrics"); - unknown_metrics.set(0); - writes_blocked = Whitebox.getInternalState(PutDataPointRpc.class, "writes_blocked"); - writes_blocked.set(0); - - timer = new FakeTaskTimer(); - - handler = mock(StorageExceptionHandler.class); - when(tsdb.getStorageExceptionHandler()).thenReturn(handler); - when(tsdb.getTimer()).thenReturn(timer); - } +public final class TestPutRpc extends BaseTestPutRpc { @Test public void constructor() { - assertNotNull(new PutDataPointRpc()); + assertNotNull(new PutDataPointRpc(tsdb.getConfig())); } // Socket RPC Tests ------------------------------------ @Test public void execute() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNotNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); verify(chan, never()).write(any()); verify(chan, never()).isConnected(); - verify(tsdb, never()).getStorageExceptionHandler(); + validateSEH(false); } @Test public void executeBadValue() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "notanum", "host=web01" }).joinUninterruptibly()); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "notanum", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); verify(chan, times(1)).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, never()).getStorageExceptionHandler(); + validateSEH(false); } @Test public void executeMissingMetric() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNull(put.execute(tsdb, chan, new String[] { "put", "", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); + put.execute(tsdb, chan, new String[] { "put", "", + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); verify(chan, times(1)).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, never()).getStorageExceptionHandler(); + validateSEH(false); } @Test public void executeMissingMetricNotWriteable() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); when(chan.isWritable()).thenReturn(false); - assertNull(put.execute(tsdb, chan, new String[] { "put", "", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - assertEquals(1, writes_blocked.get()); + put.execute(tsdb, chan, new String[] { "put", "", + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0); verify(chan, never()).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, never()).getStorageExceptionHandler(); + validateSEH(false); } @Test public void executeUnknownMetric() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNull(put.execute(tsdb, chan, new String[] { "put", "doesnotexist", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, unknown_metrics.get()); + put.execute(tsdb, chan, new String[] { "put", NSUN_METRIC, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); verify(chan, times(1)).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, never()).getStorageExceptionHandler(); + validateSEH(false); } @SuppressWarnings("unchecked") @@ -205,129 +129,164 @@ public void executeRuntimeException() throws Exception { (HashMap)any())) .thenThrow(new RuntimeException("Fail!")); - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - put.execute(tsdb, chan, new String[] { "put", "doesnotexist", - "1365465600", "42", "host=web01" }); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(true); } - @SuppressWarnings("unchecked") @Test public void executeHBaseError() throws Exception { - when(tsdb.addPoint(anyString(), anyLong(), anyLong(), - (HashMap)any(HashMap.class))) - .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, hbase_errors.get()); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); verify(chan, times(1)).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateSEH(true); } - - @SuppressWarnings("unchecked") + @Test public void executeHBaseErrorNotWriteable() throws Exception { - when(tsdb.addPoint(anyString(), anyLong(), anyLong(), - (HashMap)any())) - .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); when(chan.isWritable()).thenReturn(false); - assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, hbase_errors.get()); - assertEquals(1, writes_blocked.get()); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0); verify(chan, never()).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateSEH(true); } - @SuppressWarnings("unchecked") @Test public void executeHBaseErrorHandler() throws Exception { - when(tsdb.addPoint(anyString(), anyLong(), anyLong(), - (HashMap)any())) - .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); + setStorageExceptionHandler(); - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, hbase_errors.get()); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); verify(chan, times(1)).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, times(1)).getStorageExceptionHandler(); - verify(handler, times(1)).handleError((IncomingDataPoint)any(), - (Exception)any()); + validateSEH(true); } + @Test + public void executePleaseThrottle() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(PleaseThrottleException.class))); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executePleaseThrottleNotWriteable() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(PleaseThrottleException.class))); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + when(chan.isWritable()).thenReturn(false); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0); + verify(chan, never()).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executePleaseThrottleHandler() throws Exception { + setStorageExceptionHandler(); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(PleaseThrottleException.class))); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + @Test (expected = NullPointerException.class) public void executeNullTSDB() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - put.execute(null, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "42", "host=web01" }); + put.execute(null, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }); } @Test public void executeNullChannelOK() throws Exception { // we can pass in a null channel but since we only write when an error occurs // then we won't fail. - final PutDataPointRpc put = new PutDataPointRpc(); - assertNotNull(put.execute(tsdb, null, new String[] { "put", "sys.cpu.nice", - "1365465600", "42", "host=web01" }).joinUninterruptibly()); - assertEquals(1, requests.get()); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, null, new String[] { "put", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + assertEquals(1, telnet_requests.get()); assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test (expected = NullPointerException.class) public void executeNullChannelError() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); - put.execute(tsdb, null, new String[] { "put", "sys.cpu.nice", - "1365465600", "notanumber", "host=web01" }); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, null, new String[] { "put", METRIC_STRING, + "1365465600", "notanumber", TAGK_STRING + "=" + TAGV_STRING }); } @Test (expected = NullPointerException.class) public void executeNullArray() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); put.execute(tsdb, chan, null); } @Test (expected = ArrayIndexOutOfBoundsException.class) public void executeEmptyArray() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); put.execute(tsdb, chan, new String[0]); } @Test public void executeShortArray() throws Exception { - final PutDataPointRpc put = new PutDataPointRpc(); + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); - assertNull(put.execute(tsdb, chan, new String[] { "put", "sys.cpu.nice", - "1365465600", "42" }).joinUninterruptibly()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); + put.execute(tsdb, chan, new String[] { "put", METRIC_STRING, + "1365465600", "42" }).joinUninterruptibly(); + validateCounters(1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); verify(chan, times(1)).write(any()); verify(chan, times(1)).isConnected(); - verify(tsdb, never()).getStorageExceptionHandler(); + validateSEH(false); } // HTTP RPC Tests -------------------------------------- @@ -335,54 +294,52 @@ public void executeShortArray() throws Exception { @Test public void putSingle() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test - public void putDouble() throws Exception { + public void putTwo() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSingleSummary() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?summary", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":1")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSingleDetails() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -390,17 +347,16 @@ public void putSingleDetails() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":1")); assertTrue(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSingleSummaryAndDetails() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?summary&details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -408,301 +364,276 @@ public void putSingleSummaryAndDetails() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":1")); assertTrue(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test - public void putDoubleSummary() throws Exception { + public void putTwoSummary() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?summary", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":2")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putNegativeInt() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putFloat() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42.2,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42.2,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putNegativeFloat() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-42.2,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-42.2,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSEBig() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":4.22e3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42.22e3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSECaseBig() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":4.22E3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42.22E3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putNegativeSEBig() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-4.22e3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-42.22e3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putNegativeSECaseBig() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-4.22E3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-42.22E3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSETiny() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":4.2e-3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42.22e-3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putSECaseTiny() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":4.2E-3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42.22E-3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putNegativeSETiny() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-4.2e-3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-4.2e-3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void putNegativeSECaseTiny() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-4.2E-3,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-4.2E-3,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void badMethod() throws Exception { HttpQuery query = NettyMocks.getQuery(tsdb, "/api/put"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void badJSON() throws Exception { // missing a quotation mark HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", - "{\"metric\":\"sys.cpu.nice\",\"timestamp:1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp:1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void notJSON() throws Exception { // missing a quotation mark HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", "Hello World"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void noContent() throws Exception { // missing a quotation mark HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", ""); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void inFlightExceeded() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(PleaseThrottleException.class))); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, query); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + validateSEH(true); + } + + @Test + public void inFlightExceededHandler() throws Exception { + setStorageExceptionHandler(); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(PleaseThrottleException.class))); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, query); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + validateSEH(true); } - @SuppressWarnings("unchecked") @Test public void hbaseError() throws Exception { - when(tsdb.addPoint(anyString(), anyLong(), anyLong(), - (HashMap)any())) - .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; - try { - put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, hbase_errors.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + validateCounters(0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); } - @SuppressWarnings("unchecked") @Test public void hbaseErrorHandler() throws Exception { - final StorageExceptionHandler handler = mock(StorageExceptionHandler.class); - when(tsdb.getStorageExceptionHandler()).thenReturn(handler); - when(tsdb.addPoint(anyString(), anyLong(), anyLong(), - (HashMap)any())) - .thenReturn(Deferred.fromError(new RuntimeException("Wotcher!"))); + setStorageExceptionHandler(); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; - try { - put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, hbase_errors.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); - verify(handler, times(1)).handleError((IncomingDataPoint)any(), - (Exception)any()); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + validateCounters(0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); } @Test public void noSuchUniqueName() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"doesnotexist\",\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + NSUN_METRIC + "\",\"timestamp\":1365465600,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -710,19 +641,16 @@ public void noSuchUniqueName() throws Exception { assertTrue(response.contains("\"error\":\"Unknown metric\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - assertEquals(1, unknown_metrics.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(false); } @Test public void missingMetric() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", "{\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -730,18 +658,16 @@ public void missingMetric() throws Exception { assertTrue(response.contains("\"error\":\"Metric name was empty\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void nullMetric() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", "{\"metric\":null,\"timestamp\":1365465600,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -749,18 +675,16 @@ public void nullMetric() throws Exception { assertTrue(response.contains("\"error\":\"Metric name was empty\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void missingTimestamp() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -768,18 +692,16 @@ public void missingTimestamp() throws Exception { assertTrue(response.contains("\"error\":\"Invalid timestamp\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void nullTimestamp() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":null,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":null,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -787,18 +709,16 @@ public void nullTimestamp() throws Exception { assertTrue(response.contains("\"error\":\"Invalid timestamp\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void invalidTimestamp() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":-1,\"value\"" - +":42,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":-1,\"value\"" + +":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -806,18 +726,16 @@ public void invalidTimestamp() throws Exception { assertTrue(response.contains("\"error\":\"Invalid timestamp\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void missingValue() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"tags\":" - + "{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -825,18 +743,16 @@ public void missingValue() throws Exception { assertTrue(response.contains("\"error\":\"Empty value\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void nullValue() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":null,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":null,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -844,18 +760,16 @@ public void nullValue() throws Exception { assertTrue(response.contains("\"error\":\"Empty value\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void emptyValue() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":\"\",\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":\"\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -863,18 +777,16 @@ public void emptyValue() throws Exception { assertTrue(response.contains("\"error\":\"Empty value\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void badValue() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":\"notanumber\",\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":\"notanumber\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -882,18 +794,16 @@ public void badValue() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueNaN() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":NaN,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":NaN,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -901,37 +811,30 @@ public void ValueNaN() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueNaNCase() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":Nan,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":Nan,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueINF() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":+INF,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":+INF,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -939,18 +842,16 @@ public void ValueINF() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueNINF() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-INF,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-INF,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -958,56 +859,44 @@ public void ValueNINF() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueINFUnsigned() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":INF,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":INF,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueINFCase() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":+inf,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); - BadRequestException ex = null; + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":+inf,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); try { put.execute(tsdb, query); - } catch (BadRequestException e) { - ex = e; - } - assertNotNull(ex); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test - public void ValueInfiniy() throws Exception { + public void ValueInfinity() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":+Infinity,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":+Infinity,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1015,18 +904,16 @@ public void ValueInfiniy() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test - public void ValueNInfiniy() throws Exception { + public void ValueNInfinity() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":-Infinity,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":-Infinity,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1034,18 +921,16 @@ public void ValueNInfiniy() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void ValueInfinityUnsigned() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - +":Infinity,\"tags\":{\"host\":\"web01\"}}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + +":Infinity,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1053,18 +938,16 @@ public void ValueInfinityUnsigned() throws Exception { assertTrue(response.contains("\"error\":\"Unable to parse value to a number\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(1, invalid_values.get()); - assertEquals(0, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void missingTags() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\":42" - + "}"); - PutDataPointRpc put = new PutDataPointRpc(); + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1072,18 +955,16 @@ public void missingTags() throws Exception { assertTrue(response.contains("\"error\":\"Missing tags\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void nullTags() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" +":42,\"tags\":null}"); - PutDataPointRpc put = new PutDataPointRpc(); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1091,18 +972,16 @@ public void nullTags() throws Exception { assertTrue(response.contains("\"error\":\"Missing tags\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void emptyTags() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?details", - "{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" +":42,\"tags\":{}}"); - PutDataPointRpc put = new PutDataPointRpc(); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1110,36 +989,35 @@ public void emptyTags() throws Exception { assertTrue(response.contains("\"error\":\"Missing tags\"")); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - assertEquals(1, illegal_arguments.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); } @Test public void syncOKNoDetails() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}," + + "{\"metric\":\"" + METRIC_B_STRING + "\"," + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); verify(tsdb, never()).getTimer(); } @Test public void syncOKSummary() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true&summary", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}," + + "{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1147,9 +1025,8 @@ public void syncOKSummary() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":2")); assertFalse(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); verify(tsdb, never()).getTimer(); } @@ -1157,11 +1034,12 @@ public void syncOKSummary() throws Exception { public void syncOKSummaryDetails() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true&summary&details", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}," + + "{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1170,20 +1048,20 @@ public void syncOKSummaryDetails() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":2")); assertTrue(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); verify(tsdb, never()).getTimer(); } @Test public void syncOKDetails() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync=true&details", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}," + + "{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":24,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1191,40 +1069,46 @@ public void syncOKDetails() throws Exception { assertTrue(response.contains("\"failed\":0")); assertTrue(response.contains("\"success\":2")); assertTrue(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); verify(tsdb, never()).getTimer(); } @Test public void syncOneFailed() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromResult(null)) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"code\":400")); assertTrue(response.contains("\"message\":")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, never()).getTimer(); } @Test public void syncOneFailedSummary() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromResult(null)) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&summary", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1232,20 +1116,23 @@ public void syncOneFailedSummary() throws Exception { assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":1")); assertFalse(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, never()).getTimer(); } @Test public void syncOneFailedDetails() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromResult(null)) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&details", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1253,21 +1140,22 @@ public void syncOneFailedDetails() throws Exception { assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":1")); assertTrue(response.contains("\"errors\":[{")); - assertTrue(response.contains("Wotcher!")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, never()).getTimer(); } @Test public void syncTwoFailedDetails() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&details", - "[{\"metric\":\"doesnotexist\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + NSUN_METRIC + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1276,27 +1164,26 @@ public void syncTwoFailedDetails() throws Exception { assertTrue(response.contains("\"success\":0")); assertTrue(response.contains("\"failed\":2")); assertTrue(response.contains("\"errors\":[{")); - assertTrue(response.contains("Wotcher!")); - assertTrue(response.contains("Unknown metric")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, never()).getTimer(); } - + @Test public void syncOKTimeoutNoDetails() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&sync_timeout=30000", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); - assertEquals(1, requests.get()); + assertEquals(1, http_requests.get()); assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, times(1)).cancel(); } @@ -1305,11 +1192,12 @@ public void syncOKTimeoutNoDetails() throws Exception { public void syncOKTimeoutSummary() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&sync_timeout=30000&summary", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":24,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1318,42 +1206,49 @@ public void syncOKTimeoutSummary() throws Exception { assertTrue(response.contains("\"success\":2")); assertTrue(response.contains("\"timeouts\":0")); assertFalse(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, never()).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, times(1)).cancel(); } @Test public void syncTimeoutOneFailed() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromResult(null)) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&sync_timeout=30000", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(response.contains("\"code\":400")); assertTrue(response.contains("\"message\":")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, times(1)).cancel(); } @Test public void syncTimeoutOneFailedSummary() throws Exception { - HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&summary&sync_timeout=30000", - "[{\"metric\":\"sys.cpu.nice\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromResult(null)) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); + HttpQuery query = NettyMocks.postQuery(tsdb, + "/api/put?sync&summary&sync_timeout=30000", + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.OK, query.response().getStatus()); final String response = @@ -1362,22 +1257,24 @@ public void syncTimeoutOneFailedSummary() throws Exception { assertTrue(response.contains("\"success\":1")); assertTrue(response.contains("\"timeouts\":0")); assertFalse(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, times(1)).cancel(); } @Test public void syncTimeoutTwoFailedDetails() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&details&sync_timeout=30000", - "[{\"metric\":\"doesnotexist\",\"timestamp\":1365465600,\"value\"" - + ":42,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + NSUN_METRIC + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = @@ -1387,24 +1284,25 @@ public void syncTimeoutTwoFailedDetails() throws Exception { assertTrue(response.contains("\"failed\":2")); assertTrue(response.contains("\"timeouts\":0")); assertTrue(response.contains("\"errors\":[{")); - assertTrue(response.contains("Wotcher!")); - assertTrue(response.contains("Unknown metric")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(true); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, times(1)).cancel(); } @Test public void syncTimeoutOneFailedTimedoutSummary() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(new Deferred()) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&summary&sync_timeout=30000", - "[{\"metric\":\"sys.cpu.system\",\"timestamp\":1365465600,\"value\"" - + ":2,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, never()).cancel(); @@ -1418,21 +1316,25 @@ public void syncTimeoutOneFailedTimedoutSummary() throws Exception { assertTrue(response.contains("\"success\":0")); assertTrue(response.contains("\"timeouts\":1")); assertFalse(response.contains("\"errors\":[]")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); - verify(tsdb, times(1)).getStorageExceptionHandler(); + validateCounters(0, 1, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0); + validateSEH(true); + verify(tsdb, times(1)).getTimer(); verify(timer.timeout, never()).cancel(); } @Test public void syncTimeoutOneFailedTimedoutDetails() throws Exception { + when(client.put(any(PutRequest.class))) + .thenReturn(new Deferred()) + .thenReturn(Deferred.fromError(mock(HBaseException.class))); HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put?sync&details&sync_timeout=30000", - "[{\"metric\":\"sys.cpu.system\",\"timestamp\":1365465600,\"value\"" - + ":2,\"tags\":{\"host\":\"web01\"}},{\"metric\":\"sys.cpu.system\"," - + "\"timestamp\":1365465600,\"value\":1,\"tags\":" - + "{\"host\":\"web01\"}}]"); - PutDataPointRpc put = new PutDataPointRpc(); + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":42,\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + + "\"}},{\"metric\":\"" + METRIC_B_STRING + "\"," + + "\"timestamp\":1365465600,\"value\":1,\"tags\":" + + "{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); put.execute(tsdb, query); verify(tsdb, times(1)).getTimer(); verify(timer.timeout, never()).cancel(); @@ -1442,16 +1344,15 @@ public void syncTimeoutOneFailedTimedoutDetails() throws Exception { assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String response = query.response().getContent().toString(Charset.forName("UTF-8")); - System.out.println(response); assertTrue(response.contains("\"failed\":1")); assertTrue(response.contains("\"success\":0")); assertTrue(response.contains("\"timeouts\":1")); assertTrue(response.contains("\"errors\":[{")); assertTrue(response.contains("Write timedout")); - assertTrue(response.contains("Wotcher!")); - assertEquals(1, requests.get()); - assertEquals(0, invalid_values.get()); + validateCounters(0, 1, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0); + validateSEH(true); verify(tsdb, times(1)).getStorageExceptionHandler(); verify(timer.timeout, never()).cancel(); } + } diff --git a/test/tsd/TestRollupRpc.java b/test/tsd/TestRollupRpc.java new file mode 100644 index 0000000000..73f931b128 --- /dev/null +++ b/test/tsd/TestRollupRpc.java @@ -0,0 +1,663 @@ +// 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 net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId.UniqueIdType; + +import org.hamcrest.CoreMatchers; +import org.hbase.async.HBaseException; +import org.hbase.async.PleaseThrottleException; +import org.jboss.netty.channel.Channel; +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.PowerMockIgnore; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +public class TestRollupRpc extends BaseTestPutRpc { + private final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); + + private RollupConfig rollup_config; + + @Before + public void beforeLocal() throws Exception { + final List families = new ArrayList(); + families.add(FAMILY); + + final List rollups = new ArrayList(); + rollups.add(new RollupInterval( + "tsdb", "tsdb-agg", "1m", "1h", true)); + rollups.add(new RollupInterval( + "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1m")); + + rollup_config = new RollupConfig(rollups); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + + storage = new MockBase(tsdb, client, true, true, true, true); + storage.addTable("tsdb-rollup-1h".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1h".getBytes(), families); + + mockUID(UniqueIdType.TAGK, "_aggregate", new byte[] { 0, 0, 42 }); + mockUID(UniqueIdType.TAGV, "SUM", new byte[] { 0, 0, 42 }); + } + + @Test + public void constructor() { + assertNotNull(new RollupDataPointRpc(tsdb.getConfig())); + } + + // Socket RPC Tests ------------------------------------ + + @Test + public void execute() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + assertNotNull(rollup.execute(tsdb, chan, new String[] { "rollup", + "1h-sum", METRIC_STRING, "1365465600", "42", + TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly()); + validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + verify(chan, never()).write(any()); + verify(chan, never()).isConnected(); + validateSEH(false); + } + + @Test + public void executeRollupsDisabled() throws Exception { + Whitebox.setInternalState(tsdb, "rollup_config", (RollupConfig) null); + Whitebox.setInternalState(tsdb, "default_interval", (RollupInterval) null); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeWithAgg() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum:sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + verify(chan, never()).write(any()); + verify(chan, never()).isConnected(); + validateSEH(false); + } + + @Test + public void executeBadValue() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "10-msum", + METRIC_STRING, "1365465600", "notanum", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeBadValueNotWriteable() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + when(chan.isWritable()).thenReturn(false); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "notanum", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0); + verify(chan, never()).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeMissingMetric() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", "", + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeUnknownMetric() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", NSUN_METRIC, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @SuppressWarnings("unchecked") + @Test (expected = RuntimeException.class) + public void executeRuntimeException() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + PowerMockito.when(tsdb.addAggregatePoint(anyString(), anyLong(), anyLong(), + anyMap(), anyBoolean(), anyString(), anyString())) + .thenThrow(new RuntimeException("Fail!")); + rollup.execute(tsdb, chan, new String[] { "rollup", "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + } + + @Test + public void executeHBaseError() throws Exception { + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(HBaseException.class)); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executeHBaseErrorNotWriteable() throws Exception { + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(HBaseException.class)); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + when(chan.isWritable()).thenReturn(false); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0); + verify(chan, never()).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executeHBaseErrorHandler() throws Exception { + setStorageExceptionHandler(); + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(HBaseException.class)); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executePleaseThrottle() throws Exception { + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(PleaseThrottleException.class)); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executePleaseThrottleNotWriteable() throws Exception { + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(PleaseThrottleException.class)); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + when(chan.isWritable()).thenReturn(false); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0); + verify(chan, never()).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test + public void executePleaseThrottleHandler() throws Exception { + setStorageExceptionHandler(); + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(PleaseThrottleException.class)); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + } + + @Test (expected = NullPointerException.class) + public void executeNullTSDB() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(null, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + } + + @Test + public void executeNullChannelOK() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + // we can pass in a null channel but since we only write when an error occurs + // then we won't fail. + rollup.execute(tsdb, null, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test (expected = NullPointerException.class) + public void executeNullChannelError() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, null, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "notanumber", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + } + + @Test (expected = NullPointerException.class) + public void executeNullArray() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, null).joinUninterruptibly(); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void executeEmptyArray() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[0]).joinUninterruptibly(); + } + + @Test + public void executeShortArray() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", + METRIC_STRING, "1365465600", "42" }).joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeMissingInterval() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup","", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeUnknownInterval() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "13m-sum", + METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + // TODO - revisit +// @Test +// public void executePreAggOnly() throws Exception { +// final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); +// final Channel chan = NettyMocks.fakeChannel(); +// rollup.execute(tsdb, chan, new String[] { "rollup", "sum", METRIC_STRING, +// "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) +// .joinUninterruptibly(); +// validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); +// verify(chan, never()).write(any()); +// verify(chan, never()).isConnected(); +// validateSEH(false); +// } + + @Test + public void executeMissingAggregator() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + // TODO - test unknown aggs if we decide to implement that check + + @Test + public void executeMissingTags() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup","1h-sum", METRIC_STRING, + "1365465600", "42", "" }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeNSUNTagk() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup","1h-sum", METRIC_STRING, + "1365465600", "42", NSUN_TAGK + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeNSUNTagV() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum", METRIC_STRING, + "1365465600", "42", TAGK_STRING + "=" + NSUN_TAGV }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + +// HTTP RPC Tests -------------------------------------- + + @Test + public void httpAddSingleRollupPoint() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpAddTwoRollupPoints() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}, " + + "{\"metric\":\"" + METRIC_B_STRING + "\",\"timestamp\":1365465600,\"value\":24, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpAddTwoRollupPointsOneGoodOneBad() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}, " + + "{\"metric\":\"" + NSUN_METRIC + "\",\"timestamp\":1365465600,\"value\":24, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpMissingInterval() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"aggregator\":\"sum\",\"tags\":{\"" + TAGK_STRING + "\":\"" + + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertThat(response, CoreMatchers.containsString("\"error\":\"Missing interval\"")); + assertThat(response, CoreMatchers.containsString("\"failed\":1")); + assertThat(response, CoreMatchers.containsString("\"success\":0")); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpEmptyInterval() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertThat(response, CoreMatchers.containsString("\"error\":\"Missing interval\"")); + assertThat(response, CoreMatchers.containsString("\"failed\":1")); + assertThat(response, CoreMatchers.containsString("\"success\":0")); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpMissingAggregator() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\",\"tags\":{\"" + TAGK_STRING + "\":\"" + + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertThat(response, CoreMatchers.containsString("\"error\":\"Missing aggregator\"")); + assertThat(response, CoreMatchers.containsString("\"failed\":1")); + assertThat(response, CoreMatchers.containsString("\"success\":0")); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpInvalidAggregator() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"what?\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertThat(response, CoreMatchers.containsString("\"error\":\"Invalid aggregator\"")); + assertThat(response, CoreMatchers.containsString("\"failed\":1")); + assertThat(response, CoreMatchers.containsString("\"success\":0")); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpEmptyAggregator() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup?details", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + final String response = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertThat(response, CoreMatchers.containsString("\"error\":\"Missing aggregator\"")); + assertThat(response, CoreMatchers.containsString("\"failed\":1")); + assertThat(response, CoreMatchers.containsString("\"success\":0")); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpNSUNMetric() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + NSUN_METRIC + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpNSUNTagk() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + NSUN_TAGK + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpNSUNTagv() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + NSUN_TAGV + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void httpHBaseError() throws Exception { + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(HBaseException.class)); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(true); + } + + @Test + public void httpPleaseThrottleError() throws Exception { + storage.throwException(MockBase.stringToBytes("0000015158CE00000001000001"), + mock(PleaseThrottleException.class)); + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0); + validateSEH(true); + } + + @Test + public void httpUnknownInterval() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + + "\"interval\":\"13m\", \"aggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + } + +} diff --git a/test/tsd/TestSuggestRpc.java b/test/tsd/TestSuggestRpc.java index dbfdc426a0..c25e428ca3 100644 --- a/test/tsd/TestSuggestRpc.java +++ b/test/tsd/TestSuggestRpc.java @@ -39,7 +39,7 @@ public final class TestSuggestRpc { private SuggestRpc s = null; @Before - public void before() { + public void before() throws Exception { s = new SuggestRpc(); tsdb = NettyMocks.getMockedHTTPTSDB(); final List metrics = new ArrayList(); From 175111bdc2dd6bf5d0920cac9cb756cab29fb47f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 25 Oct 2016 21:57:02 -0700 Subject: [PATCH 568/826] Add getters to the Rollup objects in the TSDB class. Add most of the code to the TsdbQuery class in prep for rollups. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 12 ++ src/core/TsdbQuery.java | 325 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 331 insertions(+), 6 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index f55598e24f..43a88913b4 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1740,6 +1740,18 @@ public Object call(final Exception e) throws Exception { } } + /** @return the rollup config object. May be null + * @since 2.4 */ + public RollupConfig getRollupConfig() { + return rollup_config; + } + + /** @return The default rollup interval config. May be null. + * @since 2.4 */ + public RollupInterval getDefaultInterval() { + return default_interval; + } + /** * Blocks while pre-fetching meta data from the data and uid tables * so that performance improves, particularly with a large number of diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 29d91a4ce5..d6f1e79d79 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -26,20 +26,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.hbase.async.BinaryPrefixComparator; import org.hbase.async.Bytes; +import org.hbase.async.CompareFilter; import org.hbase.async.DeleteRequest; +import org.hbase.async.FilterList; import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; +import org.hbase.async.QualifierFilter; +import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.FilterList.Operator; import com.google.common.annotations.VisibleForTesting; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.core.TsdbQuery.ROLLUP_USAGE; import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.rollup.NoSuchRollupForIntervalException; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.rollup.RollupUtils; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.QueryStats; import net.opentsdb.stats.QueryStats.QueryStat; @@ -122,6 +133,20 @@ final class TsdbQuery implements Query { /** Downsampling specification to use, if any (can be {@code null}). */ private DownsamplingSpecification downsampler; + /** 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 + * aggregation. */ + private boolean pre_aggregate; + /** Optional list of TSUIDs to fetch and aggregate instead of a metric */ private List tsuids; @@ -137,6 +162,46 @@ final class TsdbQuery implements Query { /** Whether or not to match series with ONLY the given tags */ private boolean explicit_tags; + /** + * Enum for rollup fallback control. + * @since 2.4 + */ + public static enum ROLLUP_USAGE { + ROLLUP_RAW, //Don't use rollup data, instead use raw data + 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 + * @return enum param tells how to use the rollup data + */ + public static ROLLUP_USAGE parse(String str) { + ROLLUP_USAGE def = ROLLUP_NOFALLBACK; + + if (str != null) { + try { + def = ROLLUP_USAGE.valueOf(str.toUpperCase()); + } + catch(IllegalArgumentException ex) { + LOG.warn("Unknown rollup usage, " + str + ", use default usage - which" + + "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 + */ + public boolean fallback() { + return this == ROLLUP_FALLBACK || this == ROLLUP_FALLBACK_RAW; + } + } + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -144,6 +209,25 @@ public TsdbQuery(final TSDB tsdb) { .getBoolean("tsd.query.enable_fuzzy_filter"); } + /** Which rollup table it scanned to get the final result. + * @since 2.4 */ + public String getRollupTable() { + if (RollupQuery.isValidQuery(rollup_query)) { + return rollup_query.getRollupInterval().getStringInterval(); + } + else { + return "raw"; + } + } + + /** 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 @@ -339,6 +423,11 @@ public Deferred configureFromQuery(final TSQuery query, filters = sub_query.getFilters(); explicit_tags = sub_query.getExplicitTags(); + if (rollup_usage != ROLLUP_USAGE.ROLLUP_RAW) { + //Check whether the down sampler is set and rollup is enabled + transformDownSamplerToRollupQuery(sub_query.getDownsample()); + } + // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { tsuids = new ArrayList(sub_query.getTsuids()); @@ -512,7 +601,14 @@ public DataPoints[] run() throws HBaseException { @Override public Deferred runAsync() throws HBaseException { - return findSpans().addCallback(new GroupByAndAggregateCB()); + Deferred result = + findSpans().addCallback(new GroupByAndAggregateCB()); + + if (rollup_usage != null && rollup_usage.fallback()) { + result.addCallback(new FallbackRollupOnEmptyResult()); + } + + return result; } /** @@ -880,9 +976,9 @@ void close(final Exception e) { } /** - * Callback that should be attached the the output of - * {@link TsdbQuery#findSpans} to group and sort the results. - */ + * Callback that should be attached the the output of + * {@link TsdbQuery#findSpans} to group and sort the results. + */ private class GroupByAndAggregateCB implements Callback>{ @@ -1011,6 +1107,71 @@ public DataPoints[] call(final TreeMap spans) throws Exception { } } + /** + * Scan the tables again with the next best rollup match, on empty result set + */ + private class FallbackRollupOnEmptyResult implements + Callback, DataPoints[]>{ + + /** + * Creates the {@link SpanGroup}s to form the final results of this query. + * @param spans The {@link Span}s found for this query ({@link #findSpans}). + * Can be {@code null}, in which case the array returned will be empty. + * @return A possibly empty array of {@link SpanGroup}s built according to + * any 'GROUP BY' formulated in this query. + */ + public Deferred call(final DataPoints[] datapoints) throws Exception { + //TODO review this logic during spatial aggregation implementation + + if (datapoints == NO_RESULT && RollupQuery.isValidQuery(rollup_query)) { + //There are no datapoints for this query and it is a rollup query + //but not the default interval (default interval means raw). + //This will prevent redundant scan on raw data on the presense of + //default rollup interval + + //If the rollup usage is to fallback directly to raw data + //then nullyfy the rollup query, so that the recursive scan will use + //raw data and this will not called again because of isValida check + //If the rollup usage is to fallback to next best match then pupup + //next best match and attach that to the rollup query + if (rollup_usage == ROLLUP_USAGE.ROLLUP_FALLBACK_RAW) { + transformRollupQueryToDownSampler(); + return runAsync(); + } + else if (best_match_rollups != null && best_match_rollups.size() > 0) { + RollupInterval interval = best_match_rollups.remove(0); + + if (interval.isDefaultRollupInterval()) { + transformRollupQueryToDownSampler(); + } + else { + rollup_query = new RollupQuery(interval, + rollup_query.getRollupAgg(), + rollup_query.getSampleIntervalInMS()); + //Here the requested sampling rate will be higher than + //resulted result. So downsample it + if (!rollup_query.isLowerSamplingRate()) { +// sample_interval_ms = rollup_query.getSampleIntervalInMS(); +// downsampler = rollup_query.getRollupAgg(); + // TODO - default fill + downsampler = new DownsamplingSpecification( + rollup_query.getSampleIntervalInMS(), + rollup_query.getRollupAgg(), + (downsampler != null ? downsampler.getFillPolicy() : + FillPolicy.ZERO)); + } + } + + return runAsync(); + } + return Deferred.fromResult(NO_RESULT); + } + else { + return Deferred.fromResult(datapoints); + } + } + } + /** * Returns a scanner set for the given metric (from {@link #metric} or from * the first TSUID in the {@link #tsuids}s list. If one or more tags are @@ -1056,10 +1217,74 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { createAndSetTSUIDFilter(scanner); } else if (filters.size() > 0) { createAndSetFilter(scanner); + } + + if (RollupQuery.isValidQuery(rollup_query)) { + ScanFilter existing = scanner.getFilter(); + // TODO - need some UTs around this! + // Set the Scanners column qualifier pattern with rollup aggregator + // HBase allows only a single filter so if we have a row key filter, keep + // it. If not, then we can do this + if (!rollup_query.getRollupAgg().toString().equals("avg")) { + if (existing != null) { + final List filters = new ArrayList(2); + filters.add(existing); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() + .getBytes(Const.ASCII_CHARSET)))); + scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ALL)); + } else { + scanner.setFilter(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() + .getBytes(Const.ASCII_CHARSET)))); + } + } else { + final List filters = new ArrayList(2); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator("sum".getBytes()))); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator("count".getBytes()))); + + if (existing != null) { + final List combined = new ArrayList(2); + combined.add(existing); + combined.add(new FilterList(combined, Operator.MUST_PASS_ONE)); + scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ALL)); + } else { + scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ONE)); + } + } } return scanner; } + /** + * 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(); + } + else { + tableName= rollup_query.getRollupInterval().getTemporalTable(); + } + } + else if (pre_aggregate) { + tableName = tsdb.getDefaultInterval().getGroupbyTable(); + } + else { + tableName = tsdb.dataTable(); + } + + return tableName; + } + /** Returns the UNIX timestamp from which we must start scanning. */ private long getScanStartTimeSeconds() { // Begin with the raw query start time. @@ -1069,6 +1294,19 @@ 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, + rollup_query.getRollupInterval()); + if (rate) { + // scan one row back so we can get the first rate value. + base_time = RollupUtils.getRollupBasetime(base_time - 1, + rollup_query.getRollupInterval()); + } + return base_time; + } // First, we align the start timestamp to its representative value for the // interval in which it appears, if downsampling. @@ -1166,12 +1404,84 @@ private void createAndSetTSUIDFilter(final Scanner scanner) { } scanner.setKeyRegexp(regex, CHARSET); } - + + /** + * Return the query index that maps this datapoints to the original subquery + * @return index of the query in the TSQuery class + * @since 2.4 + */ @Override public int getQueryIdx() { return query_index; } + /** + * set the index that link this query to the original index. + * @param idx query index idx + * @since 2.4 + */ + public void setQueryIdx(int idx) { + query_index = idx; + } + + /** + * Transform downsampler properties to rollup properties, if the rollup + * is enabled at configuration level and down sampler is set. + * It falls back to raw data and down sampling if there is no + * RollupInterval is configured against this down sample interval + * @param str_interval String representation of the interval, for logging + * @since 2.4 + */ + public void transformDownSamplerToRollupQuery(final String str_interval) { + + if (downsampler != null && downsampler.getInterval() > 0) { + if (tsdb.getRollupConfig() != null) { + try { + best_match_rollups = tsdb.getRollupConfig(). + getRollupInterval(downsampler.getInterval() / 1000, str_interval); + //It is thread safe as eatch thread will be working on unique + // TsdbQuery object + //RollupConfig.getRollupInterval guarantees that, + // it always return a non-empty list + // TODO + rollup_query = new RollupQuery(best_match_rollups.remove(0), + downsampler.getFunction(), downsampler.getInterval()); + } + catch (NoSuchRollupForIntervalException nre) { + LOG.error("There is no such rollup for the downsample interval " + + str_interval + ". So fall back to the default tsdb down" + + " sampling approach and it requires raw data scan." ); + //nullify the rollup_query if this api is called explicitly + rollup_query = null; + return; + } + + if (rollup_query.getRollupInterval().isDefaultRollupInterval()) { + //Anyways it is a scan on raw data + rollup_query = null; + } + } + } + } + + /** + * Transform rollup query to downsampler + * It is mainly useful when it scan on raw data on fallback. + * @since 2.4 + */ + private void transformRollupQueryToDownSampler() { + + if (rollup_query != null) { + // TODO - clean up and handle fill + downsampler = new DownsamplingSpecification( + rollup_query.getRollupInterval().getInterval() * 1000, + rollup_query.getRollupAgg(), + (downsampler != null ? downsampler.getFillPolicy() : + FillPolicy.ZERO)); + rollup_query = null; + } + } + @Override public String toString() { final StringBuilder buf = new StringBuilder(); @@ -1231,7 +1541,10 @@ public String toString() { } } } - buf.append("))"); + buf.append(")") + .append(", rollup=") + .append(RollupQuery.isValidQuery(rollup_query)) + .append("))"); return buf.toString(); } From a0d8049607584f35446fbc171e86b32e31a901cf Mon Sep 17 00:00:00 2001 From: lburg Date: Tue, 25 Oct 2016 23:11:08 -0700 Subject: [PATCH 569/826] Handle double precision floating point via the API -- refs #473 Add a `fitsInFloat` function similar in function to `looksLikeInteger` to determine whether a floating point value should be stored in a Float or a Double. Add a few tests. Signed-off-by: Chris Larsen --- src/core/Tags.java | 15 +++++++++++++++ src/tsd/PutDataPointRpc.java | 9 +++++++-- test/core/TestTags.java | 30 ++++++++++++++++++++++++++++++ test/tsd/TestPutRpc.java | 26 ++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index 26984a5dd1..1ad52596ff 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -825,4 +825,19 @@ public static void setAllowSpecialChars(String characters) { static boolean isAllowSpecialChars(char character) { return allowSpecialChars.indexOf(character) != -1; } + + /** + * Returns true if the given string can fit into a float. + * @param value The String holding the float value. + * @return true if the value can fit into a float, false otherwise. + * @throws NumberFormatException if the value is not numeric. + * @since 2.0.2 + */ + public static boolean fitsInFloat(final String value) { + final float f = Float.parseFloat(value); + final String converted = Float.toString(f); + + // this will be false if there was a loss of precision. + return value.equals(converted); + } } diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index e13d199e7e..312a4531e1 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -378,7 +378,10 @@ public Boolean call(final Object obj) { .addErrback(new PutErrback()); } else { deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Float.parseFloat(dp.getValue()), dp.getTags()) + (Tags.fitsInFloat(dp.getValue()) ? + Float.parseFloat(dp.getValue()) : + Double.parseDouble(dp.getValue())), + dp.getTags()) .addCallback(new SuccessCB()) .addErrback(new PutErrback()); } @@ -657,8 +660,10 @@ protected Deferred importDataPoint(final TSDB tsdb, } if (Tags.looksLikeInteger(value)) { return tsdb.addPoint(metric, timestamp, Tags.parseLong(value), tags); - } else { // floating point value + } else if (Tags.fitsInFloat(value)) { // floating point value return tsdb.addPoint(metric, timestamp, Float.parseFloat(value), tags); + } else { + return tsdb.addPoint(metric, timestamp, Double.parseDouble(value), tags); } } diff --git a/test/core/TestTags.java b/test/core/TestTags.java index 2898807149..598faf0ffb 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -787,6 +787,36 @@ public void resolveOrCreateAllAsyncFilterBlocked() throws Exception { Tags.resolveOrCreateAllAsync(tsdb, "metric", tags).join(); } + @Test + public void looksLikeIntegerSimple() { + assertEquals(true, Tags.looksLikeInteger("123")); + } + + @Test + public void looksLikeIntegerFloat() { + assertEquals(false, Tags.looksLikeInteger("12.3")); + } + + @Test + public void looksLikeIntegerExponent() { + assertEquals(false, Tags.looksLikeInteger("1e10")); + } + + @Test + public void fitsInFloatSimple() { + assertEquals(true, Tags.fitsInFloat("12.3")); + } + + @Test + public void fitsInFloatDoublePrecision() { + assertEquals(false, Tags.fitsInFloat("1.234556789123456")); + } + + @Test(expected=NumberFormatException.class) + public void fitsInFloatMalformed() { + assertEquals(false, Tags.fitsInFloat("1.2abc34")); + } + // PRIVATE helpers to setup unit tests private void setupStorage() throws Exception { diff --git a/test/tsd/TestPutRpc.java b/test/tsd/TestPutRpc.java index 4995a02cfe..13a409b7a0 100644 --- a/test/tsd/TestPutRpc.java +++ b/test/tsd/TestPutRpc.java @@ -411,6 +411,32 @@ public void putFloat() throws Exception { validateSEH(false); } + @Test + public void putDoublePrecisionFloatingPoint() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":123456.123456789,\"tags\":{\"" + TAGK_STRING + "\":\"" + + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + } + + @Test + public void putNegativeDoublePrecisionFloatingPoint() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\"" + + ":-123456.123456789,\"tags\":{\"" + TAGK_STRING + "\":\"" + + TAGV_STRING + "\"}}"); + PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + put.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + } + @Test public void putNegativeFloat() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/put", From 9b6ad096258b4f09045e0932efaea2c7530d1add Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 25 Oct 2016 23:29:09 -0700 Subject: [PATCH 570/826] Change Tags.FitsInFloat() to cast to float then back to double for a more accurate comparrison. Also make sure the rest of the PutDataPointRpc calls are using the proper float v double. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 47 +++++++++++++++++++++++++++++++++ src/core/Tags.java | 11 ++++---- src/tsd/PutDataPointRpc.java | 5 +++- src/tsd/RollupDataPointRpc.java | 5 +++- test/core/TestTags.java | 11 +++++++- 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 43a88913b4..6a79b2884e 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1171,6 +1171,53 @@ public Deferred addAggregatePoint(final String metric, val, tags, flags, is_groupby, interval, aggregator); } + /** + * Adds a rolled up and/or groupby/pre-agged data point to the proper table. + * If {@code interval} is null then the value will be directed to the + * pre-agg table. + * If the {@code is_groupby} flag is set, then the aggregate tag, defined in + * "tsd.core.agg_tag", will be added or overwritten with the {@code aggregator} + * value in uppercase as the value. + * @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. + * @param is_groupby Whether or not the value is a pre-aggregate + * @param interval The interval the data reflects (may be null) + * @param aggregator The aggregator used to generate the data + * @return A deferred to optionally wait on to be sure the value was stored + * @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 tags list is empty or one of the + * elements contains illegal characters. + * @throws HBaseException (deferred) if there was a problem while persisting + * data. + * @since 2.4 + */ + public Deferred addAggregatePoint(final String metric, + final long timestamp, + final double value, + final Map tags, + final boolean is_groupby, + final String interval, + final String aggregator) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + throw new IllegalArgumentException("value is NaN or Infinite: " + value + + " for metric=" + metric + + " timestamp=" + timestamp); + } + + final short flags = Const.FLAG_FLOAT | 0x7; // A float stored on 4 bytes. + + final byte[] val = Bytes.fromLong(Double.doubleToRawLongBits(value)); + + return addAggregatePointInternal(metric, timestamp, + val, tags, flags, is_groupby, interval, aggregator); + } + Deferred addAggregatePointInternal(final String metric, final long timestamp, final byte[] value, diff --git a/src/core/Tags.java b/src/core/Tags.java index 1ad52596ff..b83cf2cfe2 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -831,13 +831,12 @@ static boolean isAllowSpecialChars(char character) { * @param value The String holding the float value. * @return true if the value can fit into a float, false otherwise. * @throws NumberFormatException if the value is not numeric. - * @since 2.0.2 + * @since 2.4 */ public static boolean fitsInFloat(final String value) { - final float f = Float.parseFloat(value); - final String converted = Float.toString(f); - - // this will be false if there was a loss of precision. - return value.equals(converted); + // TODO - probably still a better way to do this and we could save a lot + // of space by dropping useless precision, but for now this should help. + final double d = Double.parseDouble(value); + return ((float) d) == d; } } diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 312a4531e1..11cfa30260 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -372,7 +372,10 @@ public Boolean call(final Object obj) { if (dp instanceof RollUpDataPoint) { final RollUpDataPoint rdp = (RollUpDataPoint)dp; deferred = tsdb.addAggregatePoint(rdp.getMetric(), rdp.getTimestamp(), - Float.parseFloat(rdp.getValue()), dp.getTags(), false, + (Tags.fitsInFloat(dp.getValue()) ? + Float.parseFloat(dp.getValue()) : + Double.parseDouble(dp.getValue())), + dp.getTags(), false, rdp.getInterval(), rdp.getAggregator()) .addCallback(new SuccessCB()) .addErrback(new PutErrback()); diff --git a/src/tsd/RollupDataPointRpc.java b/src/tsd/RollupDataPointRpc.java index 32202321db..4a6c69a58a 100644 --- a/src/tsd/RollupDataPointRpc.java +++ b/src/tsd/RollupDataPointRpc.java @@ -160,9 +160,12 @@ protected Deferred importDataPoint(final TSDB tsdb, if (Tags.looksLikeInteger(value)) { return tsdb.addAggregatePoint(metric, timestamp, Tags.parseLong(value), tags, spatial_agg != null ? true : false, interval, temporal_agg); - } else { // floating point value + } else if (Tags.fitsInFloat(value)) { // floating point value return tsdb.addAggregatePoint(metric, timestamp, Float.parseFloat(value), tags, spatial_agg != null ? true : false, interval, temporal_agg); + } else { + return tsdb.addAggregatePoint(metric, timestamp, Double.parseDouble(value), + tags, spatial_agg != null ? true : false, interval, temporal_agg); } } diff --git a/test/core/TestTags.java b/test/core/TestTags.java index 598faf0ffb..1583336b43 100644 --- a/test/core/TestTags.java +++ b/test/core/TestTags.java @@ -804,7 +804,8 @@ public void looksLikeIntegerExponent() { @Test public void fitsInFloatSimple() { - assertEquals(true, Tags.fitsInFloat("12.3")); + // deceiving eh? + assertEquals(false, Tags.fitsInFloat("12.3")); } @Test @@ -817,6 +818,14 @@ public void fitsInFloatMalformed() { assertEquals(false, Tags.fitsInFloat("1.2abc34")); } + @Test + public void fitsInFloat() { + assertEquals(true, Tags.fitsInFloat("0.6116398572921753")); + assertEquals(true, Tags.fitsInFloat("1.01417856E9")); + assertEquals(false, Tags.fitsInFloat("4.508277154265837E7")); + assertEquals(false, Tags.fitsInFloat("8.208611994536002E8")); + } + // PRIVATE helpers to setup unit tests private void setupStorage() throws Exception { From 83d8c8936d229d6ee0b3ff8ede8df1c6159cfd2d Mon Sep 17 00:00:00 2001 From: nickman Date: Wed, 26 Oct 2016 11:51:20 -0700 Subject: [PATCH 571/826] Simplified deployment. With HBase accessible, and gnuplot installed, this works: java -jar ./target/opentsdb-2.1.0RC1-fat.jar tsd. No requirement for a pre-deployed UI static content directory. The FatJar contains all the static resources and unloads this content into the configured/default directory at start time, including the GWT Web App content and the gnuplot wrapper script. If any content already exists and has a newer timestamp than the corresponding resource in the FatJar, the resource is left as is. An additional FatJar provided command line tool allows for the pre-creation of the static content directory, which is useful in development. The process PID is written to a default [and configurable] pid file, and deleted on clean shutdown. URL based config and include supporting the load of configuration files from http:// or file:// based sources. Configuration properties can be tokenized with System Prop and/or Environmental Variable decodes as well as JavaScript snippets to support dynamically computed property values according to the deployment environment. Refined logging configuration with configuration override-able external logging config file location and command line options to set logging levels for major packages in OpenTSDB and associated libraries. Signed-off-by: Chris Larsen --- Makefile.am | 36 ++ fat-jar/create-src-dir-overlay.sh | 29 + fat-jar/fat-jar-pom.xml.in | 677 +++++++++++++++++++++++ fat-jar/fat-jar-readme.md | 180 +++++++ fat-jar/file-logback.xml | 72 +++ fat-jar/logback.xml | 50 ++ fat-jar/opentsdb.conf.json | 439 +++++++++++++++ fat-jar/test-logback.xml | 45 ++ src/tools/ArgValueValidator.java | 30 ++ src/tools/ConfigArgP.java | 833 +++++++++++++++++++++++++++++ src/tools/ConfigMetaType.java | 563 ++++++++++++++++++++ src/tools/GnuplotInstaller.java | 108 ++++ src/tools/Main.java | 859 ++++++++++++++++++++++++++++++ src/tsd/GraphHandler.java | 13 + src/utils/Config.java | 9 +- test/tools/TestConfigArgP.java | 628 ++++++++++++++++++++++ 16 files changed, 4570 insertions(+), 1 deletion(-) create mode 100755 fat-jar/create-src-dir-overlay.sh create mode 100644 fat-jar/fat-jar-pom.xml.in create mode 100644 fat-jar/fat-jar-readme.md create mode 100644 fat-jar/file-logback.xml create mode 100644 fat-jar/logback.xml create mode 100644 fat-jar/opentsdb.conf.json create mode 100644 fat-jar/test-logback.xml create mode 100644 src/tools/ArgValueValidator.java create mode 100644 src/tools/ConfigArgP.java create mode 100644 src/tools/ConfigMetaType.java create mode 100644 src/tools/GnuplotInstaller.java create mode 100644 src/tools/Main.java create mode 100644 test/tools/TestConfigArgP.java diff --git a/Makefile.am b/Makefile.am index ab2a3fa505..ecbc798e6f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -138,6 +138,11 @@ tsdb_SRC := \ src/tools/TextImporter.java \ src/tools/TreeSync.java \ src/tools/UidManager.java \ + src/tools/ArgValueValidator.java \ + src/tools/ConfigArgP.java \ + src/tools/ConfigMetaType.java \ + src/tools/GnuplotInstaller.java \ + src/tools/Main.java \ src/tree/Branch.java \ src/tree/Leaf.java \ src/tree/Tree.java \ @@ -811,6 +816,37 @@ pom.xml: pom.xml.in Makefile } >$@-t mv $@-t ../$@ +# Generates a maven pom called fat-jar-pom.xml that builds a fat jar +# containing all the dependencies required to run opentsdb +fat-jar-pom.xml: ./fat-jar/fat-jar-pom.xml.in Makefile + (cd $(top_srcdir) ; ./fat-jar/create-src-dir-overlay.sh) + { \ + echo ''; \ + sed <$< \ + -e 's/@ASYNCHBASE_VERSION@/$(ASYNCHBASE_VERSION)/' \ + -e 's/@GUAVA_VERSION@/$(GUAVA_VERSION)/' \ + -e 's/@GWT_VERSION@/$(GWT_VERSION)/' \ + -e 's/@HAMCREST_VERSION@/$(HAMCREST_VERSION)/' \ + -e 's/@JACKSON_VERSION@/$(JACKSON_VERSION)/' \ + -e 's/@JAVASSIST_VERSION@/$(JAVASSIST_VERSION)/' \ + -e 's/@JUNIT_VERSION@/$(JUNIT_VERSION)/' \ + -e 's/@LOG4J_OVER_SLF4J_VERSION@/$(LOG4J_OVER_SLF4J_VERSION)/' \ + -e 's/@LOGBACK_CLASSIC_VERSION@/$(LOGBACK_CLASSIC_VERSION)/' \ + -e 's/@LOGBACK_CORE_VERSION@/$(LOGBACK_CORE_VERSION)/' \ + -e 's/@MOCKITO_VERSION@/$(MOCKITO_VERSION)/' \ + -e 's/@NETTY_VERSION@/$(NETTY_VERSION)/' \ + -e 's/@OBJENESIS_VERSION@/$(OBJENESIS_VERSION)/' \ + -e 's/@POWERMOCK_MOCKITO_VERSION@/$(POWERMOCK_MOCKITO_VERSION)/' \ + -e 's/@SLF4J_API_VERSION@/$(SLF4J_API_VERSION)/' \ + -e 's/@SUASYNC_VERSION@/$(SUASYNC_VERSION)/' \ + -e 's/@ZOOKEEPER_VERSION@/$(ZOOKEEPER_VERSION)/' \ + -e 's/@spec_title@/$(spec_title)/' \ + -e 's/@spec_vendor@/$(spec_vendor)/' \ + -e 's/@spec_version@/$(PACKAGE_VERSION)/' \ + ; \ + } >$@-t + mv $@-t ../$@ + TIMESTAMP := $(shell date +"%Y%m%d%H%M%S") RPM_REVISION := 1 RPM_TARGET := noarch diff --git a/fat-jar/create-src-dir-overlay.sh b/fat-jar/create-src-dir-overlay.sh new file mode 100755 index 0000000000..34c0053ee7 --- /dev/null +++ b/fat-jar/create-src-dir-overlay.sh @@ -0,0 +1,29 @@ +# Creates directory structure overlay on top of original source directories so +# that the overlay matches Java package hierarchy. +#!/usr/bin/env bash + +if [ ! -d src-main ]; then + mkdir src-main + mkdir src-main/net + mkdir src-main/tsd + (cd src-main/net && ln -s ../../src opentsdb) + (cd src-main/tsd && ln -s ../../src/tsd/QueryUi.gwt.xml QueryUi.gwt.xml) + (cd src-main/tsd && ln -s ../../src/tsd/client client) +fi +if [ ! -d src-test ]; then + mkdir src-test + mkdir src-test/net + (cd src-test/net && ln -s ../../test opentsdb) +fi +if [ ! -d src-resources ]; then + mkdir src-resources + (cd src-resources && ln -s ../fat-jar/logback.xml) + (cd src-resources && ln -s ../fat-jar/file-logback.xml) + (cd src-resources && ln -s ../fat-jar/opentsdb.conf.json) +fi +if [ ! -d test-resources ]; then + mkdir test-resources + (cd test-resources && ln -s ../fat-jar/test-logback.xml) + (cd test-resources && ln -s ../fat-jar/opentsdb.conf.json) +fi + diff --git a/fat-jar/fat-jar-pom.xml.in b/fat-jar/fat-jar-pom.xml.in new file mode 100644 index 0000000000..4e249a8baf --- /dev/null +++ b/fat-jar/fat-jar-pom.xml.in @@ -0,0 +1,677 @@ + + + 4.0.0 + + net.opentsdb + opentsdb-fatjar + @spec_version@ + @spec_title@ + + @spec_vendor@ + http://opentsdb.net + + + OpenTSDB is a distributed, scalable Time Series Database (TSDB) + written on top of HBase. OpenTSDB was written to address a common need: + store, index and serve metrics collected from computer systems (network + gear, operating systems, applications) at a large scale, and make this + data easily accessible and graphable. + + http://opentsdb.net + + + LGPLv2.1+ + http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + repo + + + + + scm:git:git@github.com:OpenTSDB/opentsdb.git + https://github.com/OpenTSDB/opentsdb + + + GitHub + https://github.com/OpenTSDB/opentsdb/issues + + + + User List + opentsdb@googlegroups.com + opentsdb+subscribe@googlegroups.com + opentsdb+unsubscribe@googlegroups.com + https://groups.google.com/group/opentsdb + + + + + tsuna + Benoit "tsuna" Sigoure + tsunanet@gmail.com + + developer + + -8 + + + 2010 + + jar + + + + UTF-8 + 1.6 + 1.6 + + @GUAVA_VERSION@ + @JACKSON_VERSION@ + @NETTY_VERSION@ + @SUASYNC_VERSION@ + @ZOOKEEPER_VERSION@ + @SLF4J_API_VERSION@ + @ASYNCHBASE_VERSION@ + @LOG4J_OVER_SLF4J_VERSION@ + @LOGBACK_CORE_VERSION@ + @LOGBACK_CLASSIC_VERSION@ + @HAMCREST_VERSION@ + @JAVASSIST_VERSION@ + @JUNIT_VERSION@ + @MOCKITO_VERSION@ + @OBJENESIS_VERSION@ + @POWERMOCK_MOCKITO_VERSION@ + + @GWT_VERSION@ + 2.1.2 + 2.5.1 + 2.1 + 1.2.1 + 1.7 + 1.7 + 2.9 + 2.16 + 2.4 + 1.4 + 2.8.1 + + + + + src-main + src-test + + + src-resources + + + + + test-resources + + + + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + ${eclipse-plugin.version} + + true + true + + net/opentsdb/tsd/client/** + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler-plugin.version} + + ${project.source} + ${project.source.target} + -Xlint + + **/client/*.java + + + **/TestGraphHandler.java + + + + + + org.codehaus.mojo + exec-maven-plugin + ${exec-maven.version} + + + generate-build-data + + build-aux/gen_build_data.sh + + + target/generated-sources/net/opentsdb/BuildData.java + net.opentsdb + BuildData + + + generate-sources + + exec + + + + create-plugin-test-jar + + + jar + + cvfm + plugin_test.jar + test/META-INF/MANIFEST.MF + -C + target/test-classes + net/opentsdb/plugin/DummyPluginA.class + -C + target/test-classes + net/opentsdb/plugin/DummyPluginB.class + -C + target/test-classes + net/opentsdb/search/DummySearchPlugin.class + -C + target/test-classes + net/opentsdb/tsd/DummyHttpSerializer.class + -C + target/test-classes + net/opentsdb/tsd/DummyRpcPlugin.class + -C + target/test-classes + net/opentsdb/tsd/DummyRTPublisher.class + -C + test + META-INF/services/net.opentsdb.plugin.DummyPlugin + -C + test + META-INF/services/net.opentsdb.search.SearchPlugin + -C + test + META-INF/services/net.opentsdb.tsd.HttpSerializer + -C + test + META-INF/services/net.opentsdb.tsd.RpcPlugin + -C + test + META-INF/services/net.opentsdb.tsd.RTPublisher + + + test-compile + + exec + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper.version} + + + add-source + generate-sources + + add-source + + + + target/generated-sources + + + + + + + + maven-antrun-plugin + ${antrun-plugin.version} + + + process-resources + + + + + + + + + + run + + + + include-query-ui + package + + + + + + + + + + run + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire-plugin.version} + + -Xmx2048m -XX:MaxPermSize=256m + true + classes + 2 + false + + ${basedir}/test-resources/test-logback.xml + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${jar-plugin.version} + + + + true + true + + + + WEB-INF/deploy/** + queryui/** + + + + + + org.codehaus.mojo + gwt-maven-plugin + ${gwt.version} + + + + true + tsd.QueryUi + + compile + + compile + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${source-plugin.version} + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + true + true + + Copyright © {inceptionYear}-{currentYear}, + ${project.organization.name} + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + tsunanet@gmail.com + + + + + org.apache.maven.plugins + maven-shade-plugin + ${shade.version} + + + package + + shade + + + target/${project.build.finalName}-fat.jar + true + + + *:* + + + /*.proto + + + + + net.opentsdb.tools.Main + + + + + + + + + + + + + + + com.google.guava + guava + ${guava.version} + + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + io.netty + netty + ${netty.version} + + + + + com.stumbleupon + async + ${suasync.version} + + + + + org.apache.zookeeper + zookeeper + ${zookeeper.version} + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + jline + jline + + + junit + junit + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + org.hbase + asynchbase + ${asynchbase.version} + + + + + + + org.slf4j + log4j-over-slf4j + ${log4j.version} + + + + + ch.qos.logback + logback-core + ${logback-core.version} + + + + + ch.qos.logback + logback-classic + ${logback-classic.version} + + + + + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + + + org.javassist + javassist + ${javassist.version} + test + + + + + junit + junit + ${junit.version} + test + + + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + org.objenesis + objenesis + ${objenesis.version} + test + + + + + org.powermock + powermock-api-mockito + ${powermock.version} + test + + + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + + + + com.google.gwt + gwt-user + ${gwt.version} + + + + + + + + + + + windows + + + + + + org.codehaus.mojo + exec-maven-plugin + ${exec-maven.version} + + + build-data-codegen + generate-sources + + exec + + + cmd.exe + + /C + "build-aux\gen_build_data.cmd" + + + + + + + + + + + Linux + + + !windows + + + + + + + org.codehaus.mojo + exec-maven-plugin + ${exec-maven.version} + + + build-data-codegen + generate-sources + + exec + + + build-aux/gen_build_data.sh + + + target/generated-sources/net/opentsdb/BuildData.java + net.opentsdb + BuildData + + + + + + + + + + + + + + org.sonatype.oss + oss-parent + 7 + + + diff --git a/fat-jar/fat-jar-readme.md b/fat-jar/fat-jar-readme.md new file mode 100644 index 0000000000..829cdc760c --- /dev/null +++ b/fat-jar/fat-jar-readme.md @@ -0,0 +1,180 @@ +## OpenTSDB FatJar + +### Introduction + +FatJar is a modified build procedure for OpenTSDB intended to provide a single executable jar that contains all of the java artifacts and resources required to run an OpenTSDB 2.1+ instance. This includes the tsd and all associated command line tools. It does not include the following which remain external dependencies: +* The gnuplot executable which generates data visualizations in the OpenTSDB graphical web console. +* The HBase instance or cluster where time-series data is stored. +* The Java runtime. + +With a FatJar build and the above dependencies, it is possible to start an OpenTSDB as simply as: + +```java -jar opentsdb-2.1.jar``` + +#### Building the FatJar + +These steps assume building from my current fork repo. + +1. Clone: ```git clone https://github.com/nickman/opentsdb.git``` +2. Switch to project directory: ```cd opentsdb``` +3. Switch to **next** branch: ```git checkout next``` +4. Build the **fat-jar-pom.xml**: ```./build.sh fat-jar-pom.xml``` +5. Run maven, specifying the fat-jar pom (and skipping the gpg plugin and tests): ```mvn -f fat-jar-pom.xml -Dgpg.skip -DskipTests clean install``` +6. Fire her up, Scotty: ```java -jar ./target/opentsdb-2.1.0RC1-fat.jar tsd``` + +**NOTE**: FatJar requires *maven 3*. + +**ANOTHER NOTE**: THe OpenTSDB code base has some non-compliant/missing javadoc tags. If you are compiling with Java 8, [the build will fail](http://http://stackoverflow.com/questions/15886209/maven-is-not-working-in-java-8-when-javadoc-tags-are-incomplete), so: +1. Use Java 6 or 7 OR... +2. Edit the fat-jar-pom.xml, find the javadoc plugin and uncomment the line: + +`````` + +###### This is currently on line 356. It is commented because is it not supported for Java 6 & 7. + +(Note that an [unreviewed] aspect of FatJar is the provision of default values for *all* configuration parameters so the three traditionally required parameters, **port**, **staticroot** and **cachedir** are not required.) + +FatJar is accomplished with: +1. Small changes to 3 existing OpenTSDB classes (all other aspects of the code base remain unchanged) +2. The addition of 8 new classes +3. Additions to the OpenTSDB make scripts to generate the FatJar maven pom +3. A maven based build that packages the following into a single jar: + * The OpenTSDB core + * All of the maven defined dependencies (jars) + * An executable jar defining manifest + * A JSON based configuration definition repository + * All of the OpenTSDB UI resources + +### Motivation and Benefits + +The original motivation for a FatJar build was the simplification of deploying OpenTSDB to multiple "locked-down" environments where it was not feasible to build from source. The addition of RPM and DEB packages for Linux have significantly eased this process, but deployment to non-Linux environments remains a largely manual procedure. While it would be conceivable to implement Solaris and Windows install packages (our 2 pain points), it seemed simpler to adopt the FatJar approach and kill the entire flock of birds with one stone. + +The FatJar has been optimized to support simplicity of deployment and configuration, while maintaining complete backward compatability so the modifications are completely additive. + +Among some of the benefits we enjoy from the FatJar build: +* Simplified deployment +* No requirement for a pre-deployed UI static content directory. The FatJar contains all the static resources and unloads this content into the configured/default directory at start time, including the GWT Web App content and the gnuplot wrapper script. If any content already exists and has a newer timestamp than the corresponding resource in the FatJar, the resource is left as is. An additional FatJar provided command line tool allows for the pre-creation of the static content directory, which is useful in development. +* The process PID is written to a default [and configurable] pid file, and deleted on clean shutdown. +* URL based config and include supporting the load of configuration files from http:// or file:// based sources. +* Configuration properties can be tokenized with System Prop and/or Environmental Variable decodes as well as JavaScript snippets to support dynamically computed property values according to the deployment environment. +* Refined logging configuration with configuration override-able external logging config file location and command line options to set logging levels for major packages in OpenTSDB and associated libraries. + + +### Changes and Additions + +#### Modified Classes and Files + +* **.gitignore**: Added transient artifacts created by FatJar builds. +* **Makefile.am**: Added target **fat-jar-pom.xml** that generates the fat-jar symbolic source overlay and fat-jar-pom.xml. Added the new source and test files to **tsdb_SRC** and **test_SRC**. +* **src/tsd/GraphHandler.java**: FatJar introduced a class GnuplotInstaller which locates the gnuplot executable by scanning the path and installs the Gnuplot wrapper script. This modification integrates GnuplotInstaller into the GraphHandler. +* **src/tsd/PipelineFactory.java**: The pipeline's idle-timeout handler HashedWheelTimer was using a non-daemon thread and preventing an orderly shutdown. This is also a fix for #455. +* **src/utils/Config.java**: Made ```loadStaticVariables()``` public. + +#### New Classes and Files + +* **fat-jar/create-src-dir-overlay.sh**: Creates a maven oriented source overlay and generates a **fat-jar-pom.xml**. +* **fat-jar/fat-jar-pom.xml.in**: The input template for **fat-jar-pom.xml**. +* **fat-jar/logback.xml**: The FatJar OpenTSDB boot time and default logging configuration. +* **fat-jar/file-logback.xml**: The parameterized file based logging and file-rolling configuration if a log file name is configured. +* **fat-jar/test-logback.xml**: Test resource logback configuration. Pretty much like **fat-jar/logback.xml** but without the jmx component enabled since it messes up the use of PowerMock. +* **fat-jar/opentsdb.conf.json**: The configuration parameter definition repository +* **src/tools/ArgValueValidator.java**: Interface that defines a class that validates a type of configuration parameter. +* **src/tools/ConfigArgP.java**: Configuration parameter manager. Conceptually a merge of Config and ArgP. Contains a variety of testing hooks. +* **src/tools/ConfigMetaType.java**: Enumeration of configuration parameter types, each with a link to an **ArgValueValidator** so that each parameter value can be validated as accurately as possible. +* **src/tools/GnuplotInstaller.java**: Validates the presence of the gnuplot executable and installs the OpenTSDB gnuplot invocation wrapper shell script. +* **src/tools/Main.java**: The FatJar substitute for **TSDMain**, serving as the boot entry point for the TSD as well as the command line tools. +* **test/tools/TestConfigArgP.java**: Test suite for the **ConfigArgP** class. + +#### New Config Parameters + +* **tsd.logback.file**: The name of the log file the OpenTSDB logback configuration will log to when running the FatJar. +* **tsd.logback.rollpattern**: The logback rolling file appender roll pattern used to roll the file defined in **tsd.logback.file**. +* **tsd.logback.console**: If specified, logback will log to the configured file and keep logging to the console, otherwise, when the file appender is activated, the console appender is removed. +* **tsd.logback.config**: Points to an external [outside the FatJar] logback config file, in which case the internal file based logging configuration is ignored once the config has been loaded. +* **tsd.process.pid.file**: The name and location of the OpenTSDB PID file. +* **tsd.process.pid.ignore.existing**: Normally, if the PID file defined in **tsd.process.pid.file** is found at startup, startup will be aborted. This directive ignores the existing file and overwrites it. +* **tsd.core.config**: Points to a core configuration file which should be used as the base line config. This means the built in config can simply contain this item and point to a shared file or HTTP based location for the main configuration. +* **tsd.ui.noexport**: Disables the automatic UI content export to the local file system. +* **tsd.core.config.include**: Specifies a file or HTTP based config that will override the config specified in the core configuration. (See Presedence below) +* **help**: Not really a configuration, but present to flag the word as an OpenTSDB processable command. +* Default Bindings: These are default JS bindings which can be useful if scripting dynamic configuration values for thread counts and/or cache sizes. Note that all system properties and environmental variables are automatically available in the JavaScript context. + * **processors**: The number of cores available to the JVM + * **maxbytes**: The maximum amount of memory available to the JVM + +#### Configuration Presedence + +In order of presedence: +1. Command line parameters +2. Included parameters +3. Core parameters + +#### Installing the UI content + +The FATJar supports a command line tool to install the UI static content to a specified location: + +``` +Usage: java -jar exportui --d [--p] + --d=DIR The directory to export the UI content to + --p Create the directory if it does not exist +``` +Example: + +```java -jar opentsdb-2.1.0RC1.jar exportui --d /tmp/opentsdb/static-content --p``` + +Output: + +``` +2015-03-15 16:48:14,988 INFO [main] Main: Created exportui directory [/tmp/opentsdb/static-content] +2015-03-15 16:48:15,258 INFO [main] Main: + + =================================================== + Static Root Directory:[/tmp/opentsdb/static-content] + Total Files Written:24 + Total Bytes Written:1162522 + File Write Failures:0 + Existing File Newer Than Content:0 + Elapsed (ms):270 + =================================================== +``` + +#### Command Line Help + +The FatJar supports a slightly modified command line help model. The command help will print the main “menu” of options for the FatJar. + +``` +java -jar opentsdb-x.x.x.jar help +Usage: java -jar [opentsdb.jar] [command] [args] +Valid commands: + tsd: Starts a new TSDB instance + fsck: Searches for and optionally fixes corrupted data in a TSDB + import: Imports data from a file into HBase through a TSDB + mkmetric: Creates a new metric + query: Queries time series data from a TSDB + scan: Dumps data straight from HBase + uid: Provides various functions to search or modify information in the tsdb-uid table. + exportui: Exports the OpenTSDB UI static content + + Use help for details on a command +``` + +Using java -jar opentsdb-x.x.x.jar help for all CLI tools will print the tool's existing usage message. Help for tsd has a couple of additional options. Using the call above, with tsd as the command, the standard usage will be printed. With the additional argument of the keyword **extended**, the full set of configuration options is printed. Full example [here](https://gist.github.com/nickman/673852d8d88675043f45). + +#### Eclipse Support + +An [almost] incidental benefit for users of the Eclipse IDE is that the FatJar source overlay simplifies the setup of a "working-out-of-the-box" Eclipse project using maven. Just do this: + +```mvn -f fat-jar-pom.xml eclipse:eclipse``` + +Then load up the project in Eclipse and launch **net.opentsdb.tools.Main** with the parameter **tsd"**. + +All at no cost to you. + +#### Last Note + +There's some big changes here. I recognize it's not perfect. Please let me have any feedback. It will not be taken personally, but only to improve OpenTSDB. + + + + + + diff --git a/fat-jar/file-logback.xml b/fat-jar/file-logback.xml new file mode 100644 index 0000000000..dc8d45edb2 --- /dev/null +++ b/fat-jar/file-logback.xml @@ -0,0 +1,72 @@ + + + + + + %d{ISO8601} %-5level [%thread] %logger{0}: %msg%n + + + + + 1024 + + + + ${tsdb.logback.file} + true + + + ${tsd.logback.rollpattern} + + + 5MB + + + 30 + + + + UTF-8 + %d %-4relative [%thread] %-5level %logger{35} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fat-jar/logback.xml b/fat-jar/logback.xml new file mode 100644 index 0000000000..7c0877cb71 --- /dev/null +++ b/fat-jar/logback.xml @@ -0,0 +1,50 @@ + + + + + + + %d{ISO8601} %-5level [%thread] %logger{0}: %msg%n + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fat-jar/opentsdb.conf.json b/fat-jar/opentsdb.conf.json new file mode 100644 index 0000000000..9b3a283b84 --- /dev/null +++ b/fat-jar/opentsdb.conf.json @@ -0,0 +1,439 @@ +{ + "config-items": [ + { + "key": "tsd.core.auto_create_metrics", + "cl-option": "--auto-metric", + "defaultValue": "false", + "description": "Whether or not a data point with a new metric will assign a UID to the metric. When false, a data point with a metric that is not in the database will be rejected and an exception will be thrown", + "help": "default", + "meta": "BOOL" + }, + { + "key": "tsd.core.auto_create_tagks", + "cl-option": "--auto-tagk", + "defaultValue": "true", + "description": "Whether or not a data point with a new tag name will assign a UID to the tagk. When false, a data point with a tag name that is not in the database will be rejected and an exception will be thrown", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.auto_create_tagvs", + "cl-option": "--auto-tagv", + "defaultValue": "true", + "description": "Whether or not a data point with a new tag value will assign a UID to the tagv. When false, a data point with a tag value that is not in the database will be rejected and an exception will be thrown", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.meta.enable_realtime_ts", + "cl-option": "--realtime-ts", + "defaultValue": "false", + "description": "Whether or not to enable real-time TSMeta object creation", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.meta.enable_realtime_uid", + "cl-option": "--realtime-uid", + "defaultValue": "false", + "description": "Whether or not to enable real-time UIDMeta object creation", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.meta.enable_tsuid_incrementing", + "cl-option": "--tsuid-incr", + "defaultValue": "false", + "description": "Whether or not to enable tracking of TSUIDs by incrementing a counter every time a data point is recorded. (Overrides tsd.core.meta.enable_tsuid_tracking)", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.meta.enable_tsuid_tracking", + "cl-option": "--tsuid-tracking", + "defaultValue": "false", + "description": "Whether or not to enable tracking of TSUIDs by storing a 1 with the current timestamp every time a data point is recorded", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.plugin_path", + "cl-option": "--plugin-path", + "description": "A path to search for plugins when the TSD starts. If the path is invalid, the TSD will fail to start. Plugins can still be enabled if they are in the class path", + "help": "extended", + "meta": "CLASSPATH" + }, + { + "key": "tsd.core.preload_uid_cache", + "cl-option": "--tsd-preloaduids", + "defaultValue": "false", + "description": "Enables pre-population of the UID caches when starting a TSD", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.preload_uid_cache.max_entries", + "cl-option": "--tsd-maxuids", + "defaultValue": 300000, + "description": "The number of rows to scan for UID pre-loading", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.core.timezone", + "cl-option": "--tsd-timezone", + "defaultValue": "${user.timezone}", + "description": "A localized timezone identification string used to override the local system timezone used when converting absolute times to UTC when executing a query. This does not affect incoming data timestamps. E.g. America/Los_Angeles", + "help": "extended", + "meta": "TIMEZONE" + }, + { + "key": "tsd.core.tree.enable_processing", + "cl-option": "--enable-tree", + "defaultValue": "false", + "description": "Whether or not to enable processing new/edited TSMeta through tree rule sets", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.http.cachedir", + "cl-option": "--cachedir", + "defaultValue": "${java.io.tmpdir}/.tsdb/http-cache/", + "description": "The full path to a location where temporary files can be written. e.g. /tmp/opentsdb", + "help": "default", + "meta": "DIR" + }, + { + "key": "tsd.http.request.cors_domains", + "cl-option": "--cors-domains", + "description": "A comma separated list of domain names to allow access to OpenTSDB when the Origin header is specified by the client. If empty, CORS requests are passed through without validation. The list may not contain the public wildcard * and specific domains at the same time", + "help": "extended", + "meta": "LIST" + }, + { + "key": "tsd.http.request.cors_headers", + "cl-option": "--cors-headers", + "defaultValue": "Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since", + "description": "A comma separated list of headers sent to clients when executing a CORs request. The literal value of this option will be passed to clients", + "help": "extended", + "meta": "LIST" + }, + { + "key": "tsd.http.request.enable_chunked", + "cl-option": "--enable-chunked", + "defaultValue": "false", + "description": "Whether or not to enable incoming chunk support for the HTTP RPC", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.http.request.max_chunk", + "cl-option": "--max-chunk", + "defaultValue": "4096", + "description": "The maximum request body size to support for incoming HTTP requests when chunking is enabled", + "help": "extended", + "meta": "GTZEROINT" + }, + { + "key": "tsd.http.show_stack_trace", + "cl-option": "--show-stack", + "defaultValue": "true", + "description": "Whether or not to return the stack trace with an API query response when an exception occurs", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.http.staticroot", + "cl-option": "--staticroot", + "defaultValue": "${java.io.tmpdir}/.tsdb/static-content", + "description": "Location of a directory where static files, such as JavaScript files for the web interface, are located. e.g. /opt/opentsdb/staticroot", + "help": "default", + "meta": "DIR" + }, + { + "key": "tsd.mode", + "cl-option": "--tsd-mode", + "defaultValue": "rw", + "description": "Whether or not the TSD will allow writing data points. Must be either rw to allow writing data or ro to block writes", + "help": "extended", + "meta": "RWMODE" + }, + { + "key": "tsd.network.async_io", + "cl-option": "--async-io", + "defaultValue": "true", + "description": "Whether or not to use NIO or tradditional blocking IO", + "help": "default", + "meta": "BOOL" + }, + { + "key": "tsd.network.backlog", + "cl-option": "--backlog", + "defaultValue": "3072", + "description": "The connection queue depth for completed or incomplete connection requests depending on OS. The default may be limited by the 'somaxconn' kernel setting or set by Netty to 3072", + "help": "default", + "meta": "GTZEROINT" + }, + { + "key": "tsd.network.bind", + "cl-option": "--bind", + "defaultValue": "0.0.0.0", + "description": "An IPv4 address to bind to for incoming requests. The default is to listen on all interfaces. e.g. 127.0.0.1", + "help": "default", + "meta": "ADDR" + }, + { + "key": "tsd.network.keep_alive", + "cl-option": "--keep-alive", + "defaultValue": "true", + "description": "Whether or not to allow keep-alive connections", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.network.port", + "cl-option": "--port", + "defaultValue": "4242", + "description": "The TCP port to use for accepting connections", + "help": "default", + "meta": "POSINT" + }, + { + "key": "tsd.network.reuse_address", + "cl-option": "--reuse-address", + "defaultValue": "true", + "description": "Whether or not to allow reuse of the bound port within Netty", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.network.tcp_no_delay", + "cl-option": "--tcp-no-delay", + "defaultValue": "true", + "description": "Whether or not to disable TCP buffering before sending data", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.network.worker_threads", + "cl-option": "--worker-threads", + "defaultValue": "$[new Integer(cores * 2)]", + "description": "The number of asynchronous IO worker threads for Netty", + "help": "default", + "meta": "GTZEROINT" + }, + { + "key": "tsd.core.socket.timeout", + "cl-option": "--socket-timeout", + "defaultValue": 0, + "description": "The idle time timeout for allocated sockets (seconds)", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.no_diediedie", + "cl-option": "--tsd-nodie", + "defaultValue": "false", + "description": "Enable or disable the diediedie HTML and ASCII commands to shutdown a TSD", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.rpc.plugins", + "cl-option": "--rpc-plugins", + "description": "A comma delimited list of RPC plugins to load when starting a TSD. Must contain the entire class name", + "help": "extended", + "meta": "LIST" + }, + { + "key": "tsd.rtpublisher.enable", + "cl-option": "--rtplublisher", + "defaultValue": "false", + "description": "Whether or not to enable a real time publishing plugin. If true, you must supply a valid tsd.rtpublisher.plugin class name", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.rtpublisher.plugin", + "cl-option": "--rtplublisher-plugin", + "description": "The class name of a real time publishing plugin to instantiate. If tsd.rtpublisher.enable is set to false, this value is ignored. e.g. net.opentsdb.tsd.RabbitMQPublisher", + "help": "extended", + "meta": "CLASS" + }, + { + "key": "tsd.search.enable", + "cl-option": "--search", + "defaultValue": "false", + "description": "Whether or not to enable search functionality. If true, you must supply a valid tsd.search.plugin class name", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.search.plugin", + "cl-option": "--search-plugin", + "description": "The class name of a search plugin to instantiate. If tsd.search.enable is set to false, this value is ignored. e.g. net.opentsdb.search.ElasticSearch", + "help": "extended", + "meta": "CLASS" + }, + { + "key": "tsd.stats.canonical", + "cl-option": "--stats-canonical", + "defaultValue": "false", + "description": "Whether or not the FQDN should be returned with statistics requests. The default stats are returned with host= which is not guaranteed to perform a lookup and return the FQDN. Setting this to true will perform a name lookup and return the FQDN if found, otherwise it may return the IP. The stats output should be fqdn=", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.storage.enable_compaction", + "cl-option": "--compaction", + "defaultValue": "true", + "description": "Whether or not to enable compactions", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.storage.fix_duplicates", + "cl-option": "--fixdups", + "defaultValue": "false", + "description": "Whether or not to accept the last written value when parsing data points with duplicate timestamps. When enabled in conjunction with compactions, a compacted column will be written with the latest data points", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.storage.flush_interval", + "cl-option": "--flush-interval", + "defaultValue": 1000, + "description": "How often, in milliseconds, to flush the data point storage write buffer", + "help": "default", + "meta": "GTZEROINT" + }, + { + "key": "tsd.storage.hbase.data_table", + "cl-option": "--table", + "defaultValue": "tsdb", + "description": "Name of the HBase table where data points are stored", + "help": "default", + "meta": "TABLE" + }, + { + "key": "tsd.storage.hbase.meta_table", + "cl-option": "--metatable", + "defaultValue": "tsdb-meta", + "description": "Name of the HBase table where meta data are stored", + "help": "extended", + "meta": "TABLE" + }, + { + "key": "tsd.storage.hbase.tree_table", + "cl-option": "--treetable", + "defaultValue": "tsdb-tree", + "description": "Name of the HBase table where tree data are stored", + "help": "extended", + "meta": "TABLE" + }, + { + "key": "tsd.storage.hbase.uid_table", + "cl-option": "--uidtable", + "defaultValue": "tsdb-uid", + "description": "Name of the HBase table where UID information is stored", + "help": "extended", + "meta": "TABLE" + }, + { + "key": "tsd.storage.hbase.zk_basedir", + "cl-option": "--zkbasedir", + "defaultValue": "/hbase", + "description": "Path under which is the znode for the -ROOT- region (default: /hbase)", + "help": "default", + "meta": "ZPATH" + }, + { + "key": "tsd.storage.hbase.zk_quorum", + "cl-option": "--zkquorum", + "defaultValue": "localhost", + "description": "A comma-separated list of ZooKeeper hosts to connect to, with or without port specifiers. e.g. 192.168.1.1:2181,192.168.1.2:2181 (default: localhost)", + "help": "default", + "meta": "SPEC" + }, + { + "key": "tsd.logback.file", + "cl-option": "--logback", + "description": "The file name that the tsd will log to using logback", + "help": "default", + "meta": "EDIRFILE" + }, + { + "key": "tsd.logback.rollpattern", + "cl-option": "--logback-roll", + "description": "The pattern specifying the rolling file pattern, inserted between the file name base and extension", + "defaultValue" : "_%d{yyyy-MM-dd}.%i", + "help": "default" + }, + { + "key": "tsd.logback.console", + "cl-option": "--logback-console", + "description": "If specified, logback will log to the configured file and keep logging to the console", + "defaultValue": "false", + "help": "default", + "meta": "BOOL" + }, + { + "key": "tsd.logback.config", + "cl-option": "--logback-config", + "description": "The file name of an external logback configuration", + "help": "default", + "meta": "EFILE" + }, + + { + "key": "tsd.process.pid.file", + "cl-option": "--pid-file", + "defaultValue": "${java.io.tmpdir}/.tsdb/opentsdb.pid", + "description": "The file to write the process PID to. Defaults to [${user.home}.tsdb/opentsdb.pid]", + "help": "default", + "meta": "FILE" + }, + { + "key": "help", + "cl-option": "--help", + "defaultValue": "", + "description": "Prints the default command line usage options, or the extended if 'extended' is passed as an arg", + "help": "default" + }, + { + "key": "tsd.core.config", + "cl-option": "--config", + "description": "The core config file overlayed on this default", + "help": "default", + "meta": "URLORFILE" + }, + { + "key": "tsd.ui.noexport", + "cl-option": "--no-uiexport", + "defaultValue": "false", + "description": "Skips the boot time export of static UI content to tsd.http.staticroot", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.process.pid.ignore.existing", + "cl-option": "--ignore-existing-pid", + "defaultValue": "false", + "description": "If true, ignores and overwrites an existing pid file on startup", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.config.include", + "cl-option": "--include", + "description": "An additional config overlay useful when the --config file is fairly static", + "help": "extended", + "meta": "URLORFILE" + } + ], + "bindings": [ + "//importPackage(Packages.java.lang);", + "var processors = java.lang.Runtime.getRuntime().availableProcessors();", + "var maxbytes = java.lang.Runtime.getRuntime().maxMemory();" + ] +} diff --git a/fat-jar/test-logback.xml b/fat-jar/test-logback.xml new file mode 100644 index 0000000000..c0f172d9e9 --- /dev/null +++ b/fat-jar/test-logback.xml @@ -0,0 +1,45 @@ + + + + + + %d{ISO8601} %-5level [%thread] %logger{0}: %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tools/ArgValueValidator.java b/src/tools/ArgValueValidator.java new file mode 100644 index 0000000000..5a5c7478fa --- /dev/null +++ b/src/tools/ArgValueValidator.java @@ -0,0 +1,30 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2016 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.tools; + +import net.opentsdb.tools.ConfigArgP.ConfigurationItem; + +/** + *

    Title: ArgValueValidator

    + *

    Description: Defines a class that can validate a value instance of a declared ConfigMetaType

    + */ +public interface ArgValueValidator { + /** + * Validates the passed configuration item + * @param citem The item to validate + */ + public void validate(ConfigurationItem citem); + + /** A static exception that needs no stack trace or whatever */ + public static final Exception EX = new Exception(); +} diff --git a/src/tools/ConfigArgP.java b/src/tools/ConfigArgP.java new file mode 100644 index 0000000000..99651008a4 --- /dev/null +++ b/src/tools/ConfigArgP.java @@ -0,0 +1,833 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2016 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.tools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +import net.opentsdb.utils.Config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + *

    Title: ConfigArgP

    + *

    Description: Wraps {@link Config} and {@link ArgP} instances for a consolidated configuration and command line handler

    + */ + +public class ConfigArgP { + /** Static class logger */ + protected static final Logger LOG = LoggerFactory.getLogger(ConfigArgP.class); + /** The command line argument holder for all (default and extended) options */ + protected final ArgP argp = new ArgP(); + /** The command line argument holder for default options */ + protected final ArgP dargp = new ArgP(); + /** The non config option arguments */ + protected String[] nonOptionArgs = {}; + /** The base configuration */ + protected Config config; + /** The default configuration items keyed by the item key and cl-option */ + protected final Map defaultConfItems; + /** The command line args */ + protected final Set commandLineArgs; + + /** The raw configuration items loaded from the json file */ + protected final TreeSet configItemsByKey = new TreeSet(); + /** The raw configuration items loaded from the json file */ + protected final TreeSet configItemsByCl = new TreeSet(); + + /** The regex pattern to perform a substitution for
    ${<sysprop>:<default>}
    patterns in strings */ + public static final Pattern SYS_PROP_PATTERN = Pattern.compile("\\$\\{(.*?)(?::(.*?))??\\}"); + /** The regex pattern to perform a substitution for
    $[<javascript snippet>]
    patterns in strings */ + public static final Pattern JS_PATTERN = Pattern.compile("\\$\\[(.*?)\\]", Pattern.MULTILINE); + + /** The config key for the TSD RPC addin classes */ + public static final String TSD_RPC_ADDIN_KEY = "tsd.addins.rpcs"; + /** The config key for the TSD RPC addin classpath */ + public static final String TSD_RPC_ADDIN_CP_KEY = "tsd.addins.rpcs.classpath"; + + /** Indicates if we're on Windows, in which case the SysProp handling needs a few tweaks */ + public static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows"); + /** The JavaScript Engine to interpret $[<javascript snippet>] values */ + protected static final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByExtension("js"); + /** The JavaScript bindings */ + protected static final Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE); + + + + static { + // Initialize js bindings + Map map = new HashMap(); + for(String key: System.getProperties().stringPropertyNames()) { + map.put(key, System.getProperty(key)); + } + map.put("cores", ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors()); + map.put("maxheap", Runtime.getRuntime().maxMemory()); + bindings.putAll(map); + bindings.putAll(System.getenv()); + bindings.put("bindings", bindings); + } + + /** + * Creates a new ConfigArgP + * @param args The command line arguments + */ + public ConfigArgP(String...args) { + InputStream is = null; + commandLineArgs = Collections.unmodifiableSet(new LinkedHashSet(Arrays.asList(args))); + try { + config = new NoLoadConfig(); + is = ConfigArgP.class.getClassLoader().getResourceAsStream("opentsdb.conf.json"); + ObjectMapper jsonMapper = new ObjectMapper(); + JsonNode root = jsonMapper.reader().readTree(is); + JsonNode configRoot = root.get("config-items"); + scriptEngine.eval("var config = " + configRoot.toString() + ";"); + processBindings(jsonMapper, root); + final ConfigurationItem[] loadedItems = jsonMapper.reader(ConfigurationItem[].class).readValue(configRoot); + final TreeSet items = new TreeSet(Arrays.asList(loadedItems)); + Map tmpItems = new HashMap(items.size()); + for(Iterator iter = items.iterator(); iter.hasNext();) { + ConfigurationItem item = iter.next(); + if(tmpItems.containsKey(item.getKey())) throw new RuntimeException("opentsdb.conf.json contains duplicate key: [" + item.getKey() + "]"); + if(tmpItems.containsKey(item.getClOption())) throw new RuntimeException("opentsdb.conf.json contains duplicate clOption: [" + item.getClOption() + "]"); + tmpItems.put(item.getKey(), item); + tmpItems.put(item.getClOption(), item); +// if("BOOL".equals(item.getMeta())) iter.remove(); + } + defaultConfItems = Collections.unmodifiableMap(tmpItems); + LOG.debug("Loaded [{}] Configuration Items from opentsdb.conf.json", items.size()); + if(LOG.isDebugEnabled()) { + StringBuilder b = new StringBuilder("Configs:"); + for(ConfigurationItem ci: items) { + b.append("\n\t").append(ci.toString()); + } + b.append("\n"); + LOG.debug(b.toString()); + } + for(ConfigurationItem ci: items) { + LOG.debug("Processing CI [{}]", ci.getKey()); + if(ci.meta!=null) { + argp.addOption(ci.clOption, ci.meta, ci.description); + LOG.debug("Registered Meta ArgP cl:[{}], meta:[{}]", ci.clOption, ci.meta); + if("default".equals(ci.help)) dargp.addOption(ci.clOption, ci.meta, ci.description); + } else { + argp.addOption(ci.clOption, ci.description); + LOG.debug("Registered No Meta ArgP cl:[{}]", ci.clOption); + if("default".equals(ci.help)) dargp.addOption(ci.clOption, ci.description); + } + if(!configItemsByKey.add(ci)) { + throw new RuntimeException("Duplicate configuration key [" + ci.key + "] in opentsdb.conf.json. Programmer Error."); + } + if(!configItemsByCl.add(ci)) { + throw new RuntimeException("Duplicate configuration command line option [" + ci.clOption + "] in opentsdb.conf.json. Programmer Error."); + } + if(ci.getDefaultValue()!=null && !ci.getDefaultValue().trim().isEmpty()) { + ci.setValue(processConfigValue(ci.getDefaultValue())); + config.overrideConfig(ci.key, processConfigValue(ci.getValue())); + } + } + nonOptionArgs = applyArgs(args); + loadExternalConfigs(items); + config = new Config(config); + + } catch (Exception ex) { + if(ex instanceof IllegalArgumentException) { + throw (IllegalArgumentException)ex; + } + throw new RuntimeException("Failed to read opentsdb.conf.json", ex); + } finally { + if(is!=null) try { is.close(); } catch (Exception x) { /* No Op */ } + } + } + + /** + * Loads an externally defined configuration and/or a configuration include + * @param source The existing configuration items + */ + protected void loadExternalConfigs(final Set source) { + ConfigurationItem include = getConfigurationItemByKey(source, "tsd.core.config"); + if(include!=null && include.getValue()!=null) { + loadConfigSource(this, include.getValue()); + } + include = getConfigurationItemByKey(source, "tsd.core.config.include"); + if(include!=null && include.getValue()!=null) { + loadConfigSource(this, include.getValue()); + } + } + + /** + * Applies the properties from the named source to the main configuration + * @param config the main configuration to apply to + * @param source the name of the source to apply properties from + */ + protected static void loadConfigSource(ConfigArgP config, String source) { + Properties p = loadConfig(source); + Config c = config.getConfig(); + for(String key: p.stringPropertyNames()) { + String value = p.getProperty(key); + ConfigurationItem ci = config.getConfigurationItem(key); + if(ci!=null) { // if we recognize the key, validate it + ci.setValue(processConfigValue(value)); + } + c.overrideConfig(key, processConfigValue(value)); + } + } + + /** + * Loads properties from a file or url with the passed name + * @param name The name of the file or URL + * @return the loaded properties + */ + protected static Properties loadConfig(String name) { + try { + URL url = new URL(name); + return loadConfig(url); + } catch (Exception ex) { + return loadConfig(new File(name)); + } + } + + /** + * Loads properties from the passed input stream + * @param source The name of the source the properties are being loaded from + * @param is The input stream to load from + * @return the loaded properties + */ + protected static Properties loadConfig(String source, InputStream is) { + try { + Properties p = new Properties(); + p.load(is); + // trim the value as it may have trailing white-space + Set keys = p.stringPropertyNames(); + for(String key: keys) { + p.setProperty(key, p.getProperty(key).trim()); + } + return p; + } catch (IllegalArgumentException iae) { + throw iae; + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load configuration from [" + source + "]"); + } + } + + /** + * Loads properties from the passed file + * @param file The file to load from + * @return the loaded properties + */ + protected static Properties loadConfig(File file) { + InputStream is = null; + try { + is = new FileInputStream(file); + return loadConfig(file.getAbsolutePath(), is); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load configuration from [" + file.getAbsolutePath() + "]"); + }finally { + if(is!=null) try { is.close(); } catch (Exception ex) { /* No Op */ } + } + } + + /** + * Loads properties from the passed URL + * @param url The url to load from + * @return the loaded properties + */ + protected static Properties loadConfig(URL url) { + InputStream is = null; + try { + URLConnection connection = url.openConnection(); + if(connection instanceof HttpURLConnection) { + HttpURLConnection conn = (HttpURLConnection)connection; + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + } + is = connection.getInputStream(); + return loadConfig(url.toString(), is); + } catch (Exception ex) { + ex.printStackTrace(System.err); + throw new IllegalArgumentException("Failed to load configuration from [" + url + "]", ex); + }finally { + if(is!=null) try { is.close(); } catch (Exception ex) { /* No Op */ } + } + } + + + /** + *

    Title: NoLoadConfig

    + *

    Description: A {@link Config} override that does not trigger {@link Config#loadStaticVariables()} when {@link Config#overrideConfig(String, String)} is called.

    + */ + private static class NoLoadConfig extends Config { + /** + * {@inheritDoc} + * @see net.opentsdb.utils.Config#overrideConfig(java.lang.String, java.lang.String) + */ + @Override + public void overrideConfig(final String property, final String value) { + this.properties.put(property, value); + } + } + + + /** + * Parses the command line arguments, and where the options are recognized config items, the value is validated, then applied to the config + * @param clargs The command line arguments + * @return The un-applied command line arguments + */ + public String[] applyArgs(String[] clargs) { + LOG.debug("Applying Command Line Args {}", Arrays.toString(clargs)); + final List nonFlagArgs = new ArrayList(Arrays.asList(clargs)); + extractAndApplyFlags(nonFlagArgs); + + String[] args = nonFlagArgs.toArray(new String[0]); + String[] nonArgs = argp.parse(args); + LOG.debug("Applying Command Line ArgP {}", argp); + LOG.debug("configItemsByCl Keys: [{}]", configItemsByCl.toString()); + for(Map.Entry entry: argp.getParsed().entrySet()) { + String key = entry.getKey(), value = entry.getValue(); + ConfigurationItem citem = getConfigurationItemByClOpt(key); + LOG.debug("Loaded CI for command line option [{}]: Found:{}", key, citem!=null); + if("BOOL".equals(citem.getMeta())) { + citem.setValue(value!=null ? value : "true"); + } else { + if(value!=null) { + citem.setValue(processConfigValue(value)); + } + } +// log("CL Override [%s] --> [%s]", citem.getKey(), citem.getValue()); + config.overrideConfig(citem.getKey(), citem.getValue()); + } + return nonArgs; + } + + private void extractAndApplyFlags(final List args) { + for(Iterator iter = args.iterator(); iter.hasNext();) { + String arg = iter.next(); + if(!arg.startsWith("--")) continue; + ConfigurationItem ci = getConfigurationItemByClOpt(arg); + if(ci==null) continue; + if(ci.isBool()) { + LOG.debug("Enabling flag [{}]", ci.getClOption()); + config.overrideConfig(ci.getKey(), "true"); + iter.remove(); + } + } + } + + private ConfigurationItem getConfigurationItemByKey(final Set source, final String key) { + for(final ConfigurationItem ci: source) { + if(ci.getKey().equals(key)) return ci; + } + return null; + } + + /** + * Returns the {@link ConfigurationItem} with the passed key + * @param key The key of the item to fetch + * @return The matching ConfigurationItem or null if one was not found + */ + public ConfigurationItem getConfigurationItem(final String key) { + if(key==null || key.trim().isEmpty()) throw new IllegalArgumentException("The passed key was null or empty"); + return getConfigurationItemByKey(configItemsByKey, key); + } + + /** + * Returns the {@link ConfigurationItem} with the passed cl-option + * @param clopt The cl-option of the item to fetch + * @return The matching ConfigurationItem or null if one was not found + */ + public ConfigurationItem getConfigurationItemByClOpt(final String clopt) { + if(clopt==null || clopt.trim().isEmpty()) throw new IllegalArgumentException("The passed cl-opt was null or empty"); + for(final ConfigurationItem ci: configItemsByCl) { + if(ci.getClOption().equals(clopt)) return ci; + } + return null; + + } + + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder b = new StringBuilder(); + for(ConfigurationItem ci: configItemsByKey) { + b.append(ci.toString()).append("\n"); + } + return b.toString(); + } + + /** + * Returns a default usage banner with optional prefixed messages, one per line. + * @param msgs The optional message + * @return the formatted usage banner + */ + public String getDefaultUsage(String...msgs) { + StringBuilder b = new StringBuilder("\n"); + for(String msg: msgs) { + b.append(msg).append("\n"); + } + b.append(dargp.usage()); + return b.toString(); + } + + /** + * Returns an extended usage banner with optional prefixed messages, one per line. + * @param msgs The optional message + * @return the formatted usage banner + */ + public String getExtendedUsage(String...msgs) { + StringBuilder b = new StringBuilder("\n"); + for(String msg: msgs) { + b.append(msg).append("\n"); + } + b.append(argp.usage()); + return b.toString(); + } + + + /** + * System out logger + * @param format The message format + * @param args The message arg tokens + */ + public static void log(String format, Object...args) { + System.out.println(String.format(format, args)); + } + + /** + * Performs sys-prop and js evals on the passed value + * @param text The value to process + * @return the processed value + */ + public static String processConfigValue(CharSequence text) { + final String pv = evaluate(tokenReplaceSysProps(text)); + return (pv==null || pv.trim().isEmpty()) ? null : pv; + } + + /** + * Replaces all matched tokens with the matching system property value or a configured default + * @param text The text to process + * @return The substituted string + */ + public static String tokenReplaceSysProps(CharSequence text) { + if(text==null) return null; + Matcher m = SYS_PROP_PATTERN.matcher(text); + StringBuffer ret = new StringBuffer(); + while(m.find()) { + String replacement = decodeToken(m.group(1), m.group(2)==null ? "" : m.group(2)); + if(replacement==null) { + throw new IllegalArgumentException("Failed to fill in SystemProperties for expression with no default [" + text + "]"); + } + if(IS_WINDOWS) { + replacement = replacement.replace(File.separatorChar , '/'); + } + m.appendReplacement(ret, replacement); + } + m.appendTail(ret); + return ret.toString(); + } + + /** + * Evaluates JS expressions defines as configuration values + * @param text The value of a configuration item to evaluate for JS expressions + * @return The passed value with any embedded JS expressions evaluated and replaced + */ + public static String evaluate(CharSequence text) { + if(text==null) return null; + Matcher m = JS_PATTERN.matcher(text); + StringBuffer ret = new StringBuffer(); + final boolean isNas = scriptEngine.getFactory().getEngineName().toLowerCase().contains("nashorn"); + while(m.find()) { + String source = (isNas ? + "load(\"nashorn:mozilla_compat.js\");\nimportPackage(java.lang); " + : + "\nimportPackage(java.lang); " + ) + + m.group(1); + try { + Object obj = scriptEngine.eval(source, bindings); + if(obj!=null) { + //log("Evaled [%s] --> [%s]", source, obj); + m.appendReplacement(ret, obj.toString()); + } else { + m.appendReplacement(ret, ""); + } + } catch (Exception ex) { + ex.printStackTrace(System.err); + throw new IllegalArgumentException("Failed to evaluate expression [" + text + "]"); + } + } + m.appendTail(ret); + return ret.toString(); + } + + /** + * Attempts to decode the passed dot delimited as a system property, and if not found, attempts a decode as an + * environmental variable, replacing the dots with underscores. e.g. for the key: buffer.size.max, + * a system property named buffer.size.max will be looked up, and then an environmental variable + * named buffer.size.max will be looked up. + * @param key The dot delimited key to decode + * @param defaultValue The default value returned if neither source can decode the key + * @return the decoded value or the default value if neither source can decode the key + */ + public static String decodeToken(String key, String defaultValue) { + String value = System.getProperty(key, System.getenv(key.replace('.', '_'))); + return value!=null ? value : defaultValue; + } + + + /** + *

    Title: ConfigurationItem

    + *

    Description: A container class for deserialized configuration items from opentsdb.conf.json.

    + */ + public static class ConfigurationItem implements Comparable { + /** The internal configuration key */ + @JsonProperty("key") + protected String key; + /** The command line option key that maps to this item */ + @JsonProperty("cl-option") + protected String clOption; + /** The original value, loaded from opentsdb.conf.json, and never overwritten */ + @JsonProperty("defaultValue") + protected String defaultValue; + /** A description of the configuration item */ + @JsonProperty("description") + protected String description; + /** The command line help level at which this item will be displayed ('default' or 'extended') */ + @JsonProperty("help") + protected String help; + /** The meta symbol representing the type of value expected for a parameterized command line arg */ + @JsonProperty("meta") + protected String meta; + + /** The decoded or overriden value */ + protected String value; + + /** + * Creates a new ConfigurationItem + */ + public ConfigurationItem() {} + + /** + * Creates a new ConfigurationItem + * @param key The internal configuration key + * @param clOption The command line option key that maps to this item + * @param defaultValue The original value, loaded from opentsdb.conf.json, and never overwritten + * @param description A description of the configuration item + * @param help The command line help level at which this item will be displayed ('default' or 'extended') + * @param meta The meta symbol representing the type of value expected for a parameterized command line arg + */ + public ConfigurationItem(String key, String clOption, + String defaultValue, String description, String help, + String meta) { + super(); + this.key = key; + this.clOption = clOption; + this.defaultValue = defaultValue; + this.description = description; + this.help = help; + this.meta = meta; + } + + /** + * Validates the value + */ + public void validate() { + if(meta!=null && value!=null) { + ConfigMetaType.byName(meta).validate(this); + } + } + + + + /** + * Returns a descriptive name with the cl option and key + * @return a descriptive name + */ + public String getName() { + return String.format("cl: %s, key: %s", clOption, key); + } + + + /** + * Returns the item key name + * @return the itemName + */ + public String getKey() { + return key; + } + + /** + * Returns the command line option mapping to this item + * @return the clOption + */ + public String getClOption() { + return clOption; + } + + /** + * Returns the item current value + * @return the value + */ + public String getValue() { + return value!=null ? value : defaultValue; + } + + /** + * Indicates if this ConfigurationItem is a BOOL meta type + * @return true if this ConfigurationItem is a BOOL meta type, false otherwise + */ + public boolean isBool() { + return "BOOL".equals(this.meta); + } + + + /** + * Sets a new value for this item + * @param newValue The new value + */ + public void setValue(final String newValue) { + final String currValue = newValue; + value = newValue.trim(); + try { + validate(); + } catch (IllegalArgumentException ex) { + value = currValue; + throw ex; + } + } + + /** + * Returns the original raw value loaded from opentsdb.conf.json + * @return the original raw value + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * Returns the item description + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the help level for this option + * @return the help + */ + public String getHelp() { + return help; + } + + /** + * Returns the meta symbol + * @return the meta + */ + public String getMeta() { + return meta; + } + + /** + * {@inheritDoc} + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String + .format("ConfigurationItem [key=%s, clOption=%s, value=%s, description=%s, help=%s, meta=%s, defaultValue=%s]", + key, clOption, value, description, help, + meta, defaultValue); + } + + /** + * {@inheritDoc} + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + /** + * {@inheritDoc} + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ConfigurationItem other = (ConfigurationItem) obj; + if (key == null) { + if (other.key != null) + return false; + } else if (!key.equals(other.key)) + return false; + return true; + } + + /** + *

    Sorts {@link ConfigurationItem}s by the underlying {@link ConfigMetaType}

    + * {@inheritDoc} + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(final ConfigurationItem other) { + if(this==other) return 0; + if((other.meta==null || other.meta.isEmpty()) && (meta==null || meta.isEmpty())) { + return this.key.compareTo(other.key); + } + if(other.meta==null || other.meta.isEmpty()) { + return 1; + } + if(meta==null || meta.isEmpty()) { + return -1; + } + final ConfigMetaType otherType = ConfigMetaType.byName(other.meta); + final ConfigMetaType thisType = ConfigMetaType.byName(meta); + int c = thisType.compareTo(otherType); + if(c==0) { + c = this.key.compareTo(other.key); + } + return c; + } + } + + + /** + * Returns the command line argument processor + * @return the argp + */ + public ArgP getArgp() { + return argp; + } + + /** + * Returns the command line argument holder for default options + * @return the command line argument holder + */ + public ArgP getDargp() { + return dargp; + } + + /** + * Returns the TSDB config instance + * @return the config + */ + public Config getConfig() { + return config; + } + + /** + * Returns the non config option arguments + * @return the non config option arguments + */ + public String[] getNonOptionArgs() { + return nonOptionArgs; + } + + /** + * Returns the default {@link ConfigurationItem} for the passed name + * which can be the item's key or cl-option + * @param name The item's key or cl-option + * @return The named ConfigurationItem or null if one was not found + */ + public ConfigurationItem getDefaultItem(final String name) { + if(name==null) throw new IllegalArgumentException("The passed name was null"); + return defaultConfItems.get(name); + } + + + /** + * Determines if the passed key is contained in the non option args + * @param nonOptionKey The non option key to check for + * @return true if the passed key is present, false otherwise + */ + public boolean hasNonOption(String nonOptionKey) { + if(nonOptionArgs==null || nonOptionArgs.length==0 || nonOptionKey==null || nonOptionKey.trim().isEmpty()) return false; + return Arrays.binarySearch(nonOptionArgs, nonOptionKey) >= 0; + } + + /** + * Indicates if the passed arg was a command line option + * @param arg The arg to test for + * @return true if the passed arg was a command line option, false otherwise + */ + public boolean isClArg(final String arg) { + return commandLineArgs.contains(arg); + } + + /** + * Checks the opentsdb.conf.json document to see if it has a bindings segment + * which contains JS statements to evaluate which will prime variables used by the configuration. + * @param jsonMapper The JSON mapper + * @param root The root opentsdb.conf.json document + */ + protected void processBindings(ObjectMapper jsonMapper, JsonNode root) { + String script = null; + try { + if(root.has("bindings")) { + JsonNode bindingsNode = root.get("bindings"); + if(bindingsNode.isArray()) { + String[] jsLines = jsonMapper.reader(String[].class).readValue(bindingsNode); + StringBuilder b = new StringBuilder(); + for(String s: jsLines) { + b.append(s).append("\n"); + } + script = b.toString(); + scriptEngine.eval(script); + LOG.debug("Successfully evaluated [{}] lines of JS in bindings", jsLines.length); + } + } + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to evaluate opentsdb.conf.json javascript binding [" + script + "]", ex); + } + } + + +} \ No newline at end of file diff --git a/src/tools/ConfigMetaType.java b/src/tools/ConfigMetaType.java new file mode 100644 index 0000000000..3ca90af109 --- /dev/null +++ b/src/tools/ConfigMetaType.java @@ -0,0 +1,563 @@ +// 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.tools; + +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Pattern; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.loading.MLet; + +import net.opentsdb.tools.ConfigArgP.ConfigurationItem; + +/** + *

    Title: ConfigMetaType

    + *

    Description: Defines the recognized meta types for command line argument values

    + */ + +public enum ConfigMetaType implements ArgValueValidator { + /** An HBase table name */ + TABLE("An HBase table name", new StringValidator("TABLE")), + /** A zookeeper quorum spec */ + SPEC("A zookeeper quorum spec", new StringValidator("SPEC")), + /** A class name */ + CLASS("A class name", new StringValidator("CLASS")), + /** A class name of a class loadable at boot time */ + BCLASS("A class name of a class loadable at boot time", new BootLoadableClass("BCLASS")), + /** A comma separated list of values */ + LIST("A comma separated list of values", new StringValidator("List of comma sep values")), + /** An existing file */ + EFILE("An existing file", new FileSystemValidator(false, true)), + /** A list of existing files or URLs */ + FILELIST("A list of existing files or accessible URLs", new FileListValidator()), + /** A list of existing files or URLs that comprise a classpath, and when this option is loaded, a ClassLoader MBean will be registered */ + CLASSPATH("A list of comma separated existing files or accessible URLs", new ClassPathValidator()), + /** A file, optionally existing */ + FILE("A file, optionally existing", new FileSystemValidator(false, false)), + /** An existing directory */ + EDIR("An existing directory", new FileSystemValidator(true, true)), + /** A fully qualified file name where the parent directory must exist, but the file is optional */ + EDIRFILE("An existing directory", new DirFileSystemValidator()), + /** A valid URL or readable file */ + URLORFILE("A valid URL or readable file", new URLOrFileValidator()), + /** A directory, optionally existing */ + DIR("A directory, optionally existing", new FileSystemValidator(true, false)), + /** A host name or address */ + ADDR("A host name or address", new StringValidator("ADDR")), // we could use InetAddress.getByName('') but it might be really slow. + /** An integer value */ + INT("An integer value", new IntegerValidator(Integer.MAX_VALUE)), + /** A positive integer value */ + POSINT("A positive integer value", new IntegerValidator(0)), + /** A non zero positive integer value */ + GTZEROINT("A non zero positive integer value", new IntegerValidator(1)), + /** A boolean value (true|false) */ + BOOL("A boolean value (true|false)", new BooleanValidator()), + /** A znode path name */ + ZPATH("A znode path name", new StringValidator("ZPATH")), + /** The read write mode */ + RWMODE("Read/Write mode specification", new ReadWriteModeValidator()), + /** A time zone. e.g. "America/Los_Angeles"*/ + TIMEZONE("A time zone name", new TimeZoneValidator()), + /** A list of comma separated class names that should be loadable with (or without) the assistance of a {@link #CLASSPATH} configured classloader */ + BCLASSLIST("A comma separated list of loadable classes", new ClasspathConfiguredClassList()); + + /** The platform MBeanServer */ + public static final MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + /** Comma splitter */ + public static final Pattern COMMA_SPLITTER = Pattern.compile(","); + + + /** + * Decodes the passed name with trim and upper + * @param name The name to decode + * @return the decoded value + */ + public static ConfigMetaType byName(CharSequence name) { + if(name==null) throw new IllegalArgumentException("Null ConfigMetaType"); + String cname = name.toString().toUpperCase().trim(); + try { + return ConfigMetaType.valueOf(cname); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid ConfigMetaType [" + name + "]"); + } + } + + private ConfigMetaType(String description, ArgValueValidator validator) { + this.description = description; + this.validator = validator; + } + + /** A short description of the meta type */ + public final String description; + /** A validator instance for the meta type */ + public final ArgValueValidator validator; + + + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + validator.validate(citem); + } + + /** + *

    Title: ReadWriteModeValidator

    + *

    Description: Validator for ReadWrite Modes

    + */ + public static class ReadWriteModeValidator implements ArgValueValidator { + /** The supported mode codes */ + private static final Set MODES = Collections.unmodifiableSet(new HashSet(Arrays.asList( + "rw", // READ AND WRITE + "wo", // WRITE ONLY + "ro" // READ ONLY + ))); + /** + * Creates a new ReadWriteModeValidator + */ + public ReadWriteModeValidator() { + + } + + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + try { + final String _mode = citem.getValue(); + if(!MODES.contains(_mode)) throw new Exception(); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid ReadWrite Mode [" + citem.getValue() + "] for " + citem.getName()); + } + } + } + + /** + *

    Title: ClassListValidator

    + *

    Description: Validator for loadable class lists

    + */ + public static class ClassListValidator implements ArgValueValidator { + /** + * Creates a new ClassListValidator + */ + public ClassListValidator() { + + } + + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + final String val = citem.getValue(); + if(val.trim().isEmpty()) return; + final String[] classNames = COMMA_SPLITTER.split(val.trim()); + for(String className: classNames) { + className = className.trim(); + if(className.isEmpty()) continue; + try { + Class.forName(className); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load class [" + className + "] defined in configuration item [" + citem.getName() + "]", ex); + } + } + } + } + + + /** + *

    Title: IntegerValidator

    + *

    Description: Validator for integers

    + */ + public static class IntegerValidator implements ArgValueValidator { + private final int min; + /** + * Creates a new IntegerValidator + * @param min The minumum value of the integer + */ + public IntegerValidator(final int min) { + this.min = min; + } + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + try { + int i = Integer.parseInt(citem.getValue()); + if(i < min) throw EX; + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid Integer value [" + citem.getValue() + "] for " + citem.getName()); + } + } + } + + /** + *

    Title: BooleanValidator

    + *

    Description: Validator for booleans (true|false)

    + */ + public static class BooleanValidator implements ArgValueValidator { + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + if(!"true".equalsIgnoreCase(citem.getValue()) && !"false".equalsIgnoreCase(citem.getValue())) { + throw new IllegalArgumentException("Invalid Boolean value [" + citem.getValue() + "] for " + citem.getName()); + } + } + } + + /** + *

    Title: URLOrFileValidator

    + *

    Description: Validator for URLs or Files

    + */ + public static class URLOrFileValidator implements ArgValueValidator { + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + try { + new URL(citem.getValue()); + } catch (Exception ex) { + File f = new File(citem.getValue()); + if(!f.exists() || !f.isFile() || !f.canRead()) { + throw new IllegalArgumentException("Invalid URL or File [" + citem.getValue() + "]", ex); + } + } + } + } + + + /** + *

    Title: StringValidator

    + *

    Description: Empty validator for generic string values, used when we don't have a decent or practical validation routine

    + */ + public static class StringValidator implements ArgValueValidator { + /** Informational name */ + protected final String name; + + /** + * Creates a new StringValidator + * @param name The name of the type + */ + public StringValidator(String name) { + this.name = name; + } + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + if(citem.getValue()==null || citem.getValue().trim().isEmpty()) { + throw new IllegalArgumentException("Null or empty " + name + " value for " + citem.getName()); + } + } + } + + /** + *

    Title: BootLoadableClassValidator

    + *

    Description: A validator for boot time loadable class names

    + */ + public static class BootLoadableClass extends StringValidator { + /** + * Creates a new BootLoadableClass + * @param name The name of the type + */ + public BootLoadableClass(String name) { + super(name); + } + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ConfigMetaType.StringValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + super.validate(citem); + try { + Class.forName(citem.getValue().trim(), true, ConfigMetaType.class.getClassLoader()); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load boot time class [" + citem.getValue() + "] for " + citem.getName()); + } + } + } + + /** + *

    Title: BootLoadableClassValidator

    + *

    Description: A validator for a comma separated list of classnames for which a supplementary + * classpath has been specified in a {@link #FILELIST} configuration item named the same as this one + * but with .classpath appended.

    + */ + public static class ClasspathConfiguredClassList implements ArgValueValidator { + /** The template for the classloader MBean's ObjectName */ + public static final String CLASSLOADER_OBJECTNAME = "net.opentsdb.classpath:type=ClassLoader,name=%s.classpath"; + + /** + * Creates a new ClasspathConfiguredClassList + */ + public ClasspathConfiguredClassList() { + + } + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ConfigMetaType.StringValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + Set classNames = new LinkedHashSet(Arrays.asList(COMMA_SPLITTER.split(citem.getValue()))); + if(classNames.isEmpty()) return; + for(final Iterator iter = classNames.iterator(); iter.hasNext();) { + String fileName = iter.next().trim(); + if(fileName.isEmpty()) iter.remove(); + } + if(classNames.isEmpty()) return; + String className = null; + try { + final ClassLoader CL; + final ObjectName on = new ObjectName(String.format(CLASSLOADER_OBJECTNAME, citem.getKey())); + if(server.isRegistered(on)) { + CL = server.getClassLoader(on); + } else { + CL = Thread.currentThread().getContextClassLoader(); + } + for(String cl: classNames) { + className = cl.trim(); + Class.forName(className, true, CL); + } + Class.forName(citem.getValue().trim(), true, ConfigMetaType.class.getClassLoader()); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load boot time class [" + className + "] for " + citem.getName()); + } + } + } + + /** + *

    Title: DirFileSystemValidator

    + *

    Description: Validator for fully qualified file names where the parent directory + * must exist but the file is optional

    + */ + public static class DirFileSystemValidator implements ArgValueValidator { + + @Override + public void validate(final ConfigurationItem citem) { + final String name = citem.getValue(); + if(name==null || name.trim().isEmpty()) return; + final File f = new File(citem.getValue()).getAbsoluteFile(); + final File dir = f.getParentFile(); + if(dir.exists() && dir.isDirectory()) return; + if(!dir.exists()) throw new IllegalArgumentException("No directory named [" + dir + "] exists. Invalid value for config item:" + citem.getKey()); + if(!dir.isDirectory()) throw new IllegalArgumentException("Specified parent directory [" + dir + "] is not a directory. Invalid value for config item:" + citem.getKey()); + } + + } + + + /** + *

    Title: FileSystemValidator

    + *

    Description: Validator for files and directories

    + */ + public static class FileSystemValidator implements ArgValueValidator { + private final boolean dir; + private final boolean mustExist; + + /** + * Creates a new FileSystemValidator + * @param dir true for validating directories, false for files + * @param mustExist true if the target must exist, false if it can be created if it does not exist + */ + public FileSystemValidator(boolean dir, boolean mustExist) { + this.dir = dir; + this.mustExist = mustExist; + } + + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ArgValueValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + String value = citem.getValue(); + File target = new File(value); + if(mustExist) { + if(!target.exists()) throw new IllegalArgumentException((dir ? "Directory [" : " File [") + value + "] does not exist for " + citem.getName()); + if(dir) { + if(!target.isDirectory()) throw new IllegalArgumentException(("[") + value + "] is a file not a directory for " + citem.getName()); + } else { + if(!target.isFile()) throw new IllegalArgumentException(("[") + value + "] is a directory not a file for " + citem.getName()); + } + } else { + if(dir) { + if(!target.exists()) { + if(!target.mkdirs()) throw new IllegalArgumentException(("Could not create directory [") + value + "] for " + citem.getName()); + } else { + if(!target.isDirectory()) throw new IllegalArgumentException(("[") + value + "] is a file not a directory for " + citem.getName()); + } + } else { + if(!target.getParentFile().exists()) { + if(!target.getParentFile().mkdirs()) throw new IllegalArgumentException(("Could not create parent directory for file [") + value + "] for " + citem.getName()); + } + + if(!target.exists()) { + try { + if(!target.createNewFile()) throw new IllegalArgumentException(("Could not create file [") + value + "] for " + citem.getName()); + } catch (IOException e) { + throw new IllegalArgumentException(("Could not create file [") + value + "] for " + citem.getName()); + } finally { + target.delete(); + } + } + } + } + } + } + + /** + *

    Title: TimeZoneValidator

    + *

    Description: Validates time zone typed configuration items.

    + *

    net.opentsdb.tools.TimeZoneValidator

    + */ + public static class TimeZoneValidator implements ArgValueValidator { + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + @Override + public void validate(final ConfigurationItem citem) { + final String tz = citem.getValue(); + if("DEFAULT".equalsIgnoreCase(tz)) { + citem.setValue(TimeZone.getDefault().getDisplayName()); + return; + } + TimeZone timeZone = TimeZone.getTimeZone(tz); + if("GMT".equals(timeZone.getID())) { + if(!tz.toUpperCase().contains("GMT")) { + throw new IllegalArgumentException("Unrecognized TimeZone [" + tz + "]"); + } + } + } + + } + + /** + *

    Title: FileListValidator

    + *

    Description: A validator for a list of existing files or accessible URLs

    + */ + public static class FileListValidator implements ArgValueValidator { + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ConfigMetaType.StringValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + buildURLSet(citem); + } + + /** + * Builds a set of URLs from the configured file list and validates each one + * @param citem The configuration item + * @return A [possibly empty] set of URLs + */ + protected Set buildURLSet(final ConfigurationItem citem) { + final Set urls = new LinkedHashSet(); + String[] files = COMMA_SPLITTER.split(citem.getValue()); + Set failed = new LinkedHashSet(); + for(String file: files) { + String name = file.trim(); + if(name.isEmpty()) continue; + boolean isURL = false; + URL url = null; + try { + url = new URL(name); + isURL = true; + urls.add(url); + } catch (Exception ex) { /* No Op */ } + if(isURL) continue; + File f = new File(name); + if(f.exists() && f.canRead()) { +// if(f.isDirectory()) { +// failed.add(name + ":Not a file"); +// } + try { + urls.add(f.toURI().toURL()); + } catch (Exception ex) { + failed.add(name + ":Could not convert to URL"); + } + } else { + failed.add(name + ":Not found"); + continue; + } + } + if(!failed.isEmpty()) { + StringBuilder b = new StringBuilder("FileList Validation Failures:"); + for(String s: failed) { + b.append("\n\t").append(s); + } + throw new IllegalArgumentException("Invalid Files or URLs in File List for " + citem.getName() + "\n" + b.toString()); + } + + return urls; + } + + } + + /** + *

    Title: ClassPathValidator

    + *

    Description: A class path validator and ClassLoader factory

    + *

    Company: Helios Development Group LLC

    + */ + public static class ClassPathValidator extends FileListValidator { + /** The template for the classloader MBean's ObjectName */ + public static final String CLASSLOADER_OBJECTNAME = "net.opentsdb.classpath:type=ClassLoader,name=%s"; + + /** + * {@inheritDoc} + * @see net.opentsdb.tools.ConfigMetaType.FileListValidator#validate(net.opentsdb.tools.ConfigArgP.ConfigurationItem) + */ + @Override + public void validate(final ConfigurationItem citem) { + super.validate(citem); + Set urls = buildURLSet(citem); + final MLet classLoader = new MLet(urls.toArray(new URL[urls.size()])); + try { + final ObjectName on = new ObjectName(String.format(CLASSLOADER_OBJECTNAME, citem.getKey())); + if(server.isRegistered(on)) { + server.unregisterMBean(on); + } + server.registerMBean(classLoader, on); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to create class loader for [" + citem.getName() + "]", ex); + } + + } + } + + +} diff --git a/src/tools/GnuplotInstaller.java b/src/tools/GnuplotInstaller.java new file mode 100644 index 0000000000..6356a1723d --- /dev/null +++ b/src/tools/GnuplotInstaller.java @@ -0,0 +1,108 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2010-2016 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.tools; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.DynamicChannelBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

    Title: GnuplotInstaller

    + *

    Description: Installs the gnuplot invocation shell file

    + */ + +public class GnuplotInstaller { + private static final boolean IS_WINDOWS = + System.getProperty("os.name", "").contains("Windows"); + + /** Static class logger */ + private static final Logger LOG = LoggerFactory.getLogger(GnuplotInstaller.class); + + /** The name of the shell file */ + public static final String GP_BATCH_FILE_NAME = IS_WINDOWS ? "mygnuplot.bat" : "mygnuplot.sh"; + /** The name of the gnuplot executable */ + public static final String GP_NAME = IS_WINDOWS ? "gnuplot.exe" : "gnuplot"; + + /** The directory where shell file will be installed */ + public static final String GP_DIR = System.getProperty("java.io.tmpdir") + File.separator + ".tsdb" + File.separator + "tsdb-gnuplot"; + /** The java/io file representing the gnuplot shell file */ + public static final File GP_FILE = new File(GP_DIR, GP_BATCH_FILE_NAME); + /** Indicates if gnuplot was found on the path */ + public static final boolean FOUND_GP; + + static { + boolean found = false; + final String PATH = System.getenv("PATH"); + if(PATH!=null) { + final String[] paths = PATH.split(File.pathSeparator); + for(String path: paths) { + LOG.debug("Inspecting PATH for Gnuplot Exe: [{}]", path); + File dir = new File(path.trim()); + if(dir.exists() && dir.isDirectory()) { + File gp = new File(dir, GP_NAME); + if(gp.exists()) { + found = true; + LOG.info("Found gnuplot at [{}]", gp.getAbsolutePath()); + break; + } + + } + } + } + FOUND_GP = found; + if(!found) LOG.warn("Failed to locate Gnuplot executable"); + } + + private GnuplotInstaller() { + + } + + /** + * Installs the mygnuplot shell file + */ + public static void installMyGnuPlot() { + if(!FOUND_GP) { + LOG.warn("Skipping Gnuplot Shell Script Install since Gnuplot executable was not found"); + return; + } + if(!GP_FILE.exists()) { + if(!GP_FILE.getParentFile().exists()) { + GP_FILE.getParentFile().mkdirs(); + } + InputStream is = null; + FileOutputStream fos = null; + try { + is = GnuplotInstaller.class.getClassLoader().getResourceAsStream(GP_BATCH_FILE_NAME); + ChannelBuffer buff = new DynamicChannelBuffer(is.available()); + buff.writeBytes(is, is.available()); + is.close(); is = null; + fos = new FileOutputStream(GP_FILE); + buff.readBytes(fos, buff.readableBytes()); + fos.close(); fos = null; + GP_FILE.setExecutable(true); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to install mygnuplot", ex); + } finally { + if( is!=null ) try { is.close(); } catch (Exception x) { /* No Op */ } + if( fos!=null ) try { fos.close(); } catch (Exception x) { /* No Op */ } + } + } + } + + +} \ No newline at end of file diff --git a/src/tools/Main.java b/src/tools/Main.java new file mode 100644 index 0000000000..87218ba886 --- /dev/null +++ b/src/tools/Main.java @@ -0,0 +1,859 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.tools; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javax.management.ObjectName; + +import net.opentsdb.core.TSDB; +import net.opentsdb.tools.ConfigArgP.ConfigurationItem; +import net.opentsdb.tsd.PipelineFactory; +import net.opentsdb.utils.Config; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.socket.ServerSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; +import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.BasicStatusManager; +import ch.qos.logback.core.joran.spi.JoranException; +import ch.qos.logback.core.status.StatusListener; + +/** + *

    Title: Main

    + *

    Description: OpenTSDB fat-jar main entry point

    + */ + +public class Main { + /** The platform EOL string */ + public static final String EOL = System.getProperty("line.separator", "\n"); + /** Static class logger */ + private static final Logger log = LoggerFactory.getLogger(Main.class); + /** The content prefix */ + public static final String CONTENT_PREFIX = "queryui"; + + /** The default pid file directory which is ${user.home}/.tsdb/ */ + public static final File DEFAULT_PID_DIR = new File(System.getProperty("user.home") + File.separator + ".tsdb"); + /** The default pid file which is ${user.home}/.tsdb/opentsdb.pid */ + public static final File DEFAULT_PID_FILE = new File(DEFAULT_PID_DIR, "opentsdb.pid"); + + /** The default flush interval */ + public static final short DEFAULT_FLUSH_INTERVAL = 1000; + + /** Boot classes keyed by the commands we recognize */ + public static final Map> COMMANDS; + + static { + Map> tmp = new HashMap>(); + tmp.put("fsck", Fsck.class); + tmp.put("import", TextImporter.class); + tmp.put("mkmetric", UidManager.class); // -> shift --> set uid assign metrics "$@" + tmp.put("query", CliQuery.class); + tmp.put("tsd", Main.class); + tmp.put("scan", DumpSeries.class); + tmp.put("uid", UidManager.class); + tmp.put("exportui", UIContentExporter.class); + tmp.put("help", HelpProcessor.class); + COMMANDS = Collections.unmodifiableMap(tmp); + } + + /** + * Prints the main usage banner + */ + public static void mainUsage(PrintStream ps) { + StringBuilder b = new StringBuilder("\nUsage: java -jar [opentsdb.jar] [command] [args]\nValid commands:") + .append("\n\ttsd: Starts a new TSDB instance") + .append("\n\tfsck: Searches for and optionally fixes corrupted data in a TSDB") + .append("\n\timport: Imports data from a file into HBase through a TSDB") + .append("\n\tmkmetric: Creates a new metric") + .append("\n\tquery: Queries time series data from a TSDB ") + .append("\n\tscan: Dumps data straight from HBase") + .append("\n\tuid: Provides various functions to search or modify information in the tsdb-uid table. ") + .append("\n\texportui: Exports the OpenTSDB UI static content") + .append("\n\n\tUse help for details on a command\n"); + ps.println(b); + } + + + /** + * The OpenTSDB fat-jar main entry point + * @param args See usage banner {@link Main#mainUsage(PrintStream)} + */ + public static void main(String[] args) { + log.info("Starting."); + log.info(BuildData.revisionString()); + log.info(BuildData.buildString()); + try { + System.in.close(); // Release a FD we don't need. + } catch (Exception e) { + log.warn("Failed to close stdin", e); + } + if(args.length==0) { + log.error("No command supplied"); + mainUsage(System.err); + System.exit(-1); + } + // This is not normally needed since values passed on the CL are auto-trimmed, + // but since the Main may be called programatically in some embedded scenarios, + // let's save us some time and trim the values here. + for(int i = 0; i < args.length; i++) { + args[i] = args[i].trim(); + } + String targetTool = args[0].toLowerCase(); + if(!COMMANDS.containsKey(targetTool)) { + log.error("Command not recognized: [" + targetTool + "]"); + mainUsage(System.err); + System.exit(-1); + } + process(targetTool, shift(args)); + } + + /** + * Executes the target tool + * @param targetTool the name of the target tool to execute + * @param args The command line arguments minus the tool name + */ + private static void process(String targetTool, String[] args) { + if("mkmetric".equals(targetTool)) { + shift(args); + } + if(!"tsd".equals(targetTool)) { + try { + COMMANDS.get(targetTool).getDeclaredMethod("main", String[].class).invoke(null, new Object[] {args}); + } catch(Exception x) { + log.error("Failed to call [" + targetTool + "].", x); + System.exit(-1); + } + } else { + launchTSD(args); + } + } + + + /** + * Applies and processes the pre-tsd command line + * @param cap The main configuration wrapper + * @param argp The preped command line argument handler + */ + protected static void applyCommandLine(ConfigArgP cap, ArgP argp) { + // --config, --include-config, --help + if(argp.has("--help")) { + if(cap.hasNonOption("extended")) { + System.out.println(cap.getExtendedUsage("tsd extended usage:")); + } else { + System.out.println(cap.getDefaultUsage("tsd usage:")); + } + System.exit(0); + } + if(argp.has("--config")) { + loadConfigSource(cap, argp.get("--config").trim()); + } + if(argp.has("--include")) { + String[] sources = argp.get("--include").split(","); + for(String s: sources) { + loadConfigSource(cap, s.trim()); + } + } + } + + /** + * Applies the properties from the named source to the main configuration + * @param config the main configuration to apply to + * @param source the name of the source to apply properties from + */ + protected static void loadConfigSource(ConfigArgP config, String source) { + Properties p = loadConfig(source); + Config c = config.getConfig(); + for(String key: p.stringPropertyNames()) { + String value = p.getProperty(key); + ConfigurationItem ci = config.getConfigurationItem(key); + if(ci!=null) { // if we recognize the key, validate it + ci.setValue(value); + } + c.overrideConfig(key, value); + } + } + + /** + * Loads properties from a file or url with the passed name + * @param name The name of the file or URL + * @return the loaded properties + */ + protected static Properties loadConfig(String name) { + try { + URL url = new URL(name); + return loadConfig(url); + } catch (Exception ex) { + return loadConfig(new File(name)); + } + } + + /** + * Loads properties from the passed input stream + * @param source The name of the source the properties are being loaded from + * @param is The input stream to load from + * @return the loaded properties + */ + protected static Properties loadConfig(String source, InputStream is) { + try { + Properties p = new Properties(); + p.load(is); + // trim the value as it may have trailing white-space + Set keys = p.stringPropertyNames(); + for(String key: keys) { + p.setProperty(key, p.getProperty(key).trim()); + } + return p; + } catch (IllegalArgumentException iae) { + throw iae; + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load configuration from [" + source + "]"); + } + } + + /** + * Loads properties from the passed file + * @param file The file to load from + * @return the loaded properties + */ + protected static Properties loadConfig(File file) { + InputStream is = null; + try { + is = new FileInputStream(file); + return loadConfig(file.getAbsolutePath(), is); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load configuration from [" + file.getAbsolutePath() + "]"); + }finally { + if(is!=null) try { is.close(); } catch (Exception ex) { /* No Op */ } + } + } + + /** + * Loads properties from the passed URL + * @param url The url to load from + * @return the loaded properties + */ + protected static Properties loadConfig(URL url) { + InputStream is = null; + try { + URLConnection connection = url.openConnection(); + if(connection instanceof HttpURLConnection) { + ((HttpURLConnection)connection).setConnectTimeout(2000); + } + is = connection.getInputStream(); + return loadConfig(url.toString(), is); + } catch (Exception ex) { + throw new IllegalArgumentException("Failed to load configuration from [" + url + "]"); + }finally { + if(is!=null) try { is.close(); } catch (Exception ex) { /* No Op */ } + } + } + + /** Prints usage and exits with the given retval. */ + static void usage(final ArgP argp, final String errmsg, final int retval) { + System.err.println(errmsg); + System.err.println(new ConfigArgP().getDefaultUsage()); + if (argp != null) { + System.err.print(argp.usage()); + } + System.exit(retval); + } + + + + /** + * Starts the TSD. + * @param args The command line arguments + */ + private static void launchTSD(String[] args) { + ConfigArgP cap = new ConfigArgP(args); + Config config = cap.getConfig(); + ArgP argp = cap.getArgp(); + applyCommandLine(cap, argp); + config.loadStaticVariables(); + // All options are now correctly set in config + setJVMName(config.getInt("tsd.network.port"), config.getString("tsd.network.bind")); + // Configure the logging + if(config.hasProperty("tsd.logback.file")) { + final String logBackFile = config.getString("tsd.logback.file"); + final String rollPattern = config.hasProperty("tsd.logback.rollpattern") ? config.getString("tsd.logback.rollpattern") : null; + final boolean keepConsoleOpen = config.hasProperty("tsd.logback.console") ? config.getBoolean("tsd.logback.console") : false; + log.info("\n\t===================================\n\tReconfiguring logback. Logging to file:\n\t{}\n\t===================================\n", logBackFile); + setLogbackInternal(logBackFile, rollPattern, keepConsoleOpen); + } else { + final String logBackConfig; + if(config.hasProperty("tsd.logback.config")) { + logBackConfig = config.getString("tsd.logback.config"); + } else { + logBackConfig = System.getProperty("tsd.logback.config", null); + } + if(logBackConfig!=null && !logBackConfig.trim().isEmpty() && new File(logBackConfig.trim()).canRead()) { + setLogbackExternal(logBackConfig.trim()); + } + } + if(config.auto_metric()) { + log.info("\n\t==========================================\n\tAuto-Metric Enabled\n\t==========================================\n"); + } else { + log.warn("\n\t==========================================\n\tAuto-Metric Disabled\n\t==========================================\n"); + } + try { + // Write the PID file + writePid(config.getString("tsd.process.pid.file"), config.getBoolean("tsd.process.pid.ignore.existing")); + // Export the UI content + if(!config.getBoolean("tsd.ui.noexport")) { + loadContent(config.getString("tsd.http.staticroot")); + } + // Create the cache dir if it does not exist + File cacheDir = new File(config.getString("tsd.http.cachedir")); + if(cacheDir.exists()) { + if(!cacheDir.isDirectory()) { + throw new IllegalArgumentException("The http cache directory [" + cacheDir + "] is not a directory, but a file, which is bad"); + } + } else { + if(!cacheDir.mkdirs()) { + throw new IllegalArgumentException("Failed to create the http cache directory [" + cacheDir + "]"); + } + } + } catch (Exception ex) { + log.error("Failed to process tsd configuration", ex); + System.exit(-1); + } + + // ===================================================================== + // Command line processing complete, ready to start TSD. + // The code from here to the end of the method is an exact duplicate + // of {@link TSDMain#main(String[])} once configuration is complete. + // At the time of this writing, this is at line 123 starting with the + // code: final ServerSocketChannelFactory factory; + // ===================================================================== + + log.info("Configuration complete. Starting TSDB"); + final ServerSocketChannelFactory factory; + if (config.getBoolean("tsd.network.async_io")) { + int workers = Runtime.getRuntime().availableProcessors() * 2; + if (config.hasProperty("tsd.network.worker_threads")) { + try { + workers = config.getInt("tsd.network.worker_threads"); + } catch (NumberFormatException nfe) { + usage(argp, "Invalid worker thread count", 1); + } + } + factory = new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool(), + workers); + } else { + factory = new OioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); + } + + TSDB tsdb = null; + try { + tsdb = new TSDB(config); + tsdb.initializePlugins(true); + + // Make sure we don't even start if we can't find our tables. + tsdb.checkNecessaryTablesExist().joinUninterruptibly(); + + registerShutdownHook(tsdb); + final ServerBootstrap server = new ServerBootstrap(factory); + + server.setPipelineFactory(new PipelineFactory(tsdb)); + if (config.hasProperty("tsd.network.backlog")) { + server.setOption("backlog", config.getInt("tsd.network.backlog")); + } + server.setOption("child.tcpNoDelay", + config.getBoolean("tsd.network.tcp_no_delay")); + server.setOption("child.keepAlive", + config.getBoolean("tsd.network.keep_alive")); + server.setOption("reuseAddress", + config.getBoolean("tsd.network.reuse_address")); + + // null is interpreted as the wildcard address. + InetAddress bindAddress = null; + if (config.hasProperty("tsd.network.bind")) { + bindAddress = InetAddress.getByName(config.getString("tsd.network.bind")); + } + + // we validated the network port config earlier + final InetSocketAddress addr = new InetSocketAddress(bindAddress, + config.getInt("tsd.network.port")); + server.bind(addr); + log.info("Ready to serve on " + addr); + } catch (Throwable e) { + factory.releaseExternalResources(); + try { + if (tsdb != null) + tsdb.shutdown().joinUninterruptibly(); + } catch (Exception e2) { + log.error("Failed to shutdown HBase client", e2); + } + throw new RuntimeException("Initialization failed", e); + } + // The server is now running in separate threads, we can exit main. + } + + + /** + * Attempts to set the vm agent property that identifies the vm's display name. + * This is the name displayed for tools such as jconsole and jps when using auto-dicsovery. + * When using a fat-jar, this provides a much more identifiable name + * @param port The listening port + * @param iface The bound interface + */ + protected static void setJVMName(final int port, final String iface) { + final Properties p = getAgentProperties(); + if(p!=null) { + final String ifc = (iface==null || iface.trim().isEmpty()) ? "" : (iface.trim() + ":"); + final String name = "opentsdb[" + ifc + port + "]"; + p.setProperty("sun.java.command", name); + p.setProperty("sun.rt.javaCommand", name); + System.setProperty("sun.java.command", name); + System.setProperty("sun.rt.javaCommand", name); + } + } + + /** + * Returns the agent properties + * @return the agent properties or null if reflective call failed + */ + protected static Properties getAgentProperties() { + try { + Class clazz = Class.forName("sun.misc.VMSupport"); + Method m = clazz.getDeclaredMethod("getAgentProperties"); + m.setAccessible(true); + Properties p = (Properties)m.invoke(null); + return p; + } catch (Throwable t) { + return null; + } + } + + /** + * Sets the logback system property, tsdb.logback.file and then + * reloads the internal file based logging configuration. The property will + * not be set if it was already set, allowing the value to be set by a standard + * command line set system property. + * @param logFileName The name of the file logback will log to. + * @param rollPattern The pattern specifying the rolling file pattern, + * inserted between the file name base and extension. So for a log file name of + * /var/log/opentsdb.log and a pattern of _%d{yyyy-MM-dd}.%i, + * the configured fileNamePattern would be /var/log/opentsdb_%d{yyyy-MM-dd}.%i.log + * @param keepConsoleOpen If true, will keep the console appender open, + * otherwise closes it and logs exclusively to the file. + */ + protected static void setLogbackInternal(final String logFileName, final String rollPattern, final boolean keepConsoleOpen) { + if(System.getProperty("tsdb.logback.file", null) == null) { + System.setProperty("tsdb.logback.file", logFileName); + } + System.setProperty("tsd.logback.rollpattern", insertPattern(logFileName, rollPattern)); + BasicStatusManager bsm = new BasicStatusManager(); + for(StatusListener listener: bsm.getCopyOfStatusListenerList()) { + log.info("Status Listener: {}", listener); + } + try { + final URL url = Main.class.getClassLoader().getResource("file-logback.xml"); + final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + try { + final JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(context); + configurator.doConfigure(url); + if(!keepConsoleOpen) { + final ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.detachAppender("STDOUT"); + } + log.info("Set internal logback config with file [{}] and roll pattern [{}]", logFileName, rollPattern); + } catch (JoranException je) { + System.err.println("Failed to configure internal logback"); + je.printStackTrace(System.err); + } + } catch (Exception ex) { + log.warn("Failed to set internal logback config with file [{}]", logFileName, ex); + } + } + + /** + * Merges the roll pattern name into the file name + * @param logFileName The log file name + * @param rollPattern The rolling log file appender roll pattern + * @return the merged file pattern + */ + protected static String insertPattern(final String logFileName, final String rollPattern) { + int index = logFileName.lastIndexOf('.'); + if(index==-1) return logFileName + rollPattern; + return logFileName.substring(0, index) + rollPattern + logFileName.substring(index); + } + + + /** + * Reloads the logback configuration to an external file + * @param fileName The logback configuration file + */ + protected static void setLogbackExternal(final String fileName) { + try { + final ObjectName logbackObjectName = new ObjectName("ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator"); + ManagementFactory.getPlatformMBeanServer().invoke(logbackObjectName, "reloadByFileName", new Object[]{fileName}, new String[]{String.class.getName()}); + log.info("Set external logback config to [{}]", fileName); + } catch (Exception ex) { + log.warn("Failed to set external logback config to [{}]", fileName, ex); + } + } + + + /** + * Drops the first array item in the passed array. + * If the passed array is null or empty, returns an empty array + * @param args The array to shift + * @return the shifted array + */ + private static String[] shift(String[] args) { + if(args==null || args.length==0 | args.length==1) return new String[0]; + String[] newArgs = new String[args.length-1]; + System.arraycopy(args, 1, newArgs, 0, newArgs.length); + return newArgs; + } + + + private static void registerShutdownHook(final TSDB tsdb) { + final class TSDBShutdown extends Thread { + public TSDBShutdown() { + super("TSDBShutdown"); + } + public void run() { + try { + tsdb.shutdown().join(); + } catch (Exception e) { + LoggerFactory.getLogger(TSDBShutdown.class) + .error("Uncaught exception during shutdown", e); + } + } + } + Runtime.getRuntime().addShutdownHook(new TSDBShutdown()); + } + + + /** + *

    Title: HelpProcessor

    + *

    Description: Command line help processor

    + */ + public static class HelpProcessor { + /** + * Entry point for invoking the ui content exporter + * @param args see the ArgP + */ + public static void main(String[] args) { + if(args==null || args.length==0) { + mainUsage(System.out); + } else { + Class command = COMMANDS.get(args[0].trim()); + if(command==null) { + System.err.println("\nUnrecognized command [" + args[0] + "]\n"); + mainUsage(System.err); + } else { + try { + if(args[0].equals("tsd")) { + ConfigArgP cap = new ConfigArgP(); + if(args.length>1 && args[1].trim().equals("extended")) { + System.out.println(cap.getExtendedUsage("tsd extended usage:")); + } else { + System.out.println(cap.getExtendedUsage("tsd usage:")); + } + } else { + Method m = null; + ArgP fake = new ArgP(); + try { + m = command.getDeclaredMethod("usage", ArgP.class, String.class); + m.setAccessible(true); + m.invoke(null, new Object[] {fake, "\nHelp for [" + args[0] + "] command\n"}); + } catch (NoSuchMethodException ne) { + try { + m = command.getDeclaredMethod("usage", ArgP.class, String.class, int.class); + m.setAccessible(true); + m.invoke(null, new Object[] {fake, "\nHelp for [" + args[0] + "] command\n", 1}); + } catch (NoSuchMethodException ne2) { + m = command.getDeclaredMethod("usage", ArgP.class, int.class); + m.setAccessible(true); + m.invoke(null, new Object[] {fake, 1}); + } + } + } + } catch(Exception x) { + log.error("Failed to invoke help for [" + args[0] + "].", x); + System.exit(-1); + } + } + } + System.exit(0); + } + + /** + * Prints help usage + * @param argp Ignored + * @param errmsg Ignored + */ + static void usage(final ArgP argp, final String errmsg) { + System.out.println("help: Prints the main command line help"); + System.out.println("help: Prints help for the specified command"); + } + + } + + /** + *

    Title: UIContentExporter

    + *

    Description: Exports the queryui content from the jar to the specified directory

    + */ + public static class UIContentExporter { + private static final ArgP uiexOptions = new ArgP(); + + static { + uiexOptions.addOption("--d", "DIR", "The directory to export the UI content to"); + uiexOptions.addOption("--p", "Create the directory if it does not exist"); + } + + + /** + * Usage banner, args are not used, just approximating a consistent signature + * @param ignored ignored + * @param alsoIgnored ignored + */ + public static void usage(ArgP ignored, int alsoIgnored) { + System.out.println("Usage: java -jar exportui --d [--p]\n" + + uiexOptions.usage() ); + + } + + + /** + * Entry point for invoking the ui content exporter + * @param args see the ArgP + */ + public static void main(String[] args) { + uiexOptions.parse(args); + String dirName = uiexOptions.get("--d"); + boolean createIfNotExists = uiexOptions.has("--p"); + if(dirName==null) { + log.error("Missing argument for target directory. Usage: java -jar exportui --d [--p]"); + System.exit(1); + } + File f = new File(dirName); + if(!f.exists()) { + if(createIfNotExists) { + if(f.mkdirs()) { + log.info("Created exportui directory [{}]", f); + } else { + log.error("Failed to create target directory [{}]", f); + System.exit(1); + } + } else { + log.error("Specified target directory [{}] does not exist. You could use the --p option, or create the directory", f); + System.exit(1); + } + } else { + if(!f.isDirectory()) { + log.error("Specified target [{}] is not a directory, but is a file. exportui cannot contine", f); + System.exit(1); + } + } + loadContent(f.getAbsolutePath()); + System.exit(0); + } + } + /** + * Loads the Static UI content files from the classpath JAR to the configured static root directory + * @param the name of the content directory to write the content to + */ + private static void loadContent(String contentDirectory) { + File gpDir = new File(contentDirectory); + final long startTime = System.currentTimeMillis(); + int filesLoaded = 0; + int fileFailures = 0; + int fileNewer = 0; + long bytesLoaded = 0; + String codeSourcePath = TSDMain.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + File file = new File(codeSourcePath); + if( codeSourcePath.endsWith(".jar") && file.exists() && file.canRead() ) { + JarFile jar = null; + ChannelBuffer contentBuffer = ChannelBuffers.dynamicBuffer(300000); + try { + jar = new JarFile(file); + final Enumeration entries = jar.entries(); + while(entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + final String name = entry.getName(); + if (name.startsWith(CONTENT_PREFIX + "/")) { + final int contentSize = (int)entry.getSize(); + final long contentTime = entry.getTime(); + if(entry.isDirectory()) { + new File(gpDir, name).mkdirs(); + continue; + } + File contentFile = new File(gpDir, name.replace(CONTENT_PREFIX + "/", "")); + if( !contentFile.getParentFile().exists() ) { + contentFile.getParentFile().mkdirs(); + } + if( contentFile.exists() ) { + if( contentFile.lastModified() >= contentTime ) { + log.debug("File in directory was newer [{}]", name); + fileNewer++; + continue; + } + contentFile.delete(); + } + log.debug("Writing content file [{}]", contentFile ); + contentFile.createNewFile(); + if( !contentFile.canWrite() ) { + log.warn("Content file [{}] not writable", contentFile); + fileFailures++; + continue; + } + FileOutputStream fos = null; + InputStream jis = null; + try { + fos = new FileOutputStream(contentFile); + jis = jar.getInputStream(entry); + contentBuffer.writeBytes(jis, contentSize); + contentBuffer.readBytes(fos, contentSize); + fos.flush(); + jis.close(); jis = null; + fos.close(); fos = null; + filesLoaded++; + bytesLoaded += contentSize; + log.debug("Wrote content file [{}] + with size [{}]", contentFile, contentSize ); + } finally { + if( jis!=null ) try { jis.close(); } catch (Exception ex) {} + if( fos!=null ) try { fos.close(); } catch (Exception ex) {} + } + } // not content + } // end of while loop + final long elapsed = System.currentTimeMillis()-startTime; + StringBuilder b = new StringBuilder("\n\n\t===================================================\n\tStatic Root Directory:[").append(contentDirectory).append("]"); + b.append("\n\tTotal Files Written:").append(filesLoaded); + b.append("\n\tTotal Bytes Written:").append(bytesLoaded); + b.append("\n\tFile Write Failures:").append(fileFailures); + b.append("\n\tExisting File Newer Than Content:").append(fileNewer); + b.append("\n\tElapsed (ms):").append(elapsed); + b.append("\n\t===================================================\n"); + log.info(b.toString()); + } catch (Exception ex) { + log.error("Failed to export ui content", ex); + } finally { + if( jar!=null ) try { jar.close(); } catch (Exception x) { /* No Op */} + } + } else { // end of was-not-a-jar + log.warn("\n\tThe OpenTSDB classpath is not a jar file, so there is no content to unload.\n\tBuild the OpenTSDB jar and run 'java -jar --d '."); + } + } + + /** + * Writes the PID to the file at the passed location + * @param file The fully qualified pid file name + * @param ignorePidFile If true, an existing pid file will be ignored after a warning log + */ + private static void writePid(String file, boolean ignorePidFile) { + File pidFile = new File(file); + if(pidFile.exists()) { + Long oldPid = getPid(pidFile); + if(oldPid==null) { + pidFile.delete(); + } else { + log.warn("\n\t==================================\n\tThe OpenTSDB PID file [" + file + "] already exists for PID [" + oldPid + "]. \n\tOpenTSDB might already be running.\n\t==================================\n"); + if(!ignorePidFile) { + log.warn("Exiting due to existing pid file. Start with option --ignore-existing-pid to overwrite"); + System.exit(-1); + } else { + log.warn("Deleting existing pid file [" + file + "]"); + pidFile.delete(); + } + } + } + pidFile.deleteOnExit(); + File pidDir = pidFile.getParentFile(); + FileOutputStream fos = null; + try { + if(!pidDir.exists()) { + if(!pidDir.mkdirs()) { + throw new Exception("Failed to create PID directory [" + file + "]"); + } + } + fos = new FileOutputStream(pidFile); + String PID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + fos.write(String.format("%s%s", PID, EOL).getBytes()); + fos.flush(); + fos.close(); + fos = null; + log.info("PID [" + PID + "] written to pid file [" + file + "]"); + } catch (Exception ex) { + log.error("Failed to write PID file to [" + file + "]", ex); + throw new IllegalArgumentException("Failed to write PID file to [" + file + "]", ex); + } finally { + if(fos!=null) try { fos.close(); } catch (Exception ex) { /* No Op */ } + } + } + + /** + * Reads the pid from the specified pid file + * @param pidFile The pid file to read from + * @return The read pid or possibly null / blank if failed to read + */ + private static Long getPid(File pidFile) { + FileReader reader = null; + BufferedReader lineReader = null; + String pidLine = null; + try { + reader = new FileReader(pidFile); + lineReader = new BufferedReader(reader); + pidLine = lineReader.readLine(); + if(pidLine!=null) { + pidLine = pidLine.trim(); + } + } catch (Exception ex) { + log.error("Failed to read PID from file [" + pidFile.getAbsolutePath() + "]", ex); + } finally { + if(reader!=null) try { reader.close(); } catch (Exception ex) { /* No Op */ } + } + try { + return Long.parseLong(pidLine); + } catch (Exception ex) { + return null; + } + } + +} \ No newline at end of file diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 2669d6b8c6..ba2b797064 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -48,6 +48,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.StatsCollector; +import net.opentsdb.tools.GnuplotInstaller; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; @@ -908,6 +909,18 @@ public Thread newThread(final Runnable r) { * @return The path to the wrapper script. */ private static String findGnuplotHelperScript() { + if(!GnuplotInstaller.FOUND_GP) { + LOG.warn("Skipping Gnuplot Shell Script Install since Gnuplot executable was not found"); + return null; + } + if(!GnuplotInstaller.GP_FILE.exists()) { + GnuplotInstaller.installMyGnuPlot(); + } + if(GnuplotInstaller.GP_FILE.exists() && GnuplotInstaller.GP_FILE.canExecute()) { + LOG.info("Auto Installed Gnuplot Invoker at [{}]", GnuplotInstaller.GP_FILE.getAbsolutePath()); + return GnuplotInstaller.GP_FILE.getAbsolutePath(); + } + final URL url = GraphHandler.class.getClassLoader().getResource(WRAPPER); if (url == null) { throw new RuntimeException("Couldn't find " + WRAPPER + " on the" diff --git a/src/utils/Config.java b/src/utils/Config.java index f51a1e98e7..e8ff25b5d3 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -160,6 +160,13 @@ public Config(final Config parent) { setDefaults(); } + /** + * Creates a new empty Config + */ + public Config() { + + } + /** @return The file that generated this config. May be null */ public String configLocation() { return config_location; @@ -639,7 +646,7 @@ protected void loadConfig(final String file) throws FileNotFoundException, * Loads the static class variables for values that are called often. This * should be called any time the configuration changes. */ - protected void loadStaticVariables() { + public void loadStaticVariables() { auto_metric = this.getBoolean("tsd.core.auto_create_metrics"); auto_tagk = this.getBoolean("tsd.core.auto_create_tagks"); auto_tagv = this.getBoolean("tsd.core.auto_create_tagvs"); diff --git a/test/tools/TestConfigArgP.java b/test/tools/TestConfigArgP.java new file mode 100644 index 0000000000..07951c8103 --- /dev/null +++ b/test/tools/TestConfigArgP.java @@ -0,0 +1,628 @@ +// 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.tools; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.management.ManagementFactory; +import java.net.InetSocketAddress; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import net.opentsdb.tools.ConfigArgP.ConfigurationItem; +import net.opentsdb.utils.Config; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.channel.socket.ServerSocketChannel; +import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpHeaders.Names; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + *

    Title: TestConfigArgP

    + *

    Description: Test cases for the fat-jar launcher configuration manager

    + */ +public class TestConfigArgP { + /** The number of cores available to this JVM */ + static final int CORES = ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors(); + /** Platform EOL */ + static final String EOL = System.getProperty("line.separator", "\n"); + /** The quickie web server */ + static QuickieWebServer webServer = null; + /** The port the web server is listening on */ + static int port = -1; + /** true/false string values */ + static final Set trueFalseValues = Collections.unmodifiableSet(new HashSet(Arrays.asList("true", "false"))); + + static final boolean runningFatJar; + + static { + boolean tmp = false; + InputStream is = null; + try { + is = TestConfigArgP.class.getClassLoader().getResourceAsStream("opentsdb.conf.json"); + BufferedReader bin = new BufferedReader(new InputStreamReader(is)); + StringBuilder b = new StringBuilder(); + String line = null; + while((line = bin.readLine())!=null) { + b.append(line); + } + JSONObject jo = new JSONObject(b.toString()); + tmp = true; + } catch (Exception x) { + tmp = false; + } finally { + if(is!=null) try { is.close(); } catch (Exception x) {/* No Op */} + } + runningFatJar = tmp; + } + + /** + * Loads a fresh new JSONObject with the config + * from the classpath loaded opentsdb.conf.json. + * @return A loaded JSONObject + */ + static JSONObject newTSDBConfig() { + InputStream is = null; + try { + is = TestConfigArgP.class.getClassLoader().getResourceAsStream("opentsdb.conf.json"); + BufferedReader bin = new BufferedReader(new InputStreamReader(is)); + StringBuilder b = new StringBuilder(); + String line = null; + while((line = bin.readLine())!=null) { + b.append(line); + } + return new JSONObject(b.toString()); + } catch (Exception ex) { + throw new RuntimeException("Failed to load opentsdb.conf.json from the classpath", ex); + } finally { + if(is!=null) try { is.close(); } catch (Exception x) {/* No Op */} + } + } + + + + /** + * Creates a new QuickieResponder that will server the passed content + * @param uri The uri to register the responder for + * @param content The content to serve + * @param contentType The optional content type + * @return the URL to get the content with + */ + public static String newResponderForContent(final String uri, final String content, final String contentType) { + final QuickieResponder qr = new QuickieResponder(){ + @Override + public void writeResponse(final HttpResponse response) { + final byte[] bytes = content.getBytes(Charset.defaultCharset()); + HttpHeaders.addHeader(response, Names.CONTENT_TYPE, contentType==null ? "text/plain" : contentType); + HttpHeaders.addHeader(response, Names.CONTENT_LENGTH, bytes.length); + response.setContent(ChannelBuffers.wrappedBuffer(bytes)); + } + }; + webServer.addResponder(uri, qr); + InetSocketAddress isa = webServer.serverChannel.getLocalAddress(); + return String.format("http://%s:%s%s", isa.getAddress().getHostAddress(), isa.getPort(), uri); + } + + + + + + /** + * Starts the test web server + */ + @BeforeClass + public static void startHttpServer() { + webServer = new QuickieWebServer(); + port = webServer.getPort(); + org.junit.Assume.assumeTrue(runningFatJar); + } + + /** + * Stops all the running servers + */ + @AfterClass + public static void stopHttpServer() { + if(webServer!=null) { + try { + webServer.stop(); + log("Stopped OIO HTTP Server on port [" + port + "]"); + } catch (Exception x) { /* No Op */ } + webServer = null; + } + } + + + + /** + * System out logger + * @param format The message format + * @param args The message arg tokens + */ + public static void log(String format, Object...args) { + System.out.println(String.format(format, args)); + } + + /** + * Validates that a the config value for tsd.network.worker_threads + * for which the default value is calced by a js scriptlet, + * is two times the number of cores. + * @throws Exception on any error + */ + @Test + public void testWorkerThreadDefault() throws Exception { + ConfigArgP cap = new ConfigArgP(); + Config config = cap.getConfig(); + assertEquals("The value for key [tsd.network.worker_threads]", (CORES * 2), config.getInt("tsd.network.worker_threads")); + // now test an override + cap = new ConfigArgP("--worker-threads", "7"); + config = cap.getConfig(); + assertEquals("The overriden value for key [tsd.network.worker_threads]", 7, config.getInt("tsd.network.worker_threads")); + // now test an override with an "=" + cap = new ConfigArgP("--worker-threads=7"); + config = cap.getConfig(); + assertEquals("The overriden value for key [tsd.network.worker_threads]", 7, config.getInt("tsd.network.worker_threads")); + + } + + /** + * Validates that a ConfigArgP with no arguments creates a Config with the expected defaults + * @throws Exception on any error + */ + @Test + public void testAllDefaults() throws Exception { + ConfigArgP cap = new ConfigArgP(); + Config config = cap.getConfig(); + int validatedItems = 0; + JSONArray configItems = newTSDBConfig().getJSONArray("config-items"); + for(int i = 0; i < configItems.length(); i++) { + JSONObject configItem = configItems.getJSONObject(i); + if(!configItem.has("defaultValue")) continue; + String key = configItem.getString("key"); + String expectedValue = ConfigArgP + // Converts any javascript evals or system property token decodes + .processConfigValue(configItem.getString("defaultValue")); + String value = config.getString(key); + assertEquals("The value for key [" + key + "]", expectedValue, value); + validatedItems++; + } + log("Validated %s Config Items", validatedItems); + } + + /** + * Validates that all {@link ConfigMetaType#BOOL} type config definitions have defaults, and vice-versa. + * This is necessary to allow a command line specifier with no value (e.g. --auto-metric) + * @throws Exception on any error + */ + @Test + public void testAllBoolMetaTypesHaveDefault() throws Exception { + JSONArray configItems = newTSDBConfig().getJSONArray("config-items"); + for(int i = 0; i < configItems.length(); i++) { + JSONObject configItem = configItems.getJSONObject(i); + final String name = configItem.getString("key"); + String meta = configItem.has("meta") ? configItem.getString("meta") : null; + if(!"BOOL".equals(meta)) continue; + String defaultValue = configItem.has("defaultValue") ? configItem.getString("defaultValue") : null; + assertNotNull("Config Item [" + name + "] of type BOOL has null default value", defaultValue); + assertTrue("Config Item [" + name + "] of type BOOL has invalid default value", trueFalseValues.contains(defaultValue)); + } + for(int i = 0; i < configItems.length(); i++) { + JSONObject configItem = configItems.getJSONObject(i); + final String name = configItem.getString("key"); + String defaultValue = configItem.has("defaultValue") ? configItem.getString("defaultValue") : null; + if(defaultValue==null || !trueFalseValues.contains(defaultValue)) continue; + String meta = configItem.has("meta") ? configItem.getString("meta") : null; + assertEquals("Config Item [" + name + "] with defaultValue of true/false meta", "BOOL", meta); + } + + } + + /** + * Validates that an HTTP URL defined config overrides the default values. + * @throws Exception thrown on any error + */ + @SuppressWarnings("unchecked") + @Test + public void httpExternalConfigTest() throws Exception { + try { + final String url = newResponderForContent("/content", configToContent( + new ConfigArgP().getConfig(), + Collections.singletonMap("tsd.network.worker_threads", "7")), + "plain/text"); + ConfigArgP cap = new ConfigArgP("--config", url); // include overwrites config + Config config = cap.getConfig(); + assertEquals("The HTTP overriden value for key [tsd.network.worker_threads]", 7, config.getInt("tsd.network.worker_threads")); + } finally { + webServer.removeResponder("/content"); + } + } + + /** + * Validates that an HTTP URL defined include successfully overrides + * the default values. + * @throws Exception thrown on any error + */ + @SuppressWarnings("unchecked") + @Test + public void httpIncludeOverrideTest() throws Exception { + try { + final String url = newResponderForContent("/content", configToContent( + new ConfigArgP().getConfig(), + Collections.singletonMap("tsd.network.worker_threads", "7")), + "plain/text"); + ConfigArgP cap = new ConfigArgP("--include", url); // include overwrites config + Config config = cap.getConfig(); + assertEquals("The HTTP overriden value for key [tsd.network.worker_threads]", 7, config.getInt("tsd.network.worker_threads")); + } finally { + webServer.removeResponder("/content"); + } + } + + + /** + * Validates that an HTTP URL defined command line config successfully loads from a URL + * @throws Exception thrown on any error + */ + @Test + public void httpCommandLineURLConfigTest() throws Exception { + try { + final Properties p = contentToProps(ALL_NON_DEFAULTS); + final String url = newResponderForContent("/content", ALL_NON_DEFAULTS, "plain/text"); + ConfigArgP cap = new ConfigArgP("--config", url); + Config config = cap.getConfig(); + assertEquals("The HTTP overriden value for key [tsd.network.worker_threads]", (CORES * 3 + 1), config.getInt("tsd.network.worker_threads")); + for(String key: p.stringPropertyNames()) { + String value = p.getProperty(key); + String cvalue = config.getString(key); + assertEquals("The value for config item [" + key + "]", value, cvalue); + } + } finally { + webServer.removeResponder("/content"); + } + } + + /** + * Validates that an HTTP URL defined command line config is overriden by an include config + * @throws Exception thrown on any error + */ + @Test + public void httpCommandLineOverridenByIncludeTest() throws Exception { + try { + final Properties p = contentToProps(ALL_DEFAULTS); + p.remove("tsd.network.worker_threads"); + final String url = newResponderForContent("/content", ALL_DEFAULTS, "plain/text"); + final String url2 = newResponderForContent("/content2", "tsd.network.worker_threads=5", "plain/text"); + ConfigArgP cap = new ConfigArgP("--config", url, "--include", url2); + Config config = cap.getConfig(); + + assertEquals("The HTTP overriden value for key [tsd.network.worker_threads]", 5, config.getInt("tsd.network.worker_threads")); + // this is a bit of a hack (and redundant ?), but we're trying to test the rest of the values. + for(String key: p.stringPropertyNames()) { + ConfigurationItem di = cap.getDefaultItem(key); + String value = di.isBool() ? // For a bool, presence means true + cap.isClArg(di.getClOption()) ? "true" : di.getDefaultValue() + : p.getProperty(key); + String cvalue = config.getString(key); + assertEquals("The value for config item [" + key + "]", value, cvalue); + } + } finally { + webServer.removeResponder("/content"); + } + } + + /** + * Validates that BOOL type items with a default value of false + * eval as true if configured on the command line + * @throws Exception thrown on any error + */ + @Test + public void validateCLEnablesFalseBools() throws Exception { + // find all the applicable items, which are bools with a default value of false + Set keysToEnable = new HashSet(); + Set clsToEnable = new HashSet(); + JSONArray configItems = newTSDBConfig().getJSONArray("config-items"); + for(int i = 0; i < configItems.length(); i++) { + JSONObject configItem = configItems.getJSONObject(i); + if(!configItem.has("meta")) continue; + if("BOOL".equals(configItem.getString("meta")) && "false".equals(configItem.getString("defaultValue")) ) { + keysToEnable.add(configItem.getString("key")); + clsToEnable.add(configItem.getString("cl-option")); + } + } + ConfigArgP cap = new ConfigArgP(clsToEnable.toArray(new String[clsToEnable.size()])); + Config cfg = cap.getConfig(); + for(String key: keysToEnable) { + assertEquals("The bool value of [" + key + "]", "true", cfg.getString(key)); + } + + } + + + /** + * Converts the passed stringy to a properties instance + * @param content The content to format + * @return the properties instance + */ + protected Properties contentToProps(final CharSequence content) { + Properties p = new Properties(); + try { + p.load(new StringReader(content.toString())); + Properties converted = new Properties(); + for(String key: p.stringPropertyNames()) { + converted.put(key, ConfigArgP.processConfigValue(p.getProperty(key))); + } + return converted; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Converts the passed config to a readable properties file, applying the passed overrides + * @param config The config to write + * @param overrides The overrides to apply + * @return The rendered content + */ + protected String configToContent(final Config config, Map...overrides) { + Properties p = new Properties(); + for(Map.Entry entry: config.getMap().entrySet()) { + String vl = entry.getValue(); + if(vl==null || vl.trim().isEmpty()) continue; + p.put(entry.getKey(), vl); + } + for(Map map: overrides) { + p.putAll(map); + } + StringBuilder b = new StringBuilder(); + for(String key: p.stringPropertyNames()) { + b.append(key).append("=") + .append(p.getProperty(key)) + .append(EOL); + } + return b.toString(); + } + + + static interface QuickieResponder { + public void writeResponse(final HttpResponse response); + } + + static class QuickieWebServer extends SimpleChannelUpstreamHandler implements ChannelPipelineFactory { + final OioServerSocketChannelFactory scf; + final ServerBootstrap bootstrap; + final ServerSocketChannel serverChannel; + final int port; + /** Thread serial for the http server handler threads */ + final AtomicInteger serial = new AtomicInteger(0); + /** The http server thread factory */ + final ThreadFactory threadFactory = new ThreadFactory() { + @Override + public Thread newThread(final Runnable r) { + Thread t = new Thread(r, "HttpServerHandlerThread#" + serial.incrementAndGet()); + t.setDaemon(true); + t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + System.err.println("Uncaught exception on [" + t + "]"); + e.printStackTrace(System.err); + } + }); + return t; + } + }; + + final ConcurrentHashMap responders = new ConcurrentHashMap(); + static final DefaultHttpResponse response404 = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + public QuickieWebServer() { + scf = new OioServerSocketChannelFactory(Executors.newCachedThreadPool(threadFactory), Executors.newCachedThreadPool(threadFactory)); + bootstrap = new ServerBootstrap(scf); + bootstrap.setOption("child.tcpNoDelay", true); + bootstrap.setPipelineFactory(this); + serverChannel = (ServerSocketChannel) bootstrap.bind(new InetSocketAddress("127.0.0.1", 0)); + port = serverChannel.getLocalAddress().getPort(); + log("Started OIO HTTP Server on port [" + port + "]"); + } + + public int getPort() { + return port; + } + + public void addResponder(final String uri, final QuickieResponder responder) { + if(responders.putIfAbsent(uri, responder)!=null) throw new RuntimeException("Handler for URI [" + uri + "] exists"); + } + + public void removeResponder(final String uri) { + responders.remove(uri); + } + + public void stop() { + scf.releaseExternalResources(); + } + /** + * {@inheritDoc} + * @see org.jboss.netty.channel.SimpleChannelUpstreamHandler#messageReceived(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.MessageEvent) + */ + @Override + public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + HttpRequest req = (HttpRequest)msg; + QuickieResponder responder = responders.get(req.getUri()); + if(responder==null) { + ctx.getChannel().write(response404).addListener(ChannelFutureListener.CLOSE); + } else { + DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + responder.writeResponse(response); + ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); + } + } + } + + /** + * {@inheritDoc} + * @see org.jboss.netty.channel.ChannelPipelineFactory#getPipeline() + */ + @Override + public ChannelPipeline getPipeline() throws Exception { + ChannelPipeline pipeline = Channels.pipeline(); + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("aggregator", new HttpChunkAggregator(1048576)); + pipeline.addLast("encoder", new HttpResponseEncoder()); + pipeline.addLast("handler", this); + return pipeline; + } + + /** + * {@inheritDoc} + * @see org.jboss.netty.channel.SimpleChannelUpstreamHandler#exceptionCaught(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ExceptionEvent) + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + } + + private static String ALL_DEFAULTS = "tsd.core.auto_create_tagvs=true\n" + + "tsd.http.request.enable_chunked=false\n" + + "tsd.network.tcp_no_delay=true\n" + + "tsd.ui.noexport=false\n" + + "tsd.core.preload_uid_cache.max_entries=300000\n" + + "tsd.http.staticroot=${java.io.tmpdir}/.tsdb/static-content\n" + + "tsd.storage.hbase.zk_basedir=/hbase\n" + + "tsd.storage.flush_interval=1000\n" + + "tsd.core.meta.enable_tsuid_tracking=false\n" + + "tsd.core.auto_create_metrics=false\n" + + "tsd.http.request.max_chunk=4096\n" + + "tsd.search.enable=false\n" + + "tsd.network.backlog=3072\n" + + "tsd.logback.rollpattern=_%d{yyyy-MM-dd}.%i\n" + + "tsd.http.request.cors_headers=Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since\n" + + "tsd.storage.hbase.data_table=tsdb\n" + + "tsd.network.keep_alive=true\n" + + "tsd.core.timezone=${user.timezone}\n" + + "tsd.network.port=4242\n" + + "tsd.core.auto_create_tagks=true\n" + + "tsd.mode=rw\n" + + "tsd.network.reuse_address=true\n" + + "tsd.http.cachedir=${java.io.tmpdir}/.tsdb/http-cache/\n" + + "tsd.core.meta.enable_realtime_ts=false\n" + + "tsd.rtpublisher.enable=false\n" + + "tsd.stats.canonical=false\n" + + "tsd.process.pid.ignore.existing=false\n" + + "tsd.core.meta.enable_tsuid_incrementing=false\n" + + "tsd.core.socket.timeout=0\n" + + "tsd.core.tree.enable_processing=false\n" + + "tsd.storage.hbase.uid_table=tsdb-uid\n" + + "tsd.storage.hbase.tree_table=tsdb-tree\n" + + "tsd.process.pid.file=${java.io.tmpdir}/.tsdb/opentsdb.pid\n" + + "tsd.core.preload_uid_cache=false\n" + + "tsd.network.async_io=true\n" + + "tsd.storage.fix_duplicates=false\n" + + "tsd.network.bind=0.0.0.0\n" + + "tsd.storage.hbase.zk_quorum=localhost\n" + + "tsd.storage.enable_compaction=true\n" + + "tsd.no_diediedie=false\n" + + "tsd.network.worker_threads=$[new Integer(cores * 2)]\n" + + "tsd.http.show_stack_trace=true\n" + + "tsd.logback.console=false\n" + + "tsd.core.meta.enable_realtime_uid=false\n" + + "tsd.storage.hbase.meta_table=tsdb-meta\n"; + + private static String ALL_NON_DEFAULTS = "tsd.core.auto_create_tagvs=true\n" + + "tsd.http.request.enable_chunked=true\n" + + "tsd.network.tcp_no_delay=false\n" + + "tsd.ui.noexport=true\n" + + "tsd.core.preload_uid_cache.max_entries=3\n" + + "tsd.http.staticroot=${user.home}/.tsdb/static-content\n" + + "tsd.storage.hbase.zk_basedir=/hbasex\n" + + "tsd.storage.flush_interval=10\n" + + "tsd.core.meta.enable_tsuid_tracking=true\n" + + "tsd.core.auto_create_metrics=true\n" + + "tsd.http.request.max_chunk=9203\n" + + "tsd.search.enable=true\n" + + "tsd.network.backlog=1\n" + + "tsd.logback.rollpattern=_%d{yyyy}.%i\n" + + "tsd.http.request.cors_headers=Authorization\n" + + "tsd.storage.hbase.data_table=tsdbx\n" + + "tsd.network.keep_alive=false\n" + + "tsd.core.timezone=Pacific/Kiritimati\n" + + "tsd.network.port=7272\n" + + "tsd.core.auto_create_tagks=false\n" + + "tsd.mode=ro\n" + + "tsd.network.reuse_address=false\n" + + "tsd.http.cachedir=${user.home}/.tsdb/http-cache/\n" + + "tsd.core.meta.enable_realtime_ts=true\n" + + "tsd.rtpublisher.enable=true\n" + + "tsd.stats.canonical=true\n" + + "tsd.process.pid.ignore.existing=true\n" + + "tsd.core.meta.enable_tsuid_incrementing=true\n" + + "tsd.core.socket.timeout=9024\n" + + "tsd.core.tree.enable_processing=true\n" + + "tsd.storage.hbase.uid_table=tsdb-uidx\n" + + "tsd.storage.hbase.tree_table=tsdb-treex\n" + + "tsd.process.pid.file=${user.home}/.tsdb/opentsdb.pid\n" + + "tsd.core.preload_uid_cache=true\n" + + "tsd.network.async_io=false\n" + + "tsd.storage.fix_duplicates=true\n" + + "tsd.network.bind=127.0.0.1\n" + + "tsd.storage.hbase.zk_quorum=127.0.0.1\n" + + "tsd.storage.enable_compaction=false\n" + + "tsd.no_diediedie=true\n" + + "tsd.network.worker_threads=$[new Integer(cores * 3 + 1)]\n" + + "tsd.http.show_stack_trace=false\n" + + "tsd.logback.console=true\n" + + "tsd.core.meta.enable_realtime_uid=true\n" + + "tsd.storage.hbase.meta_table=tsdb-metax\n"; + +} + + From 9e453731b705c820bd8f09f3ba69d035760049b0 Mon Sep 17 00:00:00 2001 From: Rajesh G Date: Fri, 28 Oct 2016 16:20:37 -0700 Subject: [PATCH 572/826] Add rollup usage and support to the downsamplers as well as queries and spans. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 51 ++++++++++++ src/core/Downsampler.java | 69 +++++++++++++++- src/core/FillingDownsampler.java | 21 ++++- src/core/SaltScanner.java | 130 ++++++++++++++++++++---------- src/core/Span.java | 27 +++++++ src/core/SpanGroup.java | 42 +++++++++- src/core/TSSubQuery.java | 63 +++++++++++++++ src/core/TsdbQuery.java | 8 +- src/tsd/QueryRpc.java | 4 + 9 files changed, 366 insertions(+), 49 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 4f37df605c..44f8dfd280 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -326,6 +326,57 @@ public static AggregationIterator create(final List spans, method, rate); } + /** + * Creates a new iterator for a {@link SpanGroup}. + * @param spans Spans in a group. + * @param start_time Any data point strictly before this timestamp will be + * ignored. + * @param end_time Any data point strictly after this timestamp will be + * ignored. + * @param aggregator The aggregation function to use. + * @param method Interpolation method to use when aggregating time series + * @param downsampler The downsampling specifier to use (cannot be null) + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @param rate If {@code true}, the rate of the series will be used instead + * of the actual values. + * @param rate_options Specifies the optional additional rate calculation + * options. + * @param is_rollup Whether or not the query is handling rollup data. + * @return an AggregationIterator + * @since 2.4 + */ + public static AggregationIterator create(final List spans, + final long start_time, + final long end_time, + final Aggregator aggregator, + final Interpolation method, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final boolean rate, + final RateOptions rate_options, + final boolean is_rollup) { + final int size = spans.size(); + final SeekableView[] iterators = new SeekableView[size]; + for (int i = 0; i < size; i++) { + SeekableView it; + if (downsampler == null || + downsampler == DownsamplingSpecification.NO_DOWNSAMPLER) { + it = spans.get(i).spanIterator(); + } else { + it = spans.get(i).downsampler(start_time, end_time, downsampler, + query_start, query_end, is_rollup); + } + if (rate) { + it = new RateSpan(it, rate_options); + } + iterators[i] = it; + } + return new AggregationIterator(iterators, start_time, end_time, aggregator, + method, rate); + } + /** * Creates an aggregation iterator for a group of data point iterators. * @param iterators An array of Seekable views of spans in a group. Ignored diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 1e4436ef07..fa9f2a8b99 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -48,6 +48,9 @@ public class Downsampler implements SeekableView, DataPoint { /** Last value as a double */ protected double value; + /** Whether or not the downsampling is on rolled up data */ + protected boolean is_rollup; + /** Whether or not to merge all DPs in the source into one vaalue */ protected final boolean run_all; @@ -95,6 +98,24 @@ public class Downsampler implements SeekableView, DataPoint { final long query_start, final long query_end ) { + this(source, specification, query_start, query_end, false); + } + + /** + * Ctor. + * @param source The iterator to access the underlying data. + * @param specification The downsampling spec to use + * @param query_start The start timestamp of the actual query for use with "all" + * @param query_end The end timestamp of the actual query for use with "all" + * @param is_rollup Whether or not this query is handling rollup data. + * @since 2.4 + */ + Downsampler(final SeekableView source, + final DownsamplingSpecification specification, + final long query_start, + final long query_end, + final boolean is_rollup + ) { this.source = source; this.specification = specification; values_in_interval = new ValuesInInterval(); @@ -119,7 +140,7 @@ public class Downsampler implements SeekableView, DataPoint { interval = unit = 0; } } - + // ------------------ // // Iterator interface // // ------------------ // @@ -135,7 +156,38 @@ public boolean hasNext() { @Override public DataPoint next() { if (hasNext()) { - value = specification.getFunction().runDouble(values_in_interval); + if (is_rollup && (specification.getFunction() == Aggregators.AVG || + specification.getFunction() == Aggregators.DEV)) { + double sum = 0; + long count = 0; + while (values_in_interval.hasNextValue()) { + count += values_in_interval.nextValueCount(); + sum += values_in_interval.nextDoubleValue(); + } + + if (specification.getFunction() == Aggregators.AVG) { + if (count == 0) { // avoid # / 0 + value = 0; + } else { + value = sum / (double)count; + } + } else { + throw new UnsupportedOperationException( + "Standard deviation over rolled up data is not supported"); + } + } else if (is_rollup && 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(); + } + value = count; + } else { + value = specification.getFunction().runDouble(values_in_interval); + } + timestamp = values_in_interval.getIntervalTimestamp(); values_in_interval.moveToNextInterval(); return this; @@ -194,6 +246,7 @@ public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("Downsampler: ") .append(", downsampler=").append(specification) + .append(", is_rollup=").append(is_rollup) .append(", queryStart=").append(query_start) .append(", queryEnd=").append(query_end) .append(", runAll=").append(run_all) @@ -393,6 +446,17 @@ public double nextDoubleValue() { + timestamp_end_interval); } + // call me BEFORE you call nextDoubleValue + public long nextValueCount() { + if (hasNextValue()) { + if (next_dp != null) { + return next_dp.valueCount(); + } + } + throw new NoSuchElementException("no more values in interval of " + + timestamp_end_interval); + } + @Override public String toString() { final StringBuilder buf = new StringBuilder(); @@ -411,7 +475,6 @@ public String toString() { return buf.toString(); } } - @Override public long valueCount() { diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index bd47205bf8..6856406e73 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -70,8 +70,27 @@ public class FillingDownsampler extends Downsampler { FillingDownsampler(final SeekableView source, final long start_time, final long end_time, final DownsamplingSpecification specification, final long query_start, final long end_start) { + this(source, start_time, end_time, specification, query_start, end_start, + false); + } + + /** + * Create a new filling downsampler. + * @param source The iterator to access the underlying data. + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. + * @param specification The downsampling spec to use + * @param query_start The start timestamp of the actual query for use with "all" + * @param query_end The end timestamp of the actual query for use with "all" + * @param is_rollup Whether or not this query is handling rollup data. + * @throws IllegalArgumentException if fill_policy is interpolation. + * @since 2.4 + */ + FillingDownsampler(final SeekableView source, final long start_time, + final long end_time, final DownsamplingSpecification specification, + final long query_start, final long end_start, final boolean is_rollup) { // Lean on the superclass implementation. - super(source, specification, query_start, end_start); + super(source, specification, query_start, end_start, is_rollup); // Ensure we aren't given a bogus fill policy. if (FillPolicy.NONE == specification.getFillPolicy()) { diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index ce4e433c1a..6df18915ee 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -24,12 +24,16 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.rollup.RollupSpan; import net.opentsdb.stats.QueryStats; import net.opentsdb.stats.QueryStats.QueryStat; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; @@ -103,6 +107,9 @@ public class SaltScanner { /** Whether or not to delete the queried data */ private final boolean delete; + /** A rollup query configuration if scanning for rolled up data. */ + private final RollupQuery rollup_query; + /** A list of filters to iterate over when processing rows */ private final List filters; @@ -126,7 +133,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters) { - this(tsdb, metric, scanners, spans, filters, false, null, 0); + this(tsdb, metric, scanners, spans, filters, false, null, null, 0); } /** @@ -137,6 +144,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, * @param scanners A list of HBase scanners, one for each bucket * @param spans The span map to store results in * @param delete Whether or not to delete the queried data + * @param rollup_query An optional rollup query config. May be null. * @param filters A list of filters for processing * @param query_stats A stats object for tracking timing * @param query_index The index of the sub query in the main query list @@ -148,6 +156,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final TreeMap spans, final List filters, final boolean delete, + final RollupQuery rollup_query, final QueryStats query_stats, final int query_index) { if (Const.SALT_WIDTH() < 1) { @@ -186,6 +195,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.tsdb = tsdb; this.filters = filters; this.delete = delete; + this.rollup_query = rollup_query; this.query_stats = query_stats; this.query_index = query_index; } @@ -243,7 +253,8 @@ private void mergeAndReturnResults() { Span datapoints = spans.get(kv.key()); if (datapoints == null) { - datapoints = new Span(tsdb); + datapoints = RollupQuery.isValidQuery(rollup_query) ? + new RollupSpan(tsdb, this.rollup_query) : new Span(tsdb); spans.put(kv.key(), datapoints); } @@ -530,51 +541,88 @@ void processRow(final byte[] key, final ArrayList row) { annotations.put(key, notes); } - // calculate estimated data point count. We don't want to deserialize - // the byte arrays so we'll just get a rough estimate of compacted - // columns. - for (final KeyValue kv : row) { - if (kv.qualifier().length % 2 == 0) { - if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { - ++dps_post_filter; - } else { - // for now we'll assume that all compacted columns are of the - // same precision. This is likely incorrect. - if (Internal.inMilliseconds(kv.qualifier())) { - dps_post_filter += (kv.qualifier().length / 4); + //TODO rollup doesn't use the column qualifier prefix right now + //Please move this logic to @CompactionQueue.compact API, if the + //qualifier prefix is set for rollup. Right now there is no way to + //identify whether a cell belong to rollup or default data table + //from the KeyValue/Hbase cell object + if (RollupQuery.isValidQuery(rollup_query)) { + //It is the rollup search result and rollup cells will not be + //compacted, so don't need to worry about complex or trivial + //compactions. It just need to consider the cells are different key + //values + for (KeyValue kv:row) { + final byte[] qual = kv.qualifier(); + + if (qual.length > 0) { + // Todo: Bug! Here we shouldn't use the first byte to check the type of this row + // Instead should parse the byte array to find the suffix and determine the actual type + if (qual[0] == Annotation.PREFIX()) { + // This could be a row with only an annotation in it + final Annotation note = JSON.parseToObject(kv.value(), + Annotation.class); + notes.add(note); } else { - dps_post_filter += (kv.qualifier().length / 2); + if (rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV) { + if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || + Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { + kvs.add(kv); + } + } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), + qual, 0, rollup_query.getRollupAggPrefix().length) == 0) { + kvs.add(kv); + } } } - } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { - // with appends we don't have a good rough estimate as the length - // can vary widely with the value length variability. Therefore we - // have to iterate. - int idx = 0; - int qlength = 0; - while (idx < kv.value().length) { - qlength = Internal.getQualifierLength(kv.value(), idx); - idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); - ++dps_post_filter; + } // end for + } else { + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_post_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_post_filter += (kv.qualifier().length / 4); + } else { + dps_post_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_post_filter; + } } } - } - - final KeyValue compacted; - // let IllegalDataExceptions bubble up so the handler above can close - // the scanner - final long compaction_start = DateTime.nanoTime(); - try { - compacted = tsdb.compact(row, notes); - } catch (IllegalDataException idex) { + + final KeyValue compacted; + // let IllegalDataExceptions bubble up so the handler above can close + // the scanner + final long compaction_start = DateTime.nanoTime(); + try { + compacted = tsdb.compact(row, notes); + } catch (IllegalDataException idex) { + compaction_time += (DateTime.nanoTime() - compaction_start); + close(false); + handleException(idex); + return; + } compaction_time += (DateTime.nanoTime() - compaction_start); - close(false); - handleException(idex); - return; - } - compaction_time += (DateTime.nanoTime() - compaction_start); - if (compacted != null) { // Can be null if we ignored all KVs. - kvs.add(compacted); + if (compacted != null) { // Can be null if we ignored all KVs. + kvs.add(compacted); + } } } diff --git a/src/core/Span.java b/src/core/Span.java index e0e5ca731f..2dd9a04d20 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -510,6 +510,33 @@ Downsampler downsampler(final long start_time, return new FillingDownsampler(spanIterator(), start_time, end_time, downsampler, query_start, query_end); } + + /** + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. + * @param downsampler The downsampling specification to use + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @param is_rollup Whether or not the downsampler is handling rolled up data. + * @return A new downsampler. + * @since 2.4 + */ + Downsampler downsampler(final long start_time, + final long end_time, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final boolean is_rollup) { + if (downsampler == null) { + return null; + } + if (FillPolicy.NONE == downsampler.getFillPolicy()) { + return new Downsampler(spanIterator(), downsampler, + query_start, query_end, is_rollup); + } + return new FillingDownsampler(spanIterator(), start_time, end_time, + downsampler, query_start, query_end, is_rollup); + } /** * RowSeq abstract factory API implementation diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 7e50bb8ef6..b3c35659e1 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -103,6 +103,9 @@ final class SpanGroup implements DataPoints { /** Index of the query in the TSQuery class */ private final int query_index; + /** Whether or not the query is for rolled up data */ + private final boolean is_rollup; + /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; @@ -226,6 +229,42 @@ final class SpanGroup implements DataPoints { final long query_start, 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, false); + } + + /** + * Ctor. + * @param tsdb The TSDB we belong to. + * @param start_time Any data point strictly before this timestamp will be + * ignored. + * @param end_time Any data point strictly after this timestamp will be + * ignored. + * @param spans A sequence of initial {@link Spans} to add to this group. + * Ignored if {@code null}. Additional spans can be added with {@link #add}. + * @param rate If {@code true}, the rate of the series will be used instead + * of the actual values. + * @param rate_options Specifies the optional additional rate calculation options. + * @param aggregator The aggregation function to use. + * @param downsampler The specification to use for downsampling, may be null. + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @param query_index index of the original query + * @param is_rollup Whether or not this query is handling rolled up data + * @since 2.4 + */ + SpanGroup(final TSDB tsdb, + final long start_time, + final long end_time, + final Iterable spans, + final boolean rate, + final RateOptions rate_options, + final Aggregator aggregator, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final int query_index, + final boolean is_rollup) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; @@ -243,6 +282,7 @@ final class SpanGroup implements DataPoints { this.query_start = query_start; this.query_end = query_end; this.query_index = query_index; + this.is_rollup = is_rollup; this.tsdb = tsdb; } @@ -487,7 +527,7 @@ public SeekableView iterator() { return AggregationIterator.create(spans, start_time, end_time, aggregator, aggregator.interpolationMethod(), downsampler, query_start, query_end, - rate, rate_options); + rate, rate_options, is_rollup); } /** diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 578a32b89f..664bb0ac39 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -22,6 +22,7 @@ import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.utils.ByteSet; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; @@ -69,6 +70,17 @@ public final class TSSubQuery { /** Parsed downsampling specification. */ private DownsamplingSpecification downsample_specifier; + /** Search the query on pre-aggregated table directly instead of post fetch + * aggregation. */ + private boolean pre_aggregate; + + /** Do not use rollup tables for down sampling */ + private TsdbQuery.ROLLUP_USAGE rollup_usage; + + /** Pointer to the related TSDB Query */ + @JsonIgnore + private TsdbQuery tsdb_query; + /** A list of filters for this query. For now these are pulled out of the * tags map. In the future we'll have special JSON objects for them. */ private List filters; @@ -385,4 +397,55 @@ public void setIndex(final int index) { this.index = index; } + /** Search the query on pre-aggregated table directly instead of post fetch + * aggregation. + * @return Whether or not to fetch data on pre-aggregates + * @since 2.4 + */ + public boolean isPreAggregate() { + return pre_aggregate; + } + + /** Search the query on pre-aggregated table directly instead of post fetch + * aggregation. + * @param pre_aggregate Whether or not to fetch data on pre-aggregated tables. + * @since 2.4 + */ + public void setPreAggregate(boolean pre_aggregate) { + this.pre_aggregate = pre_aggregate; + } + + /** @return Rollup data usage type. + * @since 2.4 */ + public TsdbQuery.ROLLUP_USAGE getRollupUsage() { + return rollup_usage; + } + + /** @param rollup_usage Rollup data usage. + * @since 2.4 */ + public void setRollupUsage(String rollup_usage) { + this.rollup_usage = TsdbQuery.ROLLUP_USAGE.parse(rollup_usage); + } + + /** Which rollup table it scanned to get the final result. + * @return The rollup table to use. + * @since 2.4 + */ + public String getRollupTable() { + if (tsdb_query != null) { + return tsdb_query.getRollupTable(); + } + else { + return "raw"; + } + } + + /** @param tsdb_query Parent TsdbQuery, which will tell the which rollup + * table it scanned to get the final result + * @since 2.4 + */ + void setTsdbQuery(TsdbQuery tsdb_query) { + this.tsdb_query = tsdb_query; + } + } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index d6f1e79d79..8612e31064 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -44,7 +44,6 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; -import net.opentsdb.core.TsdbQuery.ROLLUP_USAGE; import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.rollup.NoSuchRollupForIntervalException; @@ -420,6 +419,8 @@ public Deferred configureFromQuery(final TSQuery query, rate_options = new RateOptions(); } downsampler = sub_query.downsamplingSpecification(); + pre_aggregate = sub_query.isPreAggregate(); + rollup_usage = sub_query.getRollupUsage(); filters = sub_query.getFilters(); explicit_tags = sub_query.getExplicitTags(); @@ -650,7 +651,7 @@ private Deferred> findSpans() throws HBaseException { } scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, - delete, query_stats, query_index).scan(); + delete, rollup_query, query_stats, query_index).scan(); } scan_start_time = DateTime.nanoTime(); @@ -1019,7 +1020,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), - query_index); + query_index, + RollupQuery.isValidQuery(rollup_query)); group.add(span); groups[i++] = group; } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 2f6d92be24..98342b7379 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -643,6 +643,10 @@ private static void parseMTypeSubQuery(final String query_string, } } else if (Character.isDigit(parts[x].charAt(0))) { sub_query.setDownsample(parts[x]); + } else if (parts[x].equalsIgnoreCase("pre-agg")) { + sub_query.setPreAggregate(true); + } else if (parts[x].toLowerCase().startsWith("rollup_")) { + sub_query.setRollupUsage(parts[x]); } else if (parts[x].toLowerCase().startsWith("explicit_tags")) { sub_query.setExplicitTags(true); } From f322082cfd6207cfa2502543b0ca4c7c8526064f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Oct 2016 10:53:22 -0700 Subject: [PATCH 573/826] Fix some bugs in the downsamplers when upstreaming (missed some code). Also added the unit tests for downsampling rollups. Add RollupSpan tests. --- src/core/Downsampler.java | 1 + src/core/FillingDownsampler.java | 39 ++++- test/core/TestDownsampler.java | 132 +++++++++++++++ test/core/TestFillingDownsampler.java | 222 ++++++++++++++++++++++++ test/core/TestRollupSpan.java | 234 ++++++++++++++++++++++++++ 5 files changed, 625 insertions(+), 3 deletions(-) create mode 100644 test/core/TestRollupSpan.java diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index fa9f2a8b99..8d77de36f3 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -121,6 +121,7 @@ public class Downsampler implements SeekableView, DataPoint { values_in_interval = new ValuesInInterval(); this.query_start = query_start; this.query_end = query_end; + this.is_rollup = is_rollup; final String s = specification.getStringInterval(); if (s != null && s.toLowerCase().contains("all")) { diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index 6856406e73..1f9428a7ed 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -164,7 +164,7 @@ public boolean hasNext() { */ @Override public DataPoint next() { - // Don't proceed if we've already completed iteration. + // Don't proceed if we've already completed iteration. if (hasNext()) { // Ensure that the timestamp we request is valid. values_in_interval.initializeIfNotDone(); @@ -181,13 +181,44 @@ public DataPoint next() { values_in_interval.moveToNextInterval(); actual = values_in_interval.getIntervalTimestamp(); } - + // Check whether the timestamp of the calculation interval matches what // we expect. if (run_all || actual == timestamp) { // The calculated interval timestamp matches what we expect, so we can // do normal processing. - value = specification.getFunction().runDouble(values_in_interval); + if (is_rollup && (specification.getFunction() == Aggregators.AVG || + specification.getFunction() == Aggregators.DEV)) { + double sum = 0; + long count = 0; + while (values_in_interval.hasNextValue()) { + count += values_in_interval.nextValueCount(); + sum += values_in_interval.nextDoubleValue(); + } + + if (specification.getFunction() == Aggregators.AVG) { + if (count == 0) { // avoid # / 0 + value = 0; + } else { + value = sum / (double)count; + } + } else { + throw new UnsupportedOperationException( + "Standard deviation over rolled up data is not supported"); + } + } else if (is_rollup && + 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(); + } + value = count; + } else { + value = specification.getFunction().runDouble(values_in_interval); + } values_in_interval.moveToNextInterval(); } else { // Our expected timestamp precedes the actual, so the interval is @@ -202,6 +233,8 @@ public DataPoint next() { case ZERO: value = 0.0; break; + + // TODO - scalar default: throw new RuntimeException("unhandled fill policy"); diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 07f7296cdd..5402c9f502 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -32,6 +32,7 @@ import org.junit.Test; /** Tests {@link Downsampler}. */ +@SuppressWarnings("deprecation") public class TestDownsampler { private static final long BASE_TIME = 1356998400000L; @@ -100,7 +101,33 @@ public void testDownsampler() { assertEquals(50, values.get(4), 0.0000001); assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(4).longValue()); } + + @Test + public void testDownsamplerDeprecated() { + downsampler = new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + assertEquals(5, values.size()); + assertEquals(40, values.get(0), 0.0000001); + assertEquals(BASE_TIME - 400000L, timestamps_in_millis.get(0).longValue()); + assertEquals(50, values.get(1), 0.0000001); + assertEquals(BASE_TIME + 1600000, timestamps_in_millis.get(1).longValue()); + assertEquals(45, values.get(2), 0.0000001); + assertEquals(BASE_TIME + 3600000L, timestamps_in_millis.get(2).longValue()); + assertEquals(40, values.get(3), 0.0000001); + assertEquals(BASE_TIME + 6600000L, timestamps_in_millis.get(3).longValue()); + assertEquals(50, values.get(4), 0.0000001); + assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(4).longValue()); + } + @Test public void testDownsamplerDeprecated_10seconds() { source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { @@ -1147,6 +1174,111 @@ public void testDownsampler_1year_timezone() { } } + @Test + public void testDownsampler_rollupSum() { + 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), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 4, 16), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 5, 32), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 6, 64), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 7, 128), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 8, 256), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 9, 512), + MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 10, 1024) + })); + specification = new DownsamplingSpecification("10s-sum"); + downsampler = new Downsampler(source, specification, 0, 0, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(6, values.size()); + assertEquals(3, values.get(0), 0.0000001); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(12, values.get(1), 0.0000001); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + assertEquals(48, values.get(2), 0.0000001); + assertEquals(BASE_TIME + 20000L, timestamps_in_millis.get(2).longValue()); + assertEquals(192, values.get(3), 0.0000001); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(3).longValue()); + assertEquals(768, values.get(4), 0.0000001); + assertEquals(BASE_TIME + 40000L, timestamps_in_millis.get(4).longValue()); + assertEquals(1024, values.get(5), 0.0000001); + assertEquals(BASE_TIME + 50000L, timestamps_in_millis.get(5).longValue()); + } + + @Test + public void testDownsampler_rollupAvg() { + 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) + })); + specification = new DownsamplingSpecification("10s-avg"); + downsampler = new Downsampler(source, specification, 0, 0, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + assertEquals(1.5, values.get(0), 0.0000001); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(6, values.get(1), 0.0000001); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + } + + @Test + public void testDownsampler_rollupCount() { + 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) + })); + specification = new DownsamplingSpecification("10s-count"); + downsampler = new Downsampler(source, specification, 0, 0, true); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + DataPoint dp = downsampler.next(); + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(2, values.size()); + assertEquals(2, values.get(0), 0.0000001); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(2, values.get(1), 0.0000001); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + } + + @Test (expected = UnsupportedOperationException.class) + public void testDownsampler_rollupDev() { + specification = new DownsamplingSpecification("10s-dev"); + downsampler = new Downsampler(source, specification, 0, 0, true); + while (downsampler.hasNext()) { + downsampler.next(); // <-- throws here + } + } + @Test(expected = UnsupportedOperationException.class) public void testRemove() { new Downsampler(source, THOUSAND_SEC_INTERVAL, AVG).remove(); diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index ef42ebc963..f06580bcbc 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -813,6 +813,228 @@ public void testDownsampler_noDataCalendar() { step(downsampler, timestamp += 60000, Double.NaN); assertFalse(downsampler.hasNext()); } + + @Test + public void testDownsampler_rollup() { + final long baseTime = 1000L; + 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.), + }); + + specification = new DownsamplingSpecification("100ms-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 12L * 25L, specification, 0, 0, true); + + long timestamp = baseTime; + step(downsampler, timestamp, 42.); + step(downsampler, timestamp += 100, 26.); + step(downsampler, timestamp += 100, 10.); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_rollupMissing() { + final long baseTime = 500L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime + 25L * 4L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 5L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 7L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 12L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 15L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 24L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 25L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 26L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), + }); + + specification = new DownsamplingSpecification("100ms-sum-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 36 * 25L, specification, 0, 0, true); + + long timestamp = baseTime; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 100, 3.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 2.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 4.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_rollupAvg() { + final long baseTime = 1000L; + 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.), + }); + + specification = new DownsamplingSpecification("100ms-avg-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 12L * 25L, specification, 0, 0, true); + + long timestamp = baseTime; + step(downsampler, timestamp, 10.5); + step(downsampler, timestamp += 100, 6.5); + step(downsampler, timestamp += 100, 2.5); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_rollupAvgMissing() { + final long baseTime = 500L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime + 25L * 4L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 5L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 7L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 12L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 15L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 24L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 25L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 26L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), + }); + + specification = new DownsamplingSpecification("100ms-avg-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 36 * 25L, specification, 0, 0, true); + + long timestamp = baseTime; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 100, 1.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 1); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 1.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_rollupCount() { + final long baseTime = 1000L; + 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.), + }); + + specification = new DownsamplingSpecification("100ms-count-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 12L * 25L, specification, 0, 0, true); + + long timestamp = baseTime; + step(downsampler, timestamp, 4); + step(downsampler, timestamp += 100, 4); + step(downsampler, timestamp += 100, 4); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_rollupCountMissing() { + final long baseTime = 500L; + final SeekableView source = + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofDoubleValue(baseTime + 25L * 4L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 5L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 7L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 12L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 15L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 24L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 25L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 26L, 1.), + MutableDataPoint.ofDoubleValue(baseTime + 25L * 27L, 1.), + }); + + specification = new DownsamplingSpecification("100ms-count-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 36 * 25L, specification, 0, 0, true); + + long timestamp = baseTime; + step(downsampler, timestamp, Double.NaN); + step(downsampler, timestamp += 100, 3.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 2.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, 4.); + step(downsampler, timestamp += 100, Double.NaN); + step(downsampler, timestamp += 100, Double.NaN); + assertFalse(downsampler.hasNext()); + } + + @Test (expected = UnsupportedOperationException.class) + public void testDownsampler_rollupDev() { + final long baseTime = 1000L; + 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.), + }); + + specification = new DownsamplingSpecification("100ms-dev-nan"); + final Downsampler downsampler = new FillingDownsampler(source, baseTime, + baseTime + 12L * 25L, specification, 0, 0, true); + while (downsampler.hasNext()) { + downsampler.next(); // <-- throws here + } + } private void step(final Downsampler downsampler, final long expected_timestamp, final double expected_value) { diff --git a/test/core/TestRollupSpan.java b/test/core/TestRollupSpan.java new file mode 100644 index 0000000000..1d26413daa --- /dev/null +++ b/test/core/TestRollupSpan.java @@ -0,0 +1,234 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.rollup.RollupSpan; +import static net.opentsdb.rollup.RollupUtils.ROLLUP_QUAL_DELIM; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ RowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, +Config.class, RowKey.class }) +public final class TestRollupSpan { + private TSDB tsdb = mock(TSDB.class); + private Config config = mock(Config.class); + private UniqueId metrics = mock(UniqueId.class); + private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; + private static final byte[] HOUR1 = new byte[] + { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; + private static final byte[] HOUR2 = new byte[] + { 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 1, 0, 0, 2 }; + private static final byte[] HOUR3 = new byte[] + { 0, 0, 1, 0x50, (byte)0xE2, 0x43, 0x20, 0, 0, 1, 0, 0, 2 }; + private static final byte[] FAMILY = { 't' }; + private static final byte[] ZERO = { 0 }; + private static final Aggregator aggr_sum = Aggregators.SUM; + + private static final RollupQuery rollup_query = + new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), + aggr_sum, 1000); + + @Before + public void before() throws Exception { + // Inject the attributes we need into the "tsdb" object. + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "table", TABLE); + Whitebox.setInternalState(tsdb, "config", config); + when(tsdb.getConfig()).thenReturn(config); + when(tsdb.metrics.width()).thenReturn((short)4); + when(RowKey.metricNameAsync(tsdb, HOUR1)) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + } + + @Test + public void addRow() { + final byte[] qual1 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, FAMILY, qual1, val1)); + + assertEquals(1, span.size()); + } + + @Test (expected = NullPointerException.class) + public void addRowNull() { + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(null); + } + + @Test + public void timestampNormalized() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, FAMILY, qual1, val1)); + span.addRow(new KeyValue(HOUR1, FAMILY, qual2, val2)); + span.addRow(new KeyValue(HOUR2, FAMILY, qual1, val1)); + span.addRow(new KeyValue(HOUR2, FAMILY, qual2, val2)); + span.addRow(new KeyValue(HOUR3, FAMILY, qual1, val1)); + span.addRow(new KeyValue(HOUR3, FAMILY, qual2, val2)); + + assertEquals(6, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1356998402000L, span.timestamp(1)); + assertEquals(1357002000000L, span.timestamp(2)); + assertEquals(1357002002000L, span.timestamp(3)); + assertEquals(1357005600000L, span.timestamp(4)); + assertEquals(1357005602000L, span.timestamp(5)); + } + +// @Test + public void timestampFullSeconds() throws Exception { + //6 = num of bytes ("sum) + num of bytes (":") + num of bytes in actual qual + final byte[] agg = (aggr_sum.toString() + ROLLUP_QUAL_DELIM). + getBytes(Const.ASCII_CHARSET); + final byte[] qualifiers = new byte[agg.length + 2]; + System.arraycopy(agg, 0, qualifiers, 0, agg.length); + final Span span = new RollupSpan(tsdb, rollup_query); + + for (int i = 0; i < 100; i++) { + final short qualifier = (short) (i << Const.FLAG_BITS | 0x07); + System.arraycopy(Bytes.fromShort(qualifier), 0, qualifiers, agg.length, 2); + span.addRow(new KeyValue(HOUR1, FAMILY, qualifiers, Bytes.fromLong(i))); + span.addRow(new KeyValue(HOUR2, FAMILY, qualifiers, Bytes.fromLong(i))); + span.addRow(new KeyValue(HOUR3, FAMILY, qualifiers, Bytes.fromLong(i))); + } + + + assertEquals(3600 * 3, span.size()); + } + + @Test + public void timestampMS() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, (byte) 0xF0, 0x00, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, (byte) 0xF0, 0x00, 0x02, 0x07 }; + final byte[] val2 = Bytes.fromLong(5L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, FAMILY, qual1, val1)); + span.addRow(new KeyValue(HOUR1, FAMILY, qual2, val2)); + span.addRow(new KeyValue(HOUR2, FAMILY, qual1, val1)); + span.addRow(new KeyValue(HOUR2, FAMILY, qual2, val2)); + span.addRow(new KeyValue(HOUR3, FAMILY, qual1, val1)); + span.addRow(new KeyValue(HOUR3, FAMILY, qual2, val2)); + + assertEquals(6, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1356998400000L, span.timestamp(1)); + assertEquals(1357002000000L, span.timestamp(2)); + assertEquals(1357002000000L, span.timestamp(3)); + assertEquals(1357005600000L, span.timestamp(4)); + assertEquals(1357005600000L, span.timestamp(5)); + } + + @Test + public void iterateNormalizedMS() throws Exception { + final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, FAMILY, qual1,val1)); + span.addRow(new KeyValue(HOUR1, FAMILY, qual2,val2)); + span.addRow(new KeyValue(HOUR2, FAMILY, qual1,val1)); + span.addRow(new KeyValue(HOUR2, FAMILY, qual2,val2)); + span.addRow(new KeyValue(HOUR3, FAMILY, qual1,val1)); + span.addRow(new KeyValue(HOUR3, FAMILY, qual2,val2)); + + assertEquals(6, span.size()); + final SeekableView it = span.iterator(); + DataPoint dp = it.next(); + + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(4, dp.longValue()); + + dp = it.next(); + assertEquals(1356998402000L, dp.timestamp()); + assertEquals(5, dp.longValue()); + + dp = it.next(); + assertEquals(1357002000000L, dp.timestamp()); + assertEquals(4, dp.longValue()); + + dp = it.next(); + assertEquals(1357002002000L, dp.timestamp()); + assertEquals(5, dp.longValue()); + + dp = it.next(); + assertEquals(1357005600000L, dp.timestamp()); + assertEquals(4, dp.longValue()); + + dp = it.next(); + assertEquals(1357005602000L, dp.timestamp()); + assertEquals(5, dp.longValue()); + + assertFalse(it.hasNext()); + + + } + + @Test + public void lastTimestampInRow() throws Exception { + final byte[] qual1 = { 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + + final KeyValue kv = new KeyValue(HOUR1, FAMILY, qual2, val2); + + assertEquals(1356998402L, Span.lastTimestampInRow((short) 3, kv)); + } + + @Test + public void lastTimestampInRowMs() throws Exception { + final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; + final byte[] val2 = Bytes.fromLong(5L); + + final KeyValue kv = new KeyValue(HOUR1, FAMILY, qual2, val2); + + assertEquals(1356998400008L, Span.lastTimestampInRow((short) 3, kv)); + } +} From 1211300e0a85527cb5677006cf0ae935051cf97d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 29 Oct 2016 10:57:24 -0700 Subject: [PATCH 574/826] Add TestRollupSpan tests and fix some bugs from the upstream process. Signed-off-by: Chris Larsen --- src/core/Span.java | 28 +- src/core/TSQuery.java | 4 +- src/core/TsdbQuery.java | 143 ++- src/rollup/RollupSeq.java | 2 +- test/core/TestTsdbQueryRollup.java | 970 ++++++++++++++++++ test/rollup/TestRollupSeq.java | 8 +- test/storage/MockBase.java | 132 ++- ...asynchbase-1.8.0-20161101.210048-3.jar.md5 | 1 + ...asynchbase-1.8.0-20161103.193100-4.jar.md5 | 1 + third_party/hbase/include.mk | 6 +- 10 files changed, 1205 insertions(+), 90 deletions(-) create mode 100644 test/core/TestTsdbQueryRollup.java create mode 100644 third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.8.0-20161103.193100-4.jar.md5 diff --git a/src/core/Span.java b/src/core/Span.java index 2dd9a04d20..4b758e272d 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -362,7 +362,9 @@ private int seekRow(final long timestamp) { for (int i = 0; i < nrows; i++) { row = rows.get(i); final int sz = row.size(); - if (row.timestamp(sz - 1) < timestamp) { + if (sz < 1) { + row_index++; + } else if (row.timestamp(sz - 1) < timestamp) { row_index++; // The last DP in this row is before 'timestamp'. } else { break; @@ -414,18 +416,34 @@ final class Iterator implements SeekableView { @Override public boolean hasNext() { - return (current_row.hasNext() // more points in this row - || row_index < rows.size() - 1); // or more rows + if (current_row.hasNext()) { + return true; + } + // handle situations where a row in the middle may be empty due to some + // kind of logic kicking out data points + while (row_index < rows.size() - 1) { + row_index++; + current_row = rows.get(row_index).internalIterator(); + if (current_row.hasNext()) { + return true; + } + } + return false; } @Override public DataPoint next() { if (current_row.hasNext()) { return current_row.next(); - } else if (row_index < rows.size() - 1) { + } + // handle situations where a row in the middle may be empty due to some + // kind of logic kicking out data points + while (row_index < rows.size() - 1) { row_index++; current_row = rows.get(row_index).internalIterator(); - return current_row.next(); + if (current_row.hasNext()) { + return current_row.next(); + } } throw new NoSuchElementException("no more elements"); } diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 071b401812..4500b0aecf 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -71,7 +71,7 @@ public final class TSQuery { private boolean show_tsuids; /** A list of parsed sub queries, must have one or more to fetch data */ - private ArrayList queries; + private List queries; /** The parsed start time value * Do not set directly */ @@ -445,7 +445,7 @@ public void setShowTSUIDs(boolean show_tsuids) { } /** @param queries a list of {@link TSSubQuery} objects to store*/ - public void setQueries(ArrayList queries) { + public void setQueries(final List queries) { this.queries = queries; } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 8612e31064..cbf916b991 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -44,11 +44,13 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.meta.Annotation; import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.rollup.NoSuchRollupForIntervalException; import net.opentsdb.rollup.RollupInterval; import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.rollup.RollupSpan; import net.opentsdb.rollup.RollupUtils; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.QueryStats; @@ -57,6 +59,7 @@ import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; /** * Non-synchronized implementation of {@link Query}. @@ -428,6 +431,7 @@ public Deferred configureFromQuery(final TSQuery query, //Check whether the down sampler is set and rollup is enabled transformDownSamplerToRollupQuery(sub_query.getDownsample()); } + sub_query.setTsdbQuery(this); // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { @@ -879,49 +883,94 @@ void processRow(final byte[] key, final ArrayList row) { tsdb.getClient().delete(del); } - // calculate estimated data point count. We don't want to deserialize - // the byte arrays so we'll just get a rough estimate of compacted - // columns. - for (final KeyValue kv : row) { - if (kv.qualifier().length % 2 == 0) { - if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { - ++dps_post_filter; - } else { - // for now we'll assume that all compacted columns are of the - // same precision. This is likely incorrect. - if (Internal.inMilliseconds(kv.qualifier())) { - dps_post_filter += (kv.qualifier().length / 4); + //Please move this logic to @CompactionQueue.compact API, if the + //qualifier prefix is set for rollup. Right now there is no way to + //identify whether a cell belong to rollup or default data table + //from the KeyValue/Hbase cell object + if (RollupQuery.isValidQuery(rollup_query)) { + //It is the rollup search result and rollup cells will not be + //compacted, so don't need to worry about complex or trivial + //compactions. It just need to consider the cells are different key + //values + + Span datapoints = spans.get(key); + if (datapoints == null) { + datapoints = new RollupSpan(tsdb, rollup_query); + spans.put(key, datapoints); + } + + for (KeyValue kv:row) { + final byte[] qual = kv.qualifier(); + + if (qual.length > 0) { + // Todo: Bug! Here we shouldn't use the first byte to check the type of this row + // Instead should parse the byte array to find the suffix and determine the actual type + if (qual[0] == Annotation.PREFIX()) { + // This could be a row with only an annotation in it + final Annotation note = JSON.parseToObject(kv.value(), + Annotation.class); + datapoints.getAnnotations().add(note); } else { - dps_post_filter += (kv.qualifier().length / 2); + if (rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV) { + if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || + Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { + datapoints.addRow(kv); + } + } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), + qual, 0, rollup_query.getRollupAggPrefix().length) == 0) { + datapoints.addRow(kv); + } } } - } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { - // with appends we don't have a good rough estimate as the length - // can vary widely with the value length variability. Therefore we - // have to iterate. - int idx = 0; - int qlength = 0; - while (idx < kv.value().length) { - qlength = Internal.getQualifierLength(kv.value(), idx); - idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); - ++dps_post_filter; + } // end for + ++nrows; + seenAnnotation |= !datapoints.getAnnotations().isEmpty(); + } else { + // calculate estimated data point count. We don't want to deserialize + // the byte arrays so we'll just get a rough estimate of compacted + // columns. + for (final KeyValue kv : row) { + if (kv.qualifier().length % 2 == 0) { + if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { + ++dps_post_filter; + } else { + // for now we'll assume that all compacted columns are of the + // same precision. This is likely incorrect. + if (Internal.inMilliseconds(kv.qualifier())) { + dps_post_filter += (kv.qualifier().length / 4); + } else { + dps_post_filter += (kv.qualifier().length / 2); + } + } + } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { + // with appends we don't have a good rough estimate as the length + // can vary widely with the value length variability. Therefore we + // have to iterate. + int idx = 0; + int qlength = 0; + while (idx < kv.value().length) { + qlength = Internal.getQualifierLength(kv.value(), idx); + idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); + ++dps_post_filter; + } } } - } - - Span datapoints = spans.get(key); - if (datapoints == null) { - datapoints = new Span(tsdb); - spans.put(key, datapoints); - } - final long compaction_start = DateTime.nanoTime(); - final KeyValue compacted = - tsdb.compact(row, datapoints.getAnnotations()); - compaction_time += (DateTime.nanoTime() - compaction_start); - seenAnnotation |= !datapoints.getAnnotations().isEmpty(); - if (compacted != null) { // Can be null if we ignored all KVs. - datapoints.addRow(compacted); - ++nrows; + + Span datapoints = spans.get(key); + if (datapoints == null) { + datapoints = new Span(tsdb); + spans.put(key, datapoints); + } + final long compaction_start = DateTime.nanoTime(); + final KeyValue compacted = + tsdb.compact(row, datapoints.getAnnotations()); + compaction_time += (DateTime.nanoTime() - compaction_start); + seenAnnotation |= !datapoints.getAnnotations().isEmpty(); + if (compacted != null) { // Can be null if we ignored all KVs. + datapoints.addRow(compacted); + ++nrows; + } } } @@ -1040,7 +1089,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), - query_index); + query_index, + RollupQuery.isValidQuery(rollup_query)); if (query_stats != null) { query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); } @@ -1090,7 +1140,8 @@ public DataPoints[] call(final TreeMap spans) throws Exception { downsampler, getStartTime(), getEndTime(), - query_index); + query_index, + RollupQuery.isValidQuery(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]; @@ -1205,6 +1256,8 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { metric = UniqueId.stringToUid(metric_uid); } + final boolean is_rollup = RollupQuery.isValidQuery(rollup_query); + // We search at least one row before and one row after the start & end // time we've been given as it's quite likely that the exact timestamp // we're looking for is in the middle of a row. Plus, a number of things @@ -1214,14 +1267,16 @@ 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(), tsdb.table, TSDB.FAMILY()); + : (int) getScanEndTimeSeconds(), + is_rollup ? rollup_query.getRollupInterval().getTemporalTable() : tsdb.table, + TSDB.FAMILY()); if (tsuids != null && !tsuids.isEmpty()) { createAndSetTSUIDFilter(scanner); } else if (filters.size() > 0) { createAndSetFilter(scanner); } - if (RollupQuery.isValidQuery(rollup_query)) { + if (is_rollup) { ScanFilter existing = scanner.getFilter(); // TODO - need some UTs around this! // Set the Scanners column qualifier pattern with rollup aggregator @@ -1250,8 +1305,8 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { if (existing != null) { final List combined = new ArrayList(2); combined.add(existing); - combined.add(new FilterList(combined, Operator.MUST_PASS_ONE)); - scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ALL)); + combined.add(new FilterList(filters, Operator.MUST_PASS_ONE)); + scanner.setFilter(new FilterList(combined, Operator.MUST_PASS_ALL)); } else { scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ONE)); } diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java index 7b920064ff..a563af80de 100644 --- a/src/rollup/RollupSeq.java +++ b/src/rollup/RollupSeq.java @@ -258,7 +258,7 @@ private void append(KeyValue column, boolean is_count) { } } } else { - throw new IllegalArgumentException("The offset for " + column + throw new IllegalDataException("The offset for " + column + " of " + offset + " is <= the last offset " + last_offset + " for " + this); } diff --git a/test/core/TestTsdbQueryRollup.java b/test/core/TestTsdbQueryRollup.java new file mode 100644 index 0000000000..dac1047532 --- /dev/null +++ b/test/core/TestTsdbQueryRollup.java @@ -0,0 +1,970 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +import org.hbase.async.KeyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ RowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, +Config.class, RowKey.class }) +public class TestTsdbQueryRollup extends BaseTsdbTest { + private final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); + private TsdbQuery query = null; + private RollupConfig rollup_config; + private Map tags2; + private TSQuery ts_query; + + @Before + public void beforeLocal() throws Exception { + storeLongTimeSeriesSeconds(false, false); + final List families = new ArrayList(); + families.add(FAMILY); + + storage.addTable("tsdb-rollup-10m".getBytes(), families); + storage.addTable("tsdb-rollup-agg-10m".getBytes(), families); + storage.addTable("tsdb-rollup-1h".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1h".getBytes(), families); + storage.addTable("tsdb-rollup-1d".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1d".getBytes(), families); + + query = new TsdbQuery(tsdb); + tags2 = new HashMap(1); + tags2.put(TAGK_STRING, TAGV_B_STRING); + + final List rollups = new ArrayList(); + rollups.add(new RollupInterval( + "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "6h")); + rollups.add(new RollupInterval( + "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1d")); + rollups.add(new RollupInterval( + "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1m")); + + rollup_config = new RollupConfig(rollups); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + Whitebox.setInternalState(tsdb, "default_interval", new RollupInterval( + "tsdb", "tsdb-agg", "1m", "1h")); + } + + // This test shows us falling back to raw data if the requested downsample + // interval doesn't match a configured rollup + @Test + public void run15mSumLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(start_timestamp, end_timestamp, false, false, + interval, aggr); + + // 15 minutes down sampling that falls back to raw data + final int time_interval = 15 * 60 * 1000; + setQuery("15m", aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + 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; + double value = 435; + for (DataPoint dp : dps[0]) { + assertEquals(ts, dp.timestamp()); + assertEquals(value, dp.doubleValue(), 0.00001); + if (value >= 8535.0) { + value = 300; // last dp all by it's lonesom + } else { + value += 900; + } + ts += time_interval; + } + assertEquals(11, dps[0].size()); + } + + // In this case we're downsampling rolled up values + @Test + public void run30mSumLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041599L; + storeLongRollup(start_timestamp, end_timestamp, false, false, + interval, aggr); + + setQuery("30m", aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + double value = 3600; + long ts = start_timestamp * 1000; + + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0); + assertEquals(ts, dp.timestamp()); + value += 5400; + ts += (interval.getInterval() * 3) * 1000; + } + assertEquals(24, dps[0].size()); + } + + // Zimsum == SUM when comparing qualifiers + @Test + public void run10mZimSumLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + Aggregator aggr = Aggregators.SUM; // still have to write as sum + long start_timestamp = 1356998400L; + long end_timestamp = 1357041599L; + storeLongRollup(start_timestamp, end_timestamp, false, false, + interval, aggr); + + aggr = Aggregators.ZIMSUM; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 600; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += interval.getInterval() * 1000; + i += interval.getInterval(); + } + assertEquals(72, dps[0].size()); + } + + @Test + public void run10mMaxLongSingleTSNotFound() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041599L; + storeLongRollup(start_timestamp, end_timestamp, false, false, + interval, aggr); + + aggr = Aggregators.MAX; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(0, dps.length); + } + + @Test + public void run10mSumLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); + + final int time_interval = interval.getInterval(); + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 600; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += time_interval * 1000; + i += time_interval; + } + assertEquals(73, dps[0].size()); + } + + @Test (expected = IllegalArgumentException.class) + public void run10mSumLongSingleTSInMS() throws Exception { + RollupInterval ten_min_interval = rollup_config.getRollupInterval("10m"); + Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400000L; + + //rollup doesn't accept timestamps in milliseconds + tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, + 0, tags, false, ten_min_interval.getStringInterval(), + aggr.toString()).joinUninterruptibly(); + } + + @Test + public void run10mSumLongSingleTSRate() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + final long start_timestamp = 1356998400; + final long end_timestamp = 1357041600; + + storeLongRollup(start_timestamp, end_timestamp, false, false, + interval, aggr); + + setQuery(interval.getStringInterval(), aggr, tags, aggr); + ts_query.getQueries().get(0).setRate(true); + query.configureFromQuery(ts_query, 0); + final DataPoints[] dps = query.run(); + + assertNotNull(dps); + 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 expected_timestamp = (start_timestamp + interval.getInterval()) * 1000; + for (DataPoint dp : dps[0]) { + assertEquals(1.0F, dp.doubleValue(), 0.00001); + assertEquals(expected_timestamp, dp.timestamp()); + expected_timestamp += interval.getInterval() * 1000; + } + + assertEquals(72, dps[0].size()); + } + + @Test + public void run10mSumFloatSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + final long end_timestamp = 1357041600; + storeFloatRollup(start_timestamp, end_timestamp, true, false, interval, aggr); + + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + final DataPoints[] dps = query.run(); + + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + double value = 600.5F; + long expected_timestamp = start_timestamp * 1000; + + for (DataPoint dp : dps[0]) { + assertEquals(value, dp.doubleValue(), 0.00001); + assertEquals(expected_timestamp, dp.timestamp()); + value += interval.getInterval(); + expected_timestamp += interval.getInterval() * 1000; + } + + assertEquals(73, dps[0].size()); + } + + @Test + public void run10mSumFloatSingleTSRate() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + final long start_timestamp = 1356998400; + final long end_timestamp = 1357041600; + + storeFloatRollup(start_timestamp, end_timestamp, false, false, + interval, aggr); + + setQuery(interval.getStringInterval(), aggr, tags, aggr); + ts_query.getQueries().get(0).setRate(true); + query.configureFromQuery(ts_query, 0); + final DataPoints[] dps = query.run(); + + assertEquals(1, dps.length); + 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 expected_timestamp = (start_timestamp + interval.getInterval()) * 1000; + for (DataPoint dp : dps[0]) { + assertEquals(1.0F, dp.doubleValue(), 0.00001); + assertEquals(expected_timestamp, dp.timestamp()); + expected_timestamp += interval.getInterval() * 1000; + } + assertEquals(72, dps[0].size()); + } + + // Make sure filtering on time series still operates + @Test + public void run10mSumLongDoubleTSFilter() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, true, false, interval, aggr); + + final int time_interval = interval.getInterval(); + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 600; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += time_interval * 1000; + i += time_interval; + } + assertEquals(73, dps[0].size()); + } + + @Test + public void run10mSumLongDoubleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, true, false, interval, aggr); + + final int time_interval = interval.getInterval(); + tags.clear(); + setQuery(interval.getStringInterval(), aggr, tags, aggr); + ts_query.getQueries().get(0).getFilters().clear(); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertEquals(TAGK_STRING, dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertNull(dps[0].getTags().get(TAGK_STRING)); + + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertEquals(43800, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += time_interval * 1000; + } + assertEquals(73, dps[0].size()); + } + + // Make sure other aggregates don't polute our results + @Test + public void run10mSumLongDoubleTSFilterOtherAggs() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, true, false, interval, aggr); + storeLongRollup(1356998400L, end_timestamp, true, false, interval, + Aggregators.MAX); + storeLongRollup(1356998400L, end_timestamp, true, false, interval, + Aggregators.MIN); + + final int time_interval = interval.getInterval(); + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 600; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += time_interval * 1000; + i += time_interval; + } + assertEquals(73, dps[0].size()); + } + + @Test + public void run10mMaxLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.MAX; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); + + final int time_interval = interval.getInterval(); + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 600; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += time_interval * 1000; + i += time_interval; + } + assertEquals(73, dps[0].size()); + } + + @Test + public void run10mMinLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.MIN; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); + + final int time_interval = interval.getInterval(); + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 600; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += time_interval * 1000; + i += time_interval; + } + assertEquals(73, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTS() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(start_timestamp, end_timestamp, false, false, interval, aggr); + storeCount(start_timestamp, end_timestamp, false, false, interval, 2); + + aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + int i = 300; + long ts = start_timestamp * 1000; + + for (final DataPoint dp : dps[0]) { + assertFalse(dp.isInteger()); + assertEquals(i, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + ts += interval.getInterval() * 1000; + i += interval.getInterval() / 2; + } + assertEquals(73, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingCount() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(start_timestamp, end_timestamp, false, false, interval, aggr); + + aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals("", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertNull(dps[0].getTags().get(TAGK_STRING)); + assertFalse(dps[0].iterator().hasNext()); + assertEquals(0, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingSum() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.AVG; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeCount(start_timestamp, end_timestamp, false, false, interval, 1); + + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals("", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertNull(dps[0].getTags().get(TAGK_STRING)); + assertFalse(dps[0].iterator().hasNext()); + assertEquals(0, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingACount() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + storePoint(1356998400, 20, Aggregators.SUM, interval); + storePoint(1356998400, 2, Aggregators.COUNT, interval); + storePoint(1356999000, 40, Aggregators.SUM, interval); + //storePoint(1356999000, 5, Aggregators.COUNT, interval); + storePoint(1356999600, 60, Aggregators.SUM, interval); + storePoint(1356999600, 3, Aggregators.COUNT, interval); + storePoint(1357000200, 80, Aggregators.SUM, interval); + storePoint(1357000200, 4, Aggregators.COUNT, interval); + + Aggregator aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + final SeekableView it = dps[0].iterator(); + DataPoint dp = it.next(); + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(10, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1356999600000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1357000200000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + assertFalse(it.hasNext()); + assertEquals(3, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingASum() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + storePoint(1356998400, 20, Aggregators.SUM, interval); + storePoint(1356998400, 2, Aggregators.COUNT, interval); + //storePoint(1356999000, 40, Aggregators.SUM, interval); + storePoint(1356999000, 5, Aggregators.COUNT, interval); + storePoint(1356999600, 60, Aggregators.SUM, interval); + storePoint(1356999600, 3, Aggregators.COUNT, interval); + storePoint(1357000200, 80, Aggregators.SUM, interval); + storePoint(1357000200, 4, Aggregators.COUNT, interval); + + Aggregator aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + final SeekableView it = dps[0].iterator(); + DataPoint dp = it.next(); + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(10, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1356999600000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1357000200000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + assertFalse(it.hasNext()); + assertEquals(3, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingToZero() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + storePoint(1356998400, 20, Aggregators.SUM, interval); + //storePoint(1356998400, 2, Aggregators.COUNT, interval); + //storePoint(1356999000, 40, Aggregators.SUM, interval); + storePoint(1356999000, 5, Aggregators.COUNT, interval); + storePoint(1356999600, 60, Aggregators.SUM, interval); + //storePoint(1356999600, 3, Aggregators.COUNT, interval); + //storePoint(1357000200, 80, Aggregators.SUM, interval); + storePoint(1357000200, 4, Aggregators.COUNT, interval); + + Aggregator aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals("", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertNull(dps[0].getTags().get(TAGK_STRING)); + + final SeekableView it = dps[0].iterator(); + assertFalse(it.hasNext()); + assertEquals(0, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingToZeroOneSpan() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + // For this test, a span in the middle was zero'd out + storePoint(1356998400, 20, Aggregators.SUM, interval); + storePoint(1356998400, 2, Aggregators.COUNT, interval); + storePoint(1356999000, 40, Aggregators.SUM, interval); + storePoint(1356999000, 5, Aggregators.COUNT, interval); + + storePoint(1357084800, 60, Aggregators.SUM, interval); + //storePoint(1357084800, 3, Aggregators.COUNT, interval); + //storePoint(1357085400, 80, Aggregators.SUM, interval); + storePoint(1357085400, 4, Aggregators.COUNT, interval); + + storePoint(1357171200, 90, Aggregators.SUM, interval); + storePoint(1357171200, 3, Aggregators.COUNT, interval); + storePoint(1357171800, 100, Aggregators.SUM, interval); + storePoint(1357171800, 5, Aggregators.COUNT, interval); + + Aggregator aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + ts_query.setEnd("1359590400"); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + final SeekableView it = dps[0].iterator(); + DataPoint dp = it.next(); + assertEquals(1356998400000L, dp.timestamp()); + assertEquals(10, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1356999000000L, dp.timestamp()); + assertEquals(8, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1357171200000L, dp.timestamp()); + assertEquals(30, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1357171800000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + assertFalse(it.hasNext()); + assertEquals(4, dps[0].size()); + } + + @Test + public void run10mAvgLongSingleTSMissingToZeroBookends() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + // For this test, the spans on either side are zero'd + storePoint(1356998400, 20, Aggregators.SUM, interval); + //storePoint(1356998400, 2, Aggregators.COUNT, interval); + //storePoint(1356999000, 40, Aggregators.SUM, interval); + storePoint(1356999000, 5, Aggregators.COUNT, interval); + + storePoint(1357084800, 60, Aggregators.SUM, interval); + storePoint(1357084800, 3, Aggregators.COUNT, interval); + storePoint(1357085400, 80, Aggregators.SUM, interval); + storePoint(1357085400, 4, Aggregators.COUNT, interval); + + //storePoint(1357171200, 90, Aggregators.SUM, interval); + storePoint(1357171200, 3, Aggregators.COUNT, interval); + storePoint(1357171800, 100, Aggregators.SUM, interval); + //storePoint(1357171800, 5, Aggregators.COUNT, interval); + + Aggregator aggr = Aggregators.AVG; + setQuery(interval.getStringInterval(), aggr, tags, aggr); + ts_query.setEnd("1359590400"); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + final SeekableView it = dps[0].iterator(); + DataPoint dp = it.next(); + assertEquals(1357084800000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + dp = it.next(); + assertEquals(1357085400000L, dp.timestamp()); + assertEquals(20, dp.doubleValue(), 0.0001); + assertFalse(it.hasNext()); + assertEquals(2, dps[0].size()); + } + + @Test + public void runDupes() throws Exception { + storage.flushStorage(); + + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + + tsdb.addAggregatePoint(METRIC_STRING, 1357026600L, Integer.MAX_VALUE, tags, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + tsdb.addAggregatePoint(METRIC_STRING, 1357026600L, 42.5F, tags, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + DataPoints[] dps = null; + try { + dps = query.run(); + } catch (IllegalDataException e) { } + + config.setFixDuplicates(true); + dps = query.run(); + DataPoint dp = dps[0].iterator().next(); + assertEquals(1357026600000L, dp.timestamp()); + assertEquals(42.5F, dp.toDouble(), 0.0001); + } + + // ----------------- // + // Helper functions. // + // ----------------- // + + private void storeLongRollup(final long start_timestamp, + final long end_timestamp, + final boolean two_metrics, + final boolean offset, + final RollupInterval interval, + final Aggregator aggr) throws Exception { + + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + int time_interval = interval.getInterval(); + long start_a = start_timestamp; + long start_b = start_timestamp + (offset ? time_interval : 0); + int i = 0; + + while (start_a <= end_timestamp) { + i += time_interval; + tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + } + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); + } + + // dump a parallel set but invert the values + start_a = start_timestamp; + start_b = start_timestamp + (offset ? time_interval : 0); + + while (start_a <= end_timestamp) { + i -= time_interval; + tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags2, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags2, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + } + + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); + } + } + + private void storeCount(final long start_timestamp, + final long end_timestamp, + final boolean two_metrics, + final boolean offset, + final RollupInterval interval, + final int value) throws Exception { + + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + int time_interval = interval.getInterval(); + long start_a = start_timestamp; + long start_b = start_timestamp + (offset ? time_interval : 0); + + while (start_a <= end_timestamp) { + tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags, false, + interval.getStringInterval(), Aggregators.COUNT.toString()) + .joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags, false, + interval.getStringInterval(), Aggregators.COUNT.toString()) + .joinUninterruptibly(); + } + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); + } + + start_a = start_timestamp; + start_b = start_timestamp + (offset ? time_interval : 0); + + while (start_a <= end_timestamp) { + tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags2, false, + interval.getStringInterval(), Aggregators.COUNT.toString()) + .joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags2, false, + interval.getStringInterval(), Aggregators.COUNT.toString()) + .joinUninterruptibly(); + } + + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); + } +} + + private void storeFloatRollup(final long start_timestamp, + final long end_timestamp, + final boolean two_metrics, + final boolean offset, + final RollupInterval interval, + final Aggregator aggr) throws Exception { + + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + int time_interval = interval.getInterval(); + long start_a = start_timestamp; + long start_b = start_timestamp + (offset ? time_interval : 0); + float i = 0.5F; + + while (start_a <= end_timestamp) { + i += time_interval; + tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b,i, tags, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + } + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); + } + + // dump a parallel set but invert the values + start_a = start_timestamp; + start_b = start_timestamp + (offset ? time_interval : 0); + + while (start_a <= end_timestamp) { + i -= time_interval; + tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags2, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags2, false, + interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + } + + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); + } + } + + private void storePoint(final long ts, final long value, final Aggregator agg, + final RollupInterval interval) throws Exception { + tsdb.addAggregatePoint(METRIC_STRING, ts, value, tags, false, + interval.getStringInterval(), agg.toString()).joinUninterruptibly(); + } + + @SuppressWarnings("deprecation") + private void setQuery(final String ds_interval, final Aggregator ds_agg, + final Map tags, final Aggregator group_by) { + ts_query = new TSQuery(); + ts_query.setStart("1356998400"); + ts_query.setEnd("1357041600"); + + final TSSubQuery sub = new TSSubQuery(); + sub.setMetric(METRIC_STRING); + sub.setDownsample(ds_interval + "-" + ds_agg); + sub.setTags(new HashMap(tags)); + sub.setAggregator(group_by.toString()); + + ts_query.setQueries(Arrays.asList(sub)); + ts_query.validateAndSetQuery(); + } +} diff --git a/test/rollup/TestRollupSeq.java b/test/rollup/TestRollupSeq.java index 5e6918b1f5..5b188110b6 100644 --- a/test/rollup/TestRollupSeq.java +++ b/test/rollup/TestRollupSeq.java @@ -236,7 +236,7 @@ public void addRowMergeLater() throws Exception { } } - @Test (expected = IllegalArgumentException.class) + @Test (expected = IllegalDataException.class) public void addRowMergeEarlier() throws Exception { // this happens if the same row key is used for the addRow call final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; @@ -253,7 +253,7 @@ public void addRowMergeEarlier() throws Exception { rs.addRow(TestRowSeq.makekv( qual3, val3)); } - @Test (expected = IllegalArgumentException.class) + @Test (expected = IllegalDataException.class) public void addRowMergeMiddle() throws Exception { // this happens if the same row key is used for the addRow call final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; @@ -278,7 +278,7 @@ public void addRowMergeMiddle() throws Exception { rs.addRow(TestRowSeq.makekv( qual5, val5)); } - @Test (expected = IllegalArgumentException.class) + @Test (expected = IllegalDataException.class) public void addRowMergeDuplicateLater() throws Exception { // this happens if the same row key is used for the addRow call final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; @@ -340,7 +340,7 @@ public void addRowMergeDuplicateLaterRepair() throws Exception { } } - @Test (expected = IllegalArgumentException.class) + @Test (expected = IllegalDataException.class) public void addRowMergeDuplicateEarlier() throws Exception { // this happens if the same row key is used for the addRow call final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 0a99c24197..b84dfa657d 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -38,16 +38,20 @@ import net.opentsdb.utils.Pair; import org.hbase.async.AtomicIncrementRequest; +import org.hbase.async.BinaryPrefixComparator; import org.hbase.async.Bytes; import org.hbase.async.Bytes.ByteMap; import org.hbase.async.AppendRequest; import org.hbase.async.DeleteRequest; +import org.hbase.async.FilterComparator; import org.hbase.async.FilterList; import org.hbase.async.GetRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyRegexpFilter; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; +import org.hbase.async.QualifierFilter; +import org.hbase.async.RegexStringComparator; import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; import org.junit.Ignore; @@ -55,6 +59,7 @@ import org.mockito.stubbing.Answer; import org.powermock.reflect.Whitebox; +import com.google.common.collect.Lists; import com.stumbleupon.async.Deferred; /** @@ -1356,7 +1361,6 @@ public class MockScanner implements cursors; private ByteMap>>> cf_rows; private byte[] last_row; - private String rex; // TEMP /** * Default ctor @@ -1373,7 +1377,6 @@ public MockScanner(final Scanner mock_scanner, final byte[] table) { public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); filter = new KeyRegexpFilter((String)args[0], Const.ASCII_CHARSET); - rex = (String)args[0]; return null; } }).when(mock_scanner).setKeyRegexp(anyString()); @@ -1383,20 +1386,10 @@ public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); filter = new KeyRegexpFilter((String)args[0], (Charset)args[1]); - rex = (String)args[0]; return null; } }).when(mock_scanner).setKeyRegexp(anyString(), (Charset)any()); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - final Object[] args = invocation.getArguments(); - filter = (ScanFilter)args[0]; - return null; - } - }).when(mock_scanner).setFilter(any(ScanFilter.class)); - doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1447,6 +1440,15 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }).when(mock_scanner).setQualifiers((byte[][])any()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + final Object[] args = invocation.getArguments(); + filter = (ScanFilter)args[0]; + return null; + } + }).when(mock_scanner).setFilter(any(ScanFilter.class)); + doAnswer(new Answer() { @Override public byte[] answer(InvocationOnMock invocation) throws Throwable { @@ -1456,12 +1458,40 @@ public byte[] answer(InvocationOnMock invocation) throws Throwable { when(mock_scanner.nextRows()).thenAnswer(this); + doAnswer(new Answer() { + @Override + public ScanFilter answer(InvocationOnMock invocation) throws Throwable { + return filter; + } + }).when(mock_scanner).getFilter(); + + doAnswer(new Answer() { + @Override + public String answer(final InvocationOnMock ignored) throws Throwable { + return MockScanner.this.toString(); + } + }).when(mock_scanner).toString(); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("table=") + .append(Bytes.pretty(table)) + .append(", start=") + .append(Bytes.pretty(start)) + .append(", stop=") + .append(Bytes.pretty(stop)) + .append(", family=") + .append(Bytes.pretty(family)) + .append(", filter=") + .append(filter); + return buf.toString(); } @Override public Deferred>> answer( final InvocationOnMock invocation) throws Throwable { - if (cursors == null) { final ByteMap>>> map = storage.get(table); @@ -1501,21 +1531,15 @@ public Deferred>> answer( } // TODO - fuzzy filter support - // TODO - fix the regex comparator Pattern pattern = null; - if (rex != null) { - if (!rex.isEmpty()) { - pattern = Pattern.compile(rex); - } - } else if (filter != null) { + Charset regex_charset = null; + if (filter != null) { KeyRegexpFilter regex_filter = null; if (filter instanceof KeyRegexpFilter) { regex_filter = (KeyRegexpFilter)filter; } else if (filter instanceof FilterList) { - final List filters = - Whitebox.getInternalState(filter, "filters"); - for (final ScanFilter f : filters) { + for (final ScanFilter f : ((FilterList)filter).filters()) { if (f instanceof KeyRegexpFilter) { regex_filter = (KeyRegexpFilter)f; } @@ -1524,13 +1548,10 @@ public Deferred>> answer( if (regex_filter != null) { try { - final String regexp = new String( - (byte[])Whitebox.getInternalState(regex_filter, "regexp"), - Charset.forName(new String( - (byte[])Whitebox.getInternalState(regex_filter, "charset")))); - if (!regexp.isEmpty()) { - pattern = Pattern.compile(regexp); - } + // key regex filter uses Bytes.UTF8() + pattern = Pattern.compile(new String(regex_filter.getRegexp(), + Charset.forName("UTF-8"))); + regex_charset = regex_filter.getCharset(); } catch (PatternSyntaxException e) { e.printStackTrace(); return Deferred.fromError(e); @@ -1561,7 +1582,7 @@ public Deferred>> answer( continue; } if (pattern != null) { - final String from_bytes = new String(last_row, MockBase.ASCII); + final String from_bytes = new String(last_row, regex_charset); if (!pattern.matcher(from_bytes).find()) { continue; } @@ -1596,6 +1617,55 @@ public Deferred>> answer( continue; } + // handle qualifier filters. Just regexp for now + if (filter != null) { + List qfs = Lists.newArrayList(); + if (filter instanceof FilterList) { + for (final ScanFilter f : ((FilterList) filter).filters()) { + if (f instanceof QualifierFilter) { + qfs.add((QualifierFilter) f); + } else if (f instanceof FilterList) { // nested + for (final ScanFilter nf : ((FilterList) f).filters()) { + if (nf instanceof QualifierFilter) { + qfs.add((QualifierFilter) nf); + } + } + } + } + } else if (filter instanceof QualifierFilter) { + qfs.add((QualifierFilter) filter); + } + + if (!qfs.isEmpty()) { + boolean matched = false; + for (final QualifierFilter qf : qfs) { + final FilterComparator fc = Whitebox + .getInternalState(qf, "comparator"); + if (fc instanceof BinaryPrefixComparator) { + final byte[] comparator = Whitebox + .getInternalState(fc, "value"); + if (Bytes.memcmp(comparator, column.getKey(), 0, + comparator.length) == 0) { + matched = true; + } + } else if (fc instanceof RegexStringComparator) { + // not using this yet but.... *shrug* + final Pattern p = Pattern.compile((String) Whitebox + .getInternalState(fc, "expr")); + + final String qualifier = new String(column.getKey(), + (Charset) Whitebox.getInternalState(fc, "charset")); + if (p.matcher(qualifier).matches()) { + matched = true; + } + } + } + if (!matched) { + continue; + } + } + } + kvs.add(new KeyValue(row.getValue().getKey(), row.getKey(), column.getKey(), column.getValue().firstKey(), column.getValue().firstEntry().getValue())); @@ -1683,7 +1753,7 @@ private void advance() { } } } - + /** @return The scanner for this mock */ public Scanner getScanner() { return mock_scanner; diff --git a/third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 b/third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 new file mode 100644 index 0000000000..d9d570137b --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 @@ -0,0 +1 @@ +55bd2be1d89b940210bbedfd6e55d856 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.8.0-20161103.193100-4.jar.md5 b/third_party/hbase/asynchbase-1.8.0-20161103.193100-4.jar.md5 new file mode 100644 index 0000000000..97ce7b7a6d --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.0-20161103.193100-4.jar.md5 @@ -0,0 +1 @@ +f4cd05231ee2604e59f65667e4cc7634 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index 7cb693bb8a..1987139b08 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2014 The OpenTSDB Authors. +# Copyright (C) 2011-2016 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.2 +ASYNCHBASE_VERSION := 1.8.0-20161103.193100-4 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://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.8.0-SNAPSHOT/$(ASYNCHBASE_VERSION) $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From 6102aa87a43b95f361c73ba87ba15878edbb5e58 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 6 Nov 2016 12:21:30 -0800 Subject: [PATCH 575/826] Fix #887 wherein an empty groupby tag set with a non-empty non- groupby tag set was converting the non-groupbys to groupbys from the UI. Thanks @dcaillia. Also bump GWT to 2.6.1. TODO - Look at later GWT versions and allow for super dev mode as Chrome no longer supports the old dev mode. Signed-off-by: Chris Larsen --- third_party/gwt/gwt-dev-2.6.1.jar.md5 | 1 + third_party/gwt/gwt-user-2.6.1.jar.md5 | 1 + 2 files changed, 2 insertions(+) create mode 100644 third_party/gwt/gwt-dev-2.6.1.jar.md5 create mode 100644 third_party/gwt/gwt-user-2.6.1.jar.md5 diff --git a/third_party/gwt/gwt-dev-2.6.1.jar.md5 b/third_party/gwt/gwt-dev-2.6.1.jar.md5 new file mode 100644 index 0000000000..b5847e6a60 --- /dev/null +++ b/third_party/gwt/gwt-dev-2.6.1.jar.md5 @@ -0,0 +1 @@ +2f8df1f3b021315775506a1e0a4ee4b8 diff --git a/third_party/gwt/gwt-user-2.6.1.jar.md5 b/third_party/gwt/gwt-user-2.6.1.jar.md5 new file mode 100644 index 0000000000..822626d3e7 --- /dev/null +++ b/third_party/gwt/gwt-user-2.6.1.jar.md5 @@ -0,0 +1 @@ +ce17f82bb92e3a7416a9be5659cbcc89 From 4718155cc025da377be3eed3eae714eda5ce1dff Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 6 Nov 2016 12:21:30 -0800 Subject: [PATCH 576/826] Fix #887 wherein an empty groupby tag set with a non-empty non- groupby tag set was converting the non-groupbys to groupbys from the UI. Thanks @dcaillia. Also bump GWT to 2.6.1. TODO - Look at later GWT versions and allow for super dev mode as Chrome no longer supports the old dev mode. Signed-off-by: Chris Larsen --- third_party/gwt/gwt-dev-2.6.1.jar.md5 | 1 + third_party/gwt/gwt-user-2.6.1.jar.md5 | 1 + 2 files changed, 2 insertions(+) create mode 100644 third_party/gwt/gwt-dev-2.6.1.jar.md5 create mode 100644 third_party/gwt/gwt-user-2.6.1.jar.md5 diff --git a/third_party/gwt/gwt-dev-2.6.1.jar.md5 b/third_party/gwt/gwt-dev-2.6.1.jar.md5 new file mode 100644 index 0000000000..b5847e6a60 --- /dev/null +++ b/third_party/gwt/gwt-dev-2.6.1.jar.md5 @@ -0,0 +1 @@ +2f8df1f3b021315775506a1e0a4ee4b8 diff --git a/third_party/gwt/gwt-user-2.6.1.jar.md5 b/third_party/gwt/gwt-user-2.6.1.jar.md5 new file mode 100644 index 0000000000..822626d3e7 --- /dev/null +++ b/third_party/gwt/gwt-user-2.6.1.jar.md5 @@ -0,0 +1 @@ +ce17f82bb92e3a7416a9be5659cbcc89 From 80f4ce37d06468883fd6b5011cf94be76fc0e60a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 7 Nov 2016 17:17:46 -0800 Subject: [PATCH 577/826] Modify the Rollup APIs to accept pre-aggregated data using tags to determine whether or not it's "RAW" or an aggregate. Modify the unit tests for storing data points so we can run them salted or not easily. Signed-off-by: Chris Larsen --- src/core/IncomingDataPoints.java | 6 +- src/core/TSDB.java | 154 +++++- src/rollup/RollUpDataPoint.java | 67 ++- src/tsd/PutDataPointRpc.java | 29 +- src/tsd/RollupDataPointRpc.java | 11 +- src/utils/Config.java | 5 + test/core/BaseTsdbTest.java | 130 ++++- test/core/TestTSDB.java | 3 - test/core/TestTSDBAddAggregatePoint.java | 519 ++++++++++-------- .../core/TestTSDBAddAggregatePointSalted.java | 81 +++ test/core/TestTSDBAddPoint.java | 190 ++----- test/core/TestTSDBAddPointSalted.java | 38 ++ test/core/TestTsdbQueryRollup.java | 32 +- test/tsd/TestRollupRpc.java | 224 +++++++- 14 files changed, 983 insertions(+), 506 deletions(-) create mode 100644 test/core/TestTSDBAddAggregatePointSalted.java create mode 100644 test/core/TestTSDBAddPointSalted.java diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 4789662a7e..95aba94f2e 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -113,8 +113,10 @@ static void checkMetricAndTags(final String metric, Tags.validateString("metric name", metric); for (final Map.Entry tag : tags.entrySet()) { - Tags.validateString("tag name", tag.getKey()); - Tags.validateString("tag value", tag.getValue()); + Tags.validateString("tag name with value [" + tag.getValue() + "]", + tag.getKey()); + Tags.validateString("tag value with key [" + tag.getKey() + "]", + tag.getValue()); } } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 6a79b2884e..82622479ba 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; @@ -115,9 +116,6 @@ public final class TSDB { /** Timer used for various tasks such as idle timeouts or query timeouts */ private final HashedWheelTimer timer; - /** Name of the tag we use to determine aggregates */ - private final String agg_tag; - /** * Row keys that need to be compacted. * Whenever we write a new data point to a row, we add the row key to this @@ -153,6 +151,18 @@ public final class TSDB { /** The default rollup interval. */ private final RollupInterval default_interval; + /** Name of the tag we use to determine aggregates */ + private final String agg_tag_key; + + /** Name of the tag we use use for raw data. */ + private final String raw_agg_tag_value; + + /** Whether or not to tag raw data with the raw value tag */ + private final boolean tag_raw_data; + + /** Whether or not to block writing of derived rollups/pre-ags */ + private final boolean rollups_block_derived; + /** Writes rejected by the filter */ private final AtomicLong rejected_dps = new AtomicLong(); private final AtomicLong rejected_aggregate_dps = new AtomicLong(); @@ -215,7 +225,6 @@ public TSDB(final HBaseClient client, final Config config) { uidtable = config.getString("tsd.storage.hbase.uid_table").getBytes(CHARSET); treetable = config.getString("tsd.storage.hbase.tree_table").getBytes(CHARSET); meta_table = config.getString("tsd.storage.hbase.meta_table").getBytes(CHARSET); - agg_tag = config.getString("tsd.core.agg_tag"); if (config.getBoolean("tsd.core.uid.random_metrics")) { metrics = new UniqueId(this, uidtable, METRICS_QUAL, METRICS_WIDTH, true); @@ -248,9 +257,17 @@ public TSDB(final HBaseClient client, final Config config) { + "marked as the \"default\"."); } default_interval = config_default; + tag_raw_data = config.getBoolean("tsd.rollups.tag_raw"); + 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"); } else { rollup_config = null; default_interval = null; + tag_raw_data = false; + agg_tag_key = null; + raw_agg_tag_value = null; + rollups_block_derived = false; } QueryStats.setEnableDuplicates( @@ -870,6 +887,10 @@ public WritableDataPoints newBatch(String metric, Map tags) { /** * Adds a single integer value data point in the TSDB. + *

    + * WARNING: The tags map may be modified by this method without a lock. Give + * the method a copy if you plan to use it elsewhere. + *

    * @param metric A non-empty string. * @param timestamp The timestamp associated with the value. * @param value The value of the data point. @@ -909,6 +930,10 @@ public Deferred addPoint(final String metric, /** * Adds a double precision floating-point value data point in the TSDB. + *

    + * WARNING: The tags map may be modified by this method without a lock. Give + * the method a copy if you plan to use it elsewhere. + *

    * @param metric A non-empty string. * @param timestamp The timestamp associated with the value. * @param value The value of the data point. @@ -946,6 +971,10 @@ public Deferred addPoint(final String metric, /** * Adds a single floating-point value data point in the TSDB. + *

    + * WARNING: The tags map may be modified by this method without a lock. Give + * the method a copy if you plan to use it elsewhere. + *

    * @param metric A non-empty string. * @param timestamp The timestamp associated with the value. * @param value The value of the data point. @@ -1085,10 +1114,14 @@ public String toString() { /** * Adds a rolled up and/or groupby/pre-agged data point to the proper table. + *

    + * WARNING: The tags map may be modified by this method without a lock. Give + * the method a copy if you plan to use it elsewhere. + *

    * If {@code interval} is null then the value will be directed to the * pre-agg table. * If the {@code is_groupby} flag is set, then the aggregate tag, defined in - * "tsd.core.agg_tag", will be added or overwritten with the {@code aggregator} + * "tsd.rollups.agg_tag", will be added or overwritten with the {@code aggregator} * value in uppercase as the value. * @param metric A non-empty string. * @param timestamp The timestamp associated with the value. @@ -1096,7 +1129,8 @@ public String toString() { * @param tags The tags on this series. This map must be non-empty. * @param is_groupby Whether or not the value is a pre-aggregate * @param interval The interval the data reflects (may be null) - * @param aggregator The aggregator used to generate the data + * @param rollup_aggregator The aggregator used to generate the data + * @param groupby_aggregator = The aggregator used for pre-aggregated data. * @return A deferred to optionally wait on to be sure the value was stored * @throws IllegalArgumentException if the timestamp is less than or equal * to the previous timestamp added or 0 for the first timestamp, or if the @@ -1115,21 +1149,27 @@ public Deferred addAggregatePoint(final String metric, final Map tags, final boolean is_groupby, final String interval, - final String aggregator) { + final String rollup_aggregator, + final String groupby_aggregator) { final byte[] val = Internal.vleEncodeLong(value); final short flags = (short) (val.length - 1); // Just the length. return addAggregatePointInternal(metric, timestamp, - val, tags, flags, is_groupby, interval, aggregator); + val, tags, flags, is_groupby, interval, rollup_aggregator, + groupby_aggregator); } /** * Adds a rolled up and/or groupby/pre-agged data point to the proper table. + *

    + * WARNING: The tags map may be modified by this method without a lock. Give + * the method a copy if you plan to use it elsewhere. + *

    * If {@code interval} is null then the value will be directed to the * pre-agg table. * If the {@code is_groupby} flag is set, then the aggregate tag, defined in - * "tsd.core.agg_tag", will be added or overwritten with the {@code aggregator} + * "tsd.rollups.agg_tag", will be added or overwritten with the {@code aggregator} * value in uppercase as the value. * @param metric A non-empty string. * @param timestamp The timestamp associated with the value. @@ -1137,7 +1177,8 @@ public Deferred addAggregatePoint(final String metric, * @param tags The tags on this series. This map must be non-empty. * @param is_groupby Whether or not the value is a pre-aggregate * @param interval The interval the data reflects (may be null) - * @param aggregator The aggregator used to generate the data + * @param rollup_aggregator The aggregator used to generate the data + * @param groupby_aggregator = The aggregator used for pre-aggregated data. * @return A deferred to optionally wait on to be sure the value was stored * @throws IllegalArgumentException if the timestamp is less than or equal * to the previous timestamp added or 0 for the first timestamp, or if the @@ -1156,7 +1197,8 @@ public Deferred addAggregatePoint(final String metric, final Map tags, final boolean is_groupby, final String interval, - final String aggregator) { + final String rollup_aggregator, + final String groupby_aggregator) { if (Float.isNaN(value) || Float.isInfinite(value)) { throw new IllegalArgumentException("value is NaN or Infinite: " + value + " for metric=" + metric @@ -1168,7 +1210,8 @@ public Deferred addAggregatePoint(final String metric, final byte[] val = Bytes.fromInt(Float.floatToRawIntBits(value)); return addAggregatePointInternal(metric, timestamp, - val, tags, flags, is_groupby, interval, aggregator); + val, tags, flags, is_groupby, interval, rollup_aggregator, + groupby_aggregator); } /** @@ -1176,7 +1219,7 @@ public Deferred addAggregatePoint(final String metric, * If {@code interval} is null then the value will be directed to the * pre-agg table. * If the {@code is_groupby} flag is set, then the aggregate tag, defined in - * "tsd.core.agg_tag", will be added or overwritten with the {@code aggregator} + * "tsd.rollups.agg_tag", will be added or overwritten with the {@code aggregator} * value in uppercase as the value. * @param metric A non-empty string. * @param timestamp The timestamp associated with the value. @@ -1184,7 +1227,8 @@ public Deferred addAggregatePoint(final String metric, * @param tags The tags on this series. This map must be non-empty. * @param is_groupby Whether or not the value is a pre-aggregate * @param interval The interval the data reflects (may be null) - * @param aggregator The aggregator used to generate the data + * @param rollup_aggregator The aggregator used to generate the data + * @param groupby_aggregator = The aggregator used for pre-aggregated data. * @return A deferred to optionally wait on to be sure the value was stored * @throws IllegalArgumentException if the timestamp is less than or equal * to the previous timestamp added or 0 for the first timestamp, or if the @@ -1203,7 +1247,8 @@ public Deferred addAggregatePoint(final String metric, final Map tags, final boolean is_groupby, final String interval, - final String aggregator) { + final String rollup_aggregator, + final String groupby_aggregator) { if (Double.isNaN(value) || Double.isInfinite(value)) { throw new IllegalArgumentException("value is NaN or Infinite: " + value + " for metric=" + metric @@ -1215,7 +1260,8 @@ public Deferred addAggregatePoint(final String metric, final byte[] val = Bytes.fromLong(Double.doubleToRawLongBits(value)); return addAggregatePointInternal(metric, timestamp, - val, tags, flags, is_groupby, interval, aggregator); + val, tags, flags, is_groupby, interval, rollup_aggregator, + groupby_aggregator); } Deferred addAggregatePointInternal(final String metric, @@ -1225,12 +1271,19 @@ Deferred addAggregatePointInternal(final String metric, final short flags, final boolean is_groupby, final String interval, - final String aggregator) { + final String rollup_aggregator, + final String groupby_aggregator) { - if (rollup_config == null) { + if (interval != null && !interval.isEmpty() && rollup_config == null) { throw new IllegalArgumentException( "No rollup or aggregations were configured"); } + if (is_groupby && + (groupby_aggregator == null || groupby_aggregator.isEmpty())) { + throw new IllegalArgumentException("Cannot write a group by data point " + + "without specifying the aggregation function. Metric=" + metric + + " tags=" + tags); + } // we only accept positive unix epoch timestamps in seconds for rollups // and allow milliseconds for pre-aggregates @@ -1241,25 +1294,72 @@ Deferred addAggregatePointInternal(final String metric, + " to metric=" + metric + ", tags=" + tags); } - // enforce the aggregate tag and bump to upper case + String agg_tag_value = tags.get(agg_tag_key); + if (agg_tag_value == null) { + if (!is_groupby) { + // it's a rollup on "raw" data. + if (tag_raw_data) { + tags.put(agg_tag_key, raw_agg_tag_value); + } + agg_tag_value = raw_agg_tag_value; + } else { + // pre-agged so use the aggregator as the tag + agg_tag_value = groupby_aggregator.toUpperCase(); + tags.put(agg_tag_key, agg_tag_value); + } + } else { + // sanity check + if (!agg_tag_value.equalsIgnoreCase(groupby_aggregator)) { + throw new IllegalArgumentException("Given tag value for " + agg_tag_key + + " of " + agg_tag_value + " did not match the group by " + + "aggregator of " + groupby_aggregator + " for " + metric + + " " + tags); + } + // force upper case + agg_tag_value = groupby_aggregator.toUpperCase(); + tags.put(agg_tag_key, agg_tag_value); + } + if (is_groupby) { - tags.put(agg_tag, aggregator.toUpperCase()); + try { + Aggregators.get(groupby_aggregator.toLowerCase()); + } catch (NoSuchElementException e) { + throw new IllegalArgumentException("Invalid group by aggregator " + + groupby_aggregator + " with metric " + metric + " " + tags); + } + if (rollups_block_derived && + // TODO - create a better list of aggs to block + (agg_tag_value.equals("AVG") || + agg_tag_value.equals("DEV"))) { + throw new IllegalArgumentException("Derived group by aggregations " + + "are not allowed " + groupby_aggregator + " with metric " + + metric + " " + tags); + } } IncomingDataPoints.checkMetricAndTags(metric, tags); - final RollupInterval rollup_interval = (interval == null ? null : - rollup_config.getRollupInterval(interval)); + final RollupInterval rollup_interval = interval == null || interval.isEmpty() + ? null : rollup_config.getRollupInterval(interval); final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); - - final int base_time = interval == null ? + final String rollup_agg = rollup_aggregator != null ? + rollup_aggregator.toUpperCase() : null; + if (rollup_agg!= null && rollups_block_derived && + // TODO - create a better list of aggs to block + (rollup_agg.equals("AVG") || + rollup_agg.equals("DEV"))) { + throw new IllegalArgumentException("Derived rollup aggregations " + + "are not allowed " + rollup_agg + " with metric " + + metric + " " + tags); + } + final int base_time = interval == null || interval.isEmpty() ? (int)(timestamp - (timestamp % Const.MAX_TIMESPAN)) : RollupUtils.getRollupBasetime(timestamp, rollup_interval); - final byte[] qualifier = interval == null ? + final byte[] qualifier = interval == null || interval.isEmpty() ? Internal.buildQualifier(timestamp, flags) : RollupUtils.buildRollupQualifier( - timestamp, base_time, flags, aggregator, rollup_interval); + timestamp, base_time, flags, rollup_agg, rollup_interval); /** Callback executed for chaining filter calls to see if the value * should be written or not. */ @@ -1277,7 +1377,7 @@ public Deferred call(final Boolean allowed) throws Exception { Deferred result; final PutRequest point; - if (interval == null) { + if (interval == null || interval.isEmpty()) { if (!is_groupby) { throw new IllegalArgumentException("Interval cannot be null " + "for a non-group by point"); diff --git a/src/rollup/RollUpDataPoint.java b/src/rollup/RollUpDataPoint.java index cb3669d24b..3ad91e708a 100644 --- a/src/rollup/RollUpDataPoint.java +++ b/src/rollup/RollUpDataPoint.java @@ -17,9 +17,6 @@ import net.opentsdb.core.Aggregators; import net.opentsdb.core.IncomingDataPoint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.List; import java.util.Map; @@ -30,14 +27,16 @@ */ @JsonInclude(JsonInclude.Include.NON_NULL) public class RollUpDataPoint extends IncomingDataPoint { - private static final Logger LOG = LoggerFactory.getLogger(RollUpDataPoint.class); /** The interval in the format <#> such as 1m or 2h */ private String interval; /** The name of the aggregator that created this data point */ private String aggregator; - + + /** Optional aggregation function if this was a pre-aggregated data point. */ + protected String groupby_aggregator; + /** * Default Ctor necessary for de/serialization */ @@ -55,7 +54,9 @@ public String toString() { buf.append(entry.getKey()).append("=").append(entry.getValue()); } } - buf.append(" interval=").append(interval) + buf.append(" groupByAggregator=") + .append(groupby_aggregator) + .append(" interval=").append(interval) .append(" aggregator=").append(aggregator); return buf.toString(); } @@ -80,33 +81,51 @@ public void setAggregator(final String aggregator) { this.aggregator = aggregator; } + /** @return If pre-aggregated, the function used. May be null. */ + public final String getGroupByAggregator() { + return groupby_aggregator; + } + + /** @param an optional aggregation function if the data point was + * pre-aggregated */ + public final void setGroupByAggregator(final String groupby_aggregator) { + this.groupby_aggregator = groupby_aggregator; + } + @Override public boolean validate(final List> details) { if (!super.validate(details)) return false; - - if (this.getInterval() == null || this.getInterval().isEmpty()) { - if (details != null) { - details.add(getHttpDetails("Missing interval")); - } - LOG.warn("Missing interval: " + this); - return false; + + boolean is_groupby = false; + if (groupby_aggregator != null && !groupby_aggregator.isEmpty()) { + // Don't need to perform this check here as the addAggregatePoint() + // will handle that validation for us. + //Aggregators.get(groupby_aggregator.toLowerCase()); + is_groupby = true; } - - if (this.getAggregator() == null || this.getAggregator().isEmpty()) { - if (details != null) { - details.add(getHttpDetails("Missing aggregator")); + + // interval is only required if the the group by is NOT set + if (interval == null || interval.isEmpty()) { + if (!is_groupby) { + if (details != null) { + details.add(getHttpDetails("Missing interval")); + } + return false; } - LOG.warn("Missing aggregator: " + this); - return false; } - if (!Aggregators.set().contains(this.getAggregator().toLowerCase())) { - if (details != null) { - details.add(getHttpDetails("Invalid aggregator")); + if (aggregator == null || aggregator.isEmpty()) { + if (!is_groupby) { + // only error out if the groupby is false. + if (details != null) { + details.add(getHttpDetails("Missing aggregator")); + } + return false; } - LOG.warn("Invalid aggregator: " + this); - return false; + // Don't need to perform this check here as the addAggregatePoint() + // will handle that validation for us. + //Aggregators.get(aggregator); } return true; diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 11cfa30260..38d5657ed3 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -357,11 +357,16 @@ public Boolean call(final Object obj) { if (Tags.looksLikeInteger(dp.getValue())) { if (dp instanceof RollUpDataPoint) { final RollUpDataPoint rdp = (RollUpDataPoint)dp; - deferred = tsdb.addAggregatePoint(rdp.getMetric(), rdp.getTimestamp(), - Tags.parseLong(rdp.getValue()), dp.getTags(), false, - rdp.getInterval(), rdp.getAggregator()) - .addCallback(new SuccessCB()) - .addErrback(new PutErrback()); + deferred = tsdb.addAggregatePoint(rdp.getMetric(), + rdp.getTimestamp(), + Tags.parseLong(rdp.getValue()), + dp.getTags(), + rdp.getGroupByAggregator() != null, + rdp.getInterval(), + rdp.getAggregator(), + rdp.getGroupByAggregator()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); } else { deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), Tags.parseLong(dp.getValue()), dp.getTags()) @@ -371,14 +376,18 @@ public Boolean call(final Object obj) { } else { if (dp instanceof RollUpDataPoint) { final RollUpDataPoint rdp = (RollUpDataPoint)dp; - deferred = tsdb.addAggregatePoint(rdp.getMetric(), rdp.getTimestamp(), + deferred = tsdb.addAggregatePoint(rdp.getMetric(), + rdp.getTimestamp(), (Tags.fitsInFloat(dp.getValue()) ? Float.parseFloat(dp.getValue()) : Double.parseDouble(dp.getValue())), - dp.getTags(), false, - rdp.getInterval(), rdp.getAggregator()) - .addCallback(new SuccessCB()) - .addErrback(new PutErrback()); + dp.getTags(), + rdp.getGroupByAggregator() != null, + rdp.getInterval(), + rdp.getAggregator(), + rdp.getGroupByAggregator()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); } else { deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), (Tags.fitsInFloat(dp.getValue()) ? diff --git a/src/tsd/RollupDataPointRpc.java b/src/tsd/RollupDataPointRpc.java index 4a6c69a58a..6752775516 100644 --- a/src/tsd/RollupDataPointRpc.java +++ b/src/tsd/RollupDataPointRpc.java @@ -96,7 +96,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) @Override protected Deferred importDataPoint(final TSDB tsdb, final String[] words) { - words[TelnetIndex.COMMAND.ordinal()] = null; // Ditch the "rollup string". + words[TelnetIndex.COMMAND.ordinal()] = null; // Ditch the "rollup" string. if (words.length < TelnetIndex.TAGS.ordinal() + 1) { throw new IllegalArgumentException("not enough arguments" + " (need least 7, got " + (words.length - 1) + ')'); @@ -159,13 +159,16 @@ protected Deferred importDataPoint(final TSDB tsdb, if (Tags.looksLikeInteger(value)) { return tsdb.addAggregatePoint(metric, timestamp, Tags.parseLong(value), - tags, spatial_agg != null ? true : false, interval, temporal_agg); + tags, spatial_agg != null ? true : false, interval, temporal_agg, + spatial_agg); } else if (Tags.fitsInFloat(value)) { // floating point value return tsdb.addAggregatePoint(metric, timestamp, Float.parseFloat(value), - tags, spatial_agg != null ? true : false, interval, temporal_agg); + tags, spatial_agg != null ? true : false, interval, temporal_agg, + spatial_agg); } else { return tsdb.addAggregatePoint(metric, timestamp, Double.parseDouble(value), - tags, spatial_agg != null ? true : false, interval, temporal_agg); + tags, spatial_agg != null ? true : false, interval, temporal_agg, + spatial_agg); } } diff --git a/src/utils/Config.java b/src/utils/Config.java index e8ff25b5d3..cb30cda8fd 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -516,7 +516,12 @@ protected void setDefaults() { default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.query.enable_fuzzy_filter", "true"); default_map.put("tsd.rpc.telnet.return_errors", "true"); + // Rollup related settings default_map.put("tsd.rollups.enable", "false"); + default_map.put("tsd.rollups.tag_raw", "false"); + 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.rtpublisher.enable", "false"); default_map.put("tsd.rtpublisher.plugin", ""); default_map.put("tsd.search.enable", "false"); diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index ede3c260a2..912639a919 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2015 The OpenTSDB Authors. +// Copyright (C) 2015-2016 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 @@ -37,6 +37,7 @@ import net.opentsdb.utils.Config; import net.opentsdb.utils.Threads; +import org.hbase.async.Bytes; import org.hbase.async.HBaseClient; import org.hbase.async.Scanner; import org.jboss.netty.util.HashedWheelTimer; @@ -52,6 +53,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; +import com.google.common.collect.Maps; import com.stumbleupon.async.Deferred; /** @@ -109,9 +111,11 @@ public class BaseTsdbTest { protected UniqueId tag_values = mock(UniqueId.class); protected Map tags; protected MockBase storage; + protected Map uid_map; @Before public void before() throws Exception { + uid_map = Maps.newHashMap(); PowerMockito.mockStatic(Threads.class); timer = new FakeTaskTimer(); PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); @@ -136,6 +140,21 @@ public void before() throws Exception { setupTagkMaps(); setupTagvMaps(); + // add metrics and tags to the UIDs list for other functions to share + uid_map.put(METRIC_STRING, METRIC_BYTES); + uid_map.put(METRIC_B_STRING, METRIC_B_BYTES); + uid_map.put(NSUN_METRIC, NSUI_METRIC); + + uid_map.put(TAGK_STRING, TAGK_BYTES); + uid_map.put(TAGK_B_STRING, TAGK_B_BYTES); + uid_map.put(NSUN_TAGK, NSUI_TAGK); + + uid_map.put(TAGV_STRING, TAGV_BYTES); + uid_map.put(TAGV_B_STRING, TAGV_B_BYTES); + uid_map.put(NSUN_TAGV, NSUI_TAGV); + + uid_map.putAll(UIDS); + when(metrics.width()).thenReturn((short)3); when(tag_names.width()).thenReturn((short)3); when(tag_values.width()).thenReturn((short)3); @@ -209,6 +228,32 @@ void setupTagvMaps() { } } + /** + * Helper method that sets up UIDs for rollup and pre-agg testing. + */ + protected void setupGroupByTagValues() { + // set the aggregate tag and value + mockUID(UniqueIdType.TAGK, config.getString("tsd.rollups.agg_tag_key"), + new byte[] { 0, 0, 42 }); + uid_map.put(config.getString("tsd.rollups.agg_tag_key"), new byte[] { 0, 0, 42 }); + mockUID(UniqueIdType.TAGV, config.getString("tsd.rollups.raw_agg_tag_value"), + new byte[] { 0, 0, 42 }); + uid_map.put(config.getString("tsd.rollups.raw_agg_tag_value"), + new byte[] { 0, 0, 42 }); + mockUID(UniqueIdType.TAGV, "SUM", new byte[] { 0, 0, 43 }); + uid_map.put("SUM", new byte[] { 0, 0, 43 }); + mockUID(UniqueIdType.TAGV, "MAX", new byte[] { 0, 0, 44 }); + uid_map.put("MAX", new byte[] { 0, 0, 44 }); + mockUID(UniqueIdType.TAGV, "MIN", new byte[] { 0, 0, 45 }); + uid_map.put("MIN", new byte[] { 0, 0, 45 }); + mockUID(UniqueIdType.TAGV, "COUNT", new byte[] { 0, 0, 46 }); + uid_map.put("COUNT", new byte[] { 0, 0, 46 }); + mockUID(UniqueIdType.TAGV, "AVG", new byte[] { 0, 0, 47 }); + uid_map.put("AVG", new byte[] { 0, 0, 47 }); + mockUID(UniqueIdType.TAGV, "NOSUCHAGG", new byte[] { 0, 0, 0, 48 }); + uid_map.put("NOSUCHAGG", new byte[] { 0, 0, 48 }); + } + // ----------------- // // Helper functions. // // ----------------- // @@ -318,6 +363,89 @@ protected byte[] getRowKeyTemplate() { return IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); } + /** + * Generates a proper key storage row key based on the metric, base time + * and tags. Adds salting when mocked properly. + * @param metric A non-null byte array representing the metric. + * @param base_time The base time for the row. + * @param tags A non-null list of tag key/value pairs as UIDs. + * @return A row key to check mock storage for. + */ + protected byte[] getRowKey(final byte[] metric, final int base_time, + final byte[] tags) { + final byte[] key = new byte[Const.SALT_WIDTH() + metric.length + + Const.TIMESTAMP_BYTES + tags.length]; + + System.arraycopy(metric, 0, key, Const.SALT_WIDTH(), metric.length); + System.arraycopy(Bytes.fromInt(base_time), 0, key, + Const.SALT_WIDTH() + metric.length, Const.TIMESTAMP_BYTES); + System.arraycopy(tags, 0, key, Const.SALT_WIDTH() + metric.length + + Const.TIMESTAMP_BYTES, tags.length); + RowKey.prefixKeyWithSalt(key); + return key; + } + + /** + * Generates a proper key storage row key based on the metric, base time + * and tags. Adds salting when mocked properly. + * @param metric + * @param base_time + * @param tags + * @return + */ + protected byte[] getRowKey(final String metric, final int base_time, + final String... tags) { + final int m = TSDB.metrics_width(); + final int tk = TSDB.tagk_width(); + final int tv = TSDB.tagv_width(); + + final byte[] key = new byte[Const.SALT_WIDTH() + m + 4 + + (tags.length / 2) * tk + (tags.length / 2) * tv]; + byte[] uid = uid_map.get(metric); + + // metrics first + if (uid != null) { + System.arraycopy(uid, 0, key, Const.SALT_WIDTH(), m); + } else { + throw new IllegalArgumentException("No METRIC UID was mocked for: " + metric); + } + + // timestamp + System.arraycopy(Bytes.fromInt(base_time), 0, key, Const.SALT_WIDTH() + m, + Const.TIMESTAMP_BYTES); + + // shortcut for offsets + final int pl = Const.SALT_WIDTH() + m + Const.TIMESTAMP_BYTES; + int ctr = 0; + int offset = 0; + for (final String tag : tags) { + uid = uid_map.get(tag); + + if (ctr % 2 == 0) { + // TAGK + if (uid != null) { + System.arraycopy(uid, 0, key, pl + offset, tk); + } else { + throw new IllegalArgumentException("No TAGK UID was mocked for: " + tag); + } + offset += tk; + } else { + // TAGV + if (uid != null) { + System.arraycopy(uid, 0, key, pl + offset, tv); + } else { + throw new IllegalArgumentException("No TAGK UID was mocked for: " + tag); + } + offset += tv; + } + + ctr++; + } + + RowKey.prefixKeyWithSalt(key); + return key; + } + protected void setDataPointStorage() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); storage.setFamily("t".getBytes(MockBase.ASCII())); diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 92ef3366a1..8911fd4b44 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -17,7 +17,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import java.lang.reflect.Field; @@ -59,7 +58,6 @@ CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, Scanner.class, AtomicIncrementRequest.class, Const.class}) public final class TestTSDB extends BaseTsdbTest { - private MockBase storage; @Before public void beforeLocal() throws Exception { @@ -78,7 +76,6 @@ public void ctorNullConfig() throws Exception { @Test public void ctorRollups() throws Exception { - TSDB tsdb = new TSDB(client, config); assertNull(Whitebox.getInternalState(tsdb, "rollup_config")); assertNull(Whitebox.getInternalState(tsdb, "default_interval")); diff --git a/test/core/TestTSDBAddAggregatePoint.java b/test/core/TestTSDBAddAggregatePoint.java index 8e958c65fb..2b0280abd9 100644 --- a/test/core/TestTSDBAddAggregatePoint.java +++ b/test/core/TestTSDBAddAggregatePoint.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import net.opentsdb.rollup.NoSuchRollupForIntervalException; @@ -34,7 +33,6 @@ import net.opentsdb.rollup.RollupUtils; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueName; -import net.opentsdb.uid.UniqueId.UniqueIdType; import org.hbase.async.Bytes; import org.junit.Before; @@ -44,16 +42,16 @@ import com.stumbleupon.async.Deferred; public class TestTSDBAddAggregatePoint extends BaseTsdbTest { - private final static byte[] TSDB_TABLE = "tsdb".getBytes(MockBase.ASCII()); - private final static byte[] AGG_TABLE = "tsdb-agg".getBytes(MockBase.ASCII()); - private final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); - private HashMap tags; - private RollupConfig rollup_config; + protected final static byte[] TSDB_TABLE = "tsdb".getBytes(MockBase.ASCII()); + protected final static byte[] AGG_TABLE = "tsdb-agg".getBytes(MockBase.ASCII()); + protected final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); + protected RollupConfig rollup_config; + protected String agg_tag_key; + protected byte[] row; @Before - public void beforeLocal() { - tags = new HashMap(1); - tags.put(TAGK_STRING, TAGV_STRING); + public void beforeLocal() throws Exception { + agg_tag_key = config.getString("tsd.rollups.agg_tag_key"); storage = new MockBase(tsdb, client, true, true, true, true); final List families = new ArrayList(); @@ -80,25 +78,22 @@ public void beforeLocal() { rollup_config = new RollupConfig(rollups); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + Whitebox.setInternalState(tsdb, "rollups_block_derived", true); + 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")); + setupGroupByTagValues(); - mockUID(UniqueIdType.TAGK, "_aggregate", new byte[] { 0, 0, 42 }); - mockUID(UniqueIdType.TAGV, "SUM", new byte[] { 0, 0, 42 }); - mockUID(UniqueIdType.TAGV, "MAX", new byte[] { 0, 0, 43 }); - mockUID(UniqueIdType.TAGV, "MIN", new byte[] { 0, 0, 44 }); - mockUID(UniqueIdType.TAGV, "COUNT", new byte[] { 0, 0, 45 }); - mockUID(UniqueIdType.TAGV, "AVG", new byte[] { 0, 0, 46 }); - mockUID(UniqueIdType.TAGV, "NOSUCHAGG", new byte[] { 0, 0, 47 }); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); } @Test public void addAggregatePointLong1Byte() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -109,13 +104,10 @@ public void addAggregatePointLong1Byte() throws Exception { @Test public void addAggregatePointLong1ByteNegative() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -126,13 +118,10 @@ public void addAggregatePointLong1ByteNegative() throws Exception { @Test public void addAggregatePointLong2Bytes() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 1}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 257, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -143,13 +132,10 @@ public void addAggregatePointLong2Bytes() throws Exception { @Test public void addAggregatePointLong2BytesNegative() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 1}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -257, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -160,13 +146,10 @@ public void addAggregatePointLong2BytesNegative() throws Exception { @Test public void addAggregatePointLong4Bytes() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 3}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 65537, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -177,13 +160,10 @@ public void addAggregatePointLong4Bytes() throws Exception { @Test public void addAggregatePointLong4BytesNegative() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 3}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -65537, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -195,13 +175,10 @@ public void addAggregatePointLong4BytesNegative() throws Exception { @Test public void addAggregatePointLong8Bytes() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 7}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 4294967296L, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -212,13 +189,11 @@ public void addAggregatePointLong8Bytes() throws Exception { @Test public void addAggregatePointLong8BytesNegative() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 7}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -4294967296L, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -230,13 +205,11 @@ public void addAggregatePointLong8BytesNegative() throws Exception { @Test public void addAggregatePointFloat4Bytes() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42.5F, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -248,13 +221,11 @@ public void addAggregatePointFloat4Bytes() throws Exception { @Test public void addAggregatePointFloat4BytesNegative() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42.5F, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -266,13 +237,11 @@ public void addAggregatePointFloat4BytesNegative() throws Exception { @Test public void addAggregatePointFloat4BytesPrecision() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42.5123459999F, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -284,13 +253,11 @@ public void addAggregatePointFloat4BytesPrecision() throws Exception { @Test public void addAggregatePointFloat4BytesPrecisionNegative() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42.5123459999F, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -304,24 +271,22 @@ public void addAggregatePointFloat4BytesPrecisionNegative() throws Exception { @Test (expected = IllegalArgumentException.class) public void addAggregatePointMilliseconds() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1419992400000L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); } @Test (expected = NoSuchRollupForIntervalException.class) public void addAggregatePointNoSuchRollup() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "11m", "sum").joinUninterruptibly(); + "11m", "sum", null).joinUninterruptibly(); } @Test public void addAggregatePoint10mInDayTop() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xAF, (byte) 0xD1, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1370476800, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1370476800, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -332,13 +297,11 @@ public void addAggregatePoint10mInDayTop() throws Exception { @Test public void addAggregatePoint10mInDayMid() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xAF, (byte) 0xD1, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1370476800, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 5, (byte) 0xD0}; tsdb.addAggregatePoint(METRIC_STRING, 1370532925L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -349,13 +312,11 @@ public void addAggregatePoint10mInDayMid() throws Exception { @Test public void addAggregatePoint10mInDayEnd() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xAF, (byte) 0xD1, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1370476800, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 5, (byte) 0xF0}; tsdb.addAggregatePoint(METRIC_STRING, 1370534399L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -366,13 +327,11 @@ public void addAggregatePoint10mInDayEnd() throws Exception { @Test public void addAggregatePoint10mInDayOver() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xB1, (byte) 0x22, (byte) 0x80, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1370563200, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1370563200L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -383,13 +342,12 @@ public void addAggregatePoint10mInDayOver() throws Exception { @Test public void addAggregatePoint1hInMonthTop() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xA9, (byte) 0x39, (byte) 0x80, 0, 0, 1, 0, 0, 1}; + row = getRowKey(METRIC_STRING, 1370044800, TAGK_STRING, TAGV_STRING); RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1370044800L, 42, tags, false, - "1h", "sum").joinUninterruptibly(); + "1h", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), @@ -400,13 +358,11 @@ public void addAggregatePoint1hInMonthTop() throws Exception { @Test public void addAggregatePoint1hInMonthMid() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xA9, (byte) 0x39, (byte) 0x80, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1370044800, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0x2C, (byte) 0xF0}; tsdb.addAggregatePoint(METRIC_STRING, 1372636799L, 42, tags, false, - "1h", "sum").joinUninterruptibly(); + "1h", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), @@ -417,13 +373,11 @@ public void addAggregatePoint1hInMonthMid() throws Exception { @Test public void addAggregatePoint1hInMonthOver() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x51, (byte) 0xD0, (byte) 0xC6, (byte) 0x80, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1372636800, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1372636800L, 42, tags, false, - "1h", "sum").joinUninterruptibly(); + "1h", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), @@ -434,13 +388,11 @@ public void addAggregatePoint1hInMonthOver() throws Exception { @Test public void addAggregatePoint1dInYearTop() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, (byte) 0x27, (byte) 0x00, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1d").getTemporalTable(), @@ -451,13 +403,11 @@ public void addAggregatePoint1dInYearTop() throws Exception { @Test public void addAggregatePoint1dInYearMid() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, (byte) 0x27, (byte) 0x00, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 9, (byte) 0xC0}; tsdb.addAggregatePoint(METRIC_STRING, 1370532925L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1d").getTemporalTable(), @@ -468,13 +418,11 @@ public void addAggregatePoint1dInYearMid() throws Exception { @Test public void addAggregatePoint1dInYearEnd() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, (byte) 0x27, (byte) 0x00, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0x16, (byte) 0xC0}; tsdb.addAggregatePoint(METRIC_STRING, 1388534399L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1d").getTemporalTable(), @@ -485,13 +433,11 @@ public void addAggregatePoint1dInYearEnd() throws Exception { @Test public void addAggregatePoint1dInYearOver() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, - 0x52, (byte) 0xC3, (byte) 0x5A, (byte) 0x80, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1388534400, TAGK_STRING, TAGV_STRING); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1d").getTemporalTable(), @@ -503,7 +449,7 @@ public void addAggregatePoint1dInYearOver() throws Exception { @Test (expected = NoSuchUniqueName.class) public void addAggregatePointNSUNMetric() throws Exception { tsdb.addAggregatePoint(NSUN_METRIC, 1388534400L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); } @Test (expected = NoSuchUniqueName.class) @@ -511,26 +457,23 @@ public void addAggregatePointNSUNTagK() throws Exception { tags.clear(); tags.put(NSUN_TAGK, TAGV_STRING); tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); } @Test (expected = NoSuchUniqueName.class) public void addAggregatePointNSUNTagV() throws Exception { tags.put(TAGK_STRING, NSUN_TAGV); tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, - "1d", "sum").joinUninterruptibly(); + "1d", "sum", null).joinUninterruptibly(); } // This is allowed, we don't check the aggregation function in this method. // It's up to the RPC level to check @Test public void addAggregatePointRollupNoSuchAgg() throws Exception { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "nosuchagg").joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1 }; - RowKey.prefixKeyWithSalt(row); + "nosuchagg", null).joinUninterruptibly(); + final RollupInterval interval = rollup_config.getRollupInterval("10m"); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, @@ -541,69 +484,64 @@ public void addAggregatePointRollupNoSuchAgg() throws Exception { public void addAggregatePointRollupsNotConfigured() throws Exception { Whitebox.setInternalState(tsdb, "rollup_config", (RollupConfig)null); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "nosuchagg").joinUninterruptibly(); + "nosuchagg", null).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointNegativeTimestamp() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, -1356998400, 42, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointEmptyTags() throws Exception { tags.put(TAGK_STRING, ""); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "nosuchagg").joinUninterruptibly(); + "nosuchagg", null).joinUninterruptibly(); } // not allowed at this time @Test (expected = IllegalArgumentException.class) public void addAggregatePointMS() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400000L, 42, tags, false, "10m", - "nosuchagg").joinUninterruptibly(); + "nosuchagg", null).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointNullInterval() throws Exception { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, null, - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointEmptyInterval() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); } @Test (expected = NoSuchRollupForIntervalException.class) public void addAggregatePointIntervalNotConfigured() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "6h", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointNullAggregator() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - null).joinUninterruptibly(); + null, null).joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointEmptyAggregator() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "").joinUninterruptibly(); + "", null).joinUninterruptibly(); } @Test public void addAggregatePointRollupRouting() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1 }; - RowKey.prefixKeyWithSalt(row); - RollupInterval interval = rollup_config.getRollupInterval("10m"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", @@ -617,7 +555,7 @@ public void addAggregatePointRollupRouting() throws Exception { interval = rollup_config.getRollupInterval("1h"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", @@ -634,7 +572,7 @@ public void addAggregatePointRollupRouting() throws Exception { interval = rollup_config.getRollupInterval("1d"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1d", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", @@ -651,91 +589,92 @@ public void addAggregatePointRollupRouting() throws Exception { // other aggs interval = rollup_config.getRollupInterval("1h"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", - "max").joinUninterruptibly(); + "max", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", - "min").joinUninterruptibly(); + "min", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", - "count").joinUninterruptibly(); + "count", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", interval))[0]); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", - "avg").joinUninterruptibly(); - assertEquals(42, storage.getColumn(interval.getTemporalTable(), + // derived not allowed by default + try { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", + "avg", null).joinUninterruptibly(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + assertNull(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))[0]); + interval))); } @Test public void addAggregatePointLongs() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1 }; - RowKey.prefixKeyWithSalt(row); final RollupInterval interval = rollup_config.getRollupInterval("10m"); // 1 byte tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 0, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(0, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -42, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(-42, storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))[0]); // 2 bytes tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 257, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(257, Bytes.getShort(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, "sum", interval)))); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -257, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(-257, Bytes.getShort(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, "sum", interval)))); // 4 bytes tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 65537, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(65537, Bytes.getInt(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, "sum", interval)))); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -65537, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(-65537, Bytes.getInt(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, "sum", interval)))); // 8 bytes tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 4294967296L, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(4294967296L, Bytes.getLong(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, "sum", interval)))); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -4294967296L, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(-4294967296L, Bytes.getLong(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, "sum", interval)))); @@ -743,50 +682,144 @@ public void addAggregatePointLongs() throws Exception { @Test public void addAggregatePointFloats() throws Exception { - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1 }; - RowKey.prefixKeyWithSalt(row); final RollupInterval interval = rollup_config.getRollupInterval("10m"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 0.0F, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(0.0, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", interval)))), 0.0001); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42.5F, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(42.5, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", interval)))), 0.0001); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -42.5F, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(-42.5, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", interval)))), 0.0001); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42.5123459999F, tags, false, "10m", - "sum").joinUninterruptibly(); + "sum", null).joinUninterruptibly(); assertEquals(42.5123459999F, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", interval)))), 0.0000001); } + @Test + public void addAggregatePointGroupByOnlyRouting() throws Exception { + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + + assertNull(tags.get(agg_tag_key)); + RollupInterval interval = rollup_config.getRollupInterval("1m"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + null, "sum").joinUninterruptibly(); + + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, new byte[] { 0, 0 })[0]); + // make sure it didn't get into the tsdb table OR rollup table + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertEquals("SUM", tags.get(agg_tag_key)); + + storage.flushStorage(); + tags.remove(agg_tag_key); + + // other aggs + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "MAX"); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + null, "max").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + interval))); + assertEquals("MAX", tags.get(agg_tag_key)); + + storage.flushStorage(); + tags.remove(agg_tag_key); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "MIN"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + null, "min").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + interval))); + assertEquals("MIN", tags.get(agg_tag_key)); + + storage.flushStorage(); + tags.remove(agg_tag_key); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "COUNT"); + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + null, "count").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + interval))); + assertEquals("COUNT", tags.get(agg_tag_key)); + + storage.flushStorage(); + tags.remove(agg_tag_key); + + // derived metrics blocked by default + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "AVG"); + try { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + null, "avg").joinUninterruptibly(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + assertNull(storage.getColumn(interval.getGroupbyTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))); + assertEquals("AVG", tags.get(agg_tag_key)); + } + @Test public void addAggregatePointGroupByRollupRouting() throws Exception { - byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2A }; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + assertNull(tags.get(agg_tag_key)); RollupInterval interval = rollup_config.getRollupInterval("10m"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "10m", - "sum").joinUninterruptibly(); - + "sum", "sum").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))[0]); @@ -797,12 +830,14 @@ public void addAggregatePointGroupByRollupRouting() throws Exception { assertNull(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))); + assertEquals("SUM", tags.get(agg_tag_key)); storage.flushStorage(); + tags.remove(agg_tag_key); interval = rollup_config.getRollupInterval("1h"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", - "sum").joinUninterruptibly(); + "sum", "sum").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", @@ -813,12 +848,14 @@ public void addAggregatePointGroupByRollupRouting() throws Exception { assertNull(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))); + assertEquals("SUM", tags.get(agg_tag_key)); storage.flushStorage(); + tags.remove(agg_tag_key); interval = rollup_config.getRollupInterval("1d"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1d", - "sum").joinUninterruptibly(); + "sum", "sum").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", @@ -829,104 +866,120 @@ public void addAggregatePointGroupByRollupRouting() throws Exception { assertNull(storage.getColumn(interval.getTemporalTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", interval))); + assertEquals("SUM", tags.get(agg_tag_key)); storage.flushStorage(); + tags.remove(agg_tag_key); + // other aggs - row[row.length-1] = 0x2B; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "MAX"); interval = rollup_config.getRollupInterval("1h"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", - "max").joinUninterruptibly(); + "max", "max").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + interval))); + assertEquals("MAX", tags.get(agg_tag_key)); - row[row.length-1] = 0x2C; - RowKey.prefixKeyWithSalt(row); + storage.flushStorage(); + tags.remove(agg_tag_key); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "MIN"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", - "min").joinUninterruptibly(); + "min", "min").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + interval))); + assertEquals("MIN", tags.get(agg_tag_key)); - row[row.length-1] = 0x2D; - RowKey.prefixKeyWithSalt(row); + storage.flushStorage(); + tags.remove(agg_tag_key); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "COUNT"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", - "count").joinUninterruptibly(); + "count", "count").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", interval))[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + interval))); + assertEquals("COUNT", tags.get(agg_tag_key)); - row[row.length-1] = 0x2E; - RowKey.prefixKeyWithSalt(row); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", - "avg").joinUninterruptibly(); - assertEquals(42, storage.getColumn(interval.getGroupbyTable(), + storage.flushStorage(); + tags.remove(agg_tag_key); + + // derivced metrics blocked by default + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "AVG"); + try { + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", + "avg", "avg").joinUninterruptibly(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + assertNull(storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))[0]); + interval))); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", + interval))); + assertEquals("AVG", tags.get(agg_tag_key)); } //This is allowed, we don't check the aggregation function in this method. // It's up to the RPC level to check @Test public void addAggregatePointGroupByRollupNoSuchAgg() throws Exception { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "10m", - "nosuchagg").joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2F }; - RowKey.prefixKeyWithSalt(row); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); final RollupInterval interval = rollup_config.getRollupInterval("10m"); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "10m", + "nosuchagg", "sum").joinUninterruptibly(); + assertEquals(42, storage.getColumn(interval.getGroupbyTable(), row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "nosuchagg", interval))[0]); - } - - //This is allowed, we don't check the aggregation function in this method. - // It's up to the RPC level to check - @Test - public void addAggregatePointGroupByNoSuchAgg() throws Exception { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - "nosuchagg").joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2F }; - RowKey.prefixKeyWithSalt(row); - assertEquals(42, storage.getColumn(AGG_TABLE, row, FAMILY, - new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); + assertNull(storage.getColumn(interval.getTemporalTable(), + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + interval))); } - @Test - public void addAggregatePointGroupBy() throws Exception { + @Test (expected = IllegalArgumentException.class) + public void addAggregatePointGroupByNoSuchAgg() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - "sum").joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1, 0, 0, 0x2A, 0, 0, 0x2A }; - RowKey.prefixKeyWithSalt(row); - assertEquals(42, storage.getColumn(AGG_TABLE, row, FAMILY, - new byte[] { 0, 0 })[0]); - assertNull(storage.getColumn(TSDB_TABLE, row, FAMILY, - new byte[] { 0, 0 })); - assertNull(storage.getColumn( - rollup_config.getRollupInterval("10m").getGroupbyTable(), row, FAMILY, - new byte[] { 0, 0 })); - - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 24, tags, true, null, - "count").joinUninterruptibly(); - row[row.length-1] = 0x2D; - RowKey.prefixKeyWithSalt(row); - assertEquals(24, storage.getColumn(AGG_TABLE, row, FAMILY, - new byte[] { 0, 0 })[0]); - assertNull(storage.getColumn(TSDB_TABLE, row, FAMILY, - new byte[] { 0, 0 })); - assertNull(storage.getColumn( - rollup_config.getRollupInterval("10m").getGroupbyTable(), row, FAMILY, - new byte[] { 0, 0 })); + "sum", "nosuchagg").joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointGroupByRollupsDisabled() throws Exception { Whitebox.setInternalState(tsdb, "rollup_config", (Object) null); tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); } @Test @@ -940,11 +993,8 @@ public void dpFilterOK() throws Exception { Whitebox.setInternalState(tsdb, "ts_filter", filter); tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -964,11 +1014,8 @@ public void uidFilterBlocked() throws Exception { Whitebox.setInternalState(tsdb, "ts_filter", filter); tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "10m", "sum").joinUninterruptibly(); + "10m", "sum", null).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -987,15 +1034,13 @@ public void dpFilterReturnsException() throws Exception { Whitebox.setInternalState(tsdb, "ts_filter", filter); final Deferred deferred = tsdb.addAggregatePoint(METRIC_STRING, - 1356998400L, 42, tags, false, "10m", "sum"); + 1356998400L, 42, tags, false, "10m", "sum", null); try { deferred.join(); fail("Expected an UnitTestException"); } catch (UnitTestException e) { }; - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), @@ -1015,12 +1060,10 @@ public void dpFilterThrowsException() throws Exception { try { tsdb.addAggregatePoint(METRIC_STRING, - 1356998400L, 42, tags, false, "10m", "sum"); + 1356998400L, 42, tags, false, "10m", "sum", null); fail("Expected an UnitTestException"); } catch (UnitTestException e) { }; - final byte[] row = new byte[] { 0, 0, 1, - 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 1, 0, 0, 1}; - RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), diff --git a/test/core/TestTSDBAddAggregatePointSalted.java b/test/core/TestTSDBAddAggregatePointSalted.java new file mode 100644 index 0000000000..d2c9f405ee --- /dev/null +++ b/test/core/TestTSDBAddAggregatePointSalted.java @@ -0,0 +1,81 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.junit.Before; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.storage.MockBase; + +/** + * Integration test that runs all of the tests in {@see TestTSDBAddAggregatePoint} + * but with salting enabled just to verify nothing goes wrong with the extra + * salt bytes. + */ +@RunWith(PowerMockRunner.class) +public class TestTSDBAddAggregatePointSalted extends TestTSDBAddAggregatePoint { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + agg_tag_key = config.getString("tsd.rollups.agg_tag_key"); + + storage = new MockBase(tsdb, client, true, true, true, true); + final List families = new ArrayList(); + families.add(FAMILY); + + storage.addTable("tsdb-rollup-10m".getBytes(), families); + storage.addTable("tsdb-rollup-agg-10m".getBytes(), families); + storage.addTable("tsdb-rollup-1h".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1h".getBytes(), families); + storage.addTable("tsdb-rollup-1d".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1d".getBytes(), families); + storage.addTable(AGG_TABLE, families); + + final List rollups = new ArrayList(); + rollups.add(new RollupInterval( + "tsdb", "tsdb-agg", "1m", "1h", true)); + rollups.add(new RollupInterval( + "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "1d")); + rollups.add(new RollupInterval( + "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1m")); + rollups.add(new RollupInterval( + "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1y")); + + rollup_config = new RollupConfig(rollups); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + Whitebox.setInternalState(tsdb, "rollups_block_derived", true); + 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")); + setupGroupByTagValues(); + + setupGroupByTagValues(); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + } +} diff --git a/test/core/TestTSDBAddPoint.java b/test/core/TestTSDBAddPoint.java index 6e16f08622..1974b42d4e 100644 --- a/test/core/TestTSDBAddPoint.java +++ b/test/core/TestTSDBAddPoint.java @@ -31,7 +31,6 @@ import org.hbase.async.Bytes; import org.junit.Before; import org.junit.Test; -import org.powermock.api.mockito.PowerMockito; import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @@ -39,17 +38,17 @@ import net.opentsdb.uid.NoSuchUniqueName; public class TestTSDBAddPoint extends BaseTsdbTest { - + protected byte[] row; + @Before public void beforeLocal() throws Exception { + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); setDataPointStorage(); } @Test public void addPointLong1Byte() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); assertEquals(42, value[0]); @@ -58,8 +57,6 @@ public void addPointLong1Byte() throws Exception { @Test public void addPointLong1ByteNegative() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, -42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); assertEquals(-42, value[0]); @@ -68,8 +65,6 @@ public void addPointLong1ByteNegative() throws Exception { @Test public void addPointLong2Bytes() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 257, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); assertNotNull(value); assertEquals(257, Bytes.getShort(value)); @@ -78,8 +73,7 @@ public void addPointLong2Bytes() throws Exception { @Test public void addPointLong2BytesNegative() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, -257, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 1 }); assertNotNull(value); assertEquals(-257, Bytes.getShort(value)); @@ -88,8 +82,7 @@ public void addPointLong2BytesNegative() throws Exception { @Test public void addPointLong4Bytes() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 65537, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); assertNotNull(value); assertEquals(65537, Bytes.getInt(value)); @@ -98,8 +91,7 @@ public void addPointLong4Bytes() throws Exception { @Test public void addPointLong4BytesNegative() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, -65537, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 3 }); assertNotNull(value); assertEquals(-65537, Bytes.getInt(value)); @@ -108,8 +100,7 @@ public void addPointLong4BytesNegative() throws Exception { @Test public void addPointLong8Bytes() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 4294967296L, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); assertNotNull(value); assertEquals(4294967296L, Bytes.getLong(value)); @@ -118,8 +109,7 @@ public void addPointLong8Bytes() throws Exception { @Test public void addPointLong8BytesNegative() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, -4294967296L, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 7 }); assertNotNull(value); assertEquals(-4294967296L, Bytes.getLong(value)); @@ -128,8 +118,7 @@ public void addPointLong8BytesNegative() throws Exception { @Test public void addPointLongMs() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); assertNotNull(value); @@ -142,8 +131,6 @@ public void addPointLongMany() throws Exception { for (int i = 1; i <= 50; i++) { tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); } - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); assertEquals(1, value[0]); @@ -156,8 +143,6 @@ public void addPointLongManyMs() throws Exception { for (int i = 1; i <= 50; i++) { tsdb.addPoint(METRIC_STRING, timestamp++, i, tags).joinUninterruptibly(); } - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); assertNotNull(value); @@ -168,8 +153,6 @@ public void addPointLongManyMs() throws Exception { @Test public void addPointLongEndOfRow() throws Exception { tsdb.addPoint(METRIC_STRING, 1357001999, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, (byte) 0xF0 }); assertNotNull(value); @@ -180,8 +163,6 @@ public void addPointLongEndOfRow() throws Exception { public void addPointLongOverwrite() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998400, 24, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); assertEquals(24, value[0]); @@ -195,8 +176,8 @@ public void addPointNoAutoMetric() throws Exception { @Test public void addPointSecondZero() throws Exception { // Thu, 01 Jan 1970 00:00:00 GMT + row = getRowKey(METRIC_STRING, 0, TAGK_STRING, TAGV_STRING); tsdb.addPoint(METRIC_STRING, 0, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); assertEquals(42, value[0]); @@ -205,8 +186,8 @@ public void addPointSecondZero() throws Exception { @Test public void addPointSecondOne() throws Exception { // hey, it's valid *shrug* Thu, 01 Jan 1970 00:00:01 GMT + row = getRowKey(METRIC_STRING, 0, TAGK_STRING, TAGV_STRING); tsdb.addPoint(METRIC_STRING, 1, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 16 }); assertNotNull(value); assertEquals(42, value[0]); @@ -215,9 +196,8 @@ public void addPointSecondOne() throws Exception { @Test public void addPointSecond2106() throws Exception { // Sun, 07 Feb 2106 06:28:15 GMT + row = getRowKey(METRIC_STRING, (int) 4294965600L, TAGK_STRING, TAGV_STRING); tsdb.addPoint(METRIC_STRING, 4294967295L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, - 0x60, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0x69, (byte) 0xF0 }); assertNotNull(value); assertEquals(42, value[0]); @@ -242,9 +222,8 @@ public void addPointMS1970() throws Exception { // a millisecond timestamp since it doesn't fit in 4 bytes. // Base time is 4294800 which is Thu, 19 Feb 1970 17:00:00 GMT // offset = F0A36000 or 167296 ms + row = getRowKey(METRIC_STRING, 4294800, TAGK_STRING, TAGV_STRING); tsdb.addPoint(METRIC_STRING, 4294967296L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0, (byte) 0x41, (byte) 0x88, - (byte) 0x90, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, (byte) 0xA3, 0x60, 0}); assertNotNull(value); @@ -254,9 +233,8 @@ public void addPointMS1970() throws Exception { @Test public void addPointMS2106() throws Exception { // Sun, 07 Feb 2106 06:28:15.000 GMT + row = getRowKey(METRIC_STRING, (int) 4294965600L, TAGK_STRING, TAGV_STRING); tsdb.addPoint(METRIC_STRING, 4294967295000L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, (byte) 0xFF, (byte) 0xFF, (byte) 0xF9, - 0x60, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF6, (byte) 0x77, 0x46, 0 }); assertNotNull(value); @@ -266,9 +244,9 @@ public void addPointMS2106() throws Exception { @Test public void addPointMS2286() throws Exception { // It's an artificial limit and more thought needs to be put into it + // TODO - and doesn't conform anyways, bad timestamp! + row = getRowKey(METRIC_STRING, 1410062608, TAGK_STRING, TAGV_STRING); tsdb.addPoint(METRIC_STRING, 9999999999999L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, (byte) 0x54, (byte) 0x0B, (byte) 0xD9, - 0x10, 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xFA, (byte) 0xAE, 0x5F, (byte) 0xC0 }); assertNotNull(value); @@ -293,8 +271,7 @@ public void addPointMSNegative() throws Exception { @Test public void addPointFloat() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); assertNotNull(value); // should have 7 digits of precision @@ -306,8 +283,7 @@ public void addPointFloatNegative() throws Exception { HashMap tags = new HashMap(1); tags.put("host", "web01"); tsdb.addPoint(METRIC_STRING, 1356998400, -42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); assertNotNull(value); // should have 7 digits of precision @@ -317,8 +293,7 @@ public void addPointFloatNegative() throws Exception { @Test public void addPointFloatMs() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 11 }); assertNotNull(value); @@ -331,8 +306,7 @@ public void addPointFloatEndOfRow() throws Exception { HashMap tags = new HashMap(1); tags.put("host", "web01"); tsdb.addPoint(METRIC_STRING, 1357001999, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { (byte) 0xE0, (byte) 0xFB }); assertNotNull(value); @@ -344,8 +318,7 @@ public void addPointFloatEndOfRow() throws Exception { public void addPointFloatPrecision() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42.5123459999F, tags) .joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); assertNotNull(value); // should have 7 digits of precision @@ -356,8 +329,7 @@ public void addPointFloatPrecision() throws Exception { public void addPointFloatOverwrite() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998400, 25.4F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, new byte[] { 0, 11 }); assertNotNull(value); // should have 7 digits of precision @@ -371,8 +343,7 @@ public void addPointBothSameTimeIntAndFloat() throws Exception { // aggregators when this occurs? tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998400, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertEquals(2, storage.numColumns(row)); assertNotNull(value); @@ -390,8 +361,7 @@ public void addPointBothSameTimeIntAndFloatMs() throws Exception { // aggregators when this occurs? tsdb.addPoint(METRIC_STRING, 1356998400500L, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998400500L, 42.5F, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + byte[] value = storage.getColumn(row, new byte[] { (byte) 0xF0, 0, 0x7D, 0 }); assertEquals(2, storage.numColumns(row)); assertNotNull(value); @@ -408,8 +378,7 @@ public void addPointBothSameTimeSecondAndMs() throws Exception { // timestamp. tsdb.addPoint(METRIC_STRING, 1356998400L, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998400000L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertEquals(2, storage.numColumns(row)); assertNotNull(value); @@ -420,60 +389,12 @@ public void addPointBothSameTimeSecondAndMs() throws Exception { assertEquals(42, value[0]); } - @Test - public void addPointWithSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointWithSaltDifferentTags() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - - tags.put(TAGK_STRING, TAGV_B_STRING); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 9, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 2}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - - @Test - public void addPointWithSaltDifferentTime() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - - tsdb.addPoint(METRIC_STRING, 1359680400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x51, (byte) 0x0B, 0x13, - (byte) 0x90, 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); - assertNotNull(value); - assertEquals(42, value[0]); - } - @Test public void addPointAppend() throws Exception { Whitebox.setInternalState(config, "enable_appends", true); tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { 0, 0, 42 }, value); @@ -484,8 +405,7 @@ public void addPointAppendWithOffset() throws Exception { Whitebox.setInternalState(config, "enable_appends", true); tsdb.addPoint(METRIC_STRING, 1356998430, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { 1, -32, 42 }, value); @@ -498,8 +418,7 @@ public void addPointAppendAppending() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); @@ -513,8 +432,6 @@ public void addPointAppendAppendingOutOfOrder() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { 0, 0, 42, 3, -64, 1, 1, -32, 24 }, value); @@ -527,8 +444,7 @@ public void addPointAppendAppendingDuplicates() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998430, 1, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 1, -32, 1 }, value); @@ -539,8 +455,7 @@ public void addPointAppendMS() throws Exception { Whitebox.setInternalState(config, "enable_appends", true); tsdb.addPoint(METRIC_STRING, 1356998400050L, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { (byte) 0xF0, 0, 12, -128, 42 }, value); @@ -553,48 +468,13 @@ public void addPointAppendAppendingMixMS() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998400050L, 1, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; + final byte[] value = storage.getColumn(row, AppendDataPoints.APPEND_COLUMN_QUALIFIER); assertArrayEquals(new byte[] { 0, 0, 42, (byte) 0xF0, 0, 12, -128, 1, 1, -32, 24 }, value); } - @Test - public void addPointAppendWithSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - Whitebox.setInternalState(config, "enable_appends", true); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42 }, value); - } - - @Test - public void addPointAppendAppendingWithSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); - Whitebox.setInternalState(config, "enable_appends", true); - - tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998430, 24, tags).joinUninterruptibly(); - tsdb.addPoint(METRIC_STRING, 1356998460, 1, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 8, 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; - final byte[] value = storage.getColumn(row, - AppendDataPoints.APPEND_COLUMN_QUALIFIER); - assertArrayEquals(new byte[] { 0, 0, 42, 1, -32, 24, 3, -64, 1 }, value); - } - @Test public void dpFilterOK() throws Exception { final WriteableDataPointFilterPlugin filter = @@ -606,8 +486,6 @@ public void dpFilterOK() throws Exception { Whitebox.setInternalState(tsdb, "ts_filter", filter); tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNotNull(value); assertEquals(42, value[0]); @@ -628,8 +506,6 @@ public void dpFilterBlocked() throws Exception { Whitebox.setInternalState(tsdb, "ts_filter", filter); tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNull(value); @@ -654,8 +530,6 @@ public void dpFilterReturnsException() throws Exception { deferred.join(); fail("Expected an UnitTestException"); } catch (UnitTestException e) { }; - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNull(value); @@ -678,8 +552,6 @@ public void uidFilterThrowsException() throws Exception { tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags); fail("Expected an UnitTestException"); } catch (UnitTestException e) { }; - final byte[] row = new byte[] { 0, 0, 1, 0x50, (byte) 0xE2, 0x27, 0, - 0, 0, 1, 0, 0, 1}; final byte[] value = storage.getColumn(row, new byte[] { 0, 0 }); assertNull(value); diff --git a/test/core/TestTSDBAddPointSalted.java b/test/core/TestTSDBAddPointSalted.java new file mode 100644 index 0000000000..2045556ac6 --- /dev/null +++ b/test/core/TestTSDBAddPointSalted.java @@ -0,0 +1,38 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 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.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.modules.junit4.PowerMockRunner; + +/** + * Integration test that runs all of the tests in {@see TestTSDBAddPoint} + * but with salting enabled just to verify nothing goes wrong with the extra + * salt bytes. + */ +@RunWith(PowerMockRunner.class) +public class TestTSDBAddPointSalted extends TestTSDBAddPoint { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + setDataPointStorage(); + } +} diff --git a/test/core/TestTsdbQueryRollup.java b/test/core/TestTsdbQueryRollup.java index dac1047532..52484ffda1 100644 --- a/test/core/TestTsdbQueryRollup.java +++ b/test/core/TestTsdbQueryRollup.java @@ -246,7 +246,7 @@ public void run10mSumLongSingleTSInMS() throws Exception { //rollup doesn't accept timestamps in milliseconds tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 0, tags, false, ten_min_interval.getStringInterval(), - aggr.toString()).joinUninterruptibly(); + aggr.toString(), null).joinUninterruptibly(); } @Test @@ -790,9 +790,9 @@ public void runDupes() throws Exception { final Aggregator aggr = Aggregators.SUM; tsdb.addAggregatePoint(METRIC_STRING, 1357026600L, Integer.MAX_VALUE, tags, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); tsdb.addAggregatePoint(METRIC_STRING, 1357026600L, 42.5F, tags, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); setQuery(interval.getStringInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); @@ -829,10 +829,10 @@ private void storeLongRollup(final long start_timestamp, while (start_a <= end_timestamp) { i += time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); start_b += (offset ? time_interval * 2 : time_interval); @@ -845,10 +845,10 @@ private void storeLongRollup(final long start_timestamp, while (start_a <= end_timestamp) { i -= time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags2, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags2, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); @@ -871,11 +871,11 @@ private void storeCount(final long start_timestamp, while (start_a <= end_timestamp) { tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags, false, - interval.getStringInterval(), Aggregators.COUNT.toString()) + interval.getStringInterval(), Aggregators.COUNT.toString(), null) .joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags, false, - interval.getStringInterval(), Aggregators.COUNT.toString()) + interval.getStringInterval(), Aggregators.COUNT.toString(), null) .joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); @@ -887,11 +887,11 @@ private void storeCount(final long start_timestamp, while (start_a <= end_timestamp) { tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags2, false, - interval.getStringInterval(), Aggregators.COUNT.toString()) + interval.getStringInterval(), Aggregators.COUNT.toString(), null) .joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags2, false, - interval.getStringInterval(), Aggregators.COUNT.toString()) + interval.getStringInterval(), Aggregators.COUNT.toString(), null) .joinUninterruptibly(); } @@ -917,11 +917,11 @@ private void storeFloatRollup(final long start_timestamp, while (start_a <= end_timestamp) { i += time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b,i, tags, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); start_b += (offset ? time_interval * 2 : time_interval); @@ -934,10 +934,10 @@ private void storeFloatRollup(final long start_timestamp, while (start_a <= end_timestamp) { i -= time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags2, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags2, false, - interval.getStringInterval(), aggr.toString()).joinUninterruptibly(); + interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); @@ -948,7 +948,7 @@ private void storeFloatRollup(final long start_timestamp, private void storePoint(final long ts, final long value, final Aggregator agg, final RollupInterval interval) throws Exception { tsdb.addAggregatePoint(METRIC_STRING, ts, value, tags, false, - interval.getStringInterval(), agg.toString()).joinUninterruptibly(); + interval.getStringInterval(), agg.toString(), null).joinUninterruptibly(); } @SuppressWarnings("deprecation") diff --git a/test/tsd/TestRollupRpc.java b/test/tsd/TestRollupRpc.java index 73f931b128..da09369cdf 100644 --- a/test/tsd/TestRollupRpc.java +++ b/test/tsd/TestRollupRpc.java @@ -15,7 +15,6 @@ import net.opentsdb.rollup.RollupConfig; import net.opentsdb.rollup.RollupInterval; import net.opentsdb.storage.MockBase; -import net.opentsdb.uid.UniqueId.UniqueIdType; import org.hamcrest.CoreMatchers; import org.hbase.async.HBaseException; @@ -44,8 +43,10 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +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.assertThat; @RunWith(PowerMockRunner.class) @@ -56,11 +57,15 @@ "com.sum.*", "org.xml.*"}) public class TestRollupRpc extends BaseTestPutRpc { private final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); - + protected final static byte[] AGG_TABLE = "tsdb-agg".getBytes(MockBase.ASCII()); private RollupConfig rollup_config; + protected String agg_tag_key; + protected byte[] row; @Before public void beforeLocal() throws Exception { + agg_tag_key = config.getString("tsd.rollups.agg_tag_key"); + final List families = new ArrayList(); families.add(FAMILY); @@ -77,9 +82,17 @@ public void beforeLocal() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); storage.addTable("tsdb-rollup-1h".getBytes(), families); 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", + config.getString("tsd.rollups.agg_tag_key")); + Whitebox.setInternalState(tsdb, "raw_agg_tag_value", + config.getString("tsd.rollups.raw_agg_tag_value")); + setupGroupByTagValues(); + + setupGroupByTagValues(); - mockUID(UniqueIdType.TAGK, "_aggregate", new byte[] { 0, 0, 42 }); - mockUID(UniqueIdType.TAGV, "SUM", new byte[] { 0, 0, 42 }); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); } @Test @@ -89,18 +102,49 @@ public void constructor() { // Socket RPC Tests ------------------------------------ + // TODO - something odd going on with this timestamp falling in the wrong + // row.... hmm.. +// @Test +// public void execute() throws Exception { +// final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); +// final Channel chan = NettyMocks.fakeChannel(); +// assertNotNull(rollup.execute(tsdb, chan, new String[] { "rollup", +// "1h-sum", METRIC_STRING, "1365465600", "42", +// TAGK_STRING + "=" + TAGV_STRING }) +// .joinUninterruptibly()); +// validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); +// verify(chan, never()).write(any()); +// verify(chan, never()).isConnected(); +// validateSEH(false); +// storage.dumpToSystemOut(); +// System.out.println(MockBase.bytesToString(row)); +// final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; +// final byte[] value = storage.getColumn( +// rollup_config.getRollupInterval("1h").getTemporalTable(), +// row, FAMILY, qualifier); +// final byte[] expected = {0x2A}; +// assertArrayEquals(expected, value); +// } + @Test public void execute() throws Exception { final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); assertNotNull(rollup.execute(tsdb, chan, new String[] { "rollup", - "1h-sum", METRIC_STRING, "1365465600", "42", + "1h-sum", METRIC_STRING, "1356998400", "42", TAGK_STRING + "=" + TAGV_STRING }) .joinUninterruptibly()); validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); verify(chan, never()).write(any()); verify(chan, never()).isConnected(); validateSEH(false); + + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); } @Test @@ -119,16 +163,47 @@ public void executeRollupsDisabled() throws Exception { } @Test - public void executeWithAgg() throws Exception { + public void executeAggOnly() throws Exception { + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rollup.execute(tsdb, chan, new String[] { "rollup", "sum", + METRIC_STRING, "1356998400", "42", TAGK_STRING + "=" + TAGV_STRING }) + .joinUninterruptibly(); + validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + verify(chan, never()).write(any()); + verify(chan, never()).isConnected(); + validateSEH(false); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + final byte[] qualifier = new byte[] {0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1m").getGroupbyTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void executeRollupWithAgg() throws Exception { final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); rollup.execute(tsdb, chan, new String[] { "rollup", "1h-sum:sum", - METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) + METRIC_STRING, "1356998400", "42", TAGK_STRING + "=" + TAGV_STRING }) .joinUninterruptibly(); validateCounters(1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); verify(chan, never()).write(any()); verify(chan, never()).isConnected(); validateSEH(false); + + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getGroupbyTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); } @Test @@ -190,7 +265,7 @@ public void executeRuntimeException() throws Exception { final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); PowerMockito.when(tsdb.addAggregatePoint(anyString(), anyLong(), anyLong(), - anyMap(), anyBoolean(), anyString(), anyString())) + anyMap(), anyBoolean(), anyString(), anyString(), anyString())) .thenThrow(new RuntimeException("Fail!")); rollup.execute(tsdb, chan, new String[] { "rollup", "rollup", "1h-sum", METRIC_STRING, "1365465600", "42", TAGK_STRING + "=" + TAGV_STRING }) @@ -442,10 +517,31 @@ public void executeNSUNTagV() throws Exception { // HTTP RPC Tests -------------------------------------- + // TODO - Something odd with this timestamp +// @Test +// public void httpAddSingleRollupPoint() throws Exception { +// HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", +// "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " +// + "\"interval\":\"1h\", \"aggregator\":\"sum\"," +// + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); +// final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); +// rollup.execute(tsdb, query); +// assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); +// validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); +// validateSEH(false); +// +// final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; +// final byte[] value = storage.getColumn( +// rollup_config.getRollupInterval("1h").getTemporalTable(), +// row, FAMILY, qualifier); +// final byte[] expected = {0x2A}; +// assertArrayEquals(expected, value); +// } + @Test public void httpAddSingleRollupPoint() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", - "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400,\"value\":42, " + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); @@ -453,15 +549,69 @@ public void httpAddSingleRollupPoint() throws Exception { assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); validateSEH(false); + + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); } + @Test + public void httpAddSingleGroupByPoint() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400,\"value\":42, " + + "\"groupByAggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + + final byte[] qualifier = new byte[] { 0, 0 }; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1m").getGroupbyTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + + @Test + public void httpAddSingleRollupAndGroupByPoint() throws Exception { + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"sum\", " + + "\"groupByAggregator\":\"sum\"," + + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); + rollup.execute(tsdb, query); + + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + validateSEH(false); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + + final byte[] qualifier = new byte[] { 0x73, 0x75, 0x6D, 0x3A, 0, 0 }; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getGroupbyTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + } + @Test public void httpAddTwoRollupPoints() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", - "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400,\"value\":42, " + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}, " - + "{\"metric\":\"" + METRIC_B_STRING + "\",\"timestamp\":1365465600,\"value\":24, " + + "{\"metric\":\"" + METRIC_B_STRING + "\",\"timestamp\":1356998400,\"value\":24, " + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); @@ -469,15 +619,29 @@ public void httpAddTwoRollupPoints() throws Exception { assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); validateCounters(0, 1, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0); validateSEH(false); + + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + + row = getRowKey(METRIC_B_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + expected = new byte[] {0x18}; + assertArrayEquals(expected, value); } @Test public void httpAddTwoRollupPointsOneGoodOneBad() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup", - "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400,\"value\":42, " + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}, " - + "{\"metric\":\"" + NSUN_METRIC + "\",\"timestamp\":1365465600,\"value\":24, " + + "{\"metric\":\"" + NSUN_METRIC + "\",\"timestamp\":1356998400,\"value\":24, " + "\"interval\":\"1h\", \"aggregator\":\"sum\"," + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); @@ -485,6 +649,19 @@ public void httpAddTwoRollupPointsOneGoodOneBad() throws Exception { assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); validateCounters(0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0); validateSEH(false); + + final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + byte[] expected = {0x2A}; + assertArrayEquals(expected, value); + + row = getRowKey(METRIC_B_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + assertNull(value); } @Test @@ -545,19 +722,22 @@ public void httpMissingAggregator() throws Exception { @Test public void httpInvalidAggregator() throws Exception { HttpQuery query = NettyMocks.postQuery(tsdb, "/api/rollup?details", - "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1365465600,\"value\":42, " - + "\"interval\":\"1h\", \"aggregator\":\"what?\"," + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400,\"value\":42, " + + "\"interval\":\"1h\", \"aggregator\":\"nosuchagg\"," + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); rollup.execute(tsdb, query); - assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - final String response = - query.response().getContent().toString(Charset.forName("UTF-8")); - assertThat(response, CoreMatchers.containsString("\"error\":\"Invalid aggregator\"")); - assertThat(response, CoreMatchers.containsString("\"failed\":1")); - assertThat(response, CoreMatchers.containsString("\"success\":0")); - validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); validateSEH(false); + + final byte[] qualifier = new byte[] {0x6E, 0x6F, 0x73, 0x75, 0x63, 0x68, + 0x61, 0x67, 0x67, 0x3A, 0, 0}; + final byte[] value = storage.getColumn( + rollup_config.getRollupInterval("1h").getTemporalTable(), + row, FAMILY, qualifier); + final byte[] expected = {0x2A}; + assertArrayEquals(expected, value); } @Test From 188ff289cfa57142ac2015778c5e9e5e2e44fd4a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 20 Nov 2016 12:55:15 -0800 Subject: [PATCH 578/826] Fix the Telnet rollup API to handle the group-by agg. Cleanup unused config setting. Signed-off-by: Chris Larsen --- src/tsd/RollupDataPointRpc.java | 6 +----- src/utils/Config.java | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/tsd/RollupDataPointRpc.java b/src/tsd/RollupDataPointRpc.java index 6752775516..7545dd9f96 100644 --- a/src/tsd/RollupDataPointRpc.java +++ b/src/tsd/RollupDataPointRpc.java @@ -21,11 +21,8 @@ import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.utils.Config; -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 java.io.IOException; import java.util.HashMap; @@ -38,7 +35,6 @@ */ class RollupDataPointRpc extends PutDataPointRpc implements TelnetRpc, HttpRpc { - private static final Logger LOG = LoggerFactory.getLogger(RollupDataPointRpc.class); private enum TelnetIndex { COMMAND, @@ -204,7 +200,7 @@ protected IncomingDataPoint getDataPointFromString(final String[] words) { } dp.setInterval(interval); dp.setAggregator(temporal_agg); - // TODO - spatial agg + dp.setGroupByAggregator(spatial_agg); dp.setMetric(words[TelnetIndex.METRIC.ordinal()]); diff --git a/src/utils/Config.java b/src/utils/Config.java index cb30cda8fd..ae954365f0 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -491,7 +491,6 @@ protected void setDefaults() { default_map.put("tsd.network.tcp_no_delay", "true"); default_map.put("tsd.network.keep_alive", "true"); default_map.put("tsd.network.reuse_address", "true"); - default_map.put("tsd.core.agg_tag", "_aggregate"); default_map.put("tsd.core.auto_create_metrics", "false"); default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); From eb8494b6ac503d0eea14a321b2023359c777acbe Mon Sep 17 00:00:00 2001 From: nickman Date: Sat, 29 Oct 2016 16:33:24 -0400 Subject: [PATCH 579/826] Brought build and source up to date. Updated Async channel factory to use NioWorkerPools. Signed-off-by: Chris Larsen --- .gitignore | 5 + Makefile.am | 8 +- fat-jar/fat-jar-pom.xml.in | 86 +++++++++- fat-jar/opentsdb.conf.json | 157 +++++++++++++++++++ src/query/expression/ExpressionIterator.java | 5 + src/tools/ConfigArgP.java | 12 +- src/tools/ConfigMetaType.java | 5 +- src/tools/{Main.java => OpenTSDBMain.java} | 23 ++- 8 files changed, 281 insertions(+), 20 deletions(-) rename src/tools/{Main.java => OpenTSDBMain.java} (97%) diff --git a/.gitignore b/.gitignore index 60fb346c0f..133071f206 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,8 @@ tools/docker/libs tools/docker/*.jar tools/docker/logback.xml tools/docker/opentsdb.conf + +# FatJar +fat-jar-pom.xml +src-resources/ +test-resources/ diff --git a/Makefile.am b/Makefile.am index ecbc798e6f..d1191ac68f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -142,7 +142,7 @@ tsdb_SRC := \ src/tools/ConfigArgP.java \ src/tools/ConfigMetaType.java \ src/tools/GnuplotInstaller.java \ - src/tools/Main.java \ + src/tools/OpenTSDBMain.java \ src/tree/Branch.java \ src/tree/Leaf.java \ src/tree/Tree.java \ @@ -824,8 +824,11 @@ fat-jar-pom.xml: ./fat-jar/fat-jar-pom.xml.in Makefile echo ''; \ sed <$< \ -e 's/@ASYNCHBASE_VERSION@/$(ASYNCHBASE_VERSION)/' \ + -e 's/@ASYNCBIGTABLE_VERSION@/$(ASYNCBIGTABLE_VERSION)/' \ + -e 's/@ASYNCCASSANDRA_VERSION@/$(ASYNCCASSANDRA_VERSION)/' \ -e 's/@GUAVA_VERSION@/$(GUAVA_VERSION)/' \ -e 's/@GWT_VERSION@/$(GWT_VERSION)/' \ + -e 's/@GWT_THEME_VERSION@/$(GWT_THEME_VERSION)/' \ -e 's/@HAMCREST_VERSION@/$(HAMCREST_VERSION)/' \ -e 's/@JACKSON_VERSION@/$(JACKSON_VERSION)/' \ -e 's/@JAVASSIST_VERSION@/$(JAVASSIST_VERSION)/' \ @@ -840,6 +843,9 @@ fat-jar-pom.xml: ./fat-jar/fat-jar-pom.xml.in Makefile -e 's/@SLF4J_API_VERSION@/$(SLF4J_API_VERSION)/' \ -e 's/@SUASYNC_VERSION@/$(SUASYNC_VERSION)/' \ -e 's/@ZOOKEEPER_VERSION@/$(ZOOKEEPER_VERSION)/' \ + -e 's/@APACHE_MATH_VERSION@/$(APACHE_MATH_VERSION)/' \ + -e 's/@JEXL_VERSION@/$(JEXL_VERSION)/' \ + -e 's/@JGRAPHT_VERSION@/$(JGRAPHT_VERSION)/' \ -e 's/@spec_title@/$(spec_title)/' \ -e 's/@spec_vendor@/$(spec_vendor)/' \ -e 's/@spec_version@/$(PACKAGE_VERSION)/' \ diff --git a/fat-jar/fat-jar-pom.xml.in b/fat-jar/fat-jar-pom.xml.in index 4e249a8baf..9d2246f601 100644 --- a/fat-jar/fat-jar-pom.xml.in +++ b/fat-jar/fat-jar-pom.xml.in @@ -82,8 +82,12 @@ @MOCKITO_VERSION@ @OBJENESIS_VERSION@ @POWERMOCK_MOCKITO_VERSION@ + @APACHE_MATH_VERSION@ + @JEXL_VERSION@ + @JGRAPHT_VERSION@ @GWT_VERSION@ + @GWT_THEME_VERSION@ 2.1.2 2.5.1 2.1 @@ -157,8 +161,8 @@ build-aux/gen_build_data.sh - target/generated-sources/net/opentsdb/BuildData.java - net.opentsdb + target/generated-sources/net/opentsdb/tools/BuildData.java + net.opentsdb.tools BuildData @@ -263,6 +267,7 @@ + @@ -298,8 +303,11 @@ true - true + true + + net.opentsdb.tools.OpenTSDBMain + WEB-INF/deploy/** @@ -404,7 +412,7 @@ - net.opentsdb.tools.Main + net.opentsdb.tools.OpenTSDBMain @@ -497,6 +505,25 @@ ${asynchbase.version} + + org.apache.commons + commons-math3 + ${apache-math.version} + + + + org.apache.commons + commons-jexl + ${jexl.version} + + + + org.jgrapht + jgrapht-core + ${jgrapht.version} + + + @@ -586,6 +613,13 @@ ${gwt.version} + + net.opentsdb + opentsdb_gwt_theme + ${gwt-theme.version} + + + @@ -622,6 +656,27 @@ + + com.helger.maven + ph-javacc-maven-plugin + 2.8.0 + + + jjc + generate-sources + + javacc + + + 1.6 + true + net.opentsdb.query.expression.parser + ${basedir}/src/ + ${project.build.directory}/generated-sources/ + + + + @@ -661,6 +716,27 @@ + + com.helger.maven + ph-javacc-maven-plugin + 2.8.0 + + + jjc + generate-sources + + javacc + + + 1.6 + true + net.opentsdb.query.expression.parser + ${basedir}/src/ + ${project.build.directory}/generated-sources/ + + + + @@ -674,4 +750,4 @@ 7 - + \ No newline at end of file diff --git a/fat-jar/opentsdb.conf.json b/fat-jar/opentsdb.conf.json index 9b3a283b84..f1ade767c6 100644 --- a/fat-jar/opentsdb.conf.json +++ b/fat-jar/opentsdb.conf.json @@ -24,6 +24,14 @@ "help": "extended", "meta": "BOOL" }, + { + "key": "tsd.core.connections.limit", + "cl-option": "--max-connection", + "defaultValue": 0, + "description": "Sets the maximum number of connections a TSD will handle, additional connections are immediately closed", + "help": "default", + "meta": "POSINT" + }, { "key": "tsd.core.meta.enable_realtime_ts", "cl-option": "--realtime-ts", @@ -79,6 +87,21 @@ "help": "extended", "meta": "POSINT" }, + { + "key": "tsd.core.storage_exception_handler.enable", + "cl-option": "--enable-exhandler", + "defaultValue": "false", + "description": "Whether or not to enable the configured storage exception handler plugin", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.core.storage_exception_handler.plugin ", + "cl-option": "--exhandler-plugin", + "description": "The full class name of the storage exception handler plugin you wish to use", + "help": "extended", + "meta": "CLASS" + }, { "key": "tsd.core.timezone", "cl-option": "--tsd-timezone", @@ -238,6 +261,38 @@ "help": "extended", "meta": "BOOL" }, + { + "key": "tsd.query.allow_simultaneous_duplicates", + "cl-option": "--tsd-allowsimdups", + "defaultValue": "false", + "description": "Whether or not to allow simultaneous duplicate queries from the same host. If disabled, a second query that comes in matching one already running will receive an exception", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.query.filter.expansion_limit", + "cl-option": "--tsd-expansionlimit", + "defaultValue": "4096", + "description": "The maximum number of tag values to include in the regular expression sent to storage during scanning for data. A larger value means more computation on the HBase region servers", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.query.skip_unresolved_tagvs", + "cl-option": "--tsd-skip-unresolved", + "defaultValue": "false", + "description": "Whether or not to continue querying when the query includes a tag value that hasn't been assigned a UID yet and may not exist", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.query.timeout", + "cl-option": "--tsd-query-timeout", + "defaultValue": "0", + "description": "How long, in milliseconds, before canceling a running query. A value of 0 means queries will not timeout", + "help": "extended", + "meta": "POSINT" + }, { "key": "tsd.rpc.plugins", "cl-option": "--rpc-plugins", @@ -283,6 +338,46 @@ "help": "extended", "meta": "BOOL" }, + { + "key": "tsd.storage.compaction.flush_interval", + "cl-option": "--tsd-comp-flush-interval", + "defaultValue": "10", + "description": "How long, in seconds, to wait in between compaction queue flush calls", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.storage.compaction.flush_speed", + "cl-option": "--tsd-comp-flush-speed", + "defaultValue": "2", + "description": "A multiplier used to determine how quickly to attempt flushing the compaction queue. E.g. a value of 2 means it will try to flush the entire queue within 30 minutes. A value of 1 would take an hour", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.storage.compaction.max_concurrent_flushes", + "cl-option": "--tsd-comp-flush-maxconc", + "defaultValue": "10000", + "description": "The maximum number of compaction calls inflight to HBase at any given time", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.storage.compaction.min_flush_threshold", + "cl-option": "--tsd-comp-flush-minflush", + "defaultValue": "100", + "description": "Size of the compaction queue that must be exceeded before flushing is triggered", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.storage.enable_appends", + "cl-option": "--tsd-enable-appends", + "defaultValue": "false", + "description": "Whether or not to append data to columns when writing data points instead of creating new columns for each value. Avoids the need for compactions after each hour but can use more resources on HBase", + "help": "extended", + "meta": "BOOL" + }, { "key": "tsd.storage.enable_compaction", "cl-option": "--compaction", @@ -323,6 +418,14 @@ "help": "extended", "meta": "TABLE" }, + { + "key": "tsd.storage.hbase.prefetch_meta", + "cl-option": "--tsd-prefetch-meta", + "defaultValue": "false", + "description": "Whether or not to prefetch the regions for the TSDB tables before starting the network interface. This can improve performance", + "help": "extended", + "meta": "BOOL" + }, { "key": "tsd.storage.hbase.tree_table", "cl-option": "--treetable", @@ -355,6 +458,60 @@ "help": "default", "meta": "SPEC" }, + { + "key": "tsd.storage.repair_appends", + "cl-option": "--tsd-repair-appends", + "defaultValue": "false", + "description": "Whether or not to re-write appended data point columns at query time when the columns contain duplicate or out of order data", + "help": "extended", + "meta": "BOOL" + }, + { + "key": "tsd.storage.max_tags", + "cl-option": "--tsd-maxtags", + "defaultValue": "8", + "description": "The maximum number of tags allowed per data point. NOTE Please be aware of the performance tradeoffs of overusing tags", + "help": "extended", + "meta": "POSINT" + }, + { + "key": "tsd.storage.salt.buckets", + "cl-option": "--tsd-salt-buckets", + "description": "The number of salt buckets used to distribute load across regions. NOTE Changing this value after writing data may cause TSUID based queries to fail", + "help": "extended", + "meta": "GTZEROINT" + }, + { + "key": "tsd.storage.salt.width", + "cl-option": "--tsd-salt-width", + "description": "The width, in bytes, of the salt prefix used to indicate which bucket a time series belongs in. A value of 0 means salting is disabled. WARNING Do not change after writing data to HBase or you will corrupt your tables and not be able to query any more", + "help": "extended", + "meta": "GTZEROINT" + }, + { + "key": "tsd.storage.uid.width.metric", + "cl-option": "--tsd-width-metric", + "defaultValue": "3", + "description": "The width, in bytes, of metric UIDs. WARNING Do not change after writing data to HBase or you will corrupt your tables and not be able to query any more", + "help": "extended", + "meta": "GTZEROINT" + }, + { + "key": "tsd.storage.uid.width.tagk", + "cl-option": "--tsd-width-tagk", + "defaultValue": "3", + "description": "The width, in bytes, of tag key UIDs. WARNING Do not change after writing data to HBase or you will corrupt your tables and not be able to query any more", + "help": "extended", + "meta": "GTZEROINT" + }, + { + "key": "tsd.storage.uid.width.tagv", + "cl-option": "--tsd-width-tagv", + "defaultValue": "3", + "description": "The width, in bytes, of tag value UIDs. WARNING Do not change after writing data to HBase or you will corrupt your tables and not be able to query any more", + "help": "extended", + "meta": "GTZEROINT" + }, { "key": "tsd.logback.file", "cl-option": "--logback", diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index 3b33d5678b..89f8efbc29 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -28,6 +28,8 @@ import org.apache.commons.jexl2.JexlEngine; import org.apache.commons.jexl2.MapContext; import org.apache.commons.jexl2.Script; +import org.apache.commons.jexl2.scripting.JexlScriptEngineFactory; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,6 +65,9 @@ public class ExpressionIterator implements ITimeSyncedIterator { private static final Logger LOG = LoggerFactory.getLogger(ExpressionIterator.class); + /** This is only here to to force the shade plugin to include the class in the fat-jar */ + private static final JexlScriptEngineFactory JEXL_FACTORY = null; + /** Docs don't say whether this is thread safe or not. SOME methods are marked * as not thread safe, so I assume it's ok to instantiate one of these guys * and keep creating scripts from it. diff --git a/src/tools/ConfigArgP.java b/src/tools/ConfigArgP.java index 99651008a4..aff886766a 100644 --- a/src/tools/ConfigArgP.java +++ b/src/tools/ConfigArgP.java @@ -120,8 +120,11 @@ public ConfigArgP(String...args) { JsonNode configRoot = root.get("config-items"); scriptEngine.eval("var config = " + configRoot.toString() + ";"); processBindings(jsonMapper, root); + // Contains all the defaults final ConfigurationItem[] loadedItems = jsonMapper.reader(ConfigurationItem[].class).readValue(configRoot); + // Contains all the defaults final TreeSet items = new TreeSet(Arrays.asList(loadedItems)); + Map tmpItems = new HashMap(items.size()); for(Iterator iter = items.iterator(); iter.hasNext();) { ConfigurationItem item = iter.next(); @@ -557,7 +560,6 @@ public ConfigurationItem() {} public ConfigurationItem(String key, String clOption, String defaultValue, String description, String help, String meta) { - super(); this.key = key; this.clOption = clOption; this.defaultValue = defaultValue; @@ -571,7 +573,7 @@ public ConfigurationItem(String key, String clOption, */ public void validate() { if(meta!=null && value!=null) { - ConfigMetaType.byName(meta).validate(this); + ConfigMetaType.byName(meta, key).validate(this); } } @@ -585,6 +587,7 @@ public String getName() { return String.format("cl: %s, key: %s", clOption, key); } + /** * Returns the item key name @@ -728,8 +731,9 @@ public int compareTo(final ConfigurationItem other) { if(meta==null || meta.isEmpty()) { return -1; } - final ConfigMetaType otherType = ConfigMetaType.byName(other.meta); - final ConfigMetaType thisType = ConfigMetaType.byName(meta); + + final ConfigMetaType otherType = ConfigMetaType.byName(other.meta, other.key); + final ConfigMetaType thisType = ConfigMetaType.byName(meta, key); int c = thisType.compareTo(otherType); if(c==0) { c = this.key.compareTo(other.key); diff --git a/src/tools/ConfigMetaType.java b/src/tools/ConfigMetaType.java index 3ca90af109..9065a9df98 100644 --- a/src/tools/ConfigMetaType.java +++ b/src/tools/ConfigMetaType.java @@ -91,15 +91,16 @@ public enum ConfigMetaType implements ArgValueValidator { /** * Decodes the passed name with trim and upper * @param name The name to decode + * @param key The key of the item (for error reporting) * @return the decoded value */ - public static ConfigMetaType byName(CharSequence name) { + public static ConfigMetaType byName(CharSequence name, String key) { if(name==null) throw new IllegalArgumentException("Null ConfigMetaType"); String cname = name.toString().toUpperCase().trim(); try { return ConfigMetaType.valueOf(cname); } catch (Exception ex) { - throw new IllegalArgumentException("Invalid ConfigMetaType [" + name + "]"); + throw new IllegalArgumentException("Invalid ConfigMetaType [" + cname + "]. Key: [" + key + "]"); } } diff --git a/src/tools/Main.java b/src/tools/OpenTSDBMain.java similarity index 97% rename from src/tools/Main.java rename to src/tools/OpenTSDBMain.java index 87218ba886..6261949db2 100644 --- a/src/tools/Main.java +++ b/src/tools/OpenTSDBMain.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -42,12 +43,15 @@ import net.opentsdb.tools.ConfigArgP.ConfigurationItem; import net.opentsdb.tsd.PipelineFactory; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.socket.ServerSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioServerBossPool; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; +import org.jboss.netty.channel.socket.nio.NioWorkerPool; import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,11 +67,11 @@ *

    Description: OpenTSDB fat-jar main entry point

    */ -public class Main { +public class OpenTSDBMain { /** The platform EOL string */ public static final String EOL = System.getProperty("line.separator", "\n"); /** Static class logger */ - private static final Logger log = LoggerFactory.getLogger(Main.class); + private static final Logger log = LoggerFactory.getLogger(OpenTSDBMain.class); /** The content prefix */ public static final String CONTENT_PREFIX = "queryui"; @@ -88,7 +92,7 @@ public class Main { tmp.put("import", TextImporter.class); tmp.put("mkmetric", UidManager.class); // -> shift --> set uid assign metrics "$@" tmp.put("query", CliQuery.class); - tmp.put("tsd", Main.class); + tmp.put("tsd", OpenTSDBMain.class); tmp.put("scan", DumpSeries.class); tmp.put("uid", UidManager.class); tmp.put("exportui", UIContentExporter.class); @@ -116,7 +120,7 @@ public static void mainUsage(PrintStream ps) { /** * The OpenTSDB fat-jar main entry point - * @param args See usage banner {@link Main#mainUsage(PrintStream)} + * @param args See usage banner {@link OpenTSDBMain#mainUsage(PrintStream)} */ public static void main(String[] args) { log.info("Starting."); @@ -377,9 +381,12 @@ private static void launchTSD(String[] args) { usage(argp, "Invalid worker thread count", 1); } } - factory = new NioServerSocketChannelFactory( - Executors.newCachedThreadPool(), Executors.newCachedThreadPool(), - workers); + final Executor executor = Executors.newCachedThreadPool(); + final NioServerBossPool boss_pool = + new NioServerBossPool(executor, 1, new Threads.BossThreadNamer()); + final NioWorkerPool worker_pool = new NioWorkerPool(executor, + workers, new Threads.WorkerThreadNamer()); + factory = new NioServerSocketChannelFactory(boss_pool, worker_pool); } else { factory = new OioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); @@ -490,7 +497,7 @@ protected static void setLogbackInternal(final String logFileName, final String log.info("Status Listener: {}", listener); } try { - final URL url = Main.class.getClassLoader().getResource("file-logback.xml"); + final URL url = OpenTSDBMain.class.getClassLoader().getResource("file-logback.xml"); final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); try { final JoranConfigurator configurator = new JoranConfigurator(); From 4da2270482fc3ebaffca86cf74d7769b239c4e48 Mon Sep 17 00:00:00 2001 From: qiubz Date: Sun, 27 Nov 2016 13:37:29 -0800 Subject: [PATCH 580/826] First take at implementing multi-gets for fetching subsets of time series using HBase GetRequests instead of scans. Signed-off-by: Chris Larsen --- src/core/Internal.java | 40 + src/core/SaltMultiGetter.java | 871 ++++++++++++++++++ src/core/TsdbQuery.java | 23 +- src/utils/Config.java | 16 + test/storage/MockBase.java | 34 + ...asynchbase-1.7.1-20151004.015637-1.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.1.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.2.jar.md5 | 1 - ...asynchbase-1.8.0-20161101.210048-3.jar.md5 | 1 - ...asynchbase-1.8.0-20161127.193259-5.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 11 files changed, 984 insertions(+), 7 deletions(-) create mode 100644 src/core/SaltMultiGetter.java delete mode 100644 third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.7.1.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.7.2.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 diff --git a/src/core/Internal.java b/src/core/Internal.java index c558c8035b..229ddba072 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -946,4 +946,44 @@ public static byte[] vleEncodeLong(final long value) { return Bytes.fromLong(value); } } + + /** + * Returns true if the given TSUID matches the row key (accounting for salt and + * timestamp). + * @param tsuid A non-null TSUID array + * @param row_key A non-null row key array + * @return True if the TSUID matches the row key, false if not. + * @throws IllegalArgumentException if the arguments are invalid. + */ + public static boolean rowKeyMatchsTSUID(final byte[] tsuid, + final byte[] row_key) { + if (tsuid == null || row_key == null) { + throw new IllegalArgumentException("Neither tsuid or row key can be null"); + } + if (row_key.length <= tsuid.length) { + throw new IllegalArgumentException("Row key cannot be the same or " + + "shorter than tsuid"); + } + // check on the metric part + int index_tsuid = 0; + int index_row_key = 0; + for (index_tsuid = 0, index_row_key = Const.SALT_WIDTH(); index_tsuid < TSDB + .metrics_width(); ++index_tsuid, ++index_row_key) { + if (tsuid[index_tsuid] != row_key[index_row_key]) { + return false; + } + } + + // check on the tagk tagv part + for (index_tsuid = TSDB.metrics_width(), index_row_key = Const.SALT_WIDTH() + + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; + index_tsuid < tsuid.length; ++index_tsuid, + ++index_row_key) { + if (tsuid[index_tsuid] != row_key[index_row_key]) { + return false; + } + } // end for + + return true; + } } diff --git a/src/core/SaltMultiGetter.java b/src/core/SaltMultiGetter.java new file mode 100644 index 0000000000..48de6f01b3 --- /dev/null +++ b/src/core/SaltMultiGetter.java @@ -0,0 +1,871 @@ +// This file is part of OpenTSDB. +// Copyright (C) 20156 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.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.hbase.async.Bytes.ByteMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import org.hbase.async.Bytes; +import org.hbase.async.GetRequest; +import org.hbase.async.GetResultOrException; +import org.hbase.async.KeyValue; + +import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.rollup.RollupSpan; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; + +public class SaltMultiGetter { + private static final Logger LOG = LoggerFactory.getLogger(SaltMultiGetter.class); + private final TSDB tsdb; + private final byte[] metric; + private final ByteMap tags; + private final long start_row_time; // in sec + private final long end_row_time; // in sec + private final byte[] table_to_fetch; + private final TreeMap spans; + private final long timeout; + private final RollupQuery rollup_query; + private final QueryStats query_stats; + private final int query_index; + private final long max_bytes; + + private long max_pre_filter_dps; + private long max_dps; + private int mulget_wait_cnt; + + //////////////////////////////////////////////////////////////////////////////////////// + private final Map> kvsmap = new ConcurrentHashMap>(); + + private final Map> annotMap = Collections + .synchronizedMap(new TreeMap>(new RowKey.SaltCmp())); + + private final Deferred> results = new Deferred>(); + + private final ArrayList> mul_get_tasks; + private final ArrayList mul_get_indexs; + + //////////////////////////////////////////////////////////////////////////////////////// + private long prepare_multi_get_start_time; + + private long prepare_multi_get_end_time; + + // the timestamp of starting fetching data + private long fetch_start_time; + + // the number of data point fetched + private AtomicLong number_pre_filter_data_point; + + private AtomicLong num_post_filter_data_points; + + // the byte size of the data fetched + private AtomicLong byte_size_feted; + + // the finished multi get number + private AtomicInteger finished_mulget_cnt; + + private AtomicInteger multi_get_seq_id; + + public SaltMultiGetter(TSDB tsdb, + byte[] metric, + ByteMap tags, + final long start_row_time, + final long end_row_time, + final byte[] table_to_fetch, + TreeMap spans, + final long timeout, + final RollupQuery rollup_query, + final QueryStats query_stats, + final int query_index, + final long max_bytes, + final boolean override_count_limit) { + this.tsdb = tsdb; + this.metric = metric; + this.tags = tags; + this.start_row_time = start_row_time; + this.end_row_time = end_row_time; + this.table_to_fetch = table_to_fetch; + this.spans = spans; + this.timeout = timeout; + this.rollup_query = rollup_query; + this.query_stats = query_stats; + this.query_index = query_index; + this.max_bytes = max_bytes; + + if (override_count_limit) { + this.max_pre_filter_dps = 0; + this.max_dps = 0; + } else { + // TODO + //this.max_pre_filter_dps = tsdb.getConfig().getLong("tsd.core.scanner.max_pre_filter_dps"); + //this.max_dps = tsdb.getConfig().max_data_points(); + } + + int concurrency_multi_get = tsdb.config.mul_get_concurrency_number(); + mul_get_tasks = new ArrayList>(concurrency_multi_get); + mul_get_indexs = new ArrayList(concurrency_multi_get); + for (int i = 0; i < concurrency_multi_get; ++i) { + mul_get_tasks.add(new ArrayList()); + mul_get_indexs.add(new AtomicInteger(-1)); + } + + number_pre_filter_data_point = new AtomicLong(0); + num_post_filter_data_points = new AtomicLong(0); + byte_size_feted = new AtomicLong(0); + finished_mulget_cnt = new AtomicInteger(0); + multi_get_seq_id = new AtomicInteger(-1); + } + + final class TSUIDComparator implements Comparator { + + @Override + public int compare(byte[] left, byte[] right) { + for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) { + int a = (left[i] & 0xff); + int b = (right[j] & 0xff); + if (a != b) { + return a - b; + } + } + return left.length - right.length; + } + } + + final class MulgetTask { + private final Set tsuids; + private final List gets; + + public MulgetTask(final Set tsuids, final List gets) { + this.tsuids = tsuids; + this.gets = gets; + } + + public Set getTSUIDs() { + return this.tsuids; + } + + public List getGets() { + return this.gets; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////// + //// Call back to handle result + ///////////////////////////////////////////////////////////////////////////////////////// + final class MulGetCB implements Callback> { + private final int concurrency_index; + private final Set tsuids; + private final List gets; + private final int seq_id; + + private List keyValues = new ArrayList(); + private final Map> annotations = new ConcurrentHashMap>(); + + ///////////////////////////////////////////////////////////////////////////////////////// + // nanosecond times - trace response metrics // + //////////////////////////////////////////////////////////////////////////////////////// + + // the time to start the multi get request + private long mul_get_start_time = -1; + + // cumulation of time waiting on HBase + private long mul_get_time = 0; + + // cumulation of time resolving uid + private long mul_get_uid_resolved_time = 0; + + // how many uids is resolved + private long mul_get_uids_resolved = 0; + + // cumulation of time compacting + private long mul_get_compaction_time = 0; + + // how many data points after filtering + private long mul_get_dps_post_filter = 0; + + // how many rows after filtering + private long mul_get_rows_post_filter = 0; + + // how many rows fetched from hbase + private long mul_get_number_row_fetched = 0; + + // how many cells fetched from hbase + private long mul_get_number_column_fetched = 0; + + // how many bytes fetched from hbase + private long mul_get_number_byte_fetched = 0; + + public MulGetCB(final int concurrency_index, final Set tsuids, final List gets) { + this.concurrency_index = concurrency_index; + this.tsuids = tsuids; + this.gets = gets; + + if (query_stats != null) { + seq_id = multi_get_seq_id.incrementAndGet(); + StringBuilder sb = new StringBuilder(); + sb.append("Mulget_").append(this.concurrency_index).append("_").append(seq_id); + query_stats.addScannerId(query_index, seq_id, sb.toString()); + } else { + seq_id = 0; + } + } + + /** Error callback that will capture an exception from AsyncHBase and store + * it so we can bubble it up to the caller. + */ + class ErrorCb implements Callback { + @Override + public Object call(final Exception e) throws Exception { + LOG.error("Multi get threw an exception : ", e); + close(false); + return null; + } + } + + public Object fetch() { + mul_get_start_time = DateTime.nanoTime(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Try to fetch data for concurrency index: " + + this.concurrency_index + "; with " + this.gets.size() + " gets"); + } + return tsdb.client.get(this.gets) + .addCallback(this) + .addErrback(new ErrorCb()); + } + + /** + * Iterate through each row of the multi get results, parses out data + * points (and optional meta data). + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final List results) throws Exception { + mul_get_time = (DateTime.nanoTime() - this.mul_get_start_time); + + try { + for (final GetResultOrException result : results) { + if (null != result.getCells()) { + ArrayList row = result.getCells(); + if (row.size() == 0) { + continue; + } + + number_pre_filter_data_point.addAndGet(row.size()); + ++mul_get_number_row_fetched; + mul_get_number_column_fetched += row.size(); + + final byte[] key = row.get(0).key(); + final byte[] tsuid_key = UniqueId.getTSUIDFromKey(key, + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + + if (!this.tsuids.contains(tsuid_key)) { + LOG.error("Multi geter fetched the wrong row " + result + " when fetching metric: " + Bytes.pretty(metric)); + continue; + } + + process(key, row); + } else { + // TODO we don't get cells for some get requests + } + } // end for + } catch (Exception e) { + close(true); + return null; + } + + close(true); + return null; + } + + /** + * Handles processing of row of data into the proper list + * @param key The row key, possibly mutated + * @param row The row of KVs to process + * @return True if processing should continue, false if an exception occurred + * or the scanner was already closed (possibly due to another scanner error) + */ + boolean process(final byte[] key, final ArrayList row) { + ++mul_get_rows_post_filter; + num_post_filter_data_points.addAndGet(row.size()); + + List notes = null; + if (annotMap != null) { + notes = annotations.get(key); + + if (notes == null) { + notes = new ArrayList(); + annotations.put(key, notes); + } + } + + if (RollupQuery.isValidQuery(rollup_query)) { + processRollupQuery(key, row, notes); + } else { + processNotRollupQuery(key, row, notes); + } + + return true; + } + + private void processNotRollupQuery(final byte[] key, + final ArrayList row, + List notes) { + KeyValue compacted = null; + try { + final long compaction_start = DateTime.nanoTime(); + compacted = tsdb.compact(row, notes); + + mul_get_compaction_time += (DateTime.nanoTime() - compaction_start); + if (compacted != null) { + final byte[] compact_value = compacted.value(); + final byte[] compact_qualifier = compacted.qualifier(); + + if (compact_qualifier.length % 2 == 0) { + // The length of the qualifier is even so this is a put type + // so the size of the data is the length of the qualifier by 2 + if (compact_value[compact_value.length - 1] == 0) { + // LOG.debug("All data points we have here are either in seconds + // or Ms"); + if (Internal.inMilliseconds(compact_qualifier[0])) { + mul_get_dps_post_filter += compact_qualifier.length / 4; + } else { + mul_get_dps_post_filter += compact_qualifier.length / 2; + } + } else { + // LOG.debug("Data Points we have here are stored in second and Ms + // precision"); + // We wil make a estimate here as iterating over each qualifer + // could be expensive. + // We will just divide the qualifier by 3 to estimate the value + mul_get_dps_post_filter += compact_qualifier.length / 3; + } + } + } + } catch (IllegalDataException idex) { + LOG.error("Caught IllegalDataException exception while parsing the " + "row " + key + ", skipping index", idex); + } + + if (compacted != null) { // Can be null if we ignored all KVs. + keyValues.add(compacted); + } + } + + private void processRollupQuery(final byte[] key, final ArrayList row, List notes) { + for (KeyValue kv : row) { + final byte[] qual = kv.qualifier(); + + if (qual.length > 0) { + // Todo: Bug! Here we shouldn't use the first byte to check the type + // of this row + // Instead should parse the byte array to find the suffix and + // determine the actual type + if (qual[0] == Annotation.PREFIX()) { + // This could be a row with only an annotation in it + final Annotation note = JSON.parseToObject(kv.value(), Annotation.class); + notes.add(note); + } else { + if (rollup_query.getRollupAgg() == Aggregators.AVG || rollup_query.getRollupAgg() == Aggregators.DEV) { + if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 + || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { + keyValues.add(kv); + } + } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), qual, 0, + rollup_query.getRollupAggPrefix().length) == 0) { + keyValues.add(kv); + } + } + } + } // end for + } + + void close(final boolean ok) { + if (LOG.isDebugEnabled()) { + LOG.debug("Finished multiget on concurrency index: " + this.concurrency_index + ", seq id: " + this.seq_id + + ". Fetched rows: " + this.mul_get_number_row_fetched + ", cells: " + this.mul_get_number_column_fetched + + ", mget time(ms): " + this.mul_get_time / 1000000); + } + + if (query_stats != null) { + query_stats.addScannerStat(query_index, seq_id, QueryStat.SCANNER_TIME, + DateTime.nanoTime() - mul_get_start_time); + + // Scanner Stats + query_stats.addScannerStat(query_index, seq_id, QueryStat.ROWS_FROM_STORAGE, this.mul_get_number_row_fetched); + + query_stats.addScannerStat(query_index, seq_id, QueryStat.COLUMNS_FROM_STORAGE, + this.mul_get_number_column_fetched); + + query_stats.addScannerStat(query_index, seq_id, QueryStat.BYTES_FROM_STORAGE, this.mul_get_number_byte_fetched); + + query_stats.addScannerStat(query_index, seq_id, QueryStat.HBASE_TIME, mul_get_time); + query_stats.addScannerStat(query_index, seq_id, QueryStat.SUCCESSFUL_SCAN, ok ? 1 : 0); + + // Post Scan stats + query_stats.addScannerStat(query_index, seq_id, QueryStat.ROWS_POST_FILTER, mul_get_rows_post_filter); + query_stats.addScannerStat(query_index, seq_id, QueryStat.DPS_POST_FILTER, mul_get_dps_post_filter); + query_stats.addScannerStat(query_index, seq_id, QueryStat.SCANNER_UID_TO_STRING_TIME, + mul_get_uid_resolved_time); + query_stats.addScannerStat(query_index, seq_id, QueryStat.UID_PAIRS_RESOLVED, mul_get_uids_resolved); + query_stats.addScannerStat(query_index, seq_id, QueryStat.COMPACTION_TIME, mul_get_compaction_time); + } + + if (ok) { + validateMultigetData(keyValues, annotations); + } else { + finished_mulget_cnt.incrementAndGet(); + } + + // check we have finished all the multi get + if (!checkAllFinishAndTriggerCallback()) { + // check to fire a new multi get in this concurrency bucket + List salt_mul_get_tasks = mul_get_tasks.get(this.concurrency_index); + int task_index = mul_get_indexs.get(this.concurrency_index).incrementAndGet(); + if (task_index < salt_mul_get_tasks.size()) { + MulgetTask task = salt_mul_get_tasks.get(task_index); + MulGetCB mgcb = new MulGetCB(this.concurrency_index, task.getTSUIDs(), task.getGets()); + mgcb.fetch(); + } + } + } + } + + public Deferred> fetch() { + startFetch(); + return this.results; + } + + private void startFetch() { + prepareConcurrentMultiGetTasks(); + int concurrency_number = tsdb.config.mul_get_concurrency_number(); + + // set the time of starting + fetch_start_time = System.currentTimeMillis(); + if (LOG.isDebugEnabled()) { + LOG.debug("Start to fetch data using multiget, there will be " + mulget_wait_cnt + + " multigets to call"); + } + + for (int con_idx = 0; con_idx < concurrency_number; ++con_idx) { + List con_mul_get_tasks = mul_get_tasks.get(con_idx); + int task_index = this.mul_get_indexs.get(con_idx).incrementAndGet(); + + if (task_index < con_mul_get_tasks.size()) { + MulgetTask task = con_mul_get_tasks.get(task_index); + MulGetCB mgcb = new MulGetCB(con_idx, task.getTSUIDs(), task.getGets()); + mgcb.fetch(); + } + } // end for + } + + private void prepareConcurrentMultiGetTasks() { + int batch_size = tsdb.config.mul_get_batch_size(); + int concurrency_number = tsdb.config.mul_get_concurrency_number(); + + mulget_wait_cnt = 0; + prepare_multi_get_start_time = System.currentTimeMillis(); + + // prepare the tagvs combinations and base time list + List tagv_compinations = prepareAllTagvCompounds(); + List row_base_time_list = null; + if (RollupQuery.isValidQuery(rollup_query)) { + row_base_time_list = prepareRowBaseTimesRollup(); + } else { + row_base_time_list = prepareRowBaseTimesNotRollup(); + } + + int next_concurrency_index = 0; + List gets_to_prepare = new ArrayList(batch_size); + Set tsuids = new TreeSet(new TSUIDComparator()); + ByteMap> all_tsuids_gets = prepareGets(tagv_compinations, row_base_time_list); + + for (Map.Entry> gets_entry : all_tsuids_gets) { + byte[] tsuid = gets_entry.getKey(); + List gets = gets_entry.getValue(); + + for (int slice_offset = 0; slice_offset < gets.size();) { + int cur_sz = gets_to_prepare.size(); + int need_sz = batch_size - cur_sz; + int left_sz = gets.size() - slice_offset; + int slice_sz = (left_sz > need_sz ? need_sz : left_sz); + gets_to_prepare.addAll(gets.subList(slice_offset, slice_offset + slice_sz)); + tsuids.add(tsuid); + + // move the offset + slice_offset += slice_sz; + + // a new task is ready, add it to next concurrency task list + if (gets_to_prepare.size() == batch_size) { + MulgetTask task = new MulgetTask(tsuids, gets_to_prepare); + List mulget_task_list = mul_get_tasks.get((next_concurrency_index++) % concurrency_number); + mulget_task_list.add(task); + ++mulget_wait_cnt; + + // prepare a new task list and tsuids + gets_to_prepare = new ArrayList(batch_size); + tsuids = new TreeSet(new TSUIDComparator()); + } // end if + } // end for (int slice_offset) + } // end for (Map.Entry) + + + // add the uncompleted one + if (gets_to_prepare.size() > 0) { + MulgetTask task = new MulgetTask(tsuids, gets_to_prepare); + List mulget_task_list = mul_get_tasks.get((next_concurrency_index++) % concurrency_number); + mulget_task_list.add(task); + ++mulget_wait_cnt; + } + + prepare_multi_get_end_time = System.currentTimeMillis(); + if (LOG.isDebugEnabled()) { + LOG.debug("Finished preparing concurrency multi get task with " + mulget_wait_cnt + " tasks using " + + (prepare_multi_get_end_time - prepare_multi_get_start_time) + "ms"); + } + } + + private void validateMultigetData(List kvs, + Map> annotations) { + int tasks = finished_mulget_cnt.incrementAndGet(); + + if (kvs.size() > 0) { + kvsmap.put(tasks, kvs); + } + + if (annotMap != null) { + for (byte[] key : annotations.keySet()) { + List notes = annotations.get(key); + + if (notes.size() > 0) { + annotMap.put(key, notes); + } + } + } + } + + private boolean checkAllFinishAndTriggerCallback() { + if (this.mulget_wait_cnt == finished_mulget_cnt.get()) { + try { + mergeAndReturnResults(); + } catch (Exception ex) { + LOG.error("Failed merging and returning results, " + "calling back with exception", ex); + + this.results.callback(ex); + } + + return true; + } else { + return false; + } + } + + private void mergeAndReturnResults() { + final long hbase_time = DateTime.currentTimeMillis(); + TsdbQuery.scanlatency.add((int) (hbase_time - this.fetch_start_time)); + if (LOG.isDebugEnabled()) { + LOG.debug("Finished to fetch data for metric: " + Bytes.pretty(this.metric) + + " using " + (hbase_time - this.fetch_start_time) + "ms"); + } + + final long merge_start = DateTime.nanoTime(); + + mergeDataPoints(); + + if (LOG.isDebugEnabled()) { + LOG.debug("It took " + (DateTime.currentTimeMillis() - hbase_time) + " ms, " + + " to merge and sort the rows into a tree map"); + } + + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, (DateTime.nanoTime() - merge_start)); + } + + results.callback(this.spans); + } + + private void mergeDataPoints() { + for (List kvs : kvsmap.values()) { + if (kvs == null || kvs.isEmpty()) { + LOG.error("Found a key value list that was null or empty"); + continue; + } + for (final KeyValue kv : kvs) { + + if (kv == null) { + LOG.error("Found a key value item that was null"); + continue; + } + if (kv.key() == null) { + LOG.error("A key for a kv was null"); + continue; + } + + Span datapoints = null; + try { + datapoints = spans.get(kv.key()); + } catch (RuntimeException e) { + LOG.error("Failed to fetch the span", e); + } + + // If this tsdb follows append logic, then there will not be any + // duplicates here. But if it is not, then there can be multiple + // non-compcated or out of order rows here + if (datapoints == null) { + datapoints = RollupQuery.isValidQuery(rollup_query) ? new RollupSpan(tsdb, this.rollup_query) + : new Span(tsdb); + spans.put(kv.key(), datapoints); + } + + if (annotMap.containsKey(kv.key())) { + for (Annotation note : annotMap.get(kv.key())) { + datapoints.getAnnotations().add(note); + } + annotMap.remove(kv.key()); + } + try { + datapoints.addRow(kv); + } catch (RuntimeException e) { + LOG.error("Exception adding row to span", e); + } + } + } + + kvsmap.clear(); + + for (byte[] key : annotMap.keySet()) { + Span datapoints = (Span) spans.get(key); + + if (datapoints == null) { + datapoints = new Span(tsdb); + spans.put(key, datapoints); + } + + for (Annotation note : annotMap.get(key)) { + datapoints.getAnnotations().add(note); + } + } + + annotMap.clear(); + } + + protected ByteMap> prepareGets(final List tagv_compounds, + final List row_base_time_list) { + ByteMap> tsuids_rows = prepareTsuidRowKeys(tags, tagv_compounds, row_base_time_list); + ByteMap> tsuids_gets = new ByteMap>(); + for (Map.Entry> tsuid_rows : tsuids_rows) { + byte[] tsuid = tsuid_rows.getKey(); + List rows = tsuid_rows.getValue(); + List rows_gets = new ArrayList(); + + for (byte[] row : rows) { + GetRequest get = new GetRequest(this.table_to_fetch, row, TSDB.FAMILY); + rows_gets.add(get); + } // end for + + tsuids_gets.put(tsuid, rows_gets); + } // end for + + return tsuids_gets; + } + + private List prepareRowBaseTimesNotRollup() { + ArrayList row_base_time_list = new ArrayList(); + for (long row_base_time = start_row_time; row_base_time <= end_row_time; row_base_time += Const.MAX_TIMESPAN) { + row_base_time_list.add(row_base_time - row_base_time % Const.MAX_TIMESPAN); + } // end for + + return row_base_time_list; + } + + private List prepareRowBaseTimesRollup() { + RollupInterval interval = rollup_query.getRollupInterval(); + List rows_base_times = new ArrayList(); + + if (interval.getUnits() == 'h') { + int modulo = Const.MAX_TIMESPAN; + if (interval.getUnitMultiplier() > 1) { + modulo = interval.getUnitMultiplier() * 60 * 60; + } + + for (long row_base_time = this.start_row_time; row_base_time <= this.end_row_time; row_base_time += modulo) { + rows_base_times.add(row_base_time - row_base_time % modulo); + } // end for + } else { + Calendar pre_calendar = Calendar.getInstance(Const.UTC_TZ); + pre_calendar.setTimeInMillis(this.start_row_time * 1000); + rows_base_times.add(this.start_row_time); + + while(true) { + final Calendar calendar = Calendar.getInstance(Const.UTC_TZ); + calendar.setTimeInMillis(pre_calendar.getTimeInMillis()); + + // zero out the hour, minutes, seconds + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + switch (interval.getUnits()) { + case 'd': + int day_of_month = pre_calendar.get(Calendar.DAY_OF_MONTH); + calendar.set(Calendar.DAY_OF_MONTH, ++day_of_month); + break; + case 'm': + calendar.set(Calendar.DAY_OF_MONTH, 1); + int month = pre_calendar.get(Calendar.MONTH); + calendar.set(Calendar.MONTH, ++month); + break; + case 'y': + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.set(Calendar.MONTH, 0); // 0 for January + int year = pre_calendar.get(Calendar.YEAR); + calendar.set(Calendar.YEAR, ++year); + break; + default: + throw new IllegalArgumentException("Unrecogznied span: " + interval); + } + + long base_time = (long)(calendar.getTimeInMillis() / 1000); + if (base_time <= this.end_row_time) { + rows_base_times.add(base_time); + + // current calendar becomes the baseline for next one + pre_calendar = calendar; + } else { + break; + } + } // end while + } + + return rows_base_times; + } + + /** + * We have multiple tagks and each tagk may has multiple possible values. + * This routine generates all the possible tagv compounds basing on the + * presence sequence of the tagks. Each compound will be used to generate + * the final tsuid. + * + * @return a list contains all the possible compounds + */ + private List prepareAllTagvCompounds() { + List pre_phase_tags = new LinkedList(); + pre_phase_tags.add(new byte[tags.size()][tsdb.tag_values.width()]); + + List next_phase_tags = new LinkedList(); + int next_append_index = 0; + + for (Map.Entry tag : tags) { + byte[][] tagv = tag.getValue(); + + for (int i = 0; i < tagv.length; ++i) { + for (byte[][] pre_phase_tag : pre_phase_tags) { + byte[][] next_phase_tag = new byte[tags.size()][tsdb.tag_values.width()]; + + // copy the tagv from index 0 ~ next_append_index - 1 + for (int k = 0; k < next_append_index; ++k) { + System.arraycopy(pre_phase_tag[k], 0, next_phase_tag[k], 0, tsdb.tag_values.width()); + } + + // copy the tagv in next_append_index + System.arraycopy(tagv[i], 0, next_phase_tag[next_append_index], 0, tsdb.tag_values.width()); + next_phase_tags.add(next_phase_tag); + } + } // end for + + ++next_append_index; + pre_phase_tags = next_phase_tags; + next_phase_tags = new LinkedList(); + } // end for + return pre_phase_tags; + } + + private ByteMap> prepareTsuidRowKeys(final ByteMap tags, + final List tagv_compounds, final List base_time_list) { + + int row_size = (Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES + + tsdb.tag_names.width() * tags.size() + tsdb.tag_values.width() * tags.size()); + + ByteMap> tsuid_rows = new ByteMap>(); + for (byte[][] tagvs : tagv_compounds) { + byte[] tsuid = new byte[tsdb.metrics.width() + tags.size() * tsdb.tag_names.width() + + tags.size() * tsdb.tag_values.width()]; + List rows = new ArrayList(); + + for (Long row_base_time : base_time_list) { + byte[] row_key = new byte[row_size]; + // salt will prefix basing the hash value of the other part + + // metric + System.arraycopy(metric, 0, row_key, Const.SALT_WIDTH(), tsdb.metrics.width()); + System.arraycopy(metric, 0, tsuid, 0, tsdb.metrics.width()); + + // base time + Internal.setBaseTime(row_key, row_base_time.intValue()); + + // copy tagks and tagvs to the row key + int tag_index = 0; + int row_key_copy_offset = Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES; + int tsuid_copy_offset = tsdb.metrics.width(); + for (Map.Entry tag : tags) { + // tagk + byte[] tagk = tag.getKey(); + System.arraycopy(tagk, 0, row_key, row_key_copy_offset, tsdb.tag_names.width()); + System.arraycopy(tagk, 0, tsuid, tsuid_copy_offset, tsdb.tag_names.width()); + row_key_copy_offset += tsdb.tag_names.width(); + tsuid_copy_offset += tsdb.tag_names.width(); + + // tagv + System.arraycopy(tagvs[tag_index], 0, row_key, row_key_copy_offset, tsdb.tag_values.width()); + System.arraycopy(tagvs[tag_index], 0, tsuid, tsuid_copy_offset, tsdb.tag_values.width()); + row_key_copy_offset += tsdb.tag_values.width(); + tsuid_copy_offset += tsdb.tag_values.width(); + + // move to the next tag + ++tag_index; + } + + // salt + RowKey.prefixKeyWithSalt(row_key); + + rows.add(row_key); + } // end for + + tsuid_rows.put(tsuid, rows); + } // end for + + return tsuid_rows; + } +} diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index cbf916b991..2aa753a39e 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -164,6 +164,8 @@ final class TsdbQuery implements Query { /** Whether or not to match series with ONLY the given tags */ private boolean explicit_tags; + private boolean has_filter_cannot_use_get = false; + /** * Enum for rollup fallback control. * @since 2.4 @@ -570,6 +572,7 @@ private void findGroupBys() { tsdb.getConfig().getInt("tsd.query.filter.expansion_limit")) { LOG.debug("Skipping literals for " + current.getTagk() + " as it exceedes the limit"); + has_filter_cannot_use_get = true; } else { final byte[][] values = new byte[literals.size()][]; literals.keySet().toArray(values); @@ -582,6 +585,7 @@ private void findGroupBys() { } } else { row_key_literals.put(current.getTagkBytes(), null); + has_filter_cannot_use_get = true; } } } @@ -606,8 +610,12 @@ public DataPoints[] run() throws HBaseException { @Override public Deferred runAsync() throws HBaseException { - Deferred result = - findSpans().addCallback(new GroupByAndAggregateCB()); + Deferred result = null; + if (!this.has_filter_cannot_use_get && this.explicit_tags) { + result = this.findSpansWithMultiGetter().addCallback(new GroupByAndAggregateCB()); + } else { + result = findSpans().addCallback(new GroupByAndAggregateCB()); + } if (rollup_usage != null && rollup_usage.fallback()) { result.addCallback(new FallbackRollupOnEmptyResult()); @@ -1024,7 +1032,18 @@ void close(final Exception e) { new ScannerCB().scan(); return results; } + + 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 SaltMultiGetter(tsdb, metric, row_key_literals, getScanStartTimeSeconds(), getScanEndTimeSeconds(), + tableToBeScanned(), spans, 0, rollup_query, query_stats, query_index, 0, + false).fetch(); + } + /** * Callback that should be attached the the output of * {@link TsdbQuery#findSpans} to group and sort the results. diff --git a/src/utils/Config.java b/src/utils/Config.java index ae954365f0..673d19a3a2 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -106,6 +106,10 @@ public class Config { /** tsd.storage.hbase.scanner.maxNumRows */ private int scanner_max_num_rows = 128; + private int mul_get_batch_size = 1024; + + private int mul_get_cocurrency_number = 16; + /** * The list of properties configured to their defaults or modified by users */ @@ -260,6 +264,14 @@ public boolean enable_tree_processing() { return enable_tree_processing; } + public int mul_get_batch_size() { + return mul_get_batch_size; + } + + public int mul_get_concurrency_number() { + return mul_get_cocurrency_number; + } + /** * Allows for modifying properties after creation or loading. * @@ -557,6 +569,8 @@ protected void setDefaults() { + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, " + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since"); default_map.put("tsd.query.timeout", "0"); + default_map.put("tsd.core.mul_get_batch_size", "1024"); + default_map.put("tsd.core.mul_get_cocurrency_number", "20"); for (Map.Entry entry : default_map.entrySet()) { if (!properties.containsKey(entry.getKey())) @@ -670,6 +684,8 @@ public void loadStaticVariables() { enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); + mul_get_batch_size = this.getInt("tsd.core.mul_get_batch_size"); + mul_get_cocurrency_number = this.getInt("tsd.core.mul_get_cocurrency_number"); } /** diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index b84dfa657d..903070f047 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -46,6 +46,7 @@ import org.hbase.async.FilterComparator; import org.hbase.async.FilterList; import org.hbase.async.GetRequest; +import org.hbase.async.GetResultOrException; import org.hbase.async.HBaseClient; import org.hbase.async.KeyRegexpFilter; import org.hbase.async.KeyValue; @@ -135,6 +136,7 @@ public final class MockBase { * @param default_delete Enable the default .delete() mock * @param default_scan Enable the Scanner mock implementation */ + @SuppressWarnings("unchecked") public MockBase( final TSDB tsdb, final HBaseClient client, final boolean default_get, @@ -155,6 +157,8 @@ public MockBase( when(client.get((GetRequest)any())).thenAnswer(new MockGet()); } + when(client.get(any(List.class))).thenAnswer(new MockMultiGet(client)); + // Default put answer will store the given values in the proper location. if (default_put) { when(client.put((PutRequest)any())).thenAnswer(new MockPut()); @@ -985,6 +989,36 @@ public Deferred> answer(InvocationOnMock invocation) } } + /** + * Handles a multi-get call by routing individual requests to the MockGet + */ + private class MockMultiGet implements + Answer>> { + final HBaseClient client; + public MockMultiGet(final HBaseClient client) { + this.client = client; + } + + @Override + public Deferred> answer( + final InvocationOnMock invocation) throws Throwable { + final Object[] args = invocation.getArguments(); + @SuppressWarnings("unchecked") + final List gets = (List) args[0]; + + final List results = Lists.newArrayList(); + for (final GetRequest get : gets) { + try { + // just reuse the logic above. + results.add(new GetResultOrException(client.get(get).join())); + } catch (Exception e) { + results.add(new GetResultOrException(e)); + } + } + return Deferred.fromResult(results); + } + } + /** * Stores one or more columns in a row. If the row does not exist, it's * created. diff --git a/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 b/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 deleted file mode 100644 index 75abc13db6..0000000000 --- a/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -898d34a463b52e570addf0f0160add48 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.1.jar.md5 b/third_party/hbase/asynchbase-1.7.1.jar.md5 deleted file mode 100644 index 45ad0e9669..0000000000 --- a/third_party/hbase/asynchbase-1.7.1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -f236854721eac6d40b6710ec7d59f4a8 diff --git a/third_party/hbase/asynchbase-1.7.2.jar.md5 b/third_party/hbase/asynchbase-1.7.2.jar.md5 deleted file mode 100644 index df83960284..0000000000 --- a/third_party/hbase/asynchbase-1.7.2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -35fdde5a8e6009553e6aab5357ed8896 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 b/third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 deleted file mode 100644 index d9d570137b..0000000000 --- a/third_party/hbase/asynchbase-1.8.0-20161101.210048-3.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -55bd2be1d89b940210bbedfd6e55d856 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 b/third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 new file mode 100644 index 0000000000..cc3290d7d0 --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 @@ -0,0 +1 @@ +857f63ba713c1ba88706ee32085652b0 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index 1987139b08..7ee966a7c2 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.8.0-20161103.193100-4 +ASYNCHBASE_VERSION := 1.8.0-20161127.193259-5 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.8.0-SNAPSHOT/$(ASYNCHBASE_VERSION) From 53cb31e1f5bac277f1e4fd79fddea4b77451ab2d Mon Sep 17 00:00:00 2001 From: SumanjeetBhatti Date: Sun, 4 Dec 2016 16:22:36 -0800 Subject: [PATCH 581/826] Added new aggregator pfsum #851 Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 6 ++++++ src/core/Aggregators.java | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 44f8dfd280..4cb8ba5672 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -704,6 +704,9 @@ public long nextLongValue() { case MIN: r = Long.MIN_VALUE; break; + case PREV: + r = y0; + break; default: throw new IllegalDataException("Invalid interpolation somehow??"); } @@ -769,6 +772,9 @@ public double nextDoubleValue() { case MIN: r = Double.MIN_VALUE; break; + case PREV: + r = y0; + break; default: throw new IllegalDataException("Invalid interploation somehow??"); } diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 18995d7952..7dec97cb42 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -39,13 +39,21 @@ public enum Interpolation { LERP, /* Regular linear interpolation */ ZIM, /* Returns 0 when a data point is missing */ MAX, /* Returns the .MaxValue when a data point is missing */ - MIN /* Returns the .MinValue when a data point is missing */ + MIN, /* Returns the .MinValue when a data point is missing */ + PREV /* Returns the previous value stored, when a data point is missing */ } /** Aggregator that sums up all the data points. */ public static final Aggregator SUM = new Sum( Interpolation.LERP, "sum"); + /** + * Aggregator that sums up all the data points,and uses interpolation where + * previous value is used for data point missing. + */ + public static final Aggregator PFSUM= new Sum( + Interpolation.PREV, "pfsum"); + /** Aggregator that returns the minimum data point. */ public static final Aggregator MIN = new Min( Interpolation.LERP, "min"); @@ -172,6 +180,7 @@ public enum Interpolation { aggregators.put("mimmax", MIMMAX); aggregators.put("first", FIRST); aggregators.put("last", LAST); + aggregators.put("pfsum", PFSUM); PercentileAgg[] percentiles = { p999, p99, p95, p90, p75, p50, From e48ab5d1530121d6ed87c0420d72eb1f09271af6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 4 Dec 2016 16:46:45 -0800 Subject: [PATCH 582/826] Add a unit test for the "psfsum" interpolation method. TODO - we need to make the pfsum a downsampler fill as well. --- test/core/TestAggregationIterator.java | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/core/TestAggregationIterator.java b/test/core/TestAggregationIterator.java index 9cf49fb574..1d1d229873 100644 --- a/test/core/TestAggregationIterator.java +++ b/test/core/TestAggregationIterator.java @@ -285,4 +285,35 @@ public void testAggregate10000Spans() { public void testAggregate250000Spans() { testMeasureAggregationLatency(250000, 10.0); } + + @Test + public void pfsum() { + // TODO - More UTs around this one. + iterators = new SeekableView[] { + SeekableViewsForTest.fromArray(new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME, 40), + //MutableDataPoint.ofLongValue(BASE_TIME + 10000, 50), // skip one + MutableDataPoint.ofLongValue(BASE_TIME + 30000, 70) + }), + SeekableViewsForTest.fromArray(DATA_POINTS_2), + }; + AggregationIterator sgai = AggregationIterator.createForTesting(iterators, + start_time_ms, end_time_ms, SUM, Interpolation.PREV, rate); + // Checks if all the distinct timestamps of both spans appear and missing + // data point of one span for a timestamp of one span was interpolated. + DataPoint[] expected_data_points = new DataPoint[] { + MutableDataPoint.ofLongValue(BASE_TIME, 40), + MutableDataPoint.ofLongValue(BASE_TIME + 10000, 37 + 40), + // 60 is the interpolated value. + MutableDataPoint.ofLongValue(BASE_TIME + 20000, 48 + 40), + MutableDataPoint.ofLongValue(BASE_TIME + 30000, 70) + }; + for (DataPoint expected: expected_data_points) { + assertTrue(sgai.hasNext()); + DataPoint dp = sgai.next(); + assertEquals(expected.timestamp(), dp.timestamp()); + assertEquals(expected.longValue(), dp.longValue()); + } + assertFalse(sgai.hasNext()); + } } From 99cfc5090100b5df303b264cf375477ebcc906ed Mon Sep 17 00:00:00 2001 From: Hao Ziyu Date: Sun, 3 Jul 2016 13:04:03 +0800 Subject: [PATCH 583/826] Filter out duplicate queries Signed-off-by: Hao Ziyu Signed-off-by: Chris Larsen --- src/tsd/HttpJsonSerializer.java | 8 +++++++- src/tsd/QueryRpc.java | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 3a4a48daf6..3bcc6ef4b9 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.LinkedHashSet; import java.util.TreeMap; import org.jboss.netty.buffer.ChannelBuffer; @@ -267,7 +268,12 @@ public TSQuery parseQueryV1() { "Supply valid JSON formatted data in the body of your request"); } try { - return JSON.parseToObject(json, TSQuery.class); + TSQuery data_query = JSON.parseToObject(json, TSQuery.class); + // Filter out duplicate queries + Set query_set = new LinkedHashSet(data_query.getQueries()); + data_query.getQueries().clear(); + data_query.getQueries().addAll(query_set); + return data_query; } catch (IllegalArgumentException iae) { throw new BadRequestException("Unable to parse the given JSON", iae); } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 98342b7379..a607ce4123 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -19,6 +19,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.LinkedHashSet; import java.util.concurrent.atomic.AtomicLong; import org.hbase.async.HBaseException; @@ -597,6 +599,12 @@ public static TSQuery parseQuery(final TSDB tsdb, final HttpQuery query, if (data_query.getQueries() == null || data_query.getQueries().size() < 1) { throw new BadRequestException("Missing sub queries"); } + + // Filter out duplicate queries + Set query_set = new LinkedHashSet(data_query.getQueries()); + data_query.getQueries().clear(); + data_query.getQueries().addAll(query_set); + return data_query; } @@ -924,4 +932,5 @@ public void setTSUIDs(final List tsuids) { this.tsuids = tsuids; } } -} \ No newline at end of file +} + From 76bbe2959080ea537e9a86b7bf23946ae4235877 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 4 Dec 2016 17:20:36 -0800 Subject: [PATCH 584/826] Add a couple of UTs for #828 to validate dupes are blocked. TODO - More UTs including filters, rates and other query options. Signed-off-by: Chris Larsen --- test/tsd/TestQueryRpc.java | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index 0741efb487..b23f3ca523 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -527,7 +527,7 @@ public void executeEmpty() throws Exception { } @Test - public void execute() throws Exception { + public void executeURI() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( @@ -542,6 +542,23 @@ public void execute() throws Exception { assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } + @Test + public void executeURIDuplicates() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.user&m=sum:sys.cpu.user" + + "&m=sum:sys.cpu.user"); + NettyMocks.mockChannelFuture(query); + rpc.execute(tsdb, query); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); + } + @Test public void executeNSU() throws Exception { final DeferredGroupException dge = mock(DeferredGroupException.class); @@ -578,6 +595,41 @@ public void executeWithBadDSFill() throws Exception { } } + @Test + public void executePOST() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.postQuery(tsdb,"/api/query", + "{\"start\":\"1h-ago\",\"queries\":" + + "[{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}]}"); + NettyMocks.mockChannelFuture(query); + rpc.execute(tsdb, query); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); + } + + @Test + public void executePOSTDuplicates() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.postQuery(tsdb,"/api/query", + "{\"start\":\"1h-ago\",\"queries\":" + + "[{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}," + + "{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}]}"); + NettyMocks.mockChannelFuture(query); + rpc.execute(tsdb, query); + final String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); + } + @Test (expected = BadRequestException.class) public void deleteDatapointsBadRequest() throws Exception { HttpQuery query = NettyMocks.deleteQuery(tsdb, From f6eb6d63c5cede6707b83ff6713b740229ed0b20 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 12 Dec 2016 22:42:56 -0800 Subject: [PATCH 585/826] Fix #880 by sorting the custom hash map before creating the storage JSON so that the CAS calls can complete successfully. Thanks @dpellegrino! Signed-off-by: Chris Larsen --- src/meta/Annotation.java | 13 +++++++------ test/meta/TestAnnotation.java | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index cb17f84553..b595ed4057 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; @@ -44,6 +45,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerator; +import com.google.common.annotations.VisibleForTesting; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -512,7 +514,8 @@ public static byte PREFIX() { * successful CAS calls * @return The serialized object as a byte array */ - private byte[] getStorageJSON() { + @VisibleForTesting + byte[] getStorageJSON() { // TODO - precalculate size final ByteArrayOutputStream output = new ByteArrayOutputStream(); try { @@ -528,11 +531,9 @@ private byte[] getStorageJSON() { if (custom == null) { json.writeNullField("custom"); } else { - json.writeObjectFieldStart("custom"); - for (Map.Entry entry : custom.entrySet()) { - json.writeStringField(entry.getKey(), entry.getValue()); - } - json.writeEndObject(); + final TreeMap sorted_custom = + new TreeMap(custom); + json.writeObjectField("custom", sorted_custom); } json.writeEndObject(); diff --git a/test/meta/TestAnnotation.java b/test/meta/TestAnnotation.java index 563ae2c2bd..3da5487744 100644 --- a/test/meta/TestAnnotation.java +++ b/test/meta/TestAnnotation.java @@ -15,8 +15,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.util.List; +import java.util.Map; import net.opentsdb.core.BaseTsdbTest; import net.opentsdb.core.Const; @@ -34,6 +36,8 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.google.common.collect.Maps; + @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @@ -334,6 +338,21 @@ public void syncToStorageNoChanges() throws Exception { note.syncToStorage(tsdb, false).joinUninterruptibly(); } + @Test + public void getStorageJSONTags() throws Exception { + Map custom = Maps.newHashMap(); + custom.put("C", "C"); + custom.put("P", "P"); + custom.put("E", "E"); + System.out.println(custom); + + note.setTSUID(TSUID); + note.setStartTime(1388450562L); + note.setCustom(custom); + final String json = new String(note.getStorageJSON()); + assertTrue(json.contains("{\"C\":\"C\",\"E\":\"E\",\"P\":\"P\"}")); + } + @Test public void delete() throws Exception { setupStorage(false); From cc861b0eae48388a895553f0644168a0098b097f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 12 Dec 2016 22:42:56 -0800 Subject: [PATCH 586/826] Fix #880 by sorting the custom hash map before creating the storage JSON so that the CAS calls can complete successfully. Thanks @dpellegrino! Signed-off-by: Chris Larsen --- src/meta/Annotation.java | 13 +++++++------ test/meta/TestAnnotation.java | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index cb17f84553..b595ed4057 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; @@ -44,6 +45,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonGenerator; +import com.google.common.annotations.VisibleForTesting; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -512,7 +514,8 @@ public static byte PREFIX() { * successful CAS calls * @return The serialized object as a byte array */ - private byte[] getStorageJSON() { + @VisibleForTesting + byte[] getStorageJSON() { // TODO - precalculate size final ByteArrayOutputStream output = new ByteArrayOutputStream(); try { @@ -528,11 +531,9 @@ private byte[] getStorageJSON() { if (custom == null) { json.writeNullField("custom"); } else { - json.writeObjectFieldStart("custom"); - for (Map.Entry entry : custom.entrySet()) { - json.writeStringField(entry.getKey(), entry.getValue()); - } - json.writeEndObject(); + final TreeMap sorted_custom = + new TreeMap(custom); + json.writeObjectField("custom", sorted_custom); } json.writeEndObject(); diff --git a/test/meta/TestAnnotation.java b/test/meta/TestAnnotation.java index 563ae2c2bd..3da5487744 100644 --- a/test/meta/TestAnnotation.java +++ b/test/meta/TestAnnotation.java @@ -15,8 +15,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.util.List; +import java.util.Map; import net.opentsdb.core.BaseTsdbTest; import net.opentsdb.core.Const; @@ -34,6 +36,8 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.google.common.collect.Maps; + @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @@ -334,6 +338,21 @@ public void syncToStorageNoChanges() throws Exception { note.syncToStorage(tsdb, false).joinUninterruptibly(); } + @Test + public void getStorageJSONTags() throws Exception { + Map custom = Maps.newHashMap(); + custom.put("C", "C"); + custom.put("P", "P"); + custom.put("E", "E"); + System.out.println(custom); + + note.setTSUID(TSUID); + note.setStartTime(1388450562L); + note.setCustom(custom); + final String json = new String(note.getStorageJSON()); + assertTrue(json.contains("{\"C\":\"C\",\"E\":\"E\",\"P\":\"P\"}")); + } + @Test public void delete() throws Exception { setupStorage(false); From 53701d14f4b18e3004fcb536d1596935ff2d177c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 13 Dec 2016 21:20:22 -0800 Subject: [PATCH 587/826] Override the full test setup for TestFskSalted as the initialization order can change and cause UTs to fail. Signed-off-by: Chris Larsen --- test/tools/TestFsck.java | 18 ++++----- test/tools/TestFsckSalted.java | 74 +++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 9eea0aa6a3..f99a343cf7 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -64,15 +64,15 @@ public class TestFsck { protected byte[] ROW2 = MockBase.stringToBytes("00000150E23510000001000001"); protected byte[] ROW3 = MockBase.stringToBytes("00000150E24320000001000001"); protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; - private Config config; - private TSDB tsdb = null; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); - private MockBase storage; - private FsckOptions options = mock(FsckOptions.class); - private final static List tags = new ArrayList(1); + protected Config config; + protected TSDB tsdb = null; + protected HBaseClient client = mock(HBaseClient.class); + protected UniqueId metrics = mock(UniqueId.class); + protected UniqueId tag_names = mock(UniqueId.class); + protected UniqueId tag_values = mock(UniqueId.class); + protected MockBase storage; + protected FsckOptions options = mock(FsckOptions.class); + protected final static List tags = new ArrayList(1); static { tags.add(new byte[] { 0, 0, 1, 0, 0, 1}); } diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java index 306dd67c7f..4ccd07ef60 100644 --- a/test/tools/TestFsckSalted.java +++ b/test/tools/TestFsckSalted.java @@ -1,17 +1,29 @@ package net.opentsdb.tools; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; + import org.junit.Before; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; +import com.stumbleupon.async.Deferred; + import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.utils.Config; @PrepareForTest({ Const.class }) public class TestFsckSalted extends TestFsck { @Before - public void beforeLocal() throws Exception { + public void before() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); @@ -22,5 +34,65 @@ public void beforeLocal() throws Exception { ROW2 = MockBase.stringToBytes("0100000150E23510000001000001"); ROW3 = MockBase.stringToBytes("0100000150E24320000001000001"); BAD_KEY = new byte[] { 0x01, 0x00, 0x00, 0x01 }; + + config = new Config(false); + tsdb = new TSDB(client, config); + when(client.flush()).thenReturn(Deferred.fromResult(null)); + + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + + when(options.fix()).thenReturn(false); + when(options.compact()).thenReturn(false); + when(options.resolveDupes()).thenReturn(false); + when(options.lastWriteWins()).thenReturn(false); + when(options.deleteOrphans()).thenReturn(false); + when(options.deleteUnknownColumns()).thenReturn(false); + when(options.deleteBadValues()).thenReturn(false); + when(options.deleteBadRows()).thenReturn(false); + when(options.deleteBadCompacts()).thenReturn(false); + when(options.threads()).thenReturn(1); + + // replace the "real" field objects with mocks + Field met = tsdb.getClass().getDeclaredField("metrics"); + met.setAccessible(true); + met.set(tsdb, metrics); + + Field tagk = tsdb.getClass().getDeclaredField("tag_names"); + tagk.setAccessible(true); + tagk.set(tsdb, tag_names); + + Field tagv = tsdb.getClass().getDeclaredField("tag_values"); + tagv.setAccessible(true); + tagv.set(tsdb, tag_values); + + // mock UniqueId + when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(metrics.getId("sys.cpu.system")) + .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getName(new byte[] { 0, 0, 2 })).thenReturn("sys.cpu.nice"); + when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getName(new byte[] { 0, 0, 1 })).thenReturn("host"); + when(tag_names.getOrCreateId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getId("dc")).thenThrow(new NoSuchUniqueName("dc", "metric")); + when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getName(new byte[] { 0, 0, 1 })).thenReturn("web01"); + when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getName(new byte[] { 0, 0, 2 })).thenReturn("web02"); + when(tag_values.getOrCreateId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getId("web03")) + .thenThrow(new NoSuchUniqueName("web03", "metric")); + + PowerMockito.mockStatic(Tags.class); + when(Tags.resolveIds((TSDB)any(), (ArrayList)any())) + .thenReturn(null); // don't care + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); } } From b0ae20f5d8f5a957af90bbb07caa9957f87106ec Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 12 Dec 2016 23:14:00 -0800 Subject: [PATCH 588/826] Fix #868 by advancing the scanner end time by one second. That will force the scan to the next row and allow the serializer to filter out the unwanted results. Thanks @rcastbergw! Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 2aa753a39e..9f5a50867b 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1411,6 +1411,11 @@ private long getScanEndTimeSeconds() { // Convert to seconds if we have a query in ms. if ((end & Const.SECOND_MASK) != 0L) { end /= 1000L; + if (end - (end * 1000) < 1) { + // handle an edge case where a user may request a ms time between + // 0 and 1 seconds. Just bump it a second. + end++; + } } // The calculation depends on whether we're downsampling. From 5a9870f663f5cbe72e8401f61d54663ca57ff4d6 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 13 Dec 2016 21:20:22 -0800 Subject: [PATCH 589/826] Override the full test setup for TestFskSalted as the initialization order can change and cause UTs to fail. Signed-off-by: Chris Larsen --- test/tools/TestFsck.java | 18 ++++----- test/tools/TestFsckSalted.java | 74 +++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 9eea0aa6a3..f99a343cf7 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -64,15 +64,15 @@ public class TestFsck { protected byte[] ROW2 = MockBase.stringToBytes("00000150E23510000001000001"); protected byte[] ROW3 = MockBase.stringToBytes("00000150E24320000001000001"); protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; - private Config config; - private TSDB tsdb = null; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); - private MockBase storage; - private FsckOptions options = mock(FsckOptions.class); - private final static List tags = new ArrayList(1); + protected Config config; + protected TSDB tsdb = null; + protected HBaseClient client = mock(HBaseClient.class); + protected UniqueId metrics = mock(UniqueId.class); + protected UniqueId tag_names = mock(UniqueId.class); + protected UniqueId tag_values = mock(UniqueId.class); + protected MockBase storage; + protected FsckOptions options = mock(FsckOptions.class); + protected final static List tags = new ArrayList(1); static { tags.add(new byte[] { 0, 0, 1, 0, 0, 1}); } diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java index 306dd67c7f..4ccd07ef60 100644 --- a/test/tools/TestFsckSalted.java +++ b/test/tools/TestFsckSalted.java @@ -1,17 +1,29 @@ package net.opentsdb.tools; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; + import org.junit.Before; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; +import com.stumbleupon.async.Deferred; + import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.utils.Config; @PrepareForTest({ Const.class }) public class TestFsckSalted extends TestFsck { @Before - public void beforeLocal() throws Exception { + public void before() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); @@ -22,5 +34,65 @@ public void beforeLocal() throws Exception { ROW2 = MockBase.stringToBytes("0100000150E23510000001000001"); ROW3 = MockBase.stringToBytes("0100000150E24320000001000001"); BAD_KEY = new byte[] { 0x01, 0x00, 0x00, 0x01 }; + + config = new Config(false); + tsdb = new TSDB(client, config); + when(client.flush()).thenReturn(Deferred.fromResult(null)); + + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + + when(options.fix()).thenReturn(false); + when(options.compact()).thenReturn(false); + when(options.resolveDupes()).thenReturn(false); + when(options.lastWriteWins()).thenReturn(false); + when(options.deleteOrphans()).thenReturn(false); + when(options.deleteUnknownColumns()).thenReturn(false); + when(options.deleteBadValues()).thenReturn(false); + when(options.deleteBadRows()).thenReturn(false); + when(options.deleteBadCompacts()).thenReturn(false); + when(options.threads()).thenReturn(1); + + // replace the "real" field objects with mocks + Field met = tsdb.getClass().getDeclaredField("metrics"); + met.setAccessible(true); + met.set(tsdb, metrics); + + Field tagk = tsdb.getClass().getDeclaredField("tag_names"); + tagk.setAccessible(true); + tagk.set(tsdb, tag_names); + + Field tagv = tsdb.getClass().getDeclaredField("tag_values"); + tagv.setAccessible(true); + tagv.set(tsdb, tag_values); + + // mock UniqueId + when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(metrics.getId("sys.cpu.system")) + .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getName(new byte[] { 0, 0, 2 })).thenReturn("sys.cpu.nice"); + when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getName(new byte[] { 0, 0, 1 })).thenReturn("host"); + when(tag_names.getOrCreateId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getId("dc")).thenThrow(new NoSuchUniqueName("dc", "metric")); + when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getName(new byte[] { 0, 0, 1 })).thenReturn("web01"); + when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getName(new byte[] { 0, 0, 2 })).thenReturn("web02"); + when(tag_values.getOrCreateId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getId("web03")) + .thenThrow(new NoSuchUniqueName("web03", "metric")); + + PowerMockito.mockStatic(Tags.class); + when(Tags.resolveIds((TSDB)any(), (ArrayList)any())) + .thenReturn(null); // don't care + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); } } From 8a3146a5128e0b70555a6d3894c65657c0273a4d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 12 Dec 2016 23:14:00 -0800 Subject: [PATCH 590/826] Fix #868 by advancing the scanner end time by one second. That will force the scan to the next row and allow the serializer to filter out the unwanted results. Thanks @rcastbergw! Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 07a741457a..3489442d57 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1097,6 +1097,11 @@ private long getScanEndTimeSeconds() { // Convert to seconds if we have a query in ms. if ((end & Const.SECOND_MASK) != 0L) { end /= 1000L; + if (end - (end * 1000) < 1) { + // handle an edge case where a user may request a ms time between + // 0 and 1 seconds. Just bump it a second. + end++; + } } // The calculation depends on whether we're downsampling. From 7223183ee06017bee2f0b19eaef25b39e3d2fc15 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 13 Dec 2016 21:20:22 -0800 Subject: [PATCH 591/826] Override the full test setup for TestFskSalted as the initialization order can change and cause UTs to fail. Signed-off-by: Chris Larsen --- test/tools/TestFsck.java | 18 ++++----- test/tools/TestFsckSalted.java | 74 +++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index 9eea0aa6a3..f99a343cf7 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -64,15 +64,15 @@ public class TestFsck { protected byte[] ROW2 = MockBase.stringToBytes("00000150E23510000001000001"); protected byte[] ROW3 = MockBase.stringToBytes("00000150E24320000001000001"); protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; - private Config config; - private TSDB tsdb = null; - private HBaseClient client = mock(HBaseClient.class); - private UniqueId metrics = mock(UniqueId.class); - private UniqueId tag_names = mock(UniqueId.class); - private UniqueId tag_values = mock(UniqueId.class); - private MockBase storage; - private FsckOptions options = mock(FsckOptions.class); - private final static List tags = new ArrayList(1); + protected Config config; + protected TSDB tsdb = null; + protected HBaseClient client = mock(HBaseClient.class); + protected UniqueId metrics = mock(UniqueId.class); + protected UniqueId tag_names = mock(UniqueId.class); + protected UniqueId tag_values = mock(UniqueId.class); + protected MockBase storage; + protected FsckOptions options = mock(FsckOptions.class); + protected final static List tags = new ArrayList(1); static { tags.add(new byte[] { 0, 0, 1, 0, 0, 1}); } diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java index 306dd67c7f..4ccd07ef60 100644 --- a/test/tools/TestFsckSalted.java +++ b/test/tools/TestFsckSalted.java @@ -1,17 +1,29 @@ package net.opentsdb.tools; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; + import org.junit.Before; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; +import com.stumbleupon.async.Deferred; + import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.utils.Config; @PrepareForTest({ Const.class }) public class TestFsckSalted extends TestFsck { @Before - public void beforeLocal() throws Exception { + public void before() throws Exception { PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); @@ -22,5 +34,65 @@ public void beforeLocal() throws Exception { ROW2 = MockBase.stringToBytes("0100000150E23510000001000001"); ROW3 = MockBase.stringToBytes("0100000150E24320000001000001"); BAD_KEY = new byte[] { 0x01, 0x00, 0x00, 0x01 }; + + config = new Config(false); + tsdb = new TSDB(client, config); + when(client.flush()).thenReturn(Deferred.fromResult(null)); + + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + + when(options.fix()).thenReturn(false); + when(options.compact()).thenReturn(false); + when(options.resolveDupes()).thenReturn(false); + when(options.lastWriteWins()).thenReturn(false); + when(options.deleteOrphans()).thenReturn(false); + when(options.deleteUnknownColumns()).thenReturn(false); + when(options.deleteBadValues()).thenReturn(false); + when(options.deleteBadRows()).thenReturn(false); + when(options.deleteBadCompacts()).thenReturn(false); + when(options.threads()).thenReturn(1); + + // replace the "real" field objects with mocks + Field met = tsdb.getClass().getDeclaredField("metrics"); + met.setAccessible(true); + met.set(tsdb, metrics); + + Field tagk = tsdb.getClass().getDeclaredField("tag_names"); + tagk.setAccessible(true); + tagk.set(tsdb, tag_names); + + Field tagv = tsdb.getClass().getDeclaredField("tag_values"); + tagv.setAccessible(true); + tagv.set(tsdb, tag_values); + + // mock UniqueId + when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(metrics.getId("sys.cpu.system")) + .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getName(new byte[] { 0, 0, 2 })).thenReturn("sys.cpu.nice"); + when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getName(new byte[] { 0, 0, 1 })).thenReturn("host"); + when(tag_names.getOrCreateId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getId("dc")).thenThrow(new NoSuchUniqueName("dc", "metric")); + when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getName(new byte[] { 0, 0, 1 })).thenReturn("web01"); + when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getName(new byte[] { 0, 0, 2 })).thenReturn("web02"); + when(tag_values.getOrCreateId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getId("web03")) + .thenThrow(new NoSuchUniqueName("web03", "metric")); + + PowerMockito.mockStatic(Tags.class); + when(Tags.resolveIds((TSDB)any(), (ArrayList)any())) + .thenReturn(null); // don't care + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); } } From 3e1294852691a6e6b10cfaa1e50d39e6c097718b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 12 Dec 2016 23:14:00 -0800 Subject: [PATCH 592/826] Fix #868 by advancing the scanner end time by one second. That will force the scan to the next row and allow the serializer to filter out the unwanted results. Thanks @rcastbergw! Signed-off-by: Chris Larsen --- src/core/TsdbQuery.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 0b64ebe5c6..6348e0ba59 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1063,6 +1063,11 @@ private long getScanEndTimeSeconds() { // Convert to seconds if we have a query in ms. if ((end & Const.SECOND_MASK) != 0L) { end /= 1000L; + if (end - (end * 1000) < 1) { + // handle an edge case where a user may request a ms time between + // 0 and 1 seconds. Just bump it a second. + end++; + } } // The calculation depends on whether we're downsampling. From 8721acd0937bb0b60ad1eafe20c8e8462e327ce7 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Dec 2016 12:32:33 -0800 Subject: [PATCH 593/826] Attempt a fix for #823 wherein writes to the annotations byte map were not synchronized, potentially leading to a race condition and stuck threads. Now we'll send a fresh list to the compaction code if notes were found, synchronize on the local byte map before making any modifications. Thanks to @thatsafunnyname. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index ce4e433c1a..7a7aec7376 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Lists; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -305,8 +306,10 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); - private final Set skips = Collections.newSetFromMap(new ConcurrentHashMap()); - private final Set keepers = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set skips = Collections.newSetFromMap( + new ConcurrentHashMap()); + private final Set keepers = Collections.newSetFromMap( + new ConcurrentHashMap()); private long scanner_start = -1; /** nanosecond timestamps */ @@ -524,12 +527,6 @@ void processRow(final byte[] key, final ArrayList row) { tsdb.getClient().delete(del); } - List notes = annotations.get(key); - if (notes == null) { - notes = new ArrayList(); - annotations.put(key, notes); - } - // calculate estimated data point count. We don't want to deserialize // the byte arrays so we'll just get a rough estimate of compacted // columns. @@ -565,7 +562,18 @@ void processRow(final byte[] key, final ArrayList row) { // the scanner final long compaction_start = DateTime.nanoTime(); try { + final List notes = Lists.newArrayList(); compacted = tsdb.compact(row, notes); + if (!notes.isEmpty()) { + synchronized (annotations) { + List map_notes = annotations.get(key); + if (map_notes == null) { + annotations.put(key, notes); + } else { + map_notes.addAll(notes); + } + } + } } catch (IllegalDataException idex) { compaction_time += (DateTime.nanoTime() - compaction_start); close(false); From 76eb7405ddaeb06f152714bf46181924b8eca85b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Dec 2016 12:32:33 -0800 Subject: [PATCH 594/826] Attempt a fix for #823 wherein writes to the annotations byte map were not synchronized, potentially leading to a race condition and stuck threads. Now we'll send a fresh list to the compaction code if notes were found, synchronize on the local byte map before making any modifications. Thanks to @thatsafunnyname. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 6df18915ee..5a5543cabd 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -40,6 +40,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Lists; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -316,8 +317,10 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); - private final Set skips = Collections.newSetFromMap(new ConcurrentHashMap()); - private final Set keepers = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set skips = Collections.newSetFromMap( + new ConcurrentHashMap()); + private final Set keepers = Collections.newSetFromMap( + new ConcurrentHashMap()); private long scanner_start = -1; /** nanosecond timestamps */ @@ -535,12 +538,6 @@ void processRow(final byte[] key, final ArrayList row) { tsdb.getClient().delete(del); } - List notes = annotations.get(key); - if (notes == null) { - notes = new ArrayList(); - annotations.put(key, notes); - } - //TODO rollup doesn't use the column qualifier prefix right now //Please move this logic to @CompactionQueue.compact API, if the //qualifier prefix is set for rollup. Right now there is no way to @@ -612,7 +609,18 @@ void processRow(final byte[] key, final ArrayList row) { // the scanner final long compaction_start = DateTime.nanoTime(); try { + final List notes = Lists.newArrayList(); compacted = tsdb.compact(row, notes); + if (!notes.isEmpty()) { + synchronized (annotations) { + List map_notes = annotations.get(key); + if (map_notes == null) { + annotations.put(key, notes); + } else { + map_notes.addAll(notes); + } + } + } } catch (IllegalDataException idex) { compaction_time += (DateTime.nanoTime() - compaction_start); close(false); From 6c005ce2c69e5bed6315ac4b64772da2359f9f6a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Dec 2016 12:32:33 -0800 Subject: [PATCH 595/826] Attempt a fix for #823 wherein writes to the annotations byte map were not synchronized, potentially leading to a race condition and stuck threads. Now we'll send a fresh list to the compaction code if notes were found, synchronize on the local byte map before making any modifications. Thanks to @thatsafunnyname. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index ce4e433c1a..7a7aec7376 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Lists; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -305,8 +306,10 @@ final class ScannerCB implements Callback kvs = new ArrayList(); private final ByteMap> annotations = new ByteMap>(); - private final Set skips = Collections.newSetFromMap(new ConcurrentHashMap()); - private final Set keepers = Collections.newSetFromMap(new ConcurrentHashMap()); + private final Set skips = Collections.newSetFromMap( + new ConcurrentHashMap()); + private final Set keepers = Collections.newSetFromMap( + new ConcurrentHashMap()); private long scanner_start = -1; /** nanosecond timestamps */ @@ -524,12 +527,6 @@ void processRow(final byte[] key, final ArrayList row) { tsdb.getClient().delete(del); } - List notes = annotations.get(key); - if (notes == null) { - notes = new ArrayList(); - annotations.put(key, notes); - } - // calculate estimated data point count. We don't want to deserialize // the byte arrays so we'll just get a rough estimate of compacted // columns. @@ -565,7 +562,18 @@ void processRow(final byte[] key, final ArrayList row) { // the scanner final long compaction_start = DateTime.nanoTime(); try { + final List notes = Lists.newArrayList(); compacted = tsdb.compact(row, notes); + if (!notes.isEmpty()) { + synchronized (annotations) { + List map_notes = annotations.get(key); + if (map_notes == null) { + annotations.put(key, notes); + } else { + map_notes.addAll(notes); + } + } + } } catch (IllegalDataException idex) { compaction_time += (DateTime.nanoTime() - compaction_start); close(false); From 2c7a4e23e520d9a4f27d0fae922991ad86d75ff3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 17 Dec 2016 13:29:40 -0800 Subject: [PATCH 596/826] Fix bad merge of 76eb7405ddaeb06f152714bf46181924b8eca85b Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 5a5543cabd..3d44dfab25 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -558,7 +558,14 @@ void processRow(final byte[] key, final ArrayList row) { // This could be a row with only an annotation in it final Annotation note = JSON.parseToObject(kv.value(), Annotation.class); - notes.add(note); + synchronized (annotations) { + List map_notes = annotations.get(key); + if (map_notes == null) { + map_notes = new ArrayList(); + annotations.put(key, map_notes); + } + map_notes.add(note); + } } else { if (rollup_query.getRollupAgg() == Aggregators.AVG || rollup_query.getRollupAgg() == Aggregators.DEV) { @@ -609,18 +616,18 @@ void processRow(final byte[] key, final ArrayList row) { // the scanner final long compaction_start = DateTime.nanoTime(); try { - final List notes = Lists.newArrayList(); + final List notes = Lists.newArrayList(); compacted = tsdb.compact(row, notes); - if (!notes.isEmpty()) { - synchronized (annotations) { - List map_notes = annotations.get(key); - if (map_notes == null) { - annotations.put(key, notes); - } else { - map_notes.addAll(notes); + if (!notes.isEmpty()) { + synchronized (annotations) { + List map_notes = annotations.get(key); + if (map_notes == null) { + annotations.put(key, notes); + } else { + map_notes.addAll(notes); + } } } - } } catch (IllegalDataException idex) { compaction_time += (DateTime.nanoTime() - compaction_start); close(false); From 039bc41cc08af79d4c5e948ec4da0d6b54cc5c61 Mon Sep 17 00:00:00 2001 From: Bryan Hernandez Date: Tue, 20 Dec 2016 14:48:11 -0800 Subject: [PATCH 597/826] Add example classes for queries and writing data. Signed-off-by: Chris Larsen --- src/examples/AddDataExample.java | 174 +++++++++++++++++++++++++++++++ src/examples/QueryExample.java | 166 +++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 src/examples/AddDataExample.java create mode 100644 src/examples/QueryExample.java diff --git a/src/examples/AddDataExample.java b/src/examples/AddDataExample.java new file mode 100644 index 0000000000..a2f951fb39 --- /dev/null +++ b/src/examples/AddDataExample.java @@ -0,0 +1,174 @@ +// 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.examples; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.TSDB; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; + +/** + * Examples for how to add points to the tsdb. + * + */ +public class AddDataExample { + private static String pathToConfigFile; + + public static void processArgs(String[] args) { + // Set these as arguments so you don't have to keep path information in + // source files + if (args == null) { + System.err.println("First (and only) argument must be the full path to the opentsdb.conf file. (e.g. /User/thisUser/opentsdb/src/opentsdb.conf"); + } else { + pathToConfigFile = args[0]; + } + } + + public static void main(String[] args) throws Exception { + + + + processArgs(args); + + // Create a config object with a path to the file for parsing. Or manually + // override settings. + // e.g. config.overrideConfig("tsd.storage.hbase.zk_quorum", "localhost"); + Config config = new Config(pathToConfigFile); + final TSDB tsdb = new TSDB(config); + + // Declare new metric + String metricName = "dummyFromjavaAPI"; + // First check to see it doesn't already exist + byte[] byteMetricUID; // we don't actually need this for the first + // .addPoint() call below. + // TODO: Ideally we could just call a not-yet-implemented tsdb.uIdExists() + // function. + // Note, however, that this is optional. If autometric is enabled, the UID will be assigned in call to addPoint(). + try { + byteMetricUID = tsdb.getUID(UniqueIdType.METRIC, metricName); + } catch (IllegalArgumentException iae) { + System.out.println("Metric name not valid."); + iae.printStackTrace(); + System.exit(1); + } catch (NoSuchUniqueName nsune) { + // If not, great. Create it. + byteMetricUID = tsdb.assignUid("metric", metricName); + } + + // Make a single datum + long timestamp = System.currentTimeMillis(); + long value = 314159; + // Make key-val + Map tags = new HashMap(1); + tags.put("dummy-key", "dummy-val1"); + + + + + // Start timer + long startTime1 = System.currentTimeMillis(); + + int n = 100; + ArrayList> deferreds = new ArrayList>(n); + for (int i = 0; i < n; i++) { + Deferred deferred = tsdb.addPoint(metricName, timestamp, value + i, tags); + deferreds.add(deferred); + + } + + // Add the callbacks to the deferred object. (They might have already + // returned, btw) + // This will cause the calling thread to wait until the add has completed. + + System.out.println("Waiting for deferred result to return..."); + Deferred.groupInOrder(deferreds) + .addErrback(new AddDataExample().new errBack()) + .addCallback(new AddDataExample().new succBack()) + .join(); + + // Block the thread until the deferred returns it's result. +// deferred.join(); + + // End timer. + long elapsedTime1 = System.currentTimeMillis() - startTime1; + System.out.println("\nAdding " + n + " points took: " + elapsedTime1 + + " milliseconds.\n"); + + + // Gracefully shutdown connection to TSDB + tsdb.shutdown(); + + + } + + // This is an optional errorback to handle when there is a failure. + class errBack implements Callback { + public String call(final Exception e) throws Exception { + String message = ">>>>>>>>>>>Failure!>>>>>>>>>>>"; + System.err.println(message); + return message; + } + }; + + // This is an optional success callback to handle when there is a success. + class succBack implements Callback> { + public Object call(ArrayList results) { + for (Object res : results) { + if (res != null) { + if (res.toString().equals("MultiActionSuccess")) { + System.err.println(">>>>>>>>>>>Success!>>>>>>>>>>>"); + } + } else { + System.err.println(">>>>>>>>>>>" + res.getClass() + ">>>>>>>>>>>"); + } + } + return null; + } + }; + + public static long[] makeRandomValues(int num, Random rand) { + long[] values = new long[num]; + for (int i = 0; i < num; i++) { + // define datum + values[i] = rand.nextInt(); + } + return values; + } + + public static long[] makeTimestamps(int num, Random rand) { + + long[] timestamps = new long[num]; + for (int i = 0; i < num; i++) { + // ensures that we won't try to put two data points in the same + // millisecond + try { + Thread.sleep(1); // in millis + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + // define datum + timestamps[i] = System.currentTimeMillis(); + } + return timestamps; + } + +} diff --git a/src/examples/QueryExample.java b/src/examples/QueryExample.java new file mode 100644 index 0000000000..fc053ea46b --- /dev/null +++ b/src/examples/QueryExample.java @@ -0,0 +1,166 @@ +// 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.examples; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.Query; +import net.opentsdb.core.SeekableView; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.core.TSSubQuery; +import net.opentsdb.utils.Config; + +/** + * One example on how to query. + * Taken from this thread + * The metric and key query arguments assume that you've input data from the Quick Start tutorial + * here. + */ +public class QueryExample { + + public static void main(String[] args) throws IOException { + + // Set these as arguments so you don't have to keep path information in + // source files + String pathToConfigFile = args[0]; // e.g. "/User/thisUser/opentsdb/src/opentsdb.conf" + String hostValue = args[1]; // e.g. "myComputerName" + + // Create a config object with a path to the file for parsing. Or manually + // override settings. + // e.g. config.overrideConfig("tsd.storage.hbase.zk_quorum", "localhost"); + Config config; + config = new Config(pathToConfigFile); + final TSDB tsdb = new TSDB(config); + + // main query + final TSQuery query = new TSQuery(); + // use any string format from + // http://opentsdb.net/docs/build/html/user_guide/query/dates.html + query.setStart("1h-ago"); + // Optional: set other global query params + + // at least one sub query required. This is where you specify the metric and + // tags + final TSSubQuery subQuery = new TSSubQuery(); + subQuery.setMetric("proc.loadavg.1m"); + + // tags are optional but you can create and populate a map + final HashMap tags = new HashMap(1); + tags.put("host", hostValue); + subQuery.setTags(tags); + + // you do have to set an aggregator. Just provide the name as a string + subQuery.setAggregator("sum"); + + // IMPORTANT: don't forget to add the subQuery + final ArrayList subQueries = new ArrayList(1); + subQueries.add(subQuery); + query.setQueries(subQueries); + query.setMsResolution(true); // otherwise we aggregate on the second. + + // make sure the query is valid. This will throw exceptions if something + // is missing + query.validateAndSetQuery(); + + // compile the queries into TsdbQuery objects behind the scenes + Query[] tsdbqueries = query.buildQueries(tsdb); + + // create some arrays for storing the results and the async calls + final int nqueries = tsdbqueries.length; + final ArrayList results = new ArrayList( + nqueries); + final ArrayList> deferreds = new ArrayList>( + nqueries); + + // this executes each of the sub queries asynchronously and puts the + // deferred in an array so we can wait for them to complete. + for (int i = 0; i < nqueries; i++) { + deferreds.add(tsdbqueries[i].runAsync()); + } + + // Start timer + long startTime = System.nanoTime(); + + // This is a required callback class to store the results after each + // query has finished + class QueriesCB implements Callback> { + public Object call(final ArrayList queryResults) + throws Exception { + results.addAll(queryResults); + return null; + } + } + + // this will cause the calling thread to wait until ALL of the queries + // have completed. + try { + Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()) + .joinUninterruptibly(); + } catch (Exception e) { + e.printStackTrace(); + } + + // End timer. + long elapsedTime = (System.nanoTime() - startTime) / (1000*1000); + System.out.println("Query returned in: " + elapsedTime + " milliseconds."); + + // now all of the results are in so we just iterate over each set of + // results and do any processing necessary. + for (final DataPoints[] dataSets : results) { + for (final DataPoints data : dataSets) { + System.out.print(data.metricName()); + Map resolvedTags = data.getTags(); + for (final Map.Entry pair : resolvedTags.entrySet()) { + System.out.print(" " + pair.getKey() + "=" + pair.getValue()); + } + System.out.print("\n"); + + final SeekableView it = data.iterator(); + /* + * An important point about SeekableView: + * Because no data is copied during iteration and no new object gets + * created, the DataPoint returned must not be stored and gets + * invalidated as soon as next is called on the iterator (actually it + * doesn't get invalidated but rather its contents changes). If you want + * to store individual data points, you need to copy the timestamp and + * value out of each DataPoint into your own data structures. + * + * In the vast majority of cases, the iterator will be used to go once + * through all the data points, which is why it's not a problem if the + * iterator acts just as a transient "view". Iterating will be very + * cheap since no memory allocation is required (except to instantiate + * the actual iterator at the beginning). + */ + while (it.hasNext()) { + final DataPoint dp = it.next(); + System.out.println(" " + dp.timestamp() + " " + + (dp.isInteger() ? dp.longValue() : dp.doubleValue())); + } + System.out.println(""); + + // Gracefully shutdown connection to TSDB + tsdb.shutdown(); + } + } + } + +} \ No newline at end of file From 8993ccf297c702093ff7caec22aa2bd041350842 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 20 Dec 2016 17:08:44 -0800 Subject: [PATCH 598/826] Cleanup the examples a bit and update the query example with filters from 2.2. Signed-off-by: Chris Larsen --- src/examples/AddDataExample.java | 97 +++++++++++--------------------- src/examples/QueryExample.java | 80 ++++++++++++++++++-------- 2 files changed, 88 insertions(+), 89 deletions(-) diff --git a/src/examples/AddDataExample.java b/src/examples/AddDataExample.java index a2f951fb39..7c16c69ac3 100644 --- a/src/examples/AddDataExample.java +++ b/src/examples/AddDataExample.java @@ -16,7 +16,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import java.util.Random; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -33,36 +32,39 @@ public class AddDataExample { private static String pathToConfigFile; - public static void processArgs(String[] args) { - // Set these as arguments so you don't have to keep path information in + public static void processArgs(final String[] args) { + // Set these as arguments so you don't have to keep path information in // source files - if (args == null) { - System.err.println("First (and only) argument must be the full path to the opentsdb.conf file. (e.g. /User/thisUser/opentsdb/src/opentsdb.conf"); - } else { + if (args != null && args.length > 0) { pathToConfigFile = args[0]; } } - public static void main(String[] args) throws Exception { - - - + public static void main(final String[] args) throws Exception { processArgs(args); // Create a config object with a path to the file for parsing. Or manually // override settings. // e.g. config.overrideConfig("tsd.storage.hbase.zk_quorum", "localhost"); - Config config = new Config(pathToConfigFile); + final Config config; + if (pathToConfigFile != null && !pathToConfigFile.isEmpty()) { + config = new Config(pathToConfigFile); + } else { + // Search for a default config from /etc/opentsdb/opentsdb.conf, etc. + config = new Config(true); + } final TSDB tsdb = new TSDB(config); // Declare new metric - String metricName = "dummyFromjavaAPI"; + String metricName = "my.tsdb.test.metric"; // First check to see it doesn't already exist byte[] byteMetricUID; // we don't actually need this for the first // .addPoint() call below. // TODO: Ideally we could just call a not-yet-implemented tsdb.uIdExists() // function. - // Note, however, that this is optional. If autometric is enabled, the UID will be assigned in call to addPoint(). + // Note, however, that this is optional. If auto metric is enabled + // (tsd.core.auto_create_metrics), the UID will be assigned in call to + // addPoint(). try { byteMetricUID = tsdb.getUID(UniqueIdType.METRIC, metricName); } catch (IllegalArgumentException iae) { @@ -75,100 +77,65 @@ public static void main(String[] args) throws Exception { } // Make a single datum - long timestamp = System.currentTimeMillis(); + long timestamp = System.currentTimeMillis() / 1000; long value = 314159; // Make key-val Map tags = new HashMap(1); - tags.put("dummy-key", "dummy-val1"); - - - + tags.put("script", "example1"); // Start timer long startTime1 = System.currentTimeMillis(); + // Write a number of data points at 30 second intervals. Each write will + // return a deferred (similar to a Java Future or JS Promise) that will + // be called on completion with either a "null" value on success or an + // exception. int n = 100; ArrayList> deferreds = new ArrayList>(n); for (int i = 0; i < n; i++) { Deferred deferred = tsdb.addPoint(metricName, timestamp, value + i, tags); deferreds.add(deferred); - + timestamp += 30; } // Add the callbacks to the deferred object. (They might have already // returned, btw) // This will cause the calling thread to wait until the add has completed. - System.out.println("Waiting for deferred result to return..."); Deferred.groupInOrder(deferreds) .addErrback(new AddDataExample().new errBack()) .addCallback(new AddDataExample().new succBack()) + // Block the thread until the deferred returns it's result. .join(); - - // Block the thread until the deferred returns it's result. -// deferred.join(); + // Alternatively you can add another callback here or use a join with a + // timeout argument. // End timer. long elapsedTime1 = System.currentTimeMillis() - startTime1; System.out.println("\nAdding " + n + " points took: " + elapsedTime1 + " milliseconds.\n"); - - // Gracefully shutdown connection to TSDB - tsdb.shutdown(); - - + // Gracefully shutdown connection to TSDB. This is CRITICAL as it will + // flush any pending operations to HBase. + tsdb.shutdown().join(); } // This is an optional errorback to handle when there is a failure. class errBack implements Callback { public String call(final Exception e) throws Exception { String message = ">>>>>>>>>>>Failure!>>>>>>>>>>>"; - System.err.println(message); + System.err.println(message + " " + e.getMessage()); + e.printStackTrace(); return message; } }; // This is an optional success callback to handle when there is a success. class succBack implements Callback> { - public Object call(ArrayList results) { - for (Object res : results) { - if (res != null) { - if (res.toString().equals("MultiActionSuccess")) { - System.err.println(">>>>>>>>>>>Success!>>>>>>>>>>>"); - } - } else { - System.err.println(">>>>>>>>>>>" + res.getClass() + ">>>>>>>>>>>"); - } - } + public Object call(final ArrayList results) { + System.out.println("Successfully wrote " + results.size() + " data points"); return null; } }; - - public static long[] makeRandomValues(int num, Random rand) { - long[] values = new long[num]; - for (int i = 0; i < num; i++) { - // define datum - values[i] = rand.nextInt(); - } - return values; - } - - public static long[] makeTimestamps(int num, Random rand) { - - long[] timestamps = new long[num]; - for (int i = 0; i < num; i++) { - // ensures that we won't try to put two data points in the same - // millisecond - try { - Thread.sleep(1); // in millis - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - // define datum - timestamps[i] = System.currentTimeMillis(); - } - return timestamps; - } } diff --git a/src/examples/QueryExample.java b/src/examples/QueryExample.java index fc053ea46b..d03b23ea7c 100644 --- a/src/examples/QueryExample.java +++ b/src/examples/QueryExample.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// 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 @@ -14,7 +14,7 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.List; import java.util.Map; import com.stumbleupon.async.Callback; @@ -27,32 +27,41 @@ import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; import net.opentsdb.core.TSSubQuery; +import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; /** * One example on how to query. - * Taken from this thread + * Taken from + * + * this thread * The metric and key query arguments assume that you've input data from the Quick Start tutorial * here. */ public class QueryExample { - public static void main(String[] args) throws IOException { + public static void main(final String[] args) throws IOException { // Set these as arguments so you don't have to keep path information in // source files - String pathToConfigFile = args[0]; // e.g. "/User/thisUser/opentsdb/src/opentsdb.conf" - String hostValue = args[1]; // e.g. "myComputerName" + String pathToConfigFile = (args != null && args.length > 0 ? args[0] : null); // Create a config object with a path to the file for parsing. Or manually // override settings. // e.g. config.overrideConfig("tsd.storage.hbase.zk_quorum", "localhost"); - Config config; - config = new Config(pathToConfigFile); + final Config config; + if (pathToConfigFile != null && !pathToConfigFile.isEmpty()) { + config = new Config(pathToConfigFile); + } else { + // Search for a default config from /etc/opentsdb/opentsdb.conf, etc. + config = new Config(true); + } final TSDB tsdb = new TSDB(config); // main query final TSQuery query = new TSQuery(); + // use any string format from // http://opentsdb.net/docs/build/html/user_guide/query/dates.html query.setStart("1h-ago"); @@ -61,13 +70,18 @@ public static void main(String[] args) throws IOException { // at least one sub query required. This is where you specify the metric and // tags final TSSubQuery subQuery = new TSSubQuery(); - subQuery.setMetric("proc.loadavg.1m"); - - // tags are optional but you can create and populate a map - final HashMap tags = new HashMap(1); - tags.put("host", hostValue); - subQuery.setTags(tags); - + subQuery.setMetric("my.tsdb.test.metric"); + + // filters are optional but useful. + final List filters = new ArrayList(1); + filters.add(new TagVFilter.Builder() + .setType("literal_or") + .setFilter("example1") + .setTagk("script") + .setGroupBy(true) + .build()); + subQuery.setFilters(filters); + // you do have to set an aggregator. Just provide the name as a string subQuery.setAggregator("sum"); @@ -88,8 +102,8 @@ public static void main(String[] args) throws IOException { final int nqueries = tsdbqueries.length; final ArrayList results = new ArrayList( nqueries); - final ArrayList> deferreds = new ArrayList>( - nqueries); + final ArrayList> deferreds = + new ArrayList>(nqueries); // this executes each of the sub queries asynchronously and puts the // deferred in an array so we can wait for them to complete. @@ -98,7 +112,7 @@ public static void main(String[] args) throws IOException { } // Start timer - long startTime = System.nanoTime(); + long startTime = DateTime.nanoTime(); // This is a required callback class to store the results after each // query has finished @@ -109,18 +123,30 @@ public Object call(final ArrayList queryResults) return null; } } + + // Make sure to handle any errors that might crop up + class QueriesEB implements Callback { + @Override + public Object call(final Exception e) throws Exception { + System.err.println("Queries failed"); + e.printStackTrace(); + return null; + } + } // this will cause the calling thread to wait until ALL of the queries // have completed. try { - Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()) - .joinUninterruptibly(); + Deferred.groupInOrder(deferreds) + .addCallback(new QueriesCB()) + .addErrback(new QueriesEB()) + .join(); } catch (Exception e) { e.printStackTrace(); } // End timer. - long elapsedTime = (System.nanoTime() - startTime) / (1000*1000); + double elapsedTime = DateTime.msFromNanoDiff(DateTime.nanoTime(), startTime); System.out.println("Query returned in: " + elapsedTime + " milliseconds."); // now all of the results are in so we just iterate over each set of @@ -156,11 +182,17 @@ public Object call(final ArrayList queryResults) + (dp.isInteger() ? dp.longValue() : dp.doubleValue())); } System.out.println(""); - - // Gracefully shutdown connection to TSDB - tsdb.shutdown(); } } + + // Gracefully shutdown connection to TSDB + try { + tsdb.shutdown().join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } } } \ No newline at end of file From c185b278dfd24fd43348ab8d9b79b247ecb3335e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Thu, 29 Dec 2016 10:34:35 -0800 Subject: [PATCH 599/826] Cut 2.2.2 --- NEWS | 11 +++++++++++ configure.ac | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index b3cdd4cddb..799715d529 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,16 @@ OpenTSDB - User visible changes. +* Version 2.2.2 (2016-12-29) + +Bug Fixes: + - Fix an issue with writing metadata where using custom tags could cause the compare- + and-set to fail due to variable ordering in Java's heap. Now tags are sorted so the + custom tag ordering will be consistent. + - Fix millisecond queries that would miss data the top of the final hour if the end + time was set to 1 second or less than the top of that final hour. + - Fix a concurrent modification issue where salt scanners were not synchronized on the + annotation map and could cause spinning threads. + * Version 2.2.1 (2016-10-08) Noteworthy Changes diff --git a/configure.ac b/configure.ac index c422ff8d93..dd50581898 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.2.1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.2.2], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From d96393f7660791c4212925569f1d195498ea8417 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Mon, 2 Jan 2017 22:52:48 -0600 Subject: [PATCH 600/826] Initial Attempt at Authentication Plugin (#899) Fixes #501 Fixes #667 --- Makefile.am | 2 + src/auth/AuthenticationChannelHandler.java | 104 +++++++++++++++++++++ src/auth/AuthenticationPlugin.java | 99 ++++++++++++++++++++ src/core/TSDB.java | 35 ++++++- src/tsd/PipelineFactory.java | 6 ++ src/utils/Config.java | 2 + 6 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 src/auth/AuthenticationChannelHandler.java create mode 100644 src/auth/AuthenticationPlugin.java diff --git a/Makefile.am b/Makefile.am index ab2a3fa505..883deacb22 100644 --- a/Makefile.am +++ b/Makefile.am @@ -70,6 +70,8 @@ tsdb_SRC := \ src/core/WritableDataPoints.java \ src/core/WriteableDataPointFilterPlugin.java \ src/graph/Plot.java \ + src/auth/AuthenticationChannelHandler.java \ + src/auth/AuthenticationPlugin.java \ src/meta/Annotation.java \ src/meta/MetaDataCache.java \ src/meta/TSMeta.java \ diff --git a/src/auth/AuthenticationChannelHandler.java b/src/auth/AuthenticationChannelHandler.java new file mode 100644 index 0000000000..ca3bce4e68 --- /dev/null +++ b/src/auth/AuthenticationChannelHandler.java @@ -0,0 +1,104 @@ +package net.opentsdb.auth; +// 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 . + +import net.opentsdb.core.TSDB; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; + +import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * @since 2.3 + */ +public class AuthenticationChannelHandler extends SimpleChannelUpstreamHandler { + private static final Logger LOG = LoggerFactory.getLogger(AuthenticationChannelHandler.class); + private TSDB tsdb = null; + private AuthenticationPlugin authentication = null; + + public AuthenticationChannelHandler(TSDB tsdb) { + LOG.info("Setting up AuthenticationChannelHandler"); + this.authentication = tsdb.getAuth(); + if (this.authentication == null) { + LOG.info("No Authentication Plugin Configured"); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent authEvent) { + if (this.authentication == null) { + LOG.info("Attempted to use null authentication plugin. This should not happen"); + LOG.debug("Removing Authentication Handler from Connection"); + ctx.getPipeline().remove(this); + } + try { + final Object authCommand = authEvent.getMessage(); + String authResponse = "AUTH_FAIL\r\n"; + // Telnet Auth + if (authCommand instanceof String[]) { + LOG.debug("Passing auth command to Authentication Plugin"); + if (this.authentication.authenticateTelnet((String[]) authCommand)) { + LOG.debug("Authentication Completed"); + authResponse = "AUTH_SUCCESS.\r\n"; + LOG.debug("Removing Authentication Handler from Connection"); + ctx.getPipeline().remove(this); + } + ChannelFuture future = authEvent.getChannel().write(authResponse); + + // HTTTP Auth + } else if (authCommand instanceof HttpRequest) { + HttpResponseStatus status; + if (this.authentication.authenticateHTTP((HttpRequest) authCommand)) { + LOG.debug("Authentication Completed"); + ctx.getPipeline().remove(this); + } else { + LOG.debug("Authentication Failed"); + status = HttpResponseStatus.FORBIDDEN; + HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); + ChannelFuture future = authEvent.getChannel().write(response); + } + // Unknown Authentication + } else { + LOG.error("Unexpected message type " + + authCommand.getClass() + ": " + authCommand); + } + } catch (Exception e) { + LOG.error("Unexpected exception caught" + + " while serving: " + e); + } + } +} diff --git a/src/auth/AuthenticationPlugin.java b/src/auth/AuthenticationPlugin.java new file mode 100644 index 0000000000..5086221818 --- /dev/null +++ b/src/auth/AuthenticationPlugin.java @@ -0,0 +1,99 @@ +// 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.auth; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; +import com.stumbleupon.async.Deferred; +import org.jboss.netty.handler.codec.http.HttpRequest; + +import java.util.Map; + +/** + * @since 2.3 + */ +public abstract class AuthenticationPlugin { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws Exception if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. 2.0.1. The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + + /** + * Authenticate Telnet connections, provides the first line of the incoming + * connection. + * @param command + * @return returns a Boolean indicating whether or not the incoming connection + * was successfully authenticated. + */ + public abstract Boolean authenticateTelnet(final String[] command); + + /** + * Authenticate HTTP connections, provides the HTTPRequest object for the + * incoming connection. + * @param req + * @return returns a Boolean indicating whether or not the incoming connection + * was successfully authenticated. + */ + public abstract Boolean authenticateHTTP(final HttpRequest req); + + /** + * Allow OpenTSDB to create valid credentials + * @param fields + * @return returns a Boolean indicating whether or not the credentials + * were successfully created. + */ + public abstract Boolean storeCredentials(final Map fields); + + /** + * Allow OpenTSDB to destroy credentials + * @param fields + * @return returns a Boolean indicating whether or not the credentials + * were successfully destroyed. + */ + public abstract Boolean removeCredentials(final Map fields); +} diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 57e591ef07..7d704fc548 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -42,6 +42,7 @@ import org.jboss.netty.util.Timeout; import org.jboss.netty.util.Timer; +import net.opentsdb.auth.AuthenticationPlugin; import net.opentsdb.tree.TreeBuilder; import net.opentsdb.tsd.RTPublisher; import net.opentsdb.tsd.StorageExceptionHandler; @@ -120,7 +121,10 @@ public final class TSDB { */ private final CompactionQueue compactionq; - /** Search indexer to use if configure */ + /** Authentication Plugin to use if configured */ + private AuthenticationPlugin authentication = null; + + /** Search indexer to use if configured */ private SearchPlugin search = null; /** Optional Startup Plugin to use if configured */ @@ -306,6 +310,19 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException("Failed to instantiate filters", e); } + // load the authentication plugin if enabled + if (config.getBoolean("tsd.core.authentication.enable")) { + authentication = PluginLoader.loadSpecificPlugin(config.getString("tsd.core.authentication.plugin"), AuthenticationPlugin.class); + if (authentication == null) { + throw new IllegalArgumentException("Unable to locate authentication plugin: "+ config.getString("tsd.core.authentication.plugin")); + } + try { + authentication.initialize(this); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize authentication plugin", e); + } + } + // load the search plugin if enabled if (config.getBoolean("tsd.search.enable")) { search = PluginLoader.loadSpecificPlugin( @@ -431,7 +448,16 @@ public void initializePlugins(final boolean init_rpcs) { + uid_filter.version()); } } - + + /** + * Returns the configured Authentication Plugin + * @return The Authentication Plugin + * @since 2.3 + */ + public final AuthenticationPlugin getAuth() { + return this.authentication; + } + /** * Returns the configured HBase client * @return The HBase client @@ -1174,6 +1200,11 @@ public Object call(ArrayList compactions) throws Exception { startup.getClass().getCanonicalName()); deferreds.add(startup.shutdown()); } + if (authentication != null) { + LOG.info("Shutting down authentication plugin: " + + authentication.getClass().getCanonicalName()); + deferreds.add(authentication.shutdown()); + } if (search != null) { LOG.info("Shutting down search plugin: " + search.getClass().getCanonicalName()); diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 05414f4491..063f6a3c6f 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -14,6 +14,8 @@ import static org.jboss.netty.channel.Channels.pipeline; +import java.util.concurrent.ThreadFactory; +import net.opentsdb.auth.AuthenticationChannelHandler; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; @@ -160,6 +162,10 @@ protected Object decode(final ChannelHandlerContext ctx, pipeline.addLast("decoder", DECODER); } + if (tsdb.getAuth() != null) { + pipeline.addLast("authentication", new AuthenticationChannelHandler(tsdb)); + } + pipeline.addLast("timeout", timeoutHandler); pipeline.remove(this); pipeline.addLast("handler", rpchandler); diff --git a/src/utils/Config.java b/src/utils/Config.java index 782bf36430..424340b6ef 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -484,6 +484,8 @@ protected void setDefaults() { default_map.put("tsd.network.tcp_no_delay", "true"); default_map.put("tsd.network.keep_alive", "true"); default_map.put("tsd.network.reuse_address", "true"); + default_map.put("tsd.core.authentication.enable", "false"); + default_map.put("tsd.core.authentication.plugin", ""); default_map.put("tsd.core.auto_create_metrics", "false"); default_map.put("tsd.core.auto_create_tagks", "true"); default_map.put("tsd.core.auto_create_tagvs", "true"); From 57a88d3fd5c2d50dbcdc302d9e22235647ce0135 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 10 Jan 2017 10:50:09 -0800 Subject: [PATCH 601/826] Add the RollupDataPointRpc to the RPC manager in read/write mode. Thanks to Sean Zhang! Signed-off-by: Chris Larsen --- src/tsd/RpcManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index f3af3f271f..5dedfe761a 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -260,9 +260,12 @@ private void initializeBuiltinRpcs(final String mode, if (mode.equals("rw") || mode.equals("wo")) { final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + final RollupDataPointRpc rollups = new RollupDataPointRpc(tsdb.getConfig()); telnet.put("put", put); + telnet.put("rollup", rollups); if (enableApi) { http.put("api/put", put); + http.put("api/rollup", rollups); } } From a6a9ec4bc8a526951bc25bb19a145782bafaa8b0 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 28 Jan 2017 15:05:31 -0800 Subject: [PATCH 602/826] Avoid double computing the expressions for the /query/exp endpoint. Also make sure both versions of next() handle booleans. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionIterator.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index d090382bfb..d7dcfececd 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -338,9 +338,9 @@ public ExpressionDataPoint[] next(final long timestamp) { } final Object output = expression.execute(context); if (output instanceof Double) { - result = (Double) expression.execute(context); + result = (Double) output; } else if (output instanceof Boolean) { - result = (((Boolean) expression.execute(context)) ? 1 : 0); + result = (((Boolean) output) ? 1 : 0); } else { throw new IllegalStateException("Expression returned a result of type: " + output.getClass().getName() + " for " + this); @@ -465,7 +465,15 @@ public void next(final int i) { } } } - result = (Double)expression.execute(context); + final Object output = expression.execute(context); + if (output instanceof Double) { + result = (Double) output; + } else if (output instanceof Boolean) { + result = (((Boolean) output) ? 1 : 0); + } else { + throw new IllegalStateException("Expression returned a result of type: " + + output.getClass().getName() + " for " + this); + } dps[i].reset(ts, result); } From 3944a8bf348194e3ff639f08c482473ac6f11713 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 28 Jan 2017 15:05:31 -0800 Subject: [PATCH 603/826] Avoid double computing the expressions for the /query/exp endpoint. Also make sure both versions of next() handle booleans. Signed-off-by: Chris Larsen --- src/query/expression/ExpressionIterator.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index aa1823c801..dc32576708 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -343,9 +343,9 @@ public ExpressionDataPoint[] next(final long timestamp) { } final Object output = expression.execute(context); if (output instanceof Double) { - result = (Double) expression.execute(context); + result = (Double) output; } else if (output instanceof Boolean) { - result = (((Boolean) expression.execute(context)) ? 1 : 0); + result = (((Boolean) output) ? 1 : 0); } else { throw new IllegalStateException("Expression returned a result of type: " + output.getClass().getName() + " for " + this); @@ -470,7 +470,15 @@ public void next(final int i) { } } } - result = (Double)expression.execute(context); + final Object output = expression.execute(context); + if (output instanceof Double) { + result = (Double) output; + } else if (output instanceof Boolean) { + result = (((Boolean) output) ? 1 : 0); + } else { + throw new IllegalStateException("Expression returned a result of type: " + + output.getClass().getName() + " for " + this); + } dps[i].reset(ts, result); } From 2cedd8e394a814fa695e428ccd6d0c9550bf5b1f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 30 Jan 2017 14:34:36 -0800 Subject: [PATCH 604/826] Bump copyright and version on Auth code. Signed-off-by: Chris Larsen --- src/auth/AuthenticationChannelHandler.java | 4 ++-- src/auth/AuthenticationPlugin.java | 4 ++-- src/core/TSDB.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/auth/AuthenticationChannelHandler.java b/src/auth/AuthenticationChannelHandler.java index ca3bce4e68..b23157d410 100644 --- a/src/auth/AuthenticationChannelHandler.java +++ b/src/auth/AuthenticationChannelHandler.java @@ -1,6 +1,6 @@ package net.opentsdb.auth; // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2016 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 @@ -37,7 +37,7 @@ import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** - * @since 2.3 + * @since 2.4 */ public class AuthenticationChannelHandler extends SimpleChannelUpstreamHandler { private static final Logger LOG = LoggerFactory.getLogger(AuthenticationChannelHandler.class); diff --git a/src/auth/AuthenticationPlugin.java b/src/auth/AuthenticationPlugin.java index 5086221818..b187e1a314 100644 --- a/src/auth/AuthenticationPlugin.java +++ b/src/auth/AuthenticationPlugin.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016 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 @@ -20,7 +20,7 @@ import java.util.Map; /** - * @since 2.3 + * @since 2.4 */ public abstract class AuthenticationPlugin { diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 7e856a0783..27c4870f6d 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -503,7 +503,7 @@ public void initializePlugins(final boolean init_rpcs) { /** * Returns the configured Authentication Plugin * @return The Authentication Plugin - * @since 2.3 + * @since 2.4 */ public final AuthenticationPlugin getAuth() { return this.authentication; From b81ef90b6777295e780bbd38ca541f26e97b3648 Mon Sep 17 00:00:00 2001 From: goll Date: Wed, 1 Feb 2017 20:21:17 +0100 Subject: [PATCH 605/826] Bump javassist to 3.21.0-GA Signed-off-by: Chris Larsen --- third_party/javassist/include.mk | 2 +- third_party/javassist/javassist-3.21.0-GA.jar.md5 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/javassist/javassist-3.21.0-GA.jar.md5 diff --git a/third_party/javassist/include.mk b/third_party/javassist/include.mk index 2df2cf6063..e639914f79 100644 --- a/third_party/javassist/include.mk +++ b/third_party/javassist/include.mk @@ -23,7 +23,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -JAVASSIST_VERSION := 3.18.1-GA +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) diff --git a/third_party/javassist/javassist-3.21.0-GA.jar.md5 b/third_party/javassist/javassist-3.21.0-GA.jar.md5 new file mode 100644 index 0000000000..fba94d1657 --- /dev/null +++ b/third_party/javassist/javassist-3.21.0-GA.jar.md5 @@ -0,0 +1 @@ +3dba2305f842c2891df0a0926e18bcfa \ No newline at end of file From e53255d56c6ef11032efc0c35546c5f75a30f3f1 Mon Sep 17 00:00:00 2001 From: goll Date: Wed, 1 Feb 2017 20:21:17 +0100 Subject: [PATCH 606/826] Bump javassist to 3.21.0-GA Signed-off-by: Chris Larsen --- third_party/javassist/include.mk | 2 +- third_party/javassist/javassist-3.21.0-GA.jar.md5 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/javassist/javassist-3.21.0-GA.jar.md5 diff --git a/third_party/javassist/include.mk b/third_party/javassist/include.mk index 2df2cf6063..e639914f79 100644 --- a/third_party/javassist/include.mk +++ b/third_party/javassist/include.mk @@ -23,7 +23,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -JAVASSIST_VERSION := 3.18.1-GA +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) diff --git a/third_party/javassist/javassist-3.21.0-GA.jar.md5 b/third_party/javassist/javassist-3.21.0-GA.jar.md5 new file mode 100644 index 0000000000..fba94d1657 --- /dev/null +++ b/third_party/javassist/javassist-3.21.0-GA.jar.md5 @@ -0,0 +1 @@ +3dba2305f842c2891df0a0926e18bcfa \ No newline at end of file From 88727703feb5456a8c2de734fd500c6b4b7f0ae6 Mon Sep 17 00:00:00 2001 From: hzy001 Date: Tue, 14 Feb 2017 04:48:53 +0800 Subject: [PATCH 607/826] Fix 7 typos in comments (#917) Signed-off-by: Hao Ziyu --- src/core/Aggregators.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 7dec97cb42..a84351dc92 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -82,7 +82,7 @@ public enum Interpolation { public static final Aggregator DEV = new StdDev( Interpolation.LERP, "dev"); - /** Sums data points but will cause the SpanGroup to return a 0 if timesamps + /** Sums data points but will cause the SpanGroup to return a 0 if timestamps * don't line up instead of interpolating. */ public static final Aggregator ZIMSUM = new Sum( Interpolation.ZIM, "zimsum"); @@ -107,7 +107,7 @@ public enum Interpolation { /** Aggregator that returns the first data point. */ public static final Aggregator FIRST = new First(Interpolation.ZIM, "first"); - /** Aggregator that returns the first data point. */ + /** Aggregator that returns the last data point. */ public static final Aggregator LAST = new Last(Interpolation.ZIM, "last"); /** Maps an aggregator name to its instance. */ @@ -119,7 +119,7 @@ public enum Interpolation { public static final PercentileAgg p99 = new PercentileAgg(99d, "p99"); /** Aggregator that returns 95th percentile. */ public static final PercentileAgg p95 = new PercentileAgg(95d, "p95"); - /** Aggregator that returns 99th percentile. */ + /** Aggregator that returns 90th percentile. */ public static final PercentileAgg p90 = new PercentileAgg(90d, "p90"); /** Aggregator that returns 75th percentile. */ public static final PercentileAgg p75 = new PercentileAgg(75d, "p75"); @@ -135,10 +135,10 @@ public enum Interpolation { /** Aggregator that returns estimated 95th percentile. */ public static final PercentileAgg ep95r3 = new PercentileAgg(95d, "ep95r3", EstimationType.R_3); - /** Aggregator that returns estimated 75th percentile. */ + /** Aggregator that returns estimated 90th percentile. */ public static final PercentileAgg ep90r3 = new PercentileAgg(90d, "ep90r3", EstimationType.R_3); - /** Aggregator that returns estimated 50th percentile. */ + /** Aggregator that returns estimated 75th percentile. */ public static final PercentileAgg ep75r3 = new PercentileAgg(75d, "ep75r3", EstimationType.R_3); /** Aggregator that returns estimated 50th percentile. */ @@ -154,10 +154,10 @@ public enum Interpolation { /** Aggregator that returns estimated 95th percentile. */ public static final PercentileAgg ep95r7 = new PercentileAgg(95d, "ep95r7", EstimationType.R_7); - /** Aggregator that returns estimated 75th percentile. */ + /** Aggregator that returns estimated 90th percentile. */ public static final PercentileAgg ep90r7 = new PercentileAgg(90d, "ep90r7", EstimationType.R_7); - /** Aggregator that returns estimated 50th percentile. */ + /** Aggregator that returns estimated 75th percentile. */ public static final PercentileAgg ep75r7 = new PercentileAgg(75d, "ep75r7", EstimationType.R_7); /** Aggregator that returns estimated 50th percentile. */ From b3b2bcfedcb7aca82834716f12ab7b1cbecd4cd5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 11 Mar 2017 10:56:48 -0800 Subject: [PATCH 608/826] Fix #915 by simply copying the entire tools directory into the destination directory. Thanks @shyamraj242. Signed-off-by: Chris Larsen --- Makefile.am | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index ab2a3fa505..1ae0a175e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -601,11 +601,13 @@ install-data-tools: $(tsdb_DEPS) $(jar) destdatatoolsdir="$(DESTDIR)$(pkgdatadir)/tools" ; \ echo " $(mkdir_p) $$destdatatoolsdir"; \ $(mkdir_p) "$$destdatatoolsdir" || exit 1; \ - tools="$$tools $(top_srcdir)/tools/*" ; \ tools="$$tools $(top_srcdir)/src/create_table.sh" ; \ tools="$$tools $(top_srcdir)/src/upgrade_1to2.sh" ; \ echo " $(INSTALL_SCRIPT)" $$tools "$$destdatatoolsdir" ; \ - $(INSTALL_SCRIPT) $$tools "$$destdatatoolsdir" || exit 1; + $(INSTALL_SCRIPT) $$tools "$$destdatatoolsdir" || exit 1; \ + tools="-r $(top_srcdir)/tools/*" ; \ + echo " cp" $$tools "$$destdatatoolsdir" ; \ + cp $$tools "$$destdatatoolsdir" || exit 1; uninstall-data-tools: @$(NORMAL_UNINSTALL) From cfe1bed7a15567c116a7ecf51b45fe2ebf64980a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 11 Mar 2017 10:56:48 -0800 Subject: [PATCH 609/826] Fix #915 by simply copying the entire tools directory into the destination directory. Thanks @shyamraj242. Signed-off-by: Chris Larsen --- Makefile.am | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 367c74509f..b6b6d7089e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -608,11 +608,13 @@ install-data-tools: $(tsdb_DEPS) $(jar) destdatatoolsdir="$(DESTDIR)$(pkgdatadir)/tools" ; \ echo " $(mkdir_p) $$destdatatoolsdir"; \ $(mkdir_p) "$$destdatatoolsdir" || exit 1; \ - tools="$$tools $(top_srcdir)/tools/*" ; \ tools="$$tools $(top_srcdir)/src/create_table.sh" ; \ tools="$$tools $(top_srcdir)/src/upgrade_1to2.sh" ; \ echo " $(INSTALL_SCRIPT)" $$tools "$$destdatatoolsdir" ; \ - $(INSTALL_SCRIPT) $$tools "$$destdatatoolsdir" || exit 1; + $(INSTALL_SCRIPT) $$tools "$$destdatatoolsdir" || exit 1; \ + tools="-r $(top_srcdir)/tools/*" ; \ + echo " cp" $$tools "$$destdatatoolsdir" ; \ + cp $$tools "$$destdatatoolsdir" || exit 1; uninstall-data-tools: @$(NORMAL_UNINSTALL) From df8207322c79947bf82eb560b6c3e936876a6390 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 11 Mar 2017 11:35:10 -0800 Subject: [PATCH 610/826] Fix makefile to include new Rollup files and fix the AsyncHBase download. Thanks to @SeanZhang. Signed-off-by: Chris Larsen --- Makefile.am | 20 +++++++++++++++++++- third_party/hbase/include.mk | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index b6b6d7089e..f607173b04 100644 --- a/Makefile.am +++ b/Makefile.am @@ -59,6 +59,7 @@ tsdb_SRC := \ src/core/RowKey.java \ src/core/RowSeq.java \ src/core/SaltScanner.java \ + src/core/SaltMultiGetter.java \ src/core/SeekableView.java \ src/core/Span.java \ src/core/SpanGroup.java \ @@ -169,6 +170,7 @@ tsdb_SRC := \ src/tsd/PutDataPointRpc.java \ src/tsd/QueryExecutor.java \ src/tsd/QueryRpc.java \ + src/tsd/RollupDataPointRpc.java \ src/tsd/RpcHandler.java \ src/tsd/RpcPlugin.java \ src/tsd/RpcManager.java \ @@ -200,7 +202,17 @@ tsdb_SRC := \ src/utils/JSONException.java \ src/utils/Pair.java \ src/utils/PluginLoader.java \ - src/utils/Threads.java + src/utils/Threads.java \ + src/core/iRowSeq.java \ + src/rollup/NoSuchRollupForIntervalException.java \ + src/rollup/NoSuchRollupForTableException.java \ + src/rollup/RollUpDataPoint.java \ + src/rollup/RollupConfig.java \ + src/rollup/RollupInterval.java \ + src/rollup/RollupQuery.java \ + src/rollup/RollupSeq.java \ + src/rollup/RollupSpan.java \ + src/rollup/RollupUtils.java tsdb_DEPS = \ $(COMMONS_LOGGING) \ @@ -318,6 +330,10 @@ test_SRC := \ test/query/pojo/TestOutput.java \ test/query/pojo/TestQuery.java \ test/query/pojo/TestTimeSpan.java \ + test/rollup/TestRollupConfig.java \ + test/rollup/TestRollupInterval.java \ + test/rollup/TestRollupSeq.java \ + test/rollup/TestRollupUtils.java \ test/search/TestSearchPlugin.java \ test/search/TestSearchQuery.java \ test/search/TestTimeSeriesLookup.java \ @@ -336,6 +352,7 @@ test_SRC := \ test/tree/TestTree.java \ test/tree/TestTreeBuilder.java \ test/tree/TestTreeRule.java \ + test/tsd/BaseTestPutRpc.java \ test/tsd/NettyMocks.java \ test/tsd/TestAnnotationRpc.java \ test/tsd/TestGraphHandler.java \ @@ -346,6 +363,7 @@ test_SRC := \ test/tsd/TestQueryExecutor.java \ test/tsd/TestQueryRpc.java \ test/tsd/TestQueryRpcLastDataPoint.java \ + test/tsd/TestRollupRpc.java \ test/tsd/TestRpcHandler.java \ test/tsd/TestRpcPlugin.java \ test/tsd/TestRpcManager.java \ diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index 7ee966a7c2..e46d86205a 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -15,7 +15,7 @@ ASYNCHBASE_VERSION := 1.8.0-20161127.193259-5 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar -ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.8.0-SNAPSHOT/$(ASYNCHBASE_VERSION) +ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.8.0-SNAPSHOT/ $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From c2090c943ea896d8ab0b0ae84241d9682c7d044e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 11 Mar 2017 12:59:57 -0800 Subject: [PATCH 611/826] Modify TsdbQuery to route queries with _aggregate != RAW to the proper agg table. Signed-off-by: Chris Larsen --- src/core/TSDB.java | 12 +++++++ src/core/TsdbQuery.java | 15 +++++++-- test/core/TestTsdbQueryQueries.java | 52 +++++++++++++++++++++++++++++ test/core/TestTsdbQueryRollup.java | 36 ++++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 27c4870f6d..8fae8074cb 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1963,6 +1963,18 @@ public Timer getTimer() { return timer; } + /** @return The aggregate tag key if set. May be null. + * @since 2.4 */ + public String getAggTagKey() { + return agg_tag_key; + } + + /** @return The raw tag value if set. May be null. + * @since 2.4 */ + public String getRawTagValue() { + return raw_agg_tag_value; + } + // ------------------ // // Compaction helpers // // ------------------ // diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 9f5a50867b..c17548ff6b 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -47,6 +47,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVLiteralOrFilter; import net.opentsdb.rollup.NoSuchRollupForIntervalException; import net.opentsdb.rollup.RollupInterval; import net.opentsdb.rollup.RollupQuery; @@ -474,6 +475,16 @@ public Deferred call(final byte[] uid) throws Exception { final List> deferreds = new ArrayList>(filters.size()); for (final TagVFilter filter : filters) { + // determine if the user is asking for pre-agg data + if (filter instanceof TagVLiteralOrFilter && tsdb.getAggTagKey() != null) { + if (filter.getTagk().equals(tsdb.getAggTagKey())) { + if (tsdb.getRawTagValue() != null && + !filter.getFilter().equals(tsdb.getRawTagValue())) { + pre_aggregate = true; + } + } + } + deferreds.add(filter.resolveTagkName(tsdb)); } return Deferred.group(deferreds).addCallback(new FilterCB()); @@ -1287,13 +1298,13 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { (int) getScanStartTimeSeconds(), end_time == UNSET ? -1 // Will scan until the end (0xFFF...). : (int) getScanEndTimeSeconds(), - is_rollup ? rollup_query.getRollupInterval().getTemporalTable() : tsdb.table, + tableToBeScanned(), TSDB.FAMILY()); if (tsuids != null && !tsuids.isEmpty()) { createAndSetTSUIDFilter(scanner); } else if (filters.size() > 0) { createAndSetFilter(scanner); - } + } if (is_rollup) { ScanFilter existing = scanner.getFilter(); diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 4deaea7daf..aeffbc946c 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -13,6 +13,7 @@ package net.opentsdb.core; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -25,10 +26,13 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; import net.opentsdb.storage.MockBase; import net.opentsdb.storage.MockBase.MockScanner; import net.opentsdb.uid.NoSuchUniqueId; @@ -42,6 +46,7 @@ import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import com.stumbleupon.async.Deferred; @@ -1488,6 +1493,53 @@ public void runRegexpNoMatch() throws Exception { assertEquals(0, dps.length); } + @Test + public void runPreAggregate() 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 = 1356998400L; + 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")); + Whitebox.setInternalState(tsdb, "default_interval", new RollupInterval("tsdb", + "tsdb-agg", "1m", "1h", true)); + + tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, tags, true, null, + null, "SUM"); + + tags.put(config.getString("tsd.rollups.agg_tag_key"), "SUM"); + TSQuery ts_query = new TSQuery(); + ts_query.setStart("1356998400"); + ts_query.setEnd("1357041600"); + + final TSSubQuery sub = new TSSubQuery(); + sub.setMetric(METRIC_STRING); + sub.setTags(new HashMap(tags)); + sub.setAggregator("sum"); + + ts_query.setQueries(Arrays.asList(sub)); + ts_query.validateAndSetQuery(); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + 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(); + assertTrue(dp.isInteger()); + assertEquals(42, dp.longValue()); + assertEquals(ts, dp.timestamp()); + assertEquals(1, dps[0].size()); + } + @Test public void filterExplicitTagsOK() throws Exception { tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); diff --git a/test/core/TestTsdbQueryRollup.java b/test/core/TestTsdbQueryRollup.java index 52484ffda1..9325d480e4 100644 --- a/test/core/TestTsdbQueryRollup.java +++ b/test/core/TestTsdbQueryRollup.java @@ -808,6 +808,42 @@ public void runDupes() throws Exception { assertEquals(42.5F, dp.toDouble(), 0.0001); } + @Test + public void runRollupPreAgg() throws Exception { + setupGroupByTagValues(); + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400L; + long end_timestamp = 1357041600L; + storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); + 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")); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42L, tags, true, "10m", + "SUM", "SUM"); + + tags.put(config.getString("tsd.rollups.agg_tag_key"), "SUM"); + + setQuery(interval.getStringInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + 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(); + assertFalse(dp.isInteger()); + assertEquals(42, dp.doubleValue(), 0.0001); + assertEquals(ts, dp.timestamp()); + assertEquals(1, dps[0].size()); + } + // ----------------- // // Helper functions. // // ----------------- // From a61a24fb32d751aa9684fe5be97e1dcf107bb7bc Mon Sep 17 00:00:00 2001 From: Misha Brukman Date: Wed, 15 Mar 2017 12:37:19 -0400 Subject: [PATCH 612/826] Update Travis config: drop OpenJDK 6, use Trusty. (#935) * remove OpenJDK 6 as it's EOLed as of 2016-12-31; more info: http://mail.openjdk.java.net/pipermail/jdk6-dev/2016-October/003606.html * use Trusty, a more recent distro than Precise. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fccf400e41..be2280bb9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: java +dist: trusty before_script: ./build.sh pom.xml script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet addons: hostname: short-hostname jdk: - oraclejdk7 - - openjdk6 - oraclejdk8 notifications: - email: false + email: false From 4840f71b3065a7739c9f54389ff0228ef09cc271 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 2 Apr 2017 13:23:37 -0700 Subject: [PATCH 613/826] Fix up the Rollup queries to properly handle downsampling and counts. It will now honor the downsampling method specified on top of rollup data. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 8 ++- src/core/Downsampler.java | 82 +++++++++++++++++++-------- src/core/FillingDownsampler.java | 76 ++++++++++++++++++------- src/core/SaltMultiGetter.java | 2 +- src/core/SaltScanner.java | 4 +- src/core/Span.java | 9 +-- src/core/SpanGroup.java | 15 ++--- src/core/TsdbQuery.java | 37 +++++++----- src/rollup/RollupQuery.java | 44 +++++++++++--- src/rollup/RollupSeq.java | 12 ++-- test/core/TestDownsampler.java | 26 +++++++-- test/core/TestFillingDownsampler.java | 44 +++++++++++--- test/core/TestRollupSpan.java | 2 +- test/rollup/TestRollupSeq.java | 53 ++++++++--------- 14 files changed, 287 insertions(+), 127 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 4cb8ba5672..b58d121232 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -19,6 +19,8 @@ import com.google.common.annotations.VisibleForTesting; import net.opentsdb.core.Aggregators.Interpolation; +import net.opentsdb.rollup.RollupQuery; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -342,7 +344,7 @@ public static AggregationIterator create(final List spans, * of the actual values. * @param rate_options Specifies the optional additional rate calculation * options. - * @param is_rollup Whether or not the query is handling rollup data. + * @param rollup_query An optional rollup query. * @return an AggregationIterator * @since 2.4 */ @@ -356,7 +358,7 @@ public static AggregationIterator create(final List spans, final long query_end, final boolean rate, final RateOptions rate_options, - final boolean is_rollup) { + final RollupQuery rollup_query) { final int size = spans.size(); final SeekableView[] iterators = new SeekableView[size]; for (int i = 0; i < size; i++) { @@ -366,7 +368,7 @@ public static AggregationIterator create(final List spans, it = spans.get(i).spanIterator(); } else { it = spans.get(i).downsampler(start_time, end_time, downsampler, - query_start, query_end, is_rollup); + query_start, query_end, rollup_query); } if (rate) { it = new RateSpan(it, rate_options); diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 8d77de36f3..945f929312 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -12,9 +12,14 @@ // see . package net.opentsdb.core; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; +import net.opentsdb.core.Aggregator.Doubles; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.utils.DateTime; /** @@ -48,8 +53,8 @@ public class Downsampler implements SeekableView, DataPoint { /** Last value as a double */ protected double value; - /** Whether or not the downsampling is on rolled up data */ - protected boolean is_rollup; + /** An optional rollup query. */ + protected RollupQuery rollup_query; /** Whether or not to merge all DPs in the source into one vaalue */ protected final boolean run_all; @@ -98,7 +103,7 @@ public class Downsampler implements SeekableView, DataPoint { final long query_start, final long query_end ) { - this(source, specification, query_start, query_end, false); + this(source, specification, query_start, query_end, null); } /** @@ -107,21 +112,21 @@ public class Downsampler implements SeekableView, DataPoint { * @param specification The downsampling spec to use * @param query_start The start timestamp of the actual query for use with "all" * @param query_end The end timestamp of the actual query for use with "all" - * @param is_rollup Whether or not this query is handling rollup data. + * @param rollup_query An optional rollup query. * @since 2.4 */ Downsampler(final SeekableView source, final DownsamplingSpecification specification, final long query_start, final long query_end, - final boolean is_rollup + final RollupQuery rollup_query ) { this.source = source; this.specification = specification; values_in_interval = new ValuesInInterval(); this.query_start = query_start; this.query_end = query_end; - this.is_rollup = is_rollup; + this.rollup_query = rollup_query; final String s = specification.getStringInterval(); if (s != null && s.toLowerCase().contains("all")) { @@ -157,26 +162,55 @@ public boolean hasNext() { @Override public DataPoint next() { if (hasNext()) { - if (is_rollup && (specification.getFunction() == Aggregators.AVG || - specification.getFunction() == Aggregators.DEV)) { - double sum = 0; - long count = 0; - while (values_in_interval.hasNextValue()) { - count += values_in_interval.nextValueCount(); - sum += values_in_interval.nextDoubleValue(); - } - - if (specification.getFunction() == Aggregators.AVG) { - if (count == 0) { // avoid # / 0 - value = 0; + if (rollup_query != null && + (rollup_query.getGroupBy() == Aggregators.AVG || + rollup_query.getGroupBy() == Aggregators.DEV)) { + if (rollup_query.getGroupBy() == Aggregators.AVG) { + if (specification.getFunction() == Aggregators.AVG) { + double sum = 0; + long count = 0; + while (values_in_interval.hasNextValue()) { + count += values_in_interval.nextValueCount(); + sum += values_in_interval.nextDoubleValue(); + } + if (count == 0) { // avoid # / 0 + value = 0; + } else { + value = sum / (double)count; + } } else { - value = sum / (double)count; + class Accumulator implements Doubles { + List values = new ArrayList(); + Iterator iterator; + @Override + public boolean hasNextValue() { + return iterator.hasNext(); + } + @Override + public double nextDoubleValue() { + return iterator.next(); + } + } + + final Accumulator accumulator = new Accumulator(); + while (values_in_interval.hasNextValue()) { + long count = values_in_interval.nextValueCount(); + double sum = values_in_interval.nextDoubleValue(); + if (count == 0) { + accumulator.values.add(0D); + } else { + accumulator.values.add(sum / (double) count); + } + } + accumulator.iterator = accumulator.values.iterator(); + value = specification.getFunction().runDouble(accumulator); } - } else { - throw new UnsupportedOperationException( - "Standard deviation over rolled up data is not supported"); + } else if (rollup_query.getGroupBy() == Aggregators.DEV) { + throw new UnsupportedOperationException("Standard deviation over " + + "rolled up data is not supported at this time"); } - } else if (is_rollup && specification.getFunction() == Aggregators.COUNT) { + } else if (rollup_query != null && + specification.getFunction() == Aggregators.COUNT) { double count = 0; while (values_in_interval.hasNextValue()) { count += values_in_interval.nextValueCount(); @@ -247,7 +281,7 @@ public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("Downsampler: ") .append(", downsampler=").append(specification) - .append(", is_rollup=").append(is_rollup) + .append(", rollupQuery=").append(rollup_query) .append(", queryStart=").append(query_start) .append(", queryEnd=").append(query_end) .append(", runAll=").append(run_all) diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index 1f9428a7ed..0ba721dd51 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -12,9 +12,14 @@ // see . package net.opentsdb.core; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; +import net.opentsdb.core.Aggregator.Doubles; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.utils.DateTime; /** @@ -71,7 +76,7 @@ public class FillingDownsampler extends Downsampler { final long end_time, final DownsamplingSpecification specification, final long query_start, final long end_start) { this(source, start_time, end_time, specification, query_start, end_start, - false); + null); } /** @@ -82,15 +87,16 @@ public class FillingDownsampler extends Downsampler { * @param specification The downsampling spec to use * @param query_start The start timestamp of the actual query for use with "all" * @param query_end The end timestamp of the actual query for use with "all" - * @param is_rollup Whether or not this query is handling rollup data. + * @param rollup_query An optional rollup query. * @throws IllegalArgumentException if fill_policy is interpolation. * @since 2.4 */ FillingDownsampler(final SeekableView source, final long start_time, final long end_time, final DownsamplingSpecification specification, - final long query_start, final long end_start, final boolean is_rollup) { + final long query_start, final long end_start, + final RollupQuery rollup_query) { // Lean on the superclass implementation. - super(source, specification, query_start, end_start, is_rollup); + super(source, specification, query_start, end_start, rollup_query); // Ensure we aren't given a bogus fill policy. if (FillPolicy.NONE == specification.getFillPolicy()) { @@ -187,26 +193,54 @@ public DataPoint next() { if (run_all || actual == timestamp) { // The calculated interval timestamp matches what we expect, so we can // do normal processing. - if (is_rollup && (specification.getFunction() == Aggregators.AVG || - specification.getFunction() == Aggregators.DEV)) { - double sum = 0; - long count = 0; - while (values_in_interval.hasNextValue()) { - count += values_in_interval.nextValueCount(); - sum += values_in_interval.nextDoubleValue(); - } - - if (specification.getFunction() == Aggregators.AVG) { - if (count == 0) { // avoid # / 0 - value = 0; + if (rollup_query != null && + (rollup_query.getGroupBy() == Aggregators.AVG || + rollup_query.getGroupBy() == Aggregators.DEV)) { + if (rollup_query.getGroupBy() == Aggregators.AVG) { + if (specification.getFunction() == Aggregators.AVG) { + double sum = 0; + long count = 0; + while (values_in_interval.hasNextValue()) { + count += values_in_interval.nextValueCount(); + sum += values_in_interval.nextDoubleValue(); + } + if (count == 0) { // avoid # / 0 + value = 0; + } else { + value = sum / (double)count; + } } else { - value = sum / (double)count; + class Accumulator implements Doubles { + List values = new ArrayList(); + Iterator iterator; + @Override + public boolean hasNextValue() { + return iterator.hasNext(); + } + @Override + public double nextDoubleValue() { + return iterator.next(); + } + } + + final Accumulator accumulator = new Accumulator(); + while (values_in_interval.hasNextValue()) { + long count = values_in_interval.nextValueCount(); + double sum = values_in_interval.nextDoubleValue(); + if (count == 0) { + accumulator.values.add(0D); + } else { + accumulator.values.add(sum / (double) count); + } + } + accumulator.iterator = accumulator.values.iterator(); + value = specification.getFunction().runDouble(accumulator); } - } else { - throw new UnsupportedOperationException( - "Standard deviation over rolled up data is not supported"); + } else if (specification.getFunction() == Aggregators.DEV) { + throw new UnsupportedOperationException("Standard deviation over " + + "rolled up data is not supported at this time"); } - } else if (is_rollup && + } else if (rollup_query != null && specification.getFunction() == Aggregators.COUNT) { double count = 0; while (values_in_interval.hasNextValue()) { diff --git a/src/core/SaltMultiGetter.java b/src/core/SaltMultiGetter.java index 48de6f01b3..913ff86a72 100644 --- a/src/core/SaltMultiGetter.java +++ b/src/core/SaltMultiGetter.java @@ -398,7 +398,7 @@ private void processRollupQuery(final byte[] key, final ArrayList row, final Annotation note = JSON.parseToObject(kv.value(), Annotation.class); notes.add(note); } else { - if (rollup_query.getRollupAgg() == Aggregators.AVG || rollup_query.getRollupAgg() == Aggregators.DEV) { + if (rollup_query.getGroupBy() == Aggregators.AVG || rollup_query.getGroupBy() == Aggregators.DEV) { if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { keyValues.add(kv); diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 3d44dfab25..5215d62858 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -567,8 +567,8 @@ void processRow(final byte[] key, final ArrayList row) { map_notes.add(note); } } else { - if (rollup_query.getRollupAgg() == Aggregators.AVG || - rollup_query.getRollupAgg() == Aggregators.DEV) { + if (rollup_query.getGroupBy() == Aggregators.AVG || + rollup_query.getGroupBy() == Aggregators.DEV) { if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { kvs.add(kv); diff --git a/src/core/Span.java b/src/core/Span.java index 4b758e272d..8384ecb742 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -20,6 +20,7 @@ import java.util.NoSuchElementException; import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes; @@ -535,7 +536,7 @@ Downsampler downsampler(final long start_time, * @param downsampler The downsampling specification to use * @param query_start Start of the actual query * @param query_end End of the actual query - * @param is_rollup Whether or not the downsampler is handling rolled up data. + * @param rollup_query An optional rollup query. * @return A new downsampler. * @since 2.4 */ @@ -544,16 +545,16 @@ Downsampler downsampler(final long start_time, final DownsamplingSpecification downsampler, final long query_start, final long query_end, - final boolean is_rollup) { + final RollupQuery rollup_query) { if (downsampler == null) { return null; } if (FillPolicy.NONE == downsampler.getFillPolicy()) { return new Downsampler(spanIterator(), downsampler, - query_start, query_end, is_rollup); + query_start, query_end, rollup_query); } return new FillingDownsampler(spanIterator(), start_time, end_time, - downsampler, query_start, query_end, is_rollup); + downsampler, query_start, query_end, rollup_query); } /** diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index b3c35659e1..5baed6edeb 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -28,6 +28,7 @@ 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. @@ -103,8 +104,8 @@ final class SpanGroup implements DataPoints { /** Index of the query in the TSQuery class */ private final int query_index; - /** Whether or not the query is for rolled up data */ - private final boolean is_rollup; + /** An optional rollup query. */ + private final RollupQuery rollup_query; /** The TSDB to which we belong, used for resolution */ private final TSDB tsdb; @@ -230,7 +231,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, false); + downsampler, query_start, query_end, query_index, null); } /** @@ -250,7 +251,7 @@ final class SpanGroup implements DataPoints { * @param query_start Start of the actual query * @param query_end End of the actual query * @param query_index index of the original query - * @param is_rollup Whether or not this query is handling rolled up data + * @param rollup_query An optional rollup query. * @since 2.4 */ SpanGroup(final TSDB tsdb, @@ -264,7 +265,7 @@ final class SpanGroup implements DataPoints { final long query_start, final long query_end, final int query_index, - final boolean is_rollup) { + final RollupQuery rollup_query) { annotations = new ArrayList(); this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; @@ -282,7 +283,7 @@ final class SpanGroup implements DataPoints { this.query_start = query_start; this.query_end = query_end; this.query_index = query_index; - this.is_rollup = is_rollup; + this.rollup_query = rollup_query; this.tsdb = tsdb; } @@ -527,7 +528,7 @@ public SeekableView iterator() { return AggregationIterator.create(spans, start_time, end_time, aggregator, aggregator.interpolationMethod(), downsampler, query_start, query_end, - rate, rate_options, is_rollup); + rate, rate_options, rollup_query); } /** diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index c17548ff6b..57346cd462 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -432,7 +432,7 @@ public Deferred configureFromQuery(final TSQuery query, if (rollup_usage != ROLLUP_USAGE.ROLLUP_RAW) { //Check whether the down sampler is set and rollup is enabled - transformDownSamplerToRollupQuery(sub_query.getDownsample()); + transformDownSamplerToRollupQuery(aggregator, sub_query.getDownsample()); } sub_query.setTsdbQuery(this); @@ -930,8 +930,8 @@ void processRow(final byte[] key, final ArrayList row) { Annotation.class); datapoints.getAnnotations().add(note); } else { - if (rollup_query.getRollupAgg() == Aggregators.AVG || - rollup_query.getRollupAgg() == Aggregators.DEV) { + if (rollup_query.getGroupBy() == Aggregators.AVG || + rollup_query.getGroupBy() == Aggregators.DEV) { if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { datapoints.addRow(kv); @@ -1100,7 +1100,7 @@ public DataPoints[] call(final TreeMap spans) throws Exception { getStartTime(), getEndTime(), query_index, - RollupQuery.isValidQuery(rollup_query)); + rollup_query); group.add(span); groups[i++] = group; } @@ -1120,7 +1120,7 @@ public DataPoints[] call(final TreeMap spans) throws Exception { getStartTime(), getEndTime(), query_index, - RollupQuery.isValidQuery(rollup_query)); + rollup_query); if (query_stats != null) { query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); } @@ -1171,7 +1171,7 @@ public DataPoints[] call(final TreeMap spans) throws Exception { getStartTime(), getEndTime(), query_index, - RollupQuery.isValidQuery(rollup_query)); + 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]; @@ -1230,7 +1230,8 @@ else if (best_match_rollups != null && best_match_rollups.size() > 0) { else { rollup_query = new RollupQuery(interval, rollup_query.getRollupAgg(), - rollup_query.getSampleIntervalInMS()); + rollup_query.getSampleIntervalInMS(), + aggregator); //Here the requested sampling rate will be higher than //resulted result. So downsample it if (!rollup_query.isLowerSamplingRate()) { @@ -1312,17 +1313,17 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { // Set the Scanners column qualifier pattern with rollup aggregator // HBase allows only a single filter so if we have a row key filter, keep // it. If not, then we can do this - if (!rollup_query.getRollupAgg().toString().equals("avg")) { + if (!rollup_query.getGroupBy().toString().equals("avg")) { if (existing != null) { final List filters = new ArrayList(2); filters.add(existing); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() + new BinaryPrefixComparator(rollup_query.getGroupBy().toString() .getBytes(Const.ASCII_CHARSET)))); scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ALL)); } else { scanner.setFilter(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() + new BinaryPrefixComparator(rollup_query.getGroupBy().toString() .getBytes(Const.ASCII_CHARSET)))); } } else { @@ -1520,24 +1521,30 @@ public void setQueryIdx(int idx) { * Transform downsampler properties to rollup properties, if the rollup * is enabled at configuration level and down sampler is set. * It falls back to raw data and down sampling if there is no - * RollupInterval is configured against this down sample interval + * RollupInterval is configured against this down sample interval + * @param group_by The group by aggregator. * @param str_interval String representation of the interval, for logging * @since 2.4 */ - public void transformDownSamplerToRollupQuery(final String str_interval) { - + public void transformDownSamplerToRollupQuery(final Aggregator group_by, + final String str_interval) { + if (downsampler != null && downsampler.getInterval() > 0) { if (tsdb.getRollupConfig() != null) { try { best_match_rollups = tsdb.getRollupConfig(). getRollupInterval(downsampler.getInterval() / 1000, str_interval); - //It is thread safe as eatch thread will be working on unique + //It is thread safe as each thread will be working on unique // TsdbQuery object //RollupConfig.getRollupInterval guarantees that, // it always return a non-empty list // TODO rollup_query = new RollupQuery(best_match_rollups.remove(0), - downsampler.getFunction(), downsampler.getInterval()); + 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 " diff --git a/src/rollup/RollupQuery.java b/src/rollup/RollupQuery.java index 1ab6d25d51..449cb32dd0 100644 --- a/src/rollup/RollupQuery.java +++ b/src/rollup/RollupQuery.java @@ -30,6 +30,8 @@ public class RollupQuery { private final RollupInterval rollup_interval; private final Aggregator rollup_agg; + private final Aggregator group_by; + /** Rollup aggregate prefix along with the delimiter as byte array. It will be * the same for the same rollup aggregator, but is defined here to * reduce the number of calculations at scan time*/ @@ -41,23 +43,28 @@ public class RollupQuery { private final long sample_interval_ms; /** - * Default private constructor + * Default constructor * @param rollup_interval RollupInterval object * @param rollup_agg Aggregator object * @param sample_interval_ms Initial downsaple interval in milliseconds - * @throws IllegalStateException if rollup interval or rollup aggregator is - * null + * @param group_by Group by aggregation. + * @throws IllegalStateException if rollup interval, rollup aggregator or + * group by aggregator is null */ public RollupQuery(final RollupInterval rollup_interval, - final Aggregator rollup_agg, long sample_interval_ms) { + final Aggregator rollup_agg, + long sample_interval_ms, + final Aggregator group_by) { if (rollup_interval == null) { throw new IllegalStateException("Rollup interval is null"); } - if (rollup_agg == null) { throw new IllegalStateException("Rollup aggregator is null"); } + if (group_by == null) { + throw new IllegalStateException("Group by aggregator is null"); + } this.rollup_interval = rollup_interval; // we need to convert zimsum => sum, mimmax => max, mimmin => min so that @@ -71,7 +78,22 @@ public RollupQuery(final RollupInterval rollup_interval, } else { this.rollup_agg = rollup_agg; } - this.agg_prefix = RollupUtils.getRollupQualifierPrefix(this.rollup_agg.toString()); + if (group_by == Aggregators.ZIMSUM) { + this.group_by = Aggregators.SUM; + } else if (group_by == Aggregators.MIMMAX) { + this.group_by = Aggregators.MAX; + } else if (group_by == Aggregators.MIMMIN) { + this.group_by = Aggregators.MIN; + } else { + this.group_by = group_by; + } + if (group_by == Aggregators.AVG) { + agg_prefix = RollupUtils.getRollupQualifierPrefix( + Aggregators.SUM.toString()); + } else { + agg_prefix = RollupUtils.getRollupQualifierPrefix( + this.group_by.toString()); + } this.sample_interval_ms = sample_interval_ms; } @@ -102,7 +124,9 @@ public String toString() { buf.append("rollup interval=") .append(rollup_interval.getStringInterval()) .append(", rollup aggregator=") - .append(rollup_agg.toString()); + .append(rollup_agg.toString()) + .append(", group_by=") + .append(group_by.toString()); return buf.toString(); } @@ -120,6 +144,12 @@ public RollupInterval getRollupInterval() { public Aggregator getRollupAgg() { return rollup_agg; } + + @JsonIgnore + public Aggregator getGroupBy() { + return group_by; + } + /** * Does it contain a valid rollup interval, mainly says it is not the default * rollup. Default rollup is of same resolution as raw data. So if true, diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java index a563af80de..6ecbea2c5a 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.getRollupAgg() == Aggregators.AVG || - rollup_query.getRollupAgg() == Aggregators.DEV; + need_count = rollup_query.getGroupBy() == Aggregators.AVG || + rollup_query.getGroupBy() == Aggregators.DEV; // WARNING overallocation qualifiers = new byte[rollup_query.getRollupInterval().getIntervals() * 2]; @@ -126,7 +126,7 @@ public void setRow(final KeyValue column) { } key = column.key(); - + //Check whether the cell is generated by same rollup aggregator if (need_count) { if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, RollupQuery.SUM.length) == 0) { @@ -167,6 +167,7 @@ public void addRow(final KeyValue column) { //Check whether the cell is generated by same rollup aggregator if (need_count) { + if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, RollupQuery.SUM.length) == 0) { append(column, false); @@ -538,7 +539,6 @@ public DataPoint next() { if (!hasNext()) { throw new NoSuchElementException("no more elements"); } - value_index += Internal.getValueLengthFromQualifier(qualifiers, qual_index); qualifier = Bytes.getUnsignedShort(qualifiers, qual_index); qual_index += 2; @@ -609,7 +609,8 @@ public boolean isInteger() { @Override public long valueCount() { if (count_values == null) { - return -1; + // real values (sum, max, min) so just return 1. + return 1; } final byte flags = (byte) count_qualifier; final byte vlen = (byte) ((flags & Const.LENGTH_MASK) + 1); @@ -625,6 +626,7 @@ public long longValue() { throw new ClassCastException("value @" + qual_index + " is not a long in " + this); } + final byte flags = (byte) qualifier; final byte vlen = (byte) ((flags & Const.LENGTH_MASK) + 1); return Internal.extractIntegerValue(values, value_index - vlen, flags); diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 5402c9f502..62e9ef0755 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -26,6 +26,8 @@ import com.google.common.collect.Lists; import net.opentsdb.core.SeekableViewsForTest.MockSeekableView; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.utils.DateTime; import org.junit.Before; @@ -1176,6 +1178,10 @@ public void testDownsampler_1year_timezone() { @Test public void testDownsampler_rollupSum() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), @@ -1190,7 +1196,7 @@ public void testDownsampler_rollupSum() { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 10, 1024) })); specification = new DownsamplingSpecification("10s-sum"); - downsampler = new Downsampler(source, specification, 0, 0, true); + downsampler = new Downsampler(source, specification, 0, 0, rollup_query); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -1218,6 +1224,10 @@ public void testDownsampler_rollupSum() { @Test public void testDownsampler_rollupAvg() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.AVG); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), @@ -1225,7 +1235,7 @@ public void testDownsampler_rollupAvg() { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 3, 8) })); specification = new DownsamplingSpecification("10s-avg"); - downsampler = new Downsampler(source, specification, 0, 0, true); + downsampler = new Downsampler(source, specification, 0, 0, rollup_query); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -1245,6 +1255,10 @@ public void testDownsampler_rollupAvg() { @Test public void testDownsampler_rollupCount() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.COUNT); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), @@ -1252,7 +1266,7 @@ public void testDownsampler_rollupCount() { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 3, 8) })); specification = new DownsamplingSpecification("10s-count"); - downsampler = new Downsampler(source, specification, 0, 0, true); + downsampler = new Downsampler(source, specification, 0, 0, rollup_query); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); @@ -1272,8 +1286,12 @@ public void testDownsampler_rollupCount() { @Test (expected = UnsupportedOperationException.class) public void testDownsampler_rollupDev() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.DEV); specification = new DownsamplingSpecification("10s-dev"); - downsampler = new Downsampler(source, specification, 0, 0, true); + downsampler = new Downsampler(source, specification, 0, 0, rollup_query); while (downsampler.hasNext()) { downsampler.next(); // <-- throws here } diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index f06580bcbc..95375d37d4 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -15,6 +15,8 @@ import org.junit.Test; import net.opentsdb.core.SeekableViewsForTest.MockSeekableView; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; import net.opentsdb.utils.DateTime; import static org.junit.Assert.assertEquals; @@ -816,6 +818,10 @@ public void testDownsampler_noDataCalendar() { @Test public void testDownsampler_rollup() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); final long baseTime = 1000L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -835,7 +841,7 @@ public void testDownsampler_rollup() { specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, specification, 0, 0, true); + baseTime + 12L * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; step(downsampler, timestamp, 42.); @@ -846,6 +852,10 @@ public void testDownsampler_rollup() { @Test public void testDownsampler_rollupMissing() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); final long baseTime = 500L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -862,7 +872,7 @@ public void testDownsampler_rollupMissing() { specification = new DownsamplingSpecification("100ms-sum-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0, true); + baseTime + 36 * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; step(downsampler, timestamp, Double.NaN); @@ -879,6 +889,10 @@ public void testDownsampler_rollupMissing() { @Test public void testDownsampler_rollupAvg() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); final long baseTime = 1000L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -900,7 +914,7 @@ public void testDownsampler_rollupAvg() { specification = new DownsamplingSpecification("100ms-avg-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, specification, 0, 0, true); + baseTime + 12L * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; step(downsampler, timestamp, 10.5); @@ -911,6 +925,10 @@ public void testDownsampler_rollupAvg() { @Test public void testDownsampler_rollupAvgMissing() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); final long baseTime = 500L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -927,7 +945,7 @@ public void testDownsampler_rollupAvgMissing() { specification = new DownsamplingSpecification("100ms-avg-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0, true); + baseTime + 36 * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; step(downsampler, timestamp, Double.NaN); @@ -944,6 +962,10 @@ public void testDownsampler_rollupAvgMissing() { @Test public void testDownsampler_rollupCount() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); final long baseTime = 1000L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -965,7 +987,7 @@ public void testDownsampler_rollupCount() { specification = new DownsamplingSpecification("100ms-count-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, specification, 0, 0, true); + baseTime + 12L * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; step(downsampler, timestamp, 4); @@ -976,6 +998,10 @@ public void testDownsampler_rollupCount() { @Test public void testDownsampler_rollupCountMissing() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.SUM); final long baseTime = 500L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -992,7 +1018,7 @@ public void testDownsampler_rollupCountMissing() { specification = new DownsamplingSpecification("100ms-count-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 36 * 25L, specification, 0, 0, true); + baseTime + 36 * 25L, specification, 0, 0, rollup_query); long timestamp = baseTime; step(downsampler, timestamp, Double.NaN); @@ -1009,6 +1035,10 @@ public void testDownsampler_rollupCountMissing() { @Test (expected = UnsupportedOperationException.class) public void testDownsampler_rollupDev() { + final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", + "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + 3600000, Aggregators.DEV); final long baseTime = 1000L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { @@ -1030,7 +1060,7 @@ public void testDownsampler_rollupDev() { specification = new DownsamplingSpecification("100ms-dev-nan"); final Downsampler downsampler = new FillingDownsampler(source, baseTime, - baseTime + 12L * 25L, specification, 0, 0, true); + baseTime + 12L * 25L, specification, 0, 0, rollup_query); while (downsampler.hasNext()) { downsampler.next(); // <-- throws here } diff --git a/test/core/TestRollupSpan.java b/test/core/TestRollupSpan.java index 1d26413daa..96c0dd452a 100644 --- a/test/core/TestRollupSpan.java +++ b/test/core/TestRollupSpan.java @@ -61,7 +61,7 @@ public final class TestRollupSpan { private static final RollupQuery rollup_query = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - aggr_sum, 1000); + aggr_sum, 1000, aggr_sum); @Before public void before() throws Exception { diff --git a/test/rollup/TestRollupSeq.java b/test/rollup/TestRollupSeq.java index 5b188110b6..6dd74b4b07 100644 --- a/test/rollup/TestRollupSeq.java +++ b/test/rollup/TestRollupSeq.java @@ -64,31 +64,31 @@ public final class TestRollupSeq { public static final byte[] FAMILY = { 't' }; private static final RollupQuery rollup_query_sum = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - Aggregators.SUM, 1000); + Aggregators.SUM, 1000, Aggregators.SUM); private static final RollupQuery rollup_query_avg = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - Aggregators.AVG, 1000); + Aggregators.AVG, 1000, Aggregators.AVG); private static final RollupQuery rollup_query_sum_mimmax = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - Aggregators.MIMMAX, 1000); + Aggregators.MIMMAX, 1000, Aggregators.MIMMAX); private static final RollupQuery rollup_query_10m_sum = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), - Aggregators.SUM, 600000); + Aggregators.SUM, 600000, Aggregators.SUM); private static final RollupQuery rollup_query_10m_avg = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), - Aggregators.AVG, 600000); + Aggregators.AVG, 600000, Aggregators.AVG); private static final RollupQuery rollup_query_10m_count = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), - Aggregators.COUNT, 600000); + Aggregators.COUNT, 600000, Aggregators.COUNT); private static final RollupQuery rollup_query_1h_sum = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), - Aggregators.SUM, 3600000); + Aggregators.SUM, 3600000, Aggregators.SUM); private static final RollupQuery rollup_query_1h_avg = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), - Aggregators.AVG, 3600000); + Aggregators.AVG, 3600000, Aggregators.AVG); private static final RollupQuery rollup_query_1h_count = new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), - Aggregators.COUNT, 3600000); + Aggregators.COUNT, 3600000, Aggregators.COUNT); @Before public void before() throws Exception { @@ -113,7 +113,7 @@ public void setRow() throws Exception { assertTrue(dp.isInteger()); assertEquals(4, dp.longValue()); assertEquals(1356998400000L, dp.timestamp()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); assertFalse(it.hasNext()); } @@ -147,14 +147,14 @@ public void addRow() throws Exception { assertTrue(dp.isInteger()); assertEquals(1356998400000L, dp.timestamp()); assertEquals(4, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); assertTrue(it.hasNext()); dp = it.next(); assertTrue(dp.isInteger()); assertEquals(1356998500000L, dp.timestamp()); assertEquals(5, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); assertFalse(it.hasNext()); } @@ -196,7 +196,7 @@ public void addRowMergeDifferentSalt() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 1000; } @@ -230,7 +230,7 @@ public void addRowMergeLater() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 1000; } @@ -590,9 +590,10 @@ public void timestamp() throws Exception { assertTrue(dp.isInteger()); assertEquals(7, dp.longValue()); assertEquals(1356998400000L, dp.timestamp()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); assertFalse(it.hasNext()); } + // NOTE: many of the tests below also test RollupSeq.size() @Test public void rollup10m() throws Exception { @@ -614,7 +615,7 @@ public void rollup10m() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 600000; } @@ -640,7 +641,7 @@ public void rollup10mDouble() throws Exception { assertEquals(ts, dp.timestamp()); assertFalse(dp.isInteger()); assertEquals(value, dp.doubleValue(), 0.0001); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); value += 0.25; ts += 600000; } @@ -666,7 +667,7 @@ public void rollup10mFloat() throws Exception { assertEquals(ts, dp.timestamp()); assertFalse(dp.isInteger()); assertEquals(value, dp.doubleValue(), 0.0001); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); value += 0.50; ts += 600000; } @@ -698,7 +699,7 @@ public void rollup10mMixFloatAndLong() throws Exception { } else { assertEquals(dvalue, dp.doubleValue(), 0.0001); } - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); if (toggle) { dvalue += 1; } else { @@ -1064,7 +1065,7 @@ public void rollup1hLong() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 3600000; } @@ -1090,7 +1091,7 @@ public void rollup1hFloat() throws Exception { assertEquals(ts, dp.timestamp()); assertFalse(dp.isInteger()); assertEquals(value, dp.doubleValue(), 0.0001); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); value += 0.25; ts += 3600000; } @@ -1182,7 +1183,7 @@ public void rollup10mSeekTop() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 600000; } @@ -1212,7 +1213,7 @@ public void rollup10mSeek() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 600000; } @@ -1262,7 +1263,7 @@ public void rollup10mSeekSeconds() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 600000; } @@ -1292,7 +1293,7 @@ public void rollup10mSeekUnaligned() throws Exception { assertEquals(ts, dp.timestamp()); assertTrue(dp.isInteger()); assertEquals(value, dp.longValue()); - assertEquals(-1, dp.valueCount()); + assertEquals(1, dp.valueCount()); ++value; ts += 600000; } @@ -1601,7 +1602,7 @@ private static byte[] getQualifier(final long timestamp, final int base_time = RollupUtils.getRollupBasetime(timestamp, rollup_query.getRollupInterval()); return RollupUtils.buildRollupQualifier(timestamp, base_time, flags, - rollup_query.getRollupAgg().toString(), + rollup_query.getGroupBy().toString(), rollup_query.getRollupInterval()); } } From 2a80c5d33c5952a8d717a7d277804c4b596fbfd3 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 3 Apr 2017 11:38:13 -0700 Subject: [PATCH 614/826] Update the Authentication plugin code with handlers to validate that users have permission to access a given metric. Also add stats collection. Signed-off-by: Chris Larsen --- src/auth/AuthState.java | 83 ++++++++++ src/auth/Authentication.java | 115 +++++++++++++ src/auth/AuthenticationChannelHandler.java | 156 ++++++++++++------ ...ticationPlugin.java => Authorization.java} | 69 ++++---- src/core/TSDB.java | 20 ++- src/tsd/QueryRpc.java | 50 ++++++ test/tsd/TestHttpQuery.java | 13 +- test/tsd/TestQueryRpc.java | 37 +++++ 8 files changed, 450 insertions(+), 93 deletions(-) create mode 100644 src/auth/AuthState.java create mode 100644 src/auth/Authentication.java rename src/auth/{AuthenticationPlugin.java => Authorization.java} (63%) diff --git a/src/auth/AuthState.java b/src/auth/AuthState.java new file mode 100644 index 0000000000..4dee5fd0d4 --- /dev/null +++ b/src/auth/AuthState.java @@ -0,0 +1,83 @@ +// 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.auth; + +import org.jboss.netty.channel.Channel; + +/** + * An interface for use in authentication and authorization of calls. + *

    + * When used for successful authentication, this stage object will be attached + * to the Netty Channel via it's attachment method. It can then be read out of + * the channel and used for authorization calls. + * + * @since 2.4 + */ +public interface AuthState { + + /** + * A list of various authentication and authorization states. + */ + public enum AuthStatus { + SUCCESS, + UNAUTHORIZED, + FORBIDDEN, + REDIRECTED, + ERROR, + REVOKED + } + + /** + * Returns the user associated with this state as a string object. This should + * always return a non-null value. If authentication is enabled but anonymous + * users or unauthenticated users are allowed, return something like "anonymous". + * @return A non-null user ID. + */ + public String getUser(); + + /** + * The status associated with this authentication or authorization state. + * @return A non-null auth status enumerator value. + */ + public AuthStatus getStatus(); + + /** + * An optional message associated with the authentication or authorization + * attempt. If successful, this may be null. On an error or failure, this + * message should be populated. + * @return A useful message regarding the action taken. May be null. + */ + public String getMessage(); + + /** + * An optional exception if an error occurred during the operation. + * @return An optional exception; may be null. + */ + public Throwable getException(); + + /** + * Sets the channel associated with this state when used for authentication + * and attached to a channel. Useful for associating connection information + * (IP and port) with the user. + * @param channel A non-null channel. + */ + public void setChannel(final Channel channel); + + /** + * An optional token to use with an authentication system for validating that + * the user key is still valid. + * @return A byte array encoded depending on the authentication plugin. May + * be null. + */ + public byte[] getToken(); +} diff --git a/src/auth/Authentication.java b/src/auth/Authentication.java new file mode 100644 index 0000000000..38e0530b9a --- /dev/null +++ b/src/auth/Authentication.java @@ -0,0 +1,115 @@ +// 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.auth; + +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; +import com.stumbleupon.async.Deferred; + +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpRequest; + +/** + * A plugin interface for performing authentication for OpenTSDB API access. + * The plugin is embedded within the Netty pipeline and intercepts requests + * on new channels. Once a channel is authenticated successfully, the plugin is + * removed from the pipeline so further calls on that channel are not evaluated. + *

    + * An AuthState object is attached to the channel for evaluation later in the + * pipeline. This state cannot be changed but cane be replaced. + *

    + * The plugin also includes an acessor to an Authorization plugin to allow or + * disallow operations per user. + * + * @since 2.4 + */ +public abstract class Authentication { + + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * Note: Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws RuntimeException if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); + + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * @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}). + */ + public abstract Deferred shutdown(); + + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. 2.0.1. The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * @return A version string used to log the loaded version + */ + public abstract String version(); + + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); + + /** + * Authenticate Telnet connections, provides the first line of the incoming + * connection. + *

    + * NOTE: This method should not throw exceptions, rather return a state object + * with the AuthStatus.ERROR status. + * + * @param channel A non-null Netty channel to associate with the request. + * @param command A non-null list of "words" from a Telnet style command + * (strings or numbers separated by spaces) + * @return A non-null AuthState object with a valid AuthStatus to evaluate for + * a successful or unsuccessful authentication. + */ + public abstract AuthState authenticateTelnet(final Channel channel, + final String[] command); + + /** + * Authenticate HTTP connections, provides the HTTPRequest object for the + * incoming connection. + *

    + * NOTE: This method should not throw exceptions, rather return a state object + * with the AuthStatus.ERROR status. + * + * @param channel A non-null Netty channel to associate with the request. + * @param req A non-null HTTP request. + * @return A non-null AuthState object with a valid AuthStatus to evaluate for + * a successful or unsuccessful authentication. + */ + public abstract AuthState authenticateHTTP(final Channel channel, + final HttpRequest req); + + /** + * An optional authorization object. If authorization is not enabled, this + * call may return null. + * @return An authorization object or null; + */ + public abstract Authorization authorization(); + +} \ No newline at end of file diff --git a/src/auth/AuthenticationChannelHandler.java b/src/auth/AuthenticationChannelHandler.java index b23157d410..ec81968fe1 100644 --- a/src/auth/AuthenticationChannelHandler.java +++ b/src/auth/AuthenticationChannelHandler.java @@ -1,6 +1,5 @@ -package net.opentsdb.auth; // This file is part of OpenTSDB. -// Copyright (C) 2016 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -12,6 +11,10 @@ // of the GNU Lesser General Public License along with this program. If not, // see . +package net.opentsdb.auth; + +import net.opentsdb.auth.AuthState.AuthStatus; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; @@ -19,86 +22,139 @@ import org.jboss.netty.handler.codec.http.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import org.jboss.netty.channel.Channel; -import org.jboss.netty.channel.ChannelEvent; -import org.jboss.netty.channel.ChannelFuture; -import org.jboss.netty.channel.ChannelFutureListener; -import org.jboss.netty.channel.ChannelHandlerContext; -import org.jboss.netty.channel.ChannelStateEvent; +import com.google.common.base.Strings; + import org.jboss.netty.channel.ExceptionEvent; -import org.jboss.netty.channel.MessageEvent; -import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import org.jboss.netty.buffer.ChannelBuffers; + /** + * This is a simple authentication handler that intercepts the initial calls + * for a new channel and attempts to authenticate the caller. + *

    + * On a successful authentication, the authentication state is attached to the + * Netty channel for other components to use and the handler is removed so that + * subsequent calls across the channel do not have to go through authentication. + * At any time, the pipeline can check the auth state to determine if actions + * are allowed or if the state should be revoked (e.g. token timing out) + *

    + * On a failed auth, an error is returned to the user (message via Telnet or + * 401, 403 or 500 for HTTP). The channel is left open so callers can try again + * but if an unknown error occurs then the channel is closed. + * * @since 2.4 */ public class AuthenticationChannelHandler extends SimpleChannelUpstreamHandler { - private static final Logger LOG = LoggerFactory.getLogger(AuthenticationChannelHandler.class); - private TSDB tsdb = null; - private AuthenticationPlugin authentication = null; + private static final Logger LOG = LoggerFactory.getLogger( + AuthenticationChannelHandler.class); + + public static final String TELNET_AUTH_FAILURE = "AUTH_FAIL\r\n"; + public static final String TELNET_AUTH_SUCCESS = "AUTH_SUCCESS\r\n"; + + /** The authentication object, pulled from TSDB. */ + private final Authentication authentication; - public AuthenticationChannelHandler(TSDB tsdb) { - LOG.info("Setting up AuthenticationChannelHandler"); - this.authentication = tsdb.getAuth(); - if (this.authentication == null) { - LOG.info("No Authentication Plugin Configured"); + /** + * Default ctor. + * @param tsdb Non-null TSDB object from which to fetch the auth plugin. + * @throws IllegalArgumentException if the TSDB object is null or the auth + * object the TSDB contains is null. + */ + public AuthenticationChannelHandler(final TSDB tsdb) { + if (tsdb == null) { + throw new IllegalArgumentException("TSDB object cannot be null"); + } + if (tsdb.getAuth() == null) { + throw new IllegalArgumentException("Attempted to instantiate an " + + "authentication handler but it was not configured in TSDB."); } + authentication = tsdb.getAuth(); + LOG.info("Set up AuthenticationChannelHandler: " + authentication.getClass()); } @Override - public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { - e.getCause().printStackTrace(); + public void exceptionCaught(final ChannelHandlerContext ctx, + final ExceptionEvent e) { + LOG.error("Unexpected exception in AuthenticationChannelHandler for " + + "channel: " + e.getChannel(), e.getCause()); e.getChannel().close(); } @Override - public void messageReceived(ChannelHandlerContext ctx, MessageEvent authEvent) { - if (this.authentication == null) { - LOG.info("Attempted to use null authentication plugin. This should not happen"); - LOG.debug("Removing Authentication Handler from Connection"); - ctx.getPipeline().remove(this); - } + public void messageReceived(final ChannelHandlerContext ctx, + final MessageEvent authEvent) { try { final Object authCommand = authEvent.getMessage(); - String authResponse = "AUTH_FAIL\r\n"; + // Telnet Auth if (authCommand instanceof String[]) { - LOG.debug("Passing auth command to Authentication Plugin"); - if (this.authentication.authenticateTelnet((String[]) authCommand)) { - LOG.debug("Authentication Completed"); - authResponse = "AUTH_SUCCESS.\r\n"; - LOG.debug("Removing Authentication Handler from Connection"); + if (LOG.isDebugEnabled()) { + LOG.debug("Authenticating Telnet command from channel: " + + authEvent.getChannel()); + } + String auth_response = TELNET_AUTH_FAILURE; + final AuthState state = authentication.authenticateTelnet( + authEvent.getChannel(), (String[]) authCommand); + if (state.getStatus() == AuthStatus.SUCCESS) { + auth_response = TELNET_AUTH_SUCCESS; ctx.getPipeline().remove(this); + authEvent.getChannel().setAttachment(state); + state.setChannel(authEvent.getChannel()); } - ChannelFuture future = authEvent.getChannel().write(authResponse); + authEvent.getChannel().write(auth_response); // HTTTP Auth } else if (authCommand instanceof HttpRequest) { + if (LOG.isDebugEnabled()) { + LOG.debug("Authenticating HTTP request from channel: " + + authEvent.getChannel()); + } HttpResponseStatus status; - if (this.authentication.authenticateHTTP((HttpRequest) authCommand)) { - LOG.debug("Authentication Completed"); + final AuthState state = authentication.authenticateHTTP( + authEvent.getChannel(), (HttpRequest) authCommand); + if (state.getStatus() == AuthStatus.SUCCESS) { ctx.getPipeline().remove(this); + authEvent.getChannel().setAttachment(state); + state.setChannel(authEvent.getChannel()); + // pass it down! + super.messageReceived(ctx, authEvent); + } else if (state.getStatus() == AuthStatus.REDIRECTED) { + // do nothing here as the plugin sent the redirect answer. We want to + // keep auth inline so the next call can process the authentication. } else { - LOG.debug("Authentication Failed"); - status = HttpResponseStatus.FORBIDDEN; - HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); - ChannelFuture future = authEvent.getChannel().write(response); + switch (state.getStatus()) { + case UNAUTHORIZED: + status = HttpResponseStatus.UNAUTHORIZED; + break; + case FORBIDDEN: + status = HttpResponseStatus.FORBIDDEN; + break; + default: + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + break; + } + + final HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); + if (!Strings.isNullOrEmpty(state.getMessage())) { + // TODO - JSONify or something + response.setContent(ChannelBuffers.copiedBuffer( + state.getMessage(), Const.UTF8_CHARSET)); + } + authEvent.getChannel().write(response); } - // Unknown Authentication + // Unknown Authentication. Log and close the connection. } else { - LOG.error("Unexpected message type " - + authCommand.getClass() + ": " + authCommand); + LOG.error("Unexpected message type " + authCommand.getClass() + ": " + + authCommand + " from channel: " + authEvent.getChannel()); + authEvent.getChannel().close(); } - } catch (Exception e) { - LOG.error("Unexpected exception caught" - + " while serving: " + e); + } catch (Throwable t) { + LOG.error("Unexpected exception caught while serving channel: " + + authEvent.getChannel(), t); + authEvent.getChannel().close(); } } -} +} \ No newline at end of file diff --git a/src/auth/AuthenticationPlugin.java b/src/auth/Authorization.java similarity index 63% rename from src/auth/AuthenticationPlugin.java rename to src/auth/Authorization.java index b187e1a314..97be53f9b2 100644 --- a/src/auth/AuthenticationPlugin.java +++ b/src/auth/Authorization.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2016 The OpenTSDB Authors. +// 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 @@ -12,17 +12,20 @@ // see . package net.opentsdb.auth; -import net.opentsdb.core.TSDB; -import net.opentsdb.stats.StatsCollector; import com.stumbleupon.async.Deferred; -import org.jboss.netty.handler.codec.http.HttpRequest; -import java.util.Map; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.pojo.Query; +import net.opentsdb.stats.StatsCollector; /** + * A plugin interface for authorization calls, allowing or disallowing operations + * in OpenTSDB. + * * @since 2.4 */ -public abstract class AuthenticationPlugin { +public abstract class Authorization { /** * Called by TSDB to initialize the plugin @@ -34,7 +37,7 @@ public abstract class AuthenticationPlugin { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); @@ -62,38 +65,30 @@ public abstract class AuthenticationPlugin { * @param collector The collector used for emitting statistics */ public abstract void collectStats(final StatsCollector collector); - - /** - * Authenticate Telnet connections, provides the first line of the incoming - * connection. - * @param command - * @return returns a Boolean indicating whether or not the incoming connection - * was successfully authenticated. - */ - public abstract Boolean authenticateTelnet(final String[] command); - - /** - * Authenticate HTTP connections, provides the HTTPRequest object for the - * incoming connection. - * @param req - * @return returns a Boolean indicating whether or not the incoming connection - * was successfully authenticated. - */ - public abstract Boolean authenticateHTTP(final HttpRequest req); - + /** - * Allow OpenTSDB to create valid credentials - * @param fields - * @return returns a Boolean indicating whether or not the credentials - * were successfully created. + * Determines if the user is allowed to execute the given query. + * The returned state contains a status code regarding whether or not the query + * is allowed. If the query IS allowed, the same user state passed as an + * argument maybe returned. + * @param state A non-null auth state with the user and AuthStatus.SUCCESS. + * @param query A non-null query. + * @return An AuthState object with a valid AuthStatus to evaluate for + * permission. */ - public abstract Boolean storeCredentials(final Map fields); - + public abstract AuthState allowQuery(final AuthState state, + final TSQuery query); + /** - * Allow OpenTSDB to destroy credentials - * @param fields - * @return returns a Boolean indicating whether or not the credentials - * were successfully destroyed. + * Determines if the user is allowed to execute the given query. + * The returned state contains a status code regarding whether or not the query + * is allowed. If the query IS allowed, the same user state passed as an + * argument maybe returned. + * @param state A non-null auth state with the user and AuthStatus.SUCCESS. + * @param query A non-null query. + * @return An AuthState object with a valid AuthStatus to evaluate for + * permission. */ - public abstract Boolean removeCredentials(final Map fields); + public abstract AuthState allowQuery(final AuthState state, + final Query query); } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 8fae8074cb..c10bec4753 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -43,7 +43,7 @@ import org.jboss.netty.util.Timeout; import org.jboss.netty.util.Timer; -import net.opentsdb.auth.AuthenticationPlugin; +import net.opentsdb.auth.Authentication; import net.opentsdb.tree.TreeBuilder; import net.opentsdb.tsd.RTPublisher; import net.opentsdb.tsd.StorageExceptionHandler; @@ -126,7 +126,7 @@ public final class TSDB { private final CompactionQueue compactionq; /** Authentication Plugin to use if configured */ - private AuthenticationPlugin authentication = null; + private Authentication authentication = null; /** Search indexer to use if configured */ private SearchPlugin search = null; @@ -363,9 +363,11 @@ public void initializePlugins(final boolean init_rpcs) { // load the authentication plugin if enabled if (config.getBoolean("tsd.core.authentication.enable")) { - authentication = PluginLoader.loadSpecificPlugin(config.getString("tsd.core.authentication.plugin"), AuthenticationPlugin.class); + authentication = PluginLoader.loadSpecificPlugin( + config.getString("tsd.core.authentication.plugin"), Authentication.class); if (authentication == null) { - throw new IllegalArgumentException("Unable to locate authentication plugin: "+ config.getString("tsd.core.authentication.plugin")); + throw new IllegalArgumentException("Unable to locate authentication " + + "plugin: " + config.getString("tsd.core.authentication.plugin")); } try { authentication.initialize(this); @@ -505,7 +507,7 @@ public void initializePlugins(final boolean init_rpcs) { * @return The Authentication Plugin * @since 2.4 */ - public final AuthenticationPlugin getAuth() { + public final Authentication getAuth() { return this.authentication; } @@ -808,6 +810,14 @@ public void collectStats(final StatsCollector collector) { collector.clearExtraTag("plugin"); } } + if (authentication != null) { + try { + collector.addExtraTag("plugin", "authentication"); + authentication.collectStats(collector); + } finally { + collector.clearExtraTag("plugin"); + } + } if (search != null) { try { collector.addExtraTag("plugin", "search"); diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index a607ce4123..1888ed3dd9 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -36,6 +36,7 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.auth.AuthState; import net.opentsdb.core.DataPoints; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.Query; @@ -158,6 +159,31 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, e.getMessage(), data_query.toString(), e); } + if (tsdb.getAuth() != null && tsdb.getAuth().authorization() != null) { + if (query.channel().getAttachment() == null || + !(query.channel().getAttachment() instanceof AuthState)) { + throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + "Authentication was enabled but the authentication state for " + + "this channel was not set properly"); + } + final AuthState state = tsdb.getAuth().authorization().allowQuery( + (AuthState) query.channel().getAttachment(), data_query); + switch (state.getStatus()) { + case SUCCESS: + // cary on :) + break; + case UNAUTHORIZED: + throw new BadRequestException(HttpResponseStatus.UNAUTHORIZED, + state.getMessage()); + case FORBIDDEN: + throw new BadRequestException(HttpResponseStatus.FORBIDDEN, + state.getMessage()); + default: + throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + state.getMessage()); + } + } + // if the user tried this query multiple times from the same IP and src port // they'll be rejected on subsequent calls final QueryStats query_stats = @@ -320,6 +346,30 @@ private void handleExpressionQuery(final TSDB tsdb, final HttpQuery query) { final net.opentsdb.query.pojo.Query v2_query = JSON.parseToObject(query.getContent(), net.opentsdb.query.pojo.Query.class); v2_query.validate(); + if (tsdb.getAuth() != null && tsdb.getAuth().authorization() != null) { + if (query.channel().getAttachment() == null || + !(query.channel().getAttachment() instanceof AuthState)) { + throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + "Authentication was enabled but the authentication state for " + + "this channel was not set properly"); + } + final AuthState state = tsdb.getAuth().authorization().allowQuery( + (AuthState) query.channel().getAttachment(), v2_query); + switch (state.getStatus()) { + case SUCCESS: + // cary on :) + break; + case UNAUTHORIZED: + throw new BadRequestException(HttpResponseStatus.UNAUTHORIZED, + state.getMessage()); + case FORBIDDEN: + throw new BadRequestException(HttpResponseStatus.FORBIDDEN, + state.getMessage()); + default: + throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + state.getMessage()); + } + } final QueryExecutor executor = new QueryExecutor(tsdb, v2_query); executor.execute(query); } diff --git a/test/tsd/TestHttpQuery.java b/test/tsd/TestHttpQuery.java index ab1a64800a..a87fa66e3b 100644 --- a/test/tsd/TestHttpQuery.java +++ b/test/tsd/TestHttpQuery.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.nio.charset.Charset; @@ -34,6 +36,8 @@ import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.DefaultChannelFuture; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; @@ -1205,5 +1209,12 @@ public void getSerializerStatus() throws Exception { HttpQuery.initializeSerializerMaps(tsdb); assertNotNull(HttpQuery.getSerializerStatus()); } - + + /** @param the query to mock a future callback for */ + public static void mockChannelFuture(final HttpQuery query) { + final ChannelFuture future = new DefaultChannelFuture(query.channel(), false); + when(query.channel().write(any(ChannelBuffer.class))).thenReturn(future); + future.setSuccess(); + } + } diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index b23f3ca523..eea877a6a7 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -18,6 +18,7 @@ import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -25,6 +26,10 @@ import java.nio.charset.Charset; import java.util.List; +import net.opentsdb.auth.AuthState; +import net.opentsdb.auth.Authentication; +import net.opentsdb.auth.Authorization; +import net.opentsdb.auth.AuthState.AuthStatus; import net.opentsdb.core.DataPoints; import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; @@ -674,5 +679,37 @@ public void gexpBadExpression() throws Exception { assertTrue(json.contains("factor")); } + @Test + public void v1Auth() throws Exception { + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final Authorization authorization = mock(Authorization.class); + final Authentication authentication = mock(Authentication.class); + final AuthState state = mock(AuthState.class); + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + when(tsdb.getAuth()).thenReturn(authentication); + when(query.channel().getAttachment()).thenReturn(state); + when(state.getStatus()).thenReturn(AuthStatus.SUCCESS); + when(authentication.authorization()).thenReturn(authorization); + when(authorization.allowQuery(eq(state), any(TSQuery.class))).thenReturn(state); + TestHttpQuery.mockChannelFuture(query); + rpc.execute(tsdb, query); + String json = + query.response().getContent().toString(Charset.forName("UTF-8")); + assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); + + when(state.getStatus()).thenReturn(AuthStatus.UNAUTHORIZED); + + try { + rpc.execute(tsdb, query); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { + assertEquals(e.getStatus(), HttpResponseStatus.UNAUTHORIZED); + } + } //TODO(cl) add unit tests for the rate options parsing } \ No newline at end of file From e0e7f6b895638a5163235031cdf2123944ac344c Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Thu, 20 Apr 2017 20:51:00 -0500 Subject: [PATCH 615/826] Update HBase Version --- tools/osx_full_stack_install.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/osx_full_stack_install.sh b/tools/osx_full_stack_install.sh index e409f03c7a..a8f28418b2 100644 --- a/tools/osx_full_stack_install.sh +++ b/tools/osx_full_stack_install.sh @@ -31,9 +31,9 @@ export INSTALL_DIR=$BASE_DIR/$SUBBASE_DIR; /bin/echo "Installing into $INSTALL_DIR"; /bin/mkdir -p $INSTALL_DIR; cd $INSTALL_DIR; -/usr/bin/curl -q http://mirror.cogentco.com/pub/apache/hbase/1.1.2/hbase-1.1.2-bin.tar.gz -o $INSTALL_DIR/hbase-1.1.2-bin.tar.gz 2>/dev/null; -/usr/bin/tar -xzvf hbase-1.1.2-bin.tar.gz -C $INSTALL_DIR/; -cd $INSTALL_DIR/hbase-1.1.2; +/usr/bin/curl -q http://mirror.cogentco.com/pub/apache/hbase/1.2.5/hbase-1.2.5-bin.tar.gz -o $INSTALL_DIR/hbase-1.2.5-bin.tar.gz 2>/dev/null; +/usr/bin/tar -xzvf hbase-1.2.5-bin.tar.gz -C $INSTALL_DIR/; +cd $INSTALL_DIR/hbase-1.2.5; /bin/mkdir -p $INSTALL_DIR/data/hbase; /bin/mkdir -p $INSTALL_DIR/data/zookeeper; /bin/cat < conf/hbase-site.xml @@ -70,13 +70,13 @@ cd $INSTALL_DIR/hbase-1.1.2; EOF -$INSTALL_DIR/hbase-1.1.2/bin/start-hbase.sh; +$INSTALL_DIR/hbase-1.2.5/bin/start-hbase.sh; cd $INSTALL_DIR /usr/bin/git clone https://github.com/OpenTSDB/opentsdb.git; cd opentsdb $INSTALL_DIR/opentsdb/build.sh clean; $INSTALL_DIR/opentsdb/build.sh; /bin/mkdir $INSTALL_DIR/opentsdb/build/cache; -export HBASE_HOME=$INSTALL_DIR/hbase-1.1.2; +export HBASE_HOME=$INSTALL_DIR/hbase-1.2.5; export COMPRESSION=NONE; $INSTALL_DIR/opentsdb/src/create_table.sh; $INSTALL_DIR/opentsdb/build/tsdb tsd --config=$INSTALL_DIR/opentsdb/src/opentsdb.conf --staticroot=$INSTALL_DIR/opentsdb/build/staticroot --cachedir=$INSTALL_DIR/opentsdb/build/cache --port=4242 --zkquorum=localhost:2181 --zkbasedir=/hbase --auto-metric & From 2316752ba6a20bd8519dbffee681aecf1f72d6a4 Mon Sep 17 00:00:00 2001 From: Christos Soulios Date: Sat, 22 Apr 2017 11:01:38 +0300 Subject: [PATCH 616/826] Upgraded asyncbigtable library to v0.3.0 Signed-off-by: Chris Larsen --- Makefile.am | 1 - .../alpn-boot-7.0.0.v20140317.jar.md5 | 1 - .../alpn-boot-7.1.0.v20141016.jar.md5 | 1 - .../alpn-boot-7.1.1.v20141016.jar.md5 | 1 - .../alpn-boot-7.1.2.v20141202.jar.md5 | 1 - .../alpn-boot-7.1.3.v20150130.jar.md5 | 1 - .../alpn-boot-8.0.0.v20140317.jar.md5 | 1 - .../alpn-boot-8.1.0.v20141016.jar.md5 | 1 - .../alpn-boot-8.1.1.v20141016.jar.md5 | 1 - .../alpn-boot-8.1.2.v20141202.jar.md5 | 1 - .../alpn-boot-8.1.3.v20150130.jar.md5 | 1 - .../alpn-boot-8.1.4.v20150727.jar.md5 | 1 - .../alpn-boot-8.1.5.v20150921.jar.md5 | 1 - .../alpn-boot-8.1.6.v20151105.jar.md5 | 1 - .../alpn-boot-8.1.7.v20160121.jar.md5 | 1 - third_party/alpn-boot/include.mk | 69 ------------------- ...gtable-0.3.0-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 6 +- third_party/include.mk | 3 +- tsdb.in | 3 +- 20 files changed, 6 insertions(+), 91 deletions(-) delete mode 100644 third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 delete mode 100644 third_party/alpn-boot/include.mk create mode 100644 third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 diff --git a/Makefile.am b/Makefile.am index 1ae0a175e7..1d40e4fb32 100644 --- a/Makefile.am +++ b/Makefile.am @@ -214,7 +214,6 @@ tsdb_DEPS = \ if BIGTABLE tsdb_DEPS += \ - $(ALPN_BOOT) \ $(ASYNCBIGTABLE) maven_profile_bigtable := true maven_profile_hbase := false diff --git a/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 b/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 deleted file mode 100644 index 6e005e9074..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -81e4f665ff2bf40720f9b345cee6b429 diff --git a/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 deleted file mode 100644 index 529b4c8b5d..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b1569a1f34a0ca61d34c3c3e5020a8ef diff --git a/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 deleted file mode 100644 index d125a86172..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -d9add9c8eb6c087e408b076e6d823ddd diff --git a/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 deleted file mode 100644 index 2ea2c00f10..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -391f659c583e2ea0f05515a6f6147620 diff --git a/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 deleted file mode 100644 index b51d10325d..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b10366c9301e954bcedbf9130b6381c7 diff --git a/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 b/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 deleted file mode 100644 index b34b6459eb..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -de73395f7e20619699a07063e640d5f7 diff --git a/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 deleted file mode 100644 index 89c8317c71..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -d4a325fdb7e86bd0d9ac583998165a84 diff --git a/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 deleted file mode 100644 index 5993e27253..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -4655c087dda15743449ff31717d98e50 diff --git a/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 deleted file mode 100644 index 01c9bb0cef..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -9689564f4d7cc15918568f7006b85bf5 diff --git a/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 deleted file mode 100644 index a964d2acca..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -a5803d4ff6ce36d15c750104a117dfb1 diff --git a/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 deleted file mode 100644 index c830ed3e9f..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -1543b3403ae451ca2ec0944de403f6cc diff --git a/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 deleted file mode 100644 index 38a5c04b33..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b05ac69bd8697c4bfc4cf896dea63c94 diff --git a/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 deleted file mode 100644 index 209c34b148..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -0f7bbc8e3da3948082c4d3a510d6fe43 \ No newline at end of file diff --git a/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 deleted file mode 100644 index a70c6c1356..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -4af7a18a9b4549a1796b182dabca9062 \ No newline at end of file diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk deleted file mode 100644 index a949d1da29..0000000000 --- a/third_party/alpn-boot/include.mk +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2015 The OpenTSDB Authors. -# -# This library 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 library 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 library. If not, see . - -# ALPN_BOOT_VERSION := 7.1.3.v20150130 - -ALPN_BOOT_VERSION = $(shell version= ;\ - if [[ "@JAVA@" ]]; then \ - version=$$("@JAVA@" -version 2>&1 | awk -F '"' '/version/ {print $$2}'); \ - else\ - echo "Failed to parse Java version";\ - exit 1;\ - fi; \ - if [[ $$version =~ ^([0-9]+\.[0-9]+)\.([0-9])[_Uu]([0-9]+) ]]; then \ - major=$${BASH_REMATCH[1]};\ - minor=$${BASH_REMATCH[2]}; \ - sub=$${BASH_REMATCH[3]}; \ - if [[ $$major = "1.7" ]]; then \ - if [[ $$sub -lt 71 ]]; then \ - echo "7.1.0.v20141016"; \ - elif [[ $$sub -lt 75 ]]; then \ - echo "7.1.2.v20141202"; \ - else \ - echo "7.1.3.v20150130"; \ - fi \ - elif [[ $$major = "1.8" ]]; then \ - if [[ $$sub -lt 25 ]]; then \ - echo "8.1.0.v20141016"; \ - elif [[ $$sub -lt 31 ]]; then \ - echo "8.1.2.v20141202"; \ - elif [[ $$sub -lt 51 ]]; then \ - echo "8.1.3.v20150130"; \ - elif [[ $$sub -lt 60 ]]; then \ - echo "8.1.4.v20150727"; \ - elif [[ $$sub -lt 65 ]]; then \ - echo "8.1.5.v20150921"; \ - elif [[ $$sub -lt 71 ]]; then \ - echo "8.1.6.v20151105"; \ - else \ - echo "8.1.7.v20160121"; \ - fi \ - else \ - echo "Unsupported major Java version: $$major"; \ - exit 1; \ - fi \ - else \ - echo "Possibly invalid Java version (couldn't parse): $$version"; \ - exit 1; \ - fi) - - -ALPN_BOOT := third_party/alpn-boot/alpn-boot-$(ALPN_BOOT_VERSION).jar -ALBPN_BOOT_BASE_URL := http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/$(ALPN_BOOT_VERSION) - -$(ALPN_BOOT): $(ALPN_BOOT).md5 - set dummy "$(ALBPN_BOOT_BASE_URL)" "$(ALPN_BOOT)"; shift; $(FETCH_DEPENDENCY) - -THIRD_PARTY += $(ALPN_BOOT) diff --git a/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..ba351e7405 --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +4384ac07967ee99f54d4c29f9806d7e7 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index 8549f1451c..9fd3bc0449 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -13,11 +13,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCBIGTABLE_VERSION := 0.2.1-20160228.235952-3 +ASYNCBIGTABLE_VERSION := 0.3.0 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.2.1-SNAPSHOT/ +ASYNCBIGTABLE_BASE_URL := https://oss.sonatype.org/content/repositories/releases/com/pythian/opentsdb/asyncbigtable/0.3.0/ $(ASYNCBIGTABLE): $(ASYNCBIGTABLE).md5 set dummy "$(ASYNCBIGTABLE_BASE_URL)" "$(ASYNCBIGTABLE)"; shift; $(FETCH_DEPENDENCY) -THIRD_PARTY += $(ASYNCBIGTABLE) \ No newline at end of file +THIRD_PARTY += $(ASYNCBIGTABLE) diff --git a/third_party/include.mk b/third_party/include.mk index dc2f22d20b..01743e4e28 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -38,7 +38,6 @@ include third_party/validation-api/include.mk include third_party/apache/include.mk if BIGTABLE -include third_party/alpn-boot/include.mk include third_party/asyncbigtable/include.mk ASYNCCASSANDRA_VERSION = 0.0 ASYNCHBASE_VERSION = 0.0 @@ -56,4 +55,4 @@ include third_party/zookeeper/include.mk ASYNCBIGTABLE_VERSION = 0.0 ASYNCCASSANDRA_VERSION = 0.0 endif -endif \ No newline at end of file +endif diff --git a/tsdb.in b/tsdb.in index 534deb03c1..b68eaf2c89 100644 --- a/tsdb.in +++ b/tsdb.in @@ -112,8 +112,7 @@ then USE_BIGTABLE=1 echo "Running OpenTSDB with Bigtable support" - ALPN_BOOT_JAR=$(find $localdir -name alpn-boot\*.jar) - exec $JAVA $JVMARGS -classpath "$CLASSPATH:$HBASE_CONF" -Xbootclasspath/p:$ALPN_BOOT_JAR net.opentsdb.tools.$MAINCLASS "$@" + exec $JAVA $JVMARGS -classpath "$CLASSPATH:$HBASE_CONF" net.opentsdb.tools.$MAINCLASS "$@" else exec $JAVA $JVMARGS -classpath "$CLASSPATH" net.opentsdb.tools.$MAINCLASS "$@" fi From e831735e43ef567cc62513f597ae647d37e53094 Mon Sep 17 00:00:00 2001 From: Christos Soulios Date: Sat, 22 Apr 2017 11:01:38 +0300 Subject: [PATCH 617/826] Upgraded asyncbigtable library to v0.3.0 Signed-off-by: Chris Larsen --- Makefile.am | 1 - .../alpn-boot-7.0.0.v20140317.jar.md5 | 1 - .../alpn-boot-7.1.0.v20141016.jar.md5 | 1 - .../alpn-boot-7.1.1.v20141016.jar.md5 | 1 - .../alpn-boot-7.1.2.v20141202.jar.md5 | 1 - .../alpn-boot-7.1.3.v20150130.jar.md5 | 1 - .../alpn-boot-8.0.0.v20140317.jar.md5 | 1 - .../alpn-boot-8.1.0.v20141016.jar.md5 | 1 - .../alpn-boot-8.1.1.v20141016.jar.md5 | 1 - .../alpn-boot-8.1.2.v20141202.jar.md5 | 1 - .../alpn-boot-8.1.3.v20150130.jar.md5 | 1 - .../alpn-boot-8.1.4.v20150727.jar.md5 | 1 - .../alpn-boot-8.1.5.v20150921.jar.md5 | 1 - .../alpn-boot-8.1.6.v20151105.jar.md5 | 1 - .../alpn-boot-8.1.7.v20160121.jar.md5 | 1 - third_party/alpn-boot/include.mk | 69 ------------------- ...gtable-0.3.0-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 6 +- third_party/include.mk | 3 +- tsdb.in | 3 +- 20 files changed, 6 insertions(+), 91 deletions(-) delete mode 100644 third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 delete mode 100644 third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 delete mode 100644 third_party/alpn-boot/include.mk create mode 100644 third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 diff --git a/Makefile.am b/Makefile.am index f607173b04..753b79a5e5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -233,7 +233,6 @@ tsdb_DEPS = \ if BIGTABLE tsdb_DEPS += \ - $(ALPN_BOOT) \ $(ASYNCBIGTABLE) maven_profile_bigtable := true maven_profile_hbase := false diff --git a/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 b/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 deleted file mode 100644 index 6e005e9074..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.0.0.v20140317.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -81e4f665ff2bf40720f9b345cee6b429 diff --git a/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 deleted file mode 100644 index 529b4c8b5d..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.0.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b1569a1f34a0ca61d34c3c3e5020a8ef diff --git a/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 deleted file mode 100644 index d125a86172..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.1.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -d9add9c8eb6c087e408b076e6d823ddd diff --git a/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 deleted file mode 100644 index 2ea2c00f10..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.2.v20141202.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -391f659c583e2ea0f05515a6f6147620 diff --git a/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 b/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 deleted file mode 100644 index b51d10325d..0000000000 --- a/third_party/alpn-boot/alpn-boot-7.1.3.v20150130.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b10366c9301e954bcedbf9130b6381c7 diff --git a/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 b/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 deleted file mode 100644 index b34b6459eb..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.0.0.v20140317.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -de73395f7e20619699a07063e640d5f7 diff --git a/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 deleted file mode 100644 index 89c8317c71..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.0.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -d4a325fdb7e86bd0d9ac583998165a84 diff --git a/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 deleted file mode 100644 index 5993e27253..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.1.v20141016.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -4655c087dda15743449ff31717d98e50 diff --git a/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 deleted file mode 100644 index 01c9bb0cef..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.2.v20141202.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -9689564f4d7cc15918568f7006b85bf5 diff --git a/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 deleted file mode 100644 index a964d2acca..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.3.v20150130.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -a5803d4ff6ce36d15c750104a117dfb1 diff --git a/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 deleted file mode 100644 index c830ed3e9f..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.4.v20150727.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -1543b3403ae451ca2ec0944de403f6cc diff --git a/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 deleted file mode 100644 index 38a5c04b33..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.5.v20150921.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b05ac69bd8697c4bfc4cf896dea63c94 diff --git a/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 deleted file mode 100644 index 209c34b148..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.6.v20151105.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -0f7bbc8e3da3948082c4d3a510d6fe43 \ No newline at end of file diff --git a/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 b/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 deleted file mode 100644 index a70c6c1356..0000000000 --- a/third_party/alpn-boot/alpn-boot-8.1.7.v20160121.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -4af7a18a9b4549a1796b182dabca9062 \ No newline at end of file diff --git a/third_party/alpn-boot/include.mk b/third_party/alpn-boot/include.mk deleted file mode 100644 index a949d1da29..0000000000 --- a/third_party/alpn-boot/include.mk +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2015 The OpenTSDB Authors. -# -# This library 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 library 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 library. If not, see . - -# ALPN_BOOT_VERSION := 7.1.3.v20150130 - -ALPN_BOOT_VERSION = $(shell version= ;\ - if [[ "@JAVA@" ]]; then \ - version=$$("@JAVA@" -version 2>&1 | awk -F '"' '/version/ {print $$2}'); \ - else\ - echo "Failed to parse Java version";\ - exit 1;\ - fi; \ - if [[ $$version =~ ^([0-9]+\.[0-9]+)\.([0-9])[_Uu]([0-9]+) ]]; then \ - major=$${BASH_REMATCH[1]};\ - minor=$${BASH_REMATCH[2]}; \ - sub=$${BASH_REMATCH[3]}; \ - if [[ $$major = "1.7" ]]; then \ - if [[ $$sub -lt 71 ]]; then \ - echo "7.1.0.v20141016"; \ - elif [[ $$sub -lt 75 ]]; then \ - echo "7.1.2.v20141202"; \ - else \ - echo "7.1.3.v20150130"; \ - fi \ - elif [[ $$major = "1.8" ]]; then \ - if [[ $$sub -lt 25 ]]; then \ - echo "8.1.0.v20141016"; \ - elif [[ $$sub -lt 31 ]]; then \ - echo "8.1.2.v20141202"; \ - elif [[ $$sub -lt 51 ]]; then \ - echo "8.1.3.v20150130"; \ - elif [[ $$sub -lt 60 ]]; then \ - echo "8.1.4.v20150727"; \ - elif [[ $$sub -lt 65 ]]; then \ - echo "8.1.5.v20150921"; \ - elif [[ $$sub -lt 71 ]]; then \ - echo "8.1.6.v20151105"; \ - else \ - echo "8.1.7.v20160121"; \ - fi \ - else \ - echo "Unsupported major Java version: $$major"; \ - exit 1; \ - fi \ - else \ - echo "Possibly invalid Java version (couldn't parse): $$version"; \ - exit 1; \ - fi) - - -ALPN_BOOT := third_party/alpn-boot/alpn-boot-$(ALPN_BOOT_VERSION).jar -ALBPN_BOOT_BASE_URL := http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/$(ALPN_BOOT_VERSION) - -$(ALPN_BOOT): $(ALPN_BOOT).md5 - set dummy "$(ALBPN_BOOT_BASE_URL)" "$(ALPN_BOOT)"; shift; $(FETCH_DEPENDENCY) - -THIRD_PARTY += $(ALPN_BOOT) diff --git a/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..ba351e7405 --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +4384ac07967ee99f54d4c29f9806d7e7 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index 8549f1451c..9fd3bc0449 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -13,11 +13,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCBIGTABLE_VERSION := 0.2.1-20160228.235952-3 +ASYNCBIGTABLE_VERSION := 0.3.0 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.2.1-SNAPSHOT/ +ASYNCBIGTABLE_BASE_URL := https://oss.sonatype.org/content/repositories/releases/com/pythian/opentsdb/asyncbigtable/0.3.0/ $(ASYNCBIGTABLE): $(ASYNCBIGTABLE).md5 set dummy "$(ASYNCBIGTABLE_BASE_URL)" "$(ASYNCBIGTABLE)"; shift; $(FETCH_DEPENDENCY) -THIRD_PARTY += $(ASYNCBIGTABLE) \ No newline at end of file +THIRD_PARTY += $(ASYNCBIGTABLE) diff --git a/third_party/include.mk b/third_party/include.mk index dc2f22d20b..01743e4e28 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -38,7 +38,6 @@ include third_party/validation-api/include.mk include third_party/apache/include.mk if BIGTABLE -include third_party/alpn-boot/include.mk include third_party/asyncbigtable/include.mk ASYNCCASSANDRA_VERSION = 0.0 ASYNCHBASE_VERSION = 0.0 @@ -56,4 +55,4 @@ include third_party/zookeeper/include.mk ASYNCBIGTABLE_VERSION = 0.0 ASYNCCASSANDRA_VERSION = 0.0 endif -endif \ No newline at end of file +endif diff --git a/tsdb.in b/tsdb.in index 534deb03c1..b68eaf2c89 100644 --- a/tsdb.in +++ b/tsdb.in @@ -112,8 +112,7 @@ then USE_BIGTABLE=1 echo "Running OpenTSDB with Bigtable support" - ALPN_BOOT_JAR=$(find $localdir -name alpn-boot\*.jar) - exec $JAVA $JVMARGS -classpath "$CLASSPATH:$HBASE_CONF" -Xbootclasspath/p:$ALPN_BOOT_JAR net.opentsdb.tools.$MAINCLASS "$@" + exec $JAVA $JVMARGS -classpath "$CLASSPATH:$HBASE_CONF" net.opentsdb.tools.$MAINCLASS "$@" else exec $JAVA $JVMARGS -classpath "$CLASSPATH" net.opentsdb.tools.$MAINCLASS "$@" fi From bf3e374fe93673fec2cc9adda99c5f4e43152952 Mon Sep 17 00:00:00 2001 From: Jagmeet Singh bali Date: Tue, 2 May 2017 18:13:52 +0530 Subject: [PATCH 618/826] Log query stats in case of Closed Channel also Signed-off-by: Chris Larsen --- src/tsd/AbstractHttpQuery.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 035404a5ad..b09bc8b8d7 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -386,6 +386,9 @@ public void notFound() { */ public void sendStatusOnly(final HttpResponseStatus status) { if (!chan.isConnected()) { + if(stats != null) { + stats.markSendFailed(); + } done(); return; } @@ -414,6 +417,9 @@ public void sendBuffer(final HttpResponseStatus status, final ChannelBuffer buf, final String contentType) { if (!chan.isConnected()) { + if(stats != null) { + stats.markSendFailed(); + } done(); return; } @@ -442,7 +448,10 @@ public void sendBuffer(final HttpResponseStatus status, private class SendSuccess implements ChannelFutureListener { @Override public void operationComplete(final ChannelFuture future) throws Exception { - stats.markSent(); + if(future.isSuccess()) { + stats.markSent();} + else + stats.markSendFailed(); } } From 52fd9969d69e127324d3df4a7661a001caa57d1e Mon Sep 17 00:00:00 2001 From: Jagmeet Singh bali Date: Tue, 2 May 2017 18:13:52 +0530 Subject: [PATCH 619/826] Log query stats in case of Closed Channel also Signed-off-by: Chris Larsen --- src/tsd/AbstractHttpQuery.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 035404a5ad..b09bc8b8d7 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -386,6 +386,9 @@ public void notFound() { */ public void sendStatusOnly(final HttpResponseStatus status) { if (!chan.isConnected()) { + if(stats != null) { + stats.markSendFailed(); + } done(); return; } @@ -414,6 +417,9 @@ public void sendBuffer(final HttpResponseStatus status, final ChannelBuffer buf, final String contentType) { if (!chan.isConnected()) { + if(stats != null) { + stats.markSendFailed(); + } done(); return; } @@ -442,7 +448,10 @@ public void sendBuffer(final HttpResponseStatus status, private class SendSuccess implements ChannelFutureListener { @Override public void operationComplete(final ChannelFuture future) throws Exception { - stats.markSent(); + if(future.isSuccess()) { + stats.markSent();} + else + stats.markSendFailed(); } } From 3ce243d16e4a7a1abf09b4bccc7c234a44c7a86a Mon Sep 17 00:00:00 2001 From: Ioan Date: Wed, 12 Apr 2017 13:09:21 +0200 Subject: [PATCH 620/826] added java-8-oracle PATH in JDK_DIRS added /usr/lib/jvm/java-8-oracle in JDK_DIRS ...if JAVA_HOME is not defined in $DEFAULT Signed-off-by: Chris Larsen --- build-aux/deb/init.d/opentsdb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build-aux/deb/init.d/opentsdb b/build-aux/deb/init.d/opentsdb index 4eb8ee3847..9e46e9037e 100644 --- a/build-aux/deb/init.d/opentsdb +++ b/build-aux/deb/init.d/opentsdb @@ -29,7 +29,8 @@ MAX_OPEN_FILES=65535 # The first existing directory is used for JAVA_HOME # (if JAVA_HOME is not defined in $DEFAULT) -JDK_DIRS="/usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ +JDK_DIRS="/usr/lib/jvm/java-8-oracle \ + /usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-6-openjdk \ /usr/lib/jvm/java-6-openjdk-amd64 /usr/lib/jvm/java-6-openjdk-i386 \ From abaa6fc405de80eedd30f30c213a08fe0a236b8c Mon Sep 17 00:00:00 2001 From: Ioan Date: Wed, 12 Apr 2017 13:09:21 +0200 Subject: [PATCH 621/826] added java-8-oracle PATH in JDK_DIRS added /usr/lib/jvm/java-8-oracle in JDK_DIRS ...if JAVA_HOME is not defined in $DEFAULT Signed-off-by: Chris Larsen --- build-aux/deb/init.d/opentsdb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build-aux/deb/init.d/opentsdb b/build-aux/deb/init.d/opentsdb index 4eb8ee3847..9e46e9037e 100644 --- a/build-aux/deb/init.d/opentsdb +++ b/build-aux/deb/init.d/opentsdb @@ -29,7 +29,8 @@ MAX_OPEN_FILES=65535 # The first existing directory is used for JAVA_HOME # (if JAVA_HOME is not defined in $DEFAULT) -JDK_DIRS="/usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ +JDK_DIRS="/usr/lib/jvm/java-8-oracle \ + /usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-6-openjdk \ /usr/lib/jvm/java-6-openjdk-amd64 /usr/lib/jvm/java-6-openjdk-i386 \ From 9de9a2103f9012c264381096c75aef9ce7276c0e Mon Sep 17 00:00:00 2001 From: Jason Harvey Date: Fri, 10 Mar 2017 23:18:40 -0900 Subject: [PATCH 622/826] Pass FAMILY to get(). Signed-off-by: Chris Larsen --- src/core/TSDB.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 57e591ef07..1a5683386f 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1586,7 +1586,7 @@ final void scheduleForCompaction(final byte[] row, final int base_time) { /** Gets the entire given row from the data table. */ final Deferred> get(final byte[] key) { - return client.get(new GetRequest(table, key)); + return client.get(new GetRequest(table, key, FAMILY)); } /** Puts the given value into the data table. */ From b3a686b5fbea65da6d5dcfbcb05867121182fd2d Mon Sep 17 00:00:00 2001 From: Jason Harvey Date: Fri, 10 Mar 2017 23:18:40 -0900 Subject: [PATCH 623/826] Pass FAMILY to get(). Signed-off-by: Chris Larsen --- src/core/TSDB.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index c10bec4753..0b0d4b2579 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -2014,7 +2014,7 @@ final void scheduleForCompaction(final byte[] row, final int base_time) { /** Gets the entire given row from the data table. */ final Deferred> get(final byte[] key) { - return client.get(new GetRequest(table, key)); + return client.get(new GetRequest(table, key, FAMILY)); } /** Puts the given value into the data table. */ From 2873229c0beae248ee431d71df7f12e14e350462 Mon Sep 17 00:00:00 2001 From: Marcin Januszkiewicz Date: Mon, 23 Jan 2017 09:55:51 +0100 Subject: [PATCH 624/826] Fix spotted incorrect conditional. Fix #708 by using better comparisons The implemented Comparable interface was very inconsistent, leading to randomly sorted lists. Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index 0c2665416f..e273b51d97 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -475,7 +475,7 @@ private List getFilters(final boolean group_by) { if (filter.tagk.isEmpty() || filter.tagv.isEmpty()) { continue; } - if (filter.is_groupby = group_by) { + if (filter.is_groupby == group_by) { filters.add(filter); } } @@ -752,8 +752,12 @@ public int compareTo(final Filter filter) { if (filter == this) { return 0; } - return tagk.compareTo(filter.tagk) + - tagv.compareTo(filter.tagv); + int tagkv_order = tagk.compareTo(filter.tagk) == 0 ? + tagv.compareTo(filter.tagv) : tagk.compareTo(filter.tagk); + + int groupby_order = is_groupby == filter.is_groupby ? 0 : (is_groupby ? 1 : -1); + + return tagkv_order == 0 ? groupby_order : tagkv_order; } } From 9efb3210d0698ee4965b603a8b0247cec4b76e9d Mon Sep 17 00:00:00 2001 From: Marcin Januszkiewicz Date: Mon, 23 Jan 2017 09:55:51 +0100 Subject: [PATCH 625/826] Fix spotted incorrect conditional. Fix #708 by using better comparisons The implemented Comparable interface was very inconsistent, leading to randomly sorted lists. Signed-off-by: Chris Larsen --- src/tsd/client/MetricForm.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tsd/client/MetricForm.java b/src/tsd/client/MetricForm.java index 0c2665416f..e273b51d97 100644 --- a/src/tsd/client/MetricForm.java +++ b/src/tsd/client/MetricForm.java @@ -475,7 +475,7 @@ private List getFilters(final boolean group_by) { if (filter.tagk.isEmpty() || filter.tagv.isEmpty()) { continue; } - if (filter.is_groupby = group_by) { + if (filter.is_groupby == group_by) { filters.add(filter); } } @@ -752,8 +752,12 @@ public int compareTo(final Filter filter) { if (filter == this) { return 0; } - return tagk.compareTo(filter.tagk) + - tagv.compareTo(filter.tagv); + int tagkv_order = tagk.compareTo(filter.tagk) == 0 ? + tagv.compareTo(filter.tagv) : tagk.compareTo(filter.tagk); + + int groupby_order = is_groupby == filter.is_groupby ? 0 : (is_groupby ? 1 : -1); + + return tagkv_order == 0 ? groupby_order : tagkv_order; } } From 2112d7ed6abace19600854d31c960c6708a25955 Mon Sep 17 00:00:00 2001 From: Karan Mehta Date: Wed, 17 May 2017 09:18:55 -0700 Subject: [PATCH 626/826] Fix #962 Allow OpenTSDB to take advantage of DateTieredCompaction Signed-off-by: Chris Larsen --- Makefile.am | 2 + src/core/AppendDataPoints.java | 4 +- src/core/BatchedDataPoints.java | 2 +- src/core/ColumnDatapointIterator.java | 40 +- src/core/CompactionQueue.java | 90 +++- src/core/IncomingDataPoints.java | 6 +- src/core/RequestBuilder.java | 34 ++ src/core/TSDB.java | 352 +++++++------- src/core/TsdbQuery.java | 5 + src/meta/Annotation.java | 6 +- src/tools/CliOptions.java | 8 +- src/tools/Fsck.java | 263 +++++------ src/utils/Config.java | 167 ++++--- test/core/TestCompactionQueue.java | 437 ++++++++++-------- test/core/TestTSDBAddPoint.java | 30 +- test/core/TestTsdbQuery.java | 127 ++--- test/core/TestTsdbQueryQueries.java | 474 ++++++++++--------- test/core/TestTsdbTSConfig.java | 189 ++++++++ test/meta/TestAnnotation.java | 12 +- test/storage/MockBase.java | 637 ++++++++++++++------------ test/tsd/TestAnnotationRpc.java | 12 +- 21 files changed, 1716 insertions(+), 1181 deletions(-) create mode 100644 src/core/RequestBuilder.java create mode 100644 test/core/TestTsdbTSConfig.java diff --git a/Makefile.am b/Makefile.am index 753b79a5e5..fcee869859 100644 --- a/Makefile.am +++ b/Makefile.am @@ -204,6 +204,7 @@ tsdb_SRC := \ src/utils/PluginLoader.java \ src/utils/Threads.java \ src/core/iRowSeq.java \ + src/core/RequestBuilder.java src/rollup/NoSuchRollupForIntervalException.java \ src/rollup/NoSuchRollupForTableException.java \ src/rollup/RollUpDataPoint.java \ @@ -289,6 +290,7 @@ test_SRC := \ test/core/TestTsdbQuerySaltedAppend.java \ test/core/TestTSQuery.java \ test/core/TestTSSubQuery.java \ + test/core/TestTsdbTSConfig.java \ test/plugin/DummyPlugin.java \ test/meta/TestAnnotation.java \ test/meta/TestTSMeta.java \ diff --git a/src/core/AppendDataPoints.java b/src/core/AppendDataPoints.java index 4647ff4b23..3646e4039b 100644 --- a/src/core/AppendDataPoints.java +++ b/src/core/AppendDataPoints.java @@ -227,8 +227,8 @@ public final Collection parseKeyValue(final TSDB tsdb, final KeyValue kv) if (repair && needs_repair) { LOG.debug("Repairing appended data column " + kv); - final PutRequest put = new PutRequest(tsdb.table, kv.key(), - TSDB.FAMILY(), kv.qualifier(), healed_cell); + final PutRequest put = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.table, kv.key(), + TSDB.FAMILY(), kv.qualifier(), healed_cell, kv.timestamp()); repaired_deferred = tsdb.getClient().put(put); } diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index e399802345..82c32063dd 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -138,7 +138,7 @@ public Deferred persist() { final byte[] v = Arrays.copyOfRange(batched_value, 0, value_index); final byte[] r = Arrays.copyOfRange(row_key, 0, row_key.length); reset(); - return tsdb.put(r, q, v); + return tsdb.put(r, q, v, base_time); } @Override diff --git a/src/core/ColumnDatapointIterator.java b/src/core/ColumnDatapointIterator.java index 423f796aa9..8df8a20626 100644 --- a/src/core/ColumnDatapointIterator.java +++ b/src/core/ColumnDatapointIterator.java @@ -12,14 +12,18 @@ // see . package net.opentsdb.core; +import java.nio.ByteBuffer; import java.util.Arrays; + import org.hbase.async.KeyValue; +import net.opentsdb.utils.Pair; + /** * Internal implementation detail for {@link net.opentsdb.core.CompactionQueue}. This * allows iterating over the datapoints in a column without creating objects for each * datapoint. -* +* * @since 2.1 */ final class ColumnDatapointIterator implements Comparable { @@ -116,6 +120,19 @@ public void writeToBuffers(ByteBufferList compQualifier, ByteBufferList compValu compValue.add(value, value_offset, current_val_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()); + } + + public Pair getOffsets() { + return new Pair(qualifier_offset, value_offset); + } + + public Pair getOffsetLengths() { + return new Pair(current_qual_length, current_val_length); + } + /** * @return the length of the qualifier for the current datapoint. */ @@ -135,9 +152,16 @@ public byte[] getCopyOfCurrentValue() { } } + /** + * @return a copy of the Qualifier of the current datapoint, after any fixups. + */ + public byte[] getCopyOfCurrentQualifier() { + return Arrays.copyOfRange(qualifier, qualifier_offset, qualifier_offset + current_qual_length); + } + /** * Advance to the next datapoint. - * + * * @return true if there is at least one more datapoint after advancing */ public boolean advance() { @@ -174,6 +198,18 @@ public int compareTo(ColumnDatapointIterator o) { return c; } + public double getCellValueAsDouble() { + byte[] copy = this.getCopyOfCurrentValue(); + byte[] qual = this.getCopyOfCurrentQualifier(); + ByteBuffer bb = ByteBuffer.wrap(copy); + if (Internal.isFloat(qual)) { + return copy.length == 4 ? bb.getFloat() : bb.getDouble(); + } else { + return ((copy.length == 1) ? bb.get() + : ((copy.length == 2) ? bb.getShort() : ((copy.length == 4) ? bb.getInt() : bb.getLong()))); + } + } + @Override public String toString() { return "q=" + Arrays.toString(qualifier) + " [ofs=" + qualifier_offset + "], v=" diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 523f4a0d6e..886d583825 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -35,6 +35,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.JSON; +import net.opentsdb.utils.Pair; /** * "Queue" of rows to compact. @@ -99,6 +100,7 @@ public CompactionQueue(final TSDB tsdb) { min_flush_threshold = tsdb.config.getInt("tsd.storage.compaction.min_flush_threshold"); max_concurrent_flushes = tsdb.config.getInt("tsd.storage.compaction.max_concurrent_flushes"); flush_speed = tsdb.config.getInt("tsd.storage.compaction.flush_speed"); + if (tsdb.config.enable_compactions()) { startCompactionThread(); } @@ -178,7 +180,7 @@ private Deferred> flush(final long cut_off, int maxflushes) { if (seed == row.hashCode() % 3) { continue; } - final long base_time = Bytes.getUnsignedInt(row, + final long base_time = Bytes.getUnsignedInt(row, Const.SALT_WIDTH() + metric_width); if (base_time > cut_off) { break; @@ -258,7 +260,7 @@ KeyValue compact(final ArrayList row, * Maintains state for a single compaction; exists to break the steps down into manageable * pieces without having to worry about returning multiple values and passing many parameters * around. - * + * * @since 2.1 */ private class Compaction { @@ -267,6 +269,7 @@ private class Compaction { private final ArrayList row; private final KeyValue[] compacted; private final List annotations; + private long compactedKVTimestamp; private final int nkvs; @@ -285,7 +288,7 @@ private class Compaction { // KeyValue containing the longest qualifier for the datapoint, used to optimize // checking if the compacted qualifier already exists. private KeyValue longest; - + // the latest append column. If set then we don't want to re-write the row // and if we only had a single column with a single value, we return this. private KeyValue last_append_column; @@ -296,6 +299,7 @@ public Compaction(ArrayList row, KeyValue[] compacted, List(nkvs); + compactedKVTimestamp = Long.MIN_VALUE; } /** @@ -336,6 +340,7 @@ public Deferred compact() { return null; } + compactedKVTimestamp = Long.MIN_VALUE; // go through all the columns, process annotations, and heap = new PriorityQueue(nkvs); int tot_values = buildHeapProcessAnnotations(); @@ -367,7 +372,7 @@ public Deferred compact() { if (compacted != null) { // Caller is interested in the compacted form. compacted[0] = compact; - final long base_time = Bytes.getUnsignedInt(compact.key(), + final long base_time = Bytes.getUnsignedInt(compact.key(), Const.SALT_WIDTH() + metric_width); final long cut_off = System.currentTimeMillis() / 1000 - Const.MAX_TIMESPAN - 1; @@ -385,7 +390,7 @@ public Deferred compact() { deleted_cells.addAndGet(to_delete.size()); // We're going to delete this. if (write) { written_cells.incrementAndGet(); - Deferred deferred = tsdb.put(key, compact.qualifier(), compact.value()); + Deferred deferred = tsdb.put(key, compact.qualifier(), compact.value(), compactedKVTimestamp); if (!to_delete.isEmpty()) { deferred = deferred.addCallbacks(new DeleteCompactedCB(to_delete), handle_write_error); } @@ -426,6 +431,7 @@ private KeyValue findFirstDatapointColumn() { */ private int buildHeapProcessAnnotations() { int tot_values = 0; + for (final KeyValue kv : row) { byte[] qual = kv.qualifier(); int len = qual.length; @@ -434,15 +440,16 @@ private int buildHeapProcessAnnotations() { if (qual[0] == Annotation.PREFIX()) { annotations.add(JSON.parseToObject(kv.value(), Annotation.class)); } else if (qual[0] == AppendDataPoints.APPEND_COLUMN_PREFIX){ + compactedKVTimestamp = Math.max(compactedKVTimestamp, kv.timestamp()); final AppendDataPoints adp = new AppendDataPoints(); tot_values += adp.parseKeyValue(tsdb, kv).size(); - last_append_column = new KeyValue(kv.key(), kv.family(), + last_append_column = new KeyValue(kv.key(), kv.family(), adp.qualifier(), kv.timestamp(), adp.value()); - if (longest == null || + if (longest == null || longest.qualifier().length < last_append_column.qualifier().length) { longest = last_append_column; } - final ColumnDatapointIterator col = + final ColumnDatapointIterator col = new ColumnDatapointIterator(last_append_column); if (col.hasMoreData()) { heap.add(col); @@ -461,6 +468,7 @@ private int buildHeapProcessAnnotations() { longest = kv; } ColumnDatapointIterator col = new ColumnDatapointIterator(kv); + compactedKVTimestamp = Math.max(compactedKVTimestamp, kv.timestamp()); if (col.hasMoreData()) { heap.add(col); } @@ -476,8 +484,59 @@ private int buildHeapProcessAnnotations() { * @param compacted_qual qualifiers for sorted datapoints * @param compacted_val values for sorted datapoints */ - private void mergeDatapoints(ByteBufferList compacted_qual, + + private void mergeDatapoints(ByteBufferList compacted_qual, ByteBufferList compacted_val) { + if (tsdb.getConfig().use_otsdb_timestamp()) { + dtcsMergeDataPoints(compacted_qual, compacted_val); + } else { + defaultMergeDataPoints(compacted_qual, compacted_val); + } + } + + private void dtcsMergeDataPoints(ByteBufferList compacted_qual, ByteBufferList compacted_val) { + // Compare timestamps for two KeyValues at the same time, if they are same compare their values + // Return maximum or minimum value depending upon tsd.storage.use_max_value parameter + // This function is called once for every RowKey, so we only care about comparing offsets, which + // are a part of column qualifier + ColumnDatapointIterator col1 = null; + ColumnDatapointIterator col2 = null; + while (!heap.isEmpty()) { + col1 = heap.remove(); + Pair offsets = col1.getOffsets(); + Pair offsetLengths = col1.getOffsetLengths(); + int ts1 = col1.getTimestampOffsetMs(); + double val1 = col1.getCellValueAsDouble(); + if (col1.advance()) { + heap.add(col1); + } + int ts2 = ts1; + while (ts1 == ts2) { + col2 = heap.peek(); + ts2 = col2 != null ? col2.getTimestampOffsetMs() : ts2; + if (col2 == null || ts1 != ts2) + break; + double val2 = col2.getCellValueAsDouble(); + if ((tsdb.config.use_max_value() && val2 > val1) || (!tsdb.config.use_max_value() && val1 > val2)) { + // Reduce copying of byte arrays by just using col1 variable to reference to either max or min KeyValue + col1 = col2; + val1 = val2; + offsets = col2.getOffsets(); + offsetLengths = col2.getOffsetLengths(); + } + heap.remove(); + if (col2.advance()) { + heap.add(col2); + } + } + col1.writeToBuffersFromOffset(compacted_qual, compacted_val, offsets, offsetLengths); + ms_in_row |= col1.isMilliseconds(); + s_in_row |= !col1.isMilliseconds(); + } + } + + private void defaultMergeDataPoints(ByteBufferList compacted_qual, + ByteBufferList compacted_val) { int prevTs = -1; while (!heap.isEmpty()) { final ColumnDatapointIterator col = heap.remove(); @@ -512,7 +571,6 @@ private void mergeDatapoints(ByteBufferList compacted_qual, } } } - /** * Build the compacted column from the list of byte buffers that were * merged together. @@ -539,7 +597,11 @@ private KeyValue buildCompactedColumn(ByteBufferList compacted_qual, } final KeyValue first = row.get(0); - return new KeyValue(first.key(), first.family(), cq, cv); + if(tsdb.getConfig().getBoolean("tsd.storage.use_otsdb_timestamp")) { + return new KeyValue(first.key(), first.family(), cq, compactedKVTimestamp, cv); + } else { + return new KeyValue(first.key(), first.family(), cq, cv); + } } /** @@ -553,11 +615,11 @@ private KeyValue buildCompactedColumn(ByteBufferList compacted_qual, */ private boolean updateDeletesCheckForWrite(KeyValue compact) { if (last_append_column != null) { - // TODO appends are involved so we may want to squash dps into the - // append or vice-versa. + // TODO appends are involved so we may want to squash dps into the + // append or vice-versa. return false; } - + // if the longest entry isn't as long as the compacted one, obviously the compacted // one can't have already existed if (longest != null && longest.qualifier().length >= compact.qualifier().length) { diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 95aba94f2e..14394fc6b3 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -351,12 +351,12 @@ public Deferred call(final Boolean allowed) throws Exception { if (tsdb.getConfig().enable_appends()) { final AppendDataPoints kv = new AppendDataPoints(qualifier, value); final AppendRequest point = new AppendRequest(tsdb.table, row, TSDB.FAMILY, - AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); + AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); point.setDurable(!batch_import); return tsdb.client.append(point);/* .addBoth(cb) */ } else { - final PutRequest point = new PutRequest(tsdb.table, row, TSDB.FAMILY, - qualifier, value); + final PutRequest point = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.table, row, TSDB.FAMILY, + qualifier, value, timestamp); point.setDurable(!batch_import); return tsdb.client.put(point)/* .addBoth(cb) */; } diff --git a/src/core/RequestBuilder.java b/src/core/RequestBuilder.java new file mode 100644 index 0000000000..e8aa47e504 --- /dev/null +++ b/src/core/RequestBuilder.java @@ -0,0 +1,34 @@ +// 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.hbase.async.AppendRequest; +import org.hbase.async.HBaseRpc; +import org.hbase.async.PutRequest; + +import net.opentsdb.utils.Config; + +public class RequestBuilder { + + public static PutRequest buildPutRequest(Config config, byte[] tableName, byte[] row, byte[] family, byte[] qualifier, byte[] value, long timestamp) { + + if(config.use_otsdb_timestamp()) + if((timestamp & Const.SECOND_MASK) != 0) + return new PutRequest(tableName, row, family, qualifier, value, timestamp); + else + return new PutRequest(tableName, row, family, qualifier, value, timestamp * 1000); + else + return new PutRequest(tableName, row, family, qualifier, value); + } + +} diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 0b0d4b2579..df3754051d 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -80,7 +80,7 @@ */ public final class TSDB { private static final Logger LOG = LoggerFactory.getLogger(TSDB.class); - + static final byte[] FAMILY = { 't' }; /** Charset used to convert Strings to byte arrays and back. */ @@ -116,7 +116,7 @@ public final class TSDB { /** Timer used for various tasks such as idle timeouts or query timeouts */ private final HashedWheelTimer timer; - + /** * Row keys that need to be compacted. * Whenever we write a new data point to a row, we add the row key to this @@ -136,19 +136,19 @@ public final class TSDB { /** Optional real time pulblisher plugin to use if configured */ private RTPublisher rt_publisher = null; - + /** Optional plugin for handling meta data caching and updating */ private MetaDataCache meta_cache = null; - + /** Plugin for dealing with data points that can't be stored */ private StorageExceptionHandler storage_exception_handler = null; /** A filter plugin for allowing or blocking time series */ private WriteableDataPointFilterPlugin ts_filter; - + /** A filter plugin for allowing or blocking UIDs */ private UniqueIdFilterPlugin uid_filter; - + /** The rollup config object for storing and querying rollups */ private final RollupConfig rollup_config; @@ -167,10 +167,10 @@ public final class TSDB { /** Whether or not to block writing of derived rollups/pre-ags */ private final boolean rollups_block_derived; - /** Writes rejected by the filter */ + /** Writes rejected by the filter */ private final AtomicLong rejected_dps = new AtomicLong(); private final AtomicLong rejected_aggregate_dps = new AtomicLong(); - + /** Datapoints Added */ private static final AtomicLong datapoints_added = new AtomicLong(); @@ -188,23 +188,23 @@ public TSDB(final HBaseClient client, final Config config) { try { async_config = new org.hbase.async.Config(config.configLocation()); } catch (final IOException e) { - throw new RuntimeException("Failed to read the config file: " + + throw new RuntimeException("Failed to read the config file: " + config.configLocation(), e); } } else { async_config = new org.hbase.async.Config(); } - async_config.overrideConfig("hbase.zookeeper.znode.parent", + async_config.overrideConfig("hbase.zookeeper.znode.parent", config.getString("tsd.storage.hbase.zk_basedir")); - async_config.overrideConfig("hbase.zookeeper.quorum", + async_config.overrideConfig("hbase.zookeeper.quorum", config.getString("tsd.storage.hbase.zk_quorum")); this.client = new HBaseClient(async_config); } else { this.client = client; } - + // SALT AND UID WIDTHS - // Users really wanted this to be set via config instead of having to + // Users really wanted this to be set via config instead of having to // compile. Hopefully they know NOT to change these after writing data. if (config.hasProperty("tsd.storage.uid.width.metric")) { METRICS_WIDTH = config.getShort("tsd.storage.uid.width.metric"); @@ -224,7 +224,7 @@ public TSDB(final HBaseClient client, final Config config) { if (config.hasProperty("tsd.storage.salt.width")) { Const.setSaltWidth(config.getInt("tsd.storage.salt.width")); } - + table = config.getString("tsd.storage.hbase.data_table").getBytes(CHARSET); uidtable = config.getString("tsd.storage.hbase.uid_table").getBytes(CHARSET); treetable = config.getString("tsd.storage.hbase.tree_table").getBytes(CHARSET); @@ -238,13 +238,13 @@ public TSDB(final HBaseClient client, final Config config) { tag_names = new UniqueId(this, uidtable, TAG_NAME_QUAL, TAG_NAME_WIDTH, false); tag_values = new UniqueId(this, uidtable, TAG_VALUE_QUAL, TAG_VALUE_WIDTH, false); compactionq = new CompactionQueue(this); - + if (config.hasProperty("tsd.core.timezone")) { DateTime.setDefaultTimezone(config.getString("tsd.core.timezone")); } - + timer = Threads.newTimer("TSDB Timer"); - + if (config.getBoolean("tsd.rollups.enable")) { rollup_config = new RollupConfig(); RollupInterval config_default = null; @@ -276,7 +276,7 @@ public TSDB(final HBaseClient client, final Config config) { QueryStats.setEnableDuplicates( config.getBoolean("tsd.query.allow_simultaneous_duplicates")); - + if (config.getBoolean("tsd.core.preload_uid_cache")) { final ByteMap uid_cache_map = new ByteMap(); uid_cache_map.put(METRICS_QUAL.getBytes(CHARSET), metrics); @@ -284,20 +284,20 @@ public TSDB(final HBaseClient client, final Config config) { uid_cache_map.put(TAG_VALUE_QUAL.getBytes(CHARSET), tag_values); UniqueId.preloadUidCache(this, uid_cache_map); } - + if (config.getString("tsd.core.tag.allow_specialchars") != null) { Tags.setAllowSpecialChars(config.getString("tsd.core.tag.allow_specialchars")); } - + // load up the functions that require the TSDB object ExpressionFactory.addTSDBFunctions(this); - + // set any extra tags from the config for stats StatsCollector.setGlobalTags(config); - + LOG.debug(config.dumpConfiguration()); } - + /** * Constructor * @param config An initialized configuration object @@ -306,7 +306,7 @@ public TSDB(final HBaseClient client, final Config config) { public TSDB(final Config config) { this(null, config); } - + /** @return The data point column family name */ public static byte[] FAMILY() { return FAMILY; @@ -381,7 +381,7 @@ public void initializePlugins(final boolean init_rpcs) { search = PluginLoader.loadSpecificPlugin( config.getString("tsd.search.plugin"), SearchPlugin.class); if (search == null) { - throw new IllegalArgumentException("Unable to locate search plugin: " + + throw new IllegalArgumentException("Unable to locate search plugin: " + config.getString("tsd.search.plugin")); } try { @@ -389,20 +389,20 @@ public void initializePlugins(final boolean init_rpcs) { } catch (Exception e) { throw new RuntimeException("Failed to initialize search plugin", e); } - LOG.info("Successfully initialized search plugin [" + - search.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized search plugin [" + + search.getClass().getCanonicalName() + "] version: " + search.version()); } else { search = null; } - + // load the real time publisher plugin if enabled if (config.getBoolean("tsd.rtpublisher.enable")) { rt_publisher = PluginLoader.loadSpecificPlugin( config.getString("tsd.rtpublisher.plugin"), RTPublisher.class); if (rt_publisher == null) { throw new IllegalArgumentException( - "Unable to locate real time publisher plugin: " + + "Unable to locate real time publisher plugin: " + config.getString("tsd.rtpublisher.plugin")); } try { @@ -411,20 +411,20 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize real time publisher plugin", e); } - LOG.info("Successfully initialized real time publisher plugin [" + - rt_publisher.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized real time publisher plugin [" + + rt_publisher.getClass().getCanonicalName() + "] version: " + rt_publisher.version()); } else { rt_publisher = null; } - + // load the meta cache plugin if enabled if (config.getBoolean("tsd.core.meta.cache.enable")) { meta_cache = PluginLoader.loadSpecificPlugin( config.getString("tsd.core.meta.cache.plugin"), MetaDataCache.class); if (meta_cache == null) { throw new IllegalArgumentException( - "Unable to locate meta cache plugin: " + + "Unable to locate meta cache plugin: " + config.getString("tsd.core.meta.cache.plugin")); } try { @@ -433,19 +433,19 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize meta cache plugin", e); } - LOG.info("Successfully initialized meta cache plugin [" + - meta_cache.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized meta cache plugin [" + + meta_cache.getClass().getCanonicalName() + "] version: " + meta_cache.version()); } - + // load the storage exception plugin if enabled if (config.getBoolean("tsd.core.storage_exception_handler.enable")) { storage_exception_handler = PluginLoader.loadSpecificPlugin( - config.getString("tsd.core.storage_exception_handler.plugin"), + config.getString("tsd.core.storage_exception_handler.plugin"), StorageExceptionHandler.class); if (storage_exception_handler == null) { throw new IllegalArgumentException( - "Unable to locate storage exception handler plugin: " + + "Unable to locate storage exception handler plugin: " + config.getString("tsd.core.storage_exception_handler.plugin")); } try { @@ -454,19 +454,19 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize storage exception handler plugin", e); } - LOG.info("Successfully initialized storage exception handler plugin [" + - storage_exception_handler.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized storage exception handler plugin [" + + storage_exception_handler.getClass().getCanonicalName() + "] version: " + storage_exception_handler.version()); } - + // Writeable Data Point Filter if (config.getBoolean("tsd.timeseriesfilter.enable")) { ts_filter = PluginLoader.loadSpecificPlugin( - config.getString("tsd.timeseriesfilter.plugin"), + config.getString("tsd.timeseriesfilter.plugin"), WriteableDataPointFilterPlugin.class); if (ts_filter == null) { throw new IllegalArgumentException( - "Unable to locate time series filter plugin plugin: " + + "Unable to locate time series filter plugin plugin: " + config.getString("tsd.timeseriesfilter.plugin")); } try { @@ -475,19 +475,19 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize time series filter plugin", e); } - LOG.info("Successfully initialized time series filter plugin [" + - ts_filter.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized time series filter plugin [" + + ts_filter.getClass().getCanonicalName() + "] version: " + ts_filter.version()); } - + // UID Filter if (config.getBoolean("tsd.uidfilter.enable")) { uid_filter = PluginLoader.loadSpecificPlugin( - config.getString("tsd.uidfilter.plugin"), + config.getString("tsd.uidfilter.plugin"), UniqueIdFilterPlugin.class); if (uid_filter == null) { throw new IllegalArgumentException( - "Unable to locate UID filter plugin plugin: " + + "Unable to locate UID filter plugin plugin: " + config.getString("tsd.uidfilter.plugin")); } try { @@ -496,8 +496,8 @@ public void initializePlugins(final boolean init_rpcs) { throw new RuntimeException( "Failed to initialize UID filter plugin", e); } - LOG.info("Successfully initialized UID filter plugin [" + - uid_filter.getClass().getCanonicalName() + "] version: " + LOG.info("Successfully initialized UID filter plugin [" + + uid_filter.getClass().getCanonicalName() + "] version: " + uid_filter.version()); } } @@ -512,43 +512,43 @@ public final Authentication getAuth() { } /** - * Returns the configured HBase client + * Returns the configured HBase client * @return The HBase client - * @since 2.0 + * @since 2.0 */ public final HBaseClient getClient() { return this.client; } /** - * Sets the startup plugin so that it can be shutdown properly. - * Note that this method will not initialize or call any other methods + * Sets the startup plugin so that it can be shutdown properly. + * Note that this method will not initialize or call any other methods * belonging to the plugin's implementation. - * @param plugin The startup plugin that was used. + * @param plugin The startup plugin that was used. * @since 2.3 */ - public final void setStartupPlugin(final StartupPlugin plugin) { - startup = plugin; + public final void setStartupPlugin(final StartupPlugin plugin) { + startup = plugin; } - + /** * Getter that returns the startup plugin object. * @return The StartupPlugin object or null if the plugin was not set. * @since 2.3 */ - public final StartupPlugin getStartupPlugin() { - return startup; + public final StartupPlugin getStartupPlugin() { + return startup; } /** * Getter that returns the configuration object * @return The configuration object - * @since 2.0 + * @since 2.0 */ public final Config getConfig() { return this.config; } - + /** * Returns the storage exception handler. May be null if not enabled * @return The storage exception handler @@ -565,15 +565,15 @@ public final StorageExceptionHandler getStorageExceptionHandler() { public WriteableDataPointFilterPlugin getTSfilter() { return ts_filter; } - - /** - * @return The UID filter object, may be null. - * @since 2.3 + + /** + * @return The UID filter object, may be null. + * @since 2.3 */ public UniqueIdFilterPlugin getUidFilter() { return uid_filter; } - + /** * Attempts to find the name for a unique identifier given a type * @param type The type of UID @@ -599,7 +599,7 @@ public Deferred getUidName(final UniqueIdType type, final byte[] uid) { throw new IllegalArgumentException("Unrecognized UID type"); } } - + /** * Attempts to find the UID matching a given name * @param type The type of UID @@ -620,7 +620,7 @@ public byte[] getUID(final UniqueIdType type, final String name) { throw new RuntimeException(e); } } - + /** * Attempts to find the UID matching a given name * @param type The type of UID @@ -644,7 +644,7 @@ public Deferred getUIDAsync(final UniqueIdType type, final String name) throw new IllegalArgumentException("Unrecognized UID type"); } } - + /** * Verifies that the data and UID tables exist in HBase and optionally the * tree and meta data tables if the user has enabled meta tracking or tree @@ -654,7 +654,7 @@ public Deferred getUIDAsync(final UniqueIdType type, final String name) * @since 2.0 */ public Deferred> checkNecessaryTablesExist() { - final ArrayList> checks = + final ArrayList> checks = new ArrayList>(2); checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.data_table"))); @@ -664,14 +664,14 @@ public Deferred> checkNecessaryTablesExist() { checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.tree_table"))); } - if (config.enable_realtime_ts() || config.enable_realtime_uid() || + if (config.enable_realtime_ts() || config.enable_realtime_uid() || config.enable_tsuid_incrementing()) { checks.add(client.ensureTableExists( config.getString("tsd.storage.hbase.meta_table"))); } return Deferred.group(checks); } - + /** Number of cache hits during lookups involving UIDs. */ public int uidCacheHits() { return (metrics.cacheHits() + tag_names.cacheHits() @@ -695,48 +695,48 @@ public int uidCacheSize() { * @param collector The collector to use. */ public void collectStats(final StatsCollector collector) { - final byte[][] kinds = { - METRICS_QUAL.getBytes(CHARSET), - TAG_NAME_QUAL.getBytes(CHARSET), - TAG_VALUE_QUAL.getBytes(CHARSET) + final byte[][] kinds = { + METRICS_QUAL.getBytes(CHARSET), + TAG_NAME_QUAL.getBytes(CHARSET), + TAG_VALUE_QUAL.getBytes(CHARSET) }; try { final Map used_uids = UniqueId.getUsedUIDs(this, kinds) .joinUninterruptibly(); - + collectUidStats(metrics, collector); if (config.getBoolean("tsd.core.uid.random_metrics")) { collector.record("uid.ids-used", 0, "kind=" + METRICS_QUAL); collector.record("uid.ids-available", 0, "kind=" + METRICS_QUAL); } else { - collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), + collector.record("uid.ids-used", used_uids.get(METRICS_QUAL), "kind=" + METRICS_QUAL); - collector.record("uid.ids-available", - (Internal.getMaxUnsignedValueOnBytes(metrics.width()) - + collector.record("uid.ids-available", + (Internal.getMaxUnsignedValueOnBytes(metrics.width()) - used_uids.get(METRICS_QUAL)), "kind=" + METRICS_QUAL); } - + collectUidStats(tag_names, collector); - collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), + collector.record("uid.ids-used", used_uids.get(TAG_NAME_QUAL), "kind=" + TAG_NAME_QUAL); - collector.record("uid.ids-available", - (Internal.getMaxUnsignedValueOnBytes(tag_names.width()) - - used_uids.get(TAG_NAME_QUAL)), + collector.record("uid.ids-available", + (Internal.getMaxUnsignedValueOnBytes(tag_names.width()) - + used_uids.get(TAG_NAME_QUAL)), "kind=" + TAG_NAME_QUAL); - + collectUidStats(tag_values, collector); - collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), + collector.record("uid.ids-used", used_uids.get(TAG_VALUE_QUAL), "kind=" + TAG_VALUE_QUAL); - collector.record("uid.ids-available", - (Internal.getMaxUnsignedValueOnBytes(tag_values.width()) - + collector.record("uid.ids-available", + (Internal.getMaxUnsignedValueOnBytes(tag_values.width()) - used_uids.get(TAG_VALUE_QUAL)), "kind=" + TAG_VALUE_QUAL); - + } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } - + collector.record("uid.filter.rejected", rejected_dps.get(), "kind=raw"); - collector.record("uid.filter.rejected", rejected_aggregate_dps.get(), + collector.record("uid.filter.rejected", rejected_aggregate_dps.get(), "kind=aggregate"); { @@ -808,7 +808,7 @@ public void collectStats(final StatsCollector collector) { rt_publisher.collectStats(collector); } finally { collector.clearExtraTag("plugin"); - } + } } if (authentication != null) { try { @@ -824,7 +824,7 @@ public void collectStats(final StatsCollector collector) { search.collectStats(collector); } finally { collector.clearExtraTag("plugin"); - } + } } if (storage_exception_handler != null) { try { @@ -872,9 +872,9 @@ private static void collectUidStats(final UniqueId uid, collector.record("uid.cache-hit", uid.cacheHits(), "kind=" + uid.kind()); collector.record("uid.cache-miss", uid.cacheMisses(), "kind=" + uid.kind()); collector.record("uid.cache-size", uid.cacheSize(), "kind=" + uid.kind()); - collector.record("uid.random-collisions", uid.randomIdCollisions(), + collector.record("uid.random-collisions", uid.randomIdCollisions(), "kind=" + uid.kind()); - collector.record("uid.rejected-assignments", uid.rejectedAssignments(), + collector.record("uid.rejected-assignments", uid.rejectedAssignments(), "kind=" + uid.kind()); } @@ -882,17 +882,17 @@ private static void collectUidStats(final UniqueId uid, public static short metrics_width() { return METRICS_WIDTH; } - + /** @return the width, in bytes, of tagk UIDs */ public static short tagk_width() { return TAG_NAME_WIDTH; } - + /** @return the width, in bytes, of tagv UIDs */ public static short tagv_width() { return TAG_VALUE_WIDTH; } - + /** * Returns a new {@link Query} instance suitable for this TSDB. */ @@ -912,7 +912,7 @@ public WritableDataPoints newDataPoints() { /** * Returns a new {@link BatchedDataPoints} instance suitable for this TSDB. - * + * * @param metric Every data point that gets appended must be associated to this metric. * @param tags The associated tags for all data points being added. * @return data structure which can have data points appended. @@ -1051,7 +1051,7 @@ Deferred addPointInternal(final String metric, final Map tags, final short flags) { // we only accept positive unix epoch timestamps in seconds or milliseconds - if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && + if (timestamp < 0 || ((timestamp & Const.SECOND_MASK) != 0 && timestamp > 9999999999999L)) { throw new IllegalArgumentException((timestamp < 0 ? "negative " : "bad") + " timestamp=" + timestamp @@ -1062,15 +1062,15 @@ Deferred addPointInternal(final String metric, 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) - + base_time = ((timestamp / 1000) - ((timestamp / 1000) % Const.MAX_TIMESPAN)); } else { base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); } - + /** Callback executed for chaining filter calls to see if the value * should be written or not. */ final class WriteCB implements Callback, Boolean> { @@ -1080,19 +1080,22 @@ public Deferred call(final Boolean allowed) throws Exception { rejected_dps.incrementAndGet(); return Deferred.fromResult(null); } - + Bytes.setInt(row, (int) base_time, metrics.width() + Const.SALT_WIDTH()); RowKey.prefixKeyWithSalt(row); Deferred result = null; if (config.enable_appends()) { + if(config.use_otsdb_timestamp()) { + LOG.error("Cannot use Date Tiered Compaction with AppendPoints. Please turn off either of them."); + } final AppendDataPoints kv = new AppendDataPoints(qualifier, value); - final AppendRequest point = new AppendRequest(table, row, FAMILY, - AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); + final AppendRequest point = new AppendRequest(table, row, FAMILY, + AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); result = client.append(point); } else { scheduleForCompaction(row, (int) base_time); - final PutRequest point = new PutRequest(table, row, FAMILY, qualifier, value); + final PutRequest point = RequestBuilder.buildPutRequest(config, table, row, FAMILY, qualifier, value, timestamp); result = client.put(point); } @@ -1102,15 +1105,15 @@ public Deferred call(final Boolean allowed) throws Exception { // TODO(tsuna): Add a callback to time the latency of HBase and store the // timing in a moving Histogram (once we have a class for this). - - if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && + + if (!config.enable_realtime_ts() && !config.enable_tsuid_incrementing() && !config.enable_tsuid_tracking() && rt_publisher == null) { return result; } - - final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, + + final byte[] tsuid = UniqueId.getTSUIDFromKey(row, METRICS_WIDTH, Const.TIMESTAMP_BYTES); - + // if the meta cache plugin is instantiated then tracking goes through it if (meta_cache != null) { meta_cache.increment(tsuid); @@ -1123,7 +1126,7 @@ public Deferred call(final Boolean allowed) throws Exception { TSMeta.storeIfNecessary(TSDB.this, tsuid); } } else { - final PutRequest tracking = new PutRequest(meta_table, tsuid, + final PutRequest tracking = new PutRequest(meta_table, tsuid, TSMeta.FAMILY(), TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(1)); client.put(tracking); } @@ -1140,7 +1143,7 @@ public String toString() { return "addPointInternal Write Callback"; } } - + if (ts_filter != null && ts_filter.filterDataPoints()) { return ts_filter.allowDataPoint(metric, timestamp, value, tags, flags) .addCallbackDeferring(new WriteCB()); @@ -1449,7 +1452,7 @@ public Deferred call(final Boolean allowed) throws Exception { } /** - * Forces a flush of any un-committed in memory data including left over + * Forces a flush of any un-committed in memory data including left over * compactions. *

    * For instance, any data point not persisted will be sent to HBase. @@ -1463,10 +1466,10 @@ public Deferred call(final Boolean allowed) throws Exception { */ public Deferred flush() throws HBaseException { final class HClientFlush implements Callback> { - public Object call(final ArrayList args) { + public Object call(final ArrayList args) { return client.flush(); } - public String toString() { + public String toString() { return "flush HBase client"; } } @@ -1491,9 +1494,9 @@ public String toString() { * recoverable by retrying, some are not. */ public Deferred shutdown() { - final ArrayList> deferreds = + final ArrayList> deferreds = new ArrayList>(); - + final class FinalShutdown implements Callback { @Override public Object call(Object result) throws Exception { @@ -1509,14 +1512,14 @@ public Object call(Object result) throws Exception { return Deferred.fromResult(null); } } - + final class SEHShutdown implements Callback { @Override public Object call(Object result) throws Exception { if (result instanceof Exception) { LOG.error("Shutdown of the HBase client failed", (Exception)result); } - LOG.info("Shutting down storage exception handler plugin: " + + LOG.info("Shutting down storage exception handler plugin: " + storage_exception_handler.getClass().getCanonicalName()); return storage_exception_handler.shutdown().addBoth(new FinalShutdown()); } @@ -1525,21 +1528,21 @@ public String toString() { return "SEHShutdown"; } } - + final class HClientShutdown implements Callback, ArrayList> { - public Deferred call(final ArrayList args) { + public Deferred call(final ArrayList args) { if (storage_exception_handler != null) { return client.shutdown().addBoth(new SEHShutdown()); } return client.shutdown().addBoth(new FinalShutdown()); } - 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; @@ -1553,17 +1556,17 @@ 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; } } - + if (config.enable_compactions()) { LOG.info("Flushing compaction queue"); deferreds.add(compactionq.flush().addCallback(new CompactCB())); @@ -1579,36 +1582,36 @@ public Object call(ArrayList compactions) throws Exception { deferreds.add(authentication.shutdown()); } if (search != null) { - LOG.info("Shutting down search plugin: " + + LOG.info("Shutting down search plugin: " + search.getClass().getCanonicalName()); deferreds.add(search.shutdown()); } if (rt_publisher != null) { - LOG.info("Shutting down RT plugin: " + + LOG.info("Shutting down RT plugin: " + rt_publisher.getClass().getCanonicalName()); deferreds.add(rt_publisher.shutdown()); } if (meta_cache != null) { - LOG.info("Shutting down meta cache plugin: " + + LOG.info("Shutting down meta cache plugin: " + meta_cache.getClass().getCanonicalName()); deferreds.add(meta_cache.shutdown()); } if (storage_exception_handler != null) { - LOG.info("Shutting down storage exception handler plugin: " + + LOG.info("Shutting down storage exception handler plugin: " + storage_exception_handler.getClass().getCanonicalName()); deferreds.add(storage_exception_handler.shutdown()); } if (ts_filter != null) { - LOG.info("Shutting down time series filter plugin: " + + LOG.info("Shutting down time series filter plugin: " + ts_filter.getClass().getCanonicalName()); deferreds.add(ts_filter.shutdown()); } if (uid_filter != null) { - LOG.info("Shutting down UID filter plugin: " + + LOG.info("Shutting down UID filter plugin: " + uid_filter.getClass().getCanonicalName()); deferreds.add(uid_filter.shutdown()); } - + // wait for plugins to shutdown before we close the client return deferreds.size() > 0 ? Deferred.group(deferreds).addCallbackDeferring(new HClientShutdown()) @@ -1623,14 +1626,14 @@ public Object call(ArrayList compactions) throws Exception { public List suggestMetrics(final String search) { return metrics.suggest(search); } - + /** * Given a prefix search, returns matching metric names. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestMetrics(final String search, + public List suggestMetrics(final String search, final int max_results) { return metrics.suggest(search, max_results); } @@ -1642,14 +1645,14 @@ public List suggestMetrics(final String search, public List suggestTagNames(final String search) { return tag_names.suggest(search); } - + /** * Given a prefix search, returns matching tagk names. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestTagNames(final String search, + public List suggestTagNames(final String search, final int max_results) { return tag_names.suggest(search, max_results); } @@ -1661,14 +1664,14 @@ public List suggestTagNames(final String search, public List suggestTagValues(final String search) { return tag_values.suggest(search); } - + /** * Given a prefix search, returns matching tag values. * @param search A prefix to search. * @param max_results Maximum number of results to return. * @since 2.0 */ - public List suggestTagValues(final String search, + public List suggestTagValues(final String search, final int max_results) { return tag_values.suggest(search, max_results); } @@ -1685,14 +1688,14 @@ public void dropCaches() { /** * Attempts to assign a UID to a name for the given type - * Used by the UniqueIdRpc call to generate IDs for new metrics, tagks or + * Used by the UniqueIdRpc call to generate IDs for new metrics, tagks or * tagvs. The name must pass validation and if it's already assigned a UID, * this method will throw an error with the proper UID. Otherwise if it can * create the UID, it will be returned * @param type The type of uid to assign, metric, tagk or tagv * @param name The name of the uid object * @return A byte array with the UID if the assignment was successful - * @throws IllegalArgumentException if the name is invalid or it already + * @throws IllegalArgumentException if the name is invalid or it already * exists * @since 2.0 */ @@ -1727,7 +1730,7 @@ public byte[] assignUid(final String type, final String name) { throw new IllegalArgumentException("Unknown type name"); } } - + /** * Attempts to delete the given UID name mapping from the storage table as * well as the local cache. @@ -1747,10 +1750,10 @@ public Deferred deleteUidAsync(final String type, final String name) { case TAGV: return tag_values.deleteAsync(name); default: - throw new IllegalArgumentException("Unrecognized UID type: " + uid_type); + throw new IllegalArgumentException("Unrecognized UID type: " + uid_type); } } - + /** * Attempts to rename a UID from existing name to the given name * Used by the UniqueIdRpc call to rename name of existing metrics, tagks or @@ -1801,17 +1804,17 @@ public void renameUid(final String type, final String oldname, public byte[] uidTable() { return this.uidtable; } - + /** @return the name of the data table as a byte array for client requests */ public byte[] dataTable() { return this.table; } - + /** @return the name of the tree table as a byte array for client requests */ public byte[] treeTable() { return this.treetable; } - + /** @return the name of the meta table as a byte array for client requests */ public byte[] metaTable() { return this.meta_table; @@ -1827,7 +1830,7 @@ public void indexTSMeta(final TSMeta meta) { search.indexTSMeta(meta).addErrback(new PluginError()); } } - + /** * Delete the timeseries meta object from the search index * @param tsuid The TSUID to delete @@ -1838,7 +1841,7 @@ public void deleteTSMeta(final String tsuid) { search.deleteTSMeta(tsuid).addErrback(new PluginError()); } } - + /** * Index the given UID meta object via the configured search plugin * @param meta The meta data object to index @@ -1849,7 +1852,7 @@ public void indexUIDMeta(final UIDMeta meta) { search.indexUIDMeta(meta).addErrback(new PluginError()); } } - + /** * Delete the UID meta object from the search index * @param meta The UID meta object to delete @@ -1860,7 +1863,7 @@ public void deleteUIDMeta(final UIDMeta meta) { search.deleteUIDMeta(meta).addErrback(new PluginError()); } } - + /** * Index the given Annotation object via the configured search plugin * @param note The annotation object to index @@ -1874,7 +1877,7 @@ public void indexAnnotation(final Annotation note) { rt_publisher.publishAnnotation(note); } } - + /** * Delete the annotation object from the search index * @param note The annotation object to delete @@ -1885,7 +1888,7 @@ public void deleteAnnotation(final Annotation note) { search.deleteAnnotation(note).addErrback(new PluginError()); } } - + /** * Processes the TSMeta through all of the trees if configured to do so * @param meta The meta data to process @@ -1897,7 +1900,7 @@ public Deferred processTSMetaThroughTrees(final TSMeta meta) { } return Deferred.fromResult(false); } - + /** * Executes a search query using the search plugin * @param query The query to execute @@ -1911,13 +1914,13 @@ public Deferred executeSearch(final SearchQuery query) { throw new IllegalStateException( "Searching has not been enabled on this TSD"); } - + return search.executeQuery(query); } - + /** - * Simply logs plugin errors when they're thrown by attaching as an errorback. - * Without this, exceptions will just disappear (unless logged by the plugin) + * Simply logs plugin errors when they're thrown by attaching as an errorback. + * Without this, exceptions will just disappear (unless logged by the plugin) * since we don't wait for a result. */ final class PluginError implements Callback { @@ -1927,7 +1930,7 @@ public Object call(final Exception e) throws Exception { return null; } } - + /** @return the rollup config object. May be null * @since 2.4 */ public RollupConfig getRollupConfig() { @@ -1942,7 +1945,7 @@ public RollupInterval getDefaultInterval() { /** * Blocks while pre-fetching meta data from the data and uid tables - * so that performance improves, particularly with a large number of + * so that performance improves, particularly with a large number of * regions and region servers. * @since 2.2 */ @@ -1952,12 +1955,12 @@ public void preFetchHBaseMeta() { final ArrayList> deferreds = new ArrayList>(); deferreds.add(client.prefetchMeta(table)); deferreds.add(client.prefetchMeta(uidtable)); - + // TODO(cl) - meta, tree, etc - + try { Deferred.group(deferreds).join(); - LOG.info("Fetched meta data for tables in " + + LOG.info("Fetched meta data for tables in " + (System.currentTimeMillis() - start) + "ms"); } catch (InterruptedException e) { LOG.error("Interrupted", e); @@ -1967,7 +1970,7 @@ public void preFetchHBaseMeta() { LOG.error("Failed to prefetch meta for our tables", e); } } - + /** @return the timer used for various house keeping functions */ public Timer getTimer() { return timer; @@ -1984,12 +1987,12 @@ public String getAggTagKey() { public String getRawTagValue() { return raw_agg_tag_value; } - + // ------------------ // // Compaction helpers // // ------------------ // - final KeyValue compact(final ArrayList row, + final KeyValue compact(final ArrayList row, List annotations) { return compactionq.compact(row, annotations); } @@ -2020,8 +2023,9 @@ final Deferred> get(final byte[] key) { /** Puts the given value into the data table. */ final Deferred put(final byte[] key, final byte[] qualifier, - final byte[] value) { - return client.put(new PutRequest(table, key, FAMILY, qualifier, value)); + final byte[] value, + long timestamp) { + return client.put(RequestBuilder.buildPutRequest(config, table, key, FAMILY, qualifier, value, timestamp)); } /** Deletes the given cells from the data table. */ diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 57346cd462..0b57b22d33 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1301,6 +1301,11 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { : (int) getScanEndTimeSeconds(), tableToBeScanned(), TSDB.FAMILY()); + if(tsdb.getConfig().use_otsdb_timestamp()) { + long stTime = (getScanStartTimeSeconds() * 1000); + long endTime = end_time == UNSET ? -1 : (getScanEndTimeSeconds() * 1000); + scanner.setTimeRange(stTime, endTime); + } if (tsuids != null && !tsuids.isEmpty()) { createAndSetTSUIDFilter(scanner); } else if (filters.size() > 0) { diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index b595ed4057..d44a6df385 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -33,6 +33,7 @@ import net.opentsdb.core.Const; import net.opentsdb.core.Internal; +import net.opentsdb.core.RequestBuilder; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.uid.UniqueId; @@ -184,10 +185,10 @@ public Deferred call(final Annotation stored_note) final byte[] tsuid_byte = tsuid != null && !tsuid.isEmpty() ? UniqueId.stringToUid(tsuid) : null; - final PutRequest put = new PutRequest(tsdb.dataTable(), + final PutRequest put = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.dataTable(), getRowKey(start_time, tsuid_byte), FAMILY, getQualifier(start_time), - Annotation.this.getStorageJSON()); + Annotation.this.getStorageJSON(), start_time); return tsdb.getClient().compareAndSet(put, original_note); } @@ -275,7 +276,6 @@ public Deferred call(final ArrayList row) if (row == null || row.isEmpty()) { return Deferred.fromResult(null); } - Annotation note = JSON.parseToObject(row.get(0).value(), Annotation.class); return Deferred.fromResult(note); diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index a87c45a19f..f78684850a 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -107,7 +107,7 @@ static final Config getConfig(final ArgP argp) throws IOException { config.setAutoMetric(config.getBoolean("tsd.core.auto_create_metrics")); return config; } - + /** * Copies the parsed command line options to the {@link Config} class * @param config Configuration instance to override @@ -152,10 +152,12 @@ static void overloadConfig(final ArgP argp, final Config config) { config.overrideConfig("tsd.network.async_io", entry.getValue()); } else if (entry.getKey().toLowerCase().equals("--worker-threads")) { config.overrideConfig("tsd.network.worker_threads", entry.getValue()); - } + } else if(entry.getKey().toLowerCase().equals("--use-otsdb-ts")) { + config.overrideConfig("tsd.storage.use_otsdb_timestamp", "true"); + } } } - + /** Changes the log level to 'WARN' unless --verbose is passed. */ private static void honorVerboseFlag(final ArgP argp) { if (argp.optionExists("--verbose") && !argp.has("--verbose") diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 6295ff32f4..6ed8f05871 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -40,6 +40,7 @@ import net.opentsdb.core.Internal; import net.opentsdb.core.Internal.Cell; import net.opentsdb.core.Query; +import net.opentsdb.core.RequestBuilder; import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; @@ -55,14 +56,14 @@ * rows matching the query will be FSCK'd. Alternatively a full table scan can * be performed. *

    - * Scanning is done in three stages: + * Scanning is done in three stages: * 1) Each row key is parsed to make sure it's a valid OpenTSDB row. If it isn't * then the user can decide to delete it. If one or more UIDs cannot be resolved * to names (metric or tags) then the user can decide to purge it. - * 2) All key value pairs in a row are parsed to determine the type of object. - * If it's a single data point, it's added to a tree map based on the data point - * timestamp. If it's a compacted column, the data points are exploded and - * added to the data point map. If it's some other object it may be purged if + * 2) All key value pairs in a row are parsed to determine the type of object. + * If it's a single data point, it's added to a tree map based on the data point + * timestamp. If it's a compacted column, the data points are exploded and + * added to the data point map. If it's some other object it may be purged if * told to, or if it's a known type (e.g. annotations) simply ignored. * 3) If any data points were found, we iterate over each one looking for * duplicates, malformed encodings or potential value-length-encoding savings. @@ -80,13 +81,13 @@ */ final class Fsck { private static final Logger LOG = LoggerFactory.getLogger(Fsck.class); - + /** The TSDB to use for access */ - private final TSDB tsdb; + private final TSDB tsdb; /** Options to use while iterating over rows */ private final FsckOptions options; - + /** Counters incremented during processing. They have to be atomic counters * as we may be running multiple fsck threads. */ final AtomicLong kvs_processed = new AtomicLong(); @@ -115,17 +116,17 @@ final class Fsck { final AtomicLong vle = new AtomicLong(); final AtomicLong vle_bytes = new AtomicLong(); final AtomicLong vle_fixed = new AtomicLong(); - + /** Length of the metric + timestamp for key validation */ private int key_prefix_length = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; - + /** Length of a tagk + tagv pair for key validation */ private int key_tags_length = TSDB.tagk_width() + TSDB.tagv_width(); - + /** How often to report progress */ private static long report_rows = 10000; - + /** * Default Ctor * @param tsdb The TSDB to use for access @@ -135,7 +136,7 @@ public Fsck(final TSDB tsdb, final FsckOptions options) { this.tsdb = tsdb; this.options = options; } - + /** * Fetches the max metric ID and splits the data table up amongst threads on * a naive split. By default we execute cores * 2 threads but the user can @@ -147,7 +148,7 @@ public void runFullTable() throws Exception { final long start_time = System.currentTimeMillis() / 1000; final int workers = options.threads() > 0 ? options.threads() : Runtime.getRuntime().availableProcessors() * 2; - + final List scanners = CliUtils.getDataTableScanners(tsdb, workers); LOG.info("Spooling up [" + scanners.size() + "] worker threads"); final List threads = new ArrayList(scanners.size()); @@ -166,12 +167,12 @@ public void runFullTable() throws Exception { LOG.info("Thread [" + thread + "] Finished"); } reporter.interrupt(); - + logResults(); final long duration = (System.currentTimeMillis() / 1000) - start_time; LOG.info("Completed fsck in [" + duration + "] seconds"); } - + /** * Scans the rows matching one or more standard queries. An aggregator is still * required though it's ignored. @@ -180,13 +181,13 @@ public void runFullTable() throws Exception { */ public void runQueries(final List queries) throws Exception { final long start_time = System.currentTimeMillis() / 1000; - - // TODO - threadify it. We *could* have hundreds of queries and we don't + + // TODO - threadify it. We *could* have hundreds of queries and we don't // want to create that many threads. For now we'll just execute each one // serially final Thread reporter = new ProgressReporter(); reporter.start(); - + for (final Query query : queries) { final List scanners = Internal.getScanners(query); final List threads = new ArrayList(scanners.size()); @@ -204,35 +205,35 @@ public void runQueries(final List queries) throws Exception { } } reporter.interrupt(); - + logResults(); final long duration = (System.currentTimeMillis() / 1000) - start_time; LOG.info("Completed fsck in [" + duration + "] seconds"); } - + /** @return The total number of errors detected during the run */ long totalErrors() { return bad_key.get() + duplicates.get() + orphans.get() + unknown.get() + - bad_values.get() + bad_compacted_columns.get() + + bad_values.get() + bad_compacted_columns.get() + fixable_compacted_columns.get() + value_encoding.get(); } - + /** @return The total number of errors fixed during the run */ long totalFixed() { return bad_key_fixed.get() + duplicates_fixed.get() + orphans_fixed.get() + - unknown_fixed.get() + value_encoding_fixed.get() + + unknown_fixed.get() + value_encoding_fixed.get() + bad_values_deleted.get(); } - + /** @return The total number of errors that could be (or may have been) fixed */ long correctable() { return bad_key.get() + duplicates.get() + orphans.get() + unknown.get() + - bad_values.get() + bad_compacted_columns.get() + + bad_values.get() + bad_compacted_columns.get() + fixable_compacted_columns.get() + value_encoding.get(); } - + /** - * A worker thread that takes a query or a chunk of the main data table and + * A worker thread that takes a query or a chunk of the main data table and * performs the actual FSCK process. */ final class FsckWorker extends Thread { @@ -245,7 +246,7 @@ final class FsckWorker extends Thread { /** Set of TSUIDs this worker has seen. Used to avoid UID resolution for * previously processed row keys */ final Set tsuids = new HashSet(); - + /** Shared flags and values for compiling a compacted column */ byte[] compact_qualifier = null; int qualifier_index = 0; @@ -254,7 +255,7 @@ final class FsckWorker extends Thread { boolean compact_row = false; int qualifier_bytes = 0; int value_bytes = 0; - + /** * Ctor for running a worker on a chunk of the data table * @param scanner The scanner to use for iterationg @@ -265,25 +266,25 @@ final class FsckWorker extends Thread { this.thread_id = thread_id; query = null; } - + /** * Determines the type of scanner to use, i.e. a specific query scanner or - * for a portion of the whole table. It then performs the actual scan, - * compiling a list of data points and fixing/compacting them when + * for a portion of the whole table. It then performs the actual scan, + * compiling a list of data points and fixing/compacting them when * appropriate. */ public void run() { - // store every data point for the row in here - final TreeMap> datapoints = + // store every data point for the row in here + final TreeMap> datapoints = new TreeMap>(); byte[] last_key = null; ArrayList> rows; - + try { while ((rows = scanner.nextRows().joinUninterruptibly()) != null) { // keep in mind that with annotations and millisecond values, a row // can now have more than 4069 key values, the default for a scanner. - // Since we don't know how many values may actually be in a row, we + // Since we don't know how many values may actually be in a row, we // don't want to set the KV limit too high. Instead we'll just keep // working through the sets until we hit a different row key, then // process all of the data points. It puts more of a burden on fsck @@ -305,7 +306,7 @@ public void run() { fsckRow(row, datapoints); } } - + // handle the last row if (!datapoints.isEmpty()) { rows_processed.getAndIncrement(); @@ -317,36 +318,36 @@ public void run() { LOG.error("Shouldn't be here", e); } } - + /** * Parses the row of KeyValues. First it validates the row key, then parses - * each KeyValue to determine what kind of object it is. Data points are + * each KeyValue to determine what kind of object it is. Data points are * stored in the tree map and non-data point columns are handled per the * option flags * @param row The row of data to parse * @param datapoints The map of datapoints to append to. * @throws Exception If something goes pear shaped. */ - private void fsckRow(final ArrayList row, + private void fsckRow(final ArrayList row, final TreeMap> datapoints) throws Exception { // The data table should contain only rows with a metric, timestamp and - // one or more tag pairs. Future version may use different prefixes or - // key formats but for now, we can safely delete any rows with invalid + // one or more tag pairs. Future version may use different prefixes or + // key formats but for now, we can safely delete any rows with invalid // keys. This may check the same row key multiple times but that's good // as it will keep the data points from being pushed to the dp map if (!fsckKey(row.get(0).key())) { return; } - - final long base_time = Bytes.getUnsignedInt(row.get(0).key(), + + final long base_time = Bytes.getUnsignedInt(row.get(0).key(), Const.SALT_WIDTH() + TSDB.metrics_width()); - + for (final KeyValue kv : row) { kvs_processed.getAndIncrement(); // these are not final as they may be modified when fixing is enabled - byte[] value = kv.value(); + byte[] value = kv.value(); byte[] qual = kv.qualifier(); - + // all qualifiers must be at least 2 bytes long, i.e. a single data point if (qual.length < 2) { unknown.getAndIncrement(); @@ -358,13 +359,13 @@ private void fsckRow(final ArrayList row, } continue; } - + // All data point columns have an even number of bytes, so if we find - // one that has an odd length, it could be an OpenTSDB object or it + // one that has an odd length, it could be an OpenTSDB object or it // could be junk that made it into the table. if (qual.length % 2 != 0) { - // If this test fails, the column is not a TSDB object such as an - // annotation or blob. Future versions may be able to compact TSDB + // If this test fails, the column is not a TSDB object such as an + // annotation or blob. Future versions may be able to compact TSDB // objects so that their qualifier would be of a different length, but // for now we'll consider it an error. if (qual.length != 3 && qual.length != 5) { @@ -378,7 +379,7 @@ private void fsckRow(final ArrayList row, } continue; } - + // TODO - create a list of TSDB objects and fsck them. Maybe a plugin // or interface. // TODO - perform validation of the annotation @@ -403,17 +404,17 @@ private void fsckRow(final ArrayList row, future.getAndIncrement(); continue; } - - // This is (hopefully) a compacted column with multiple data points. It + + // This is (hopefully) a compacted column with multiple data points. It // could have two points with second qualifiers or multiple points with // a mix of second and millisecond qualifiers if (qual.length == 4 && !Internal.inMilliseconds(qual[0]) || qual.length > 4) { if (value[value.length - 1] > Const.MS_MIXED_COMPACT) { - // TODO - figure out a way to fix these. Maybe lookup a row before + // TODO - figure out a way to fix these. Maybe lookup a row before // or after and try parsing this for values. If the values are // somewhat close to the others, then we could just set the last - // byte. Otherwise it could be a bad compaction and we'd need to + // byte. Otherwise it could be a bad compaction and we'd need to // toss it. bad_compacted_columns.getAndIncrement(); LOG.error("The last byte of a compacted should be 0 or 1. Either" @@ -421,12 +422,12 @@ private void fsckRow(final ArrayList row, + " future version of OpenTSDB.\n\t" + kv); continue; } - - // add every cell in the compacted column to the data point tree so + + // add every cell in the compacted column to the data point tree so // that we can scan for duplicate timestamps try { final ArrayList cells = Internal.extractDataPoints(kv); - + // the extractDataPoints() method will automatically fix up some // issues such as setting proper lengths on floats and sorting the // cells to be in order. Rather than reproduce the extraction code or @@ -446,11 +447,11 @@ private void fsckRow(final ArrayList row, dps.add(new DP(kv, cell)); qualifier_bytes += cell.qualifier().length; value_bytes += cell.value().length; - System.arraycopy(cell.qualifier(), 0, recompacted_qualifier, + System.arraycopy(cell.qualifier(), 0, recompacted_qualifier, qualifier_index, cell.qualifier().length); qualifier_index += cell.qualifier().length; } - + if (Bytes.memcmp(recompacted_qualifier, kv.qualifier()) != 0) { LOG.error("Compacted column was out of order or requires a " + "fixup: " + kv); @@ -468,10 +469,10 @@ private void fsckRow(final ArrayList row, } continue; } - - // at this point we *should* be dealing with a single data point encoded + + // at this point we *should* be dealing with a single data point encoded // in seconds or milliseconds. - final long timestamp = + final long timestamp = Internal.getTimestampFromQualifier(qual, base_time); ArrayList dps = datapoints.get(timestamp); if (dps == null) { @@ -485,7 +486,7 @@ private void fsckRow(final ArrayList row, } /** - * Validates the row key. It must match the format + * Validates the row key. It must match the format * {@code [...]}. If it doesn't, then * the row is considered an error. If the UIDs in a row key do not resolve * to a name, then the row is considered an orphan and the values contained @@ -501,11 +502,11 @@ private void fsckRow(final ArrayList row, * @throws Exception If something goes pear shaped. */ private boolean fsckKey(final byte[] key) throws Exception { - if (key.length < key_prefix_length || + if (key.length < key_prefix_length || (key.length - key_prefix_length) % key_tags_length != 0) { LOG.error("Invalid row key.\n\tKey: " + UniqueId.uidToString(key)); bad_key.getAndIncrement(); - + if (options.fix() && options.deleteBadRows()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(delete); @@ -513,10 +514,10 @@ private boolean fsckKey(final byte[] key) throws Exception { } return false; } - + // Process the time series ID by resolving the UIDs to names if we haven't // already seen this particular TSUID. Note that getTSUID accounts for salt - final byte[] tsuid = UniqueId.getTSUIDFromKey(key, TSDB.metrics_width(), + final byte[] tsuid = UniqueId.getTSUIDFromKey(key, TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (!tsuids.contains(tsuid)) { try { @@ -525,7 +526,7 @@ private boolean fsckKey(final byte[] key) throws Exception { LOG.error("Unable to resolve the metric from the row key.\n\tKey: " + UniqueId.uidToString(key) + "\n\t" + nsui.getMessage()); orphans.getAndIncrement(); - + if (options.fix() && options.deleteOrphans()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(delete); @@ -533,7 +534,7 @@ private boolean fsckKey(final byte[] key) throws Exception { } return false; } - + try { Tags.resolveIds(tsdb, (ArrayList) UniqueId.getTagPairsFromTSUID(tsuid)); @@ -541,7 +542,7 @@ private boolean fsckKey(final byte[] key) throws Exception { LOG.error("Unable to resolve the a tagk or tagv from the row key.\n\tKey: " + UniqueId.uidToString(key) + "\n\t" + nsui.getMessage()); orphans.getAndIncrement(); - + if (options.fix() && options.deleteOrphans()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key); tsdb.getClient().delete(delete); @@ -561,7 +562,7 @@ private boolean fsckKey(final byte[] key) throws Exception { * @param datapoints The list of data points parsed from the row * @throws Exception If something goes pear shaped. */ - private void fsckDataPoints(final Map> datapoints) + private void fsckDataPoints(final Map> datapoints) throws Exception { // store a unique set of qualifier/value columns to help us later when @@ -572,12 +573,13 @@ private void fsckDataPoints(final Map> datapoints) boolean has_milliseconds = false; boolean has_duplicates = false; boolean has_uncorrected_value_error = false; - + long timestamp = Long.MIN_VALUE; + for (final Map.Entry> time_map : datapoints.entrySet()) { if (key == null) { key = time_map.getValue().get(0).kv.key(); } - + if (time_map.getValue().size() < 2) { // there was only one data point for this timestamp, no conflicts final DP dp = time_map.getValue().get(0); @@ -595,9 +597,9 @@ private void fsckDataPoints(final Map> datapoints) // sort so we can figure out which one we're going to keep, i.e. oldest // or newest - Collections.sort(time_map.getValue()); + Collections.sort(time_map.getValue()); has_duplicates = true; - // We want to keep either the first or the last incoming datapoint + // We want to keep either the first or the last incoming datapoint // and ignore delete the middle. final StringBuilder buf = new StringBuilder(); @@ -629,6 +631,7 @@ private void fsckDataPoints(final Map> datapoints) } unique_columns.put(dp_to_keep.kv.qualifier(), dp_to_keep.kv.value()); + timestamp = Math.max(timestamp, dp_to_keep.kv.timestamp()); valid_datapoints.getAndIncrement(); has_uncorrected_value_error |= Internal.isFloat(dp_to_keep.qualifier()) ? fsckFloat(dp_to_keep) : fsckInteger(dp_to_keep); @@ -639,7 +642,7 @@ private void fsckDataPoints(final Map> datapoints) has_seconds = true; } - for (int dp_index = delete_range_start; dp_index < delete_range_stop; + for (int dp_index = delete_range_start; dp_index < delete_range_stop; dp_index++) { duplicates.getAndIncrement(); DP dp = time_map.getValue().get(dp_index); @@ -684,20 +687,20 @@ private void fsckDataPoints(final Map> datapoints) } LOG.info(buf.toString()); } - + // if an error was found in this row that was not marked for repair, then // we should bail at this point and not write a new compacted column. - if ((has_duplicates && !options.resolveDupes()) || + if ((has_duplicates && !options.resolveDupes()) || (has_uncorrected_value_error && !options.deleteBadValues())) { LOG.warn("One or more errors found in row that were not marked for repair"); return; } - - if ((options.compact() || compact_row) && options.fix() + + if ((options.compact() || compact_row) && options.fix() && qualifier_index > 0) { - if (qualifier_index == 2 || (qualifier_index == 4 && + if (qualifier_index == 2 || (qualifier_index == 4 && Internal.inMilliseconds(compact_qualifier))) { - // we may have deleted all but one value from the row and that one + // we may have deleted all but one value from the row and that one // value may have a different qualifier than it originally had. We // can't write a compacted column with a single data point as the length // will be off due to the flag at the end. Therefore we just rollback @@ -708,13 +711,13 @@ private void fsckDataPoints(final Map> datapoints) compact_value[value_index] = 1; } value_index++; - final byte[] new_qualifier = Arrays.copyOfRange(compact_qualifier, 0, + final byte[] new_qualifier = Arrays.copyOfRange(compact_qualifier, 0, qualifier_index); - final byte[] new_value = Arrays.copyOfRange(compact_value, 0, + final byte[] new_value = Arrays.copyOfRange(compact_value, 0, value_index); - final PutRequest put = new PutRequest(tsdb.dataTable(), key, - TSDB.FAMILY(), new_qualifier, new_value); - + final PutRequest put = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.dataTable(), key, + TSDB.FAMILY(), new_qualifier, new_value, timestamp); + // it's *possible* that the hash of our new compacted qualifier is in // the delete list so double check before we delete everything if (unique_columns.containsKey(new_qualifier)) { @@ -750,11 +753,11 @@ private void fsckDataPoints(final Map> datapoints) // proceeding with the deletes. tsdb.getClient().put(put).joinUninterruptibly(); } - - final List> deletes = + + final List> deletes = new ArrayList>(unique_columns.size()); for (byte[] qualifier : unique_columns.keySet()) { - final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key, + final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key, TSDB.FAMILY(), qualifier); if (LOG.isDebugEnabled()) { final StringBuilder buf = new StringBuilder(); @@ -772,9 +775,9 @@ private void fsckDataPoints(final Map> datapoints) duplicates_fixed_comp.set(0); } } - + /** - * Handles validating a floating point value. Floats must be encoded on 4 + * Handles validating a floating point value. Floats must be encoded on 4 * bytes for a Float and 8 bytes for a Double. The qualifier is compared to * the actual length in the case of single data points. In previous versions * of OpenTSDB, the qualifier flag may have been on 4 bytes but the actual @@ -810,8 +813,8 @@ private boolean fsckFloat(final DP dp) throws Exception { if (compact_row || options.compact()) { appendDP(qual, value, 4); } else if (!dp.compacted){ - final PutRequest put = new PutRequest(tsdb.dataTable(), - dp.kv.key(), dp.kv.family(), qual, value); + final PutRequest put = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.dataTable(), + dp.kv.key(), dp.kv.family(), qual, value, dp.kv.timestamp()); tsdb.getClient().put(put); } else { LOG.error("SHOULDN'T be here as we didn't compact or fix a " @@ -829,7 +832,7 @@ private boolean fsckFloat(final DP dp) throws Exception { + " not zeroed\n\t" + dp); bad_values.getAndIncrement(); if (options.fix() && options.deleteBadValues() && !dp.compacted) { - final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), + final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv); tsdb.getClient().delete(delete); bad_values_deleted.getAndIncrement(); @@ -854,7 +857,7 @@ private boolean fsckFloat(final DP dp) throws Exception { if (compact_row || options.compact()) { appendDP(qual, value, 4); } else if (!dp.compacted) { - final PutRequest put = new PutRequest(tsdb.dataTable(), + final PutRequest put = new PutRequest(tsdb.dataTable(), dp.kv.key(), dp.kv.family(), qual, value); tsdb.getClient().put(put); } else { @@ -906,10 +909,10 @@ private boolean fsckFloat(final DP dp) throws Exception { } return false; } - + /** * Handles validating an integer value. Integers must be encoded on 1, 2, 4 - * or 8 bytes. Older versions of OpenTSDB wrote all integers on 8 bytes + * or 8 bytes. Older versions of OpenTSDB wrote all integers on 8 bytes * regardless of value. If the --fix flag is specified, this method will * attempt to re-encode small values to save space (up to 7 bytes!!). It also * makes sure the value length matches the length specified in the qualifier @@ -921,7 +924,7 @@ private boolean fsckFloat(final DP dp) throws Exception { private boolean fsckInteger(final DP dp) throws Exception { byte[] qual = dp.qualifier(); byte[] value = dp.value(); - + // this should be a single integer value. Check the encoding to make // sure it's the proper length, and if the flag is set to fix encoding // we can save space with VLE. @@ -945,9 +948,9 @@ private boolean fsckInteger(final DP dp) throws Exception { } return false; } - + // OpenTSDB had support for VLE decoding of integers but only wrote - // on 8 bytes originally. Lets see how much space we could save. + // on 8 bytes originally. Lets see how much space we could save. // We'll assume that a length other than 8 bytes is already VLE'd if (length == 8) { final long decoded = Bytes.getLong(value); @@ -959,7 +962,7 @@ private boolean fsckInteger(final DP dp) throws Exception { vle.getAndIncrement(); vle_bytes.addAndGet(6); value = Bytes.fromShort((short) decoded); - } else if (Integer.MIN_VALUE <= decoded && + } else if (Integer.MIN_VALUE <= decoded && decoded <= Integer.MAX_VALUE) { vle.getAndIncrement(); vle_bytes.addAndGet(4); @@ -973,10 +976,10 @@ private boolean fsckInteger(final DP dp) throws Exception { appendDP(new_qualifier, value, value.length); } else { // put the new value, THEN delete the old - final PutRequest put = new PutRequest(tsdb.dataTable(), - dp.kv.key(), dp.kv.family(), new_qualifier, value); + final PutRequest put = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.dataTable(), + dp.kv.key(), dp.kv.family(), new_qualifier, value, dp.kv.timestamp()); tsdb.getClient().put(put).joinUninterruptibly(); - final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), + final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv.key(), dp.kv.family(), qual); tsdb.getClient().delete(delete); } @@ -991,27 +994,27 @@ private boolean fsckInteger(final DP dp) throws Exception { } /** - * Appends the given value to the running qualifier and value compaction + * Appends the given value to the running qualifier and value compaction * byte arrays. It doesn't take a {@code DP} as we may be changing the * arrays before they're re-written. * @param new_qual The qualifier to append * @param new_value The value to append * @param value_length How much of the value to append */ - private void appendDP(final byte[] new_qual, final byte[] new_value, + private void appendDP(final byte[] new_qual, final byte[] new_value, final int value_length) { System.arraycopy(new_qual, 0, compact_qualifier, qualifier_index, new_qual.length); qualifier_index += new_qual.length; System.arraycopy(new_value, 0, compact_value, value_index, value_length); - value_index += value_length; + value_index += value_length; } - + /** * Appends a representation of a datapoint to a string buffer * @param buf The buffer to modify * @param msg An optional message to append */ - private StringBuilder appendDatapointInfo(final StringBuilder buf, + private StringBuilder appendDatapointInfo(final StringBuilder buf, final DP dp, final String msg) { buf.append(" ") .append("write time: (") @@ -1026,7 +1029,7 @@ private StringBuilder appendDatapointInfo(final StringBuilder buf, } /** - * Resets the running compaction variables. This should be called AFTER a + * Resets the running compaction variables. This should be called AFTER a * {@link fsckDataPoints()} has been run and before the next row of values * is processed. Note that we may overallocate some memory when creating * the arrays. @@ -1055,7 +1058,7 @@ final class DP implements Comparable { boolean compacted; /** The specific data point qualifier/value if the data point was compacted */ Cell cell; - + /** * Default Ctor used for a single data point * @param kv The column where the value appeared. @@ -1064,7 +1067,7 @@ final class DP implements Comparable { this.kv = kv; compacted = false; } - + /** * Overload for a compacted data point * @param kv The column where the value appeared. @@ -1075,7 +1078,7 @@ final class DP implements Comparable { this.cell = cell; compacted = true; } - + /** * Compares data points. * @param dp The data point to compare to @@ -1086,27 +1089,27 @@ final class DP implements Comparable { public int compareTo(final DP dp) { if (kv.timestamp() == dp.kv.timestamp()) { return 0; - } + } return kv.timestamp() < dp.kv.timestamp() ? -1 : 1; } - + /** @return The qualifier of the data point (from the compaction or column) */ public byte[] qualifier() { return compacted ? cell.qualifier() : kv.qualifier(); } - + /** @return The value of the data point */ public byte[] value() { return compacted ? cell.value() : kv.value(); } - + /** @return The cell or key value string */ public String toString() { return compacted ? cell.toString() : kv.toString(); } } } - + /** * Silly little class to report the progress while fscking */ @@ -1122,8 +1125,8 @@ public void run() { processed_rows = (processed_rows - (processed_rows % report_rows)); if (processed_rows - last_progress >= report_rows) { last_progress = processed_rows; - LOG.info("Processed " + processed_rows + " rows, " + - valid_datapoints.get() + " valid datapoints"); + LOG.info("Processed " + processed_rows + " rows, " + + valid_datapoints.get() + " valid datapoints"); } Thread.sleep(1000); } catch (InterruptedException e) { @@ -1131,7 +1134,7 @@ public void run() { } } } - + /** Prints usage and exits with the given retval. */ private static void usage(final ArgP argp, final String errmsg, final int retval) { @@ -1169,19 +1172,19 @@ private void logResults() { LOG.info("Unparseable Datapoint Values: " + bad_values.get()); LOG.info("Unparseable Datapoint Values Deleted: " + bad_values_deleted.get()); LOG.info("Improperly Encoded Floating Point Values: " + value_encoding.get()); - LOG.info("Improperly Encoded Floating Point Values Fixed: " + + LOG.info("Improperly Encoded Floating Point Values Fixed: " + value_encoding_fixed.get()); LOG.info("Unparseable Compacted Columns: " + bad_compacted_columns.get()); - LOG.info("Unparseable Compacted Columns Deleted: " + + LOG.info("Unparseable Compacted Columns Deleted: " + bad_compacted_columns_deleted.get()); LOG.info("Datapoints Qualified for VLE : " + vle.get()); LOG.info("Datapoints Compressed with VLE: " + vle_fixed.get()); - LOG.info("Bytes Saved with VLE: " + vle_bytes.get()); + LOG.info("Bytes Saved with VLE: " + vle_bytes.get()); LOG.info("Total Errors: " + totalErrors()); LOG.info("Total Correctable Errors: " + correctable()); LOG.info("Total Errors Fixed: " + totalFixed()); } - + /** * The main class executed from the "tsdb" script * @param args Command line arguments to parse @@ -1209,7 +1212,7 @@ public static void main(String[] args) throws Exception { usage(argp, "Must supply a query or use the '--full-scan' flag", 1); } tsdb.checkNecessaryTablesExist().joinUninterruptibly(); - + argp = null; final Fsck fsck = new Fsck(tsdb, options); try { diff --git a/src/utils/Config.java b/src/utils/Config.java index 190b1c3488..8ce9a97852 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -28,18 +28,18 @@ /** * OpenTSDB Configuration Class - * + * * This handles all of the user configurable variables for a TSD. On * initialization default values are configured for all variables. Then * implementations should call the {@link #loadConfig()} methods to search for a * default configuration or try to load one provided by the user. - * + * * To add a configuration, simply set a default value in {@link #setDefaults()}. * Wherever you need to access the config value, use the proper helper to fetch * the value, accounting for exceptions that may be thrown if necessary. - * + * * The get number helpers will return NumberFormatExceptions if the - * requested property is null or unparseable. The {@link #getString(String)} + * requested property is null or unparseable. The {@link #getString(String)} * helper will return a NullPointerException if the property isn't found. *

    * Plugins can extend this class and copy the properties from the main @@ -53,11 +53,11 @@ public class Config { private static final Logger LOG = LoggerFactory.getLogger(Config.class); /** Flag to determine if we're running under Windows or not */ - public static final boolean IS_WINDOWS = + public static final boolean IS_WINDOWS = System.getProperty("os.name", "").contains("Windows"); - + // These are accessed often so need a set address for fast access (faster - // than accessing the map. Their value will be changed when the config is + // than accessing the map. Their value will be changed when the config is // loaded // NOTE: edit the setDefaults() method if you add a public field @@ -66,60 +66,69 @@ public class Config { /** tsd.core.auto_create_tagk */ private boolean auto_tagk = true; - + /** tsd.core.auto_create_tagv */ private boolean auto_tagv = true; - + /** tsd.storage.enable_compaction */ private boolean enable_compactions = true; - + /** tsd.storage.enable_appends */ private boolean enable_appends = false; - + /** tsd.storage.repair_appends */ private boolean repair_appends = false; - + /** tsd.core.meta.enable_realtime_ts */ private boolean enable_realtime_ts = false; - + /** tsd.core.meta.enable_realtime_uid */ private boolean enable_realtime_uid = false; - + /** tsd.core.meta.enable_tsuid_incrementing */ private boolean enable_tsuid_incrementing = false; - + /** tsd.core.meta.enable_tsuid_tracking */ private boolean enable_tsuid_tracking = false; - + /** tsd.http.request.enable_chunked */ private boolean enable_chunked_requests = false; - + /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; /** tsd.http.request.max_chunk */ - private int max_chunked_requests = 4096; - + private int max_chunked_requests = 4096; + /** tsd.core.tree.enable_processing */ private boolean enable_tree_processing = false; /** tsd.storage.hbase.scanner.maxNumRows */ private int scanner_max_num_rows = 128; - + private int mul_get_batch_size = 1024; private int mul_get_cocurrency_number = 16; + /** tsd.storage.use_otsdb_timestamp */ + /** Sets the HBase cell timestamp equal to metric timestamp */ + private boolean use_otsdb_timestamp = true; + + /** tsd.storage.use_max_value */ + /** Used for resolving between data coming in at same timestamp */ + /** If set to true, the maximum value will be returned, minimum */ + private boolean use_max_value = true; + /** * The list of properties configured to their defaults or modified by users */ - protected final HashMap properties = + protected final HashMap properties = new HashMap(); /** Holds default values for the config */ - protected static final HashMap default_map = + protected static final HashMap default_map = new HashMap(); - + /** Tracks the location of the file that was actually loaded */ protected String config_location; @@ -152,7 +161,7 @@ public Config(final String file) throws IOException { /** * Constructor for plugins or overloaders who want a copy of the parent * properties but without the ability to modify them - * + * * This constructor will not re-read the file, but it will copy the location * so if a child wants to reload the properties periodically, they may do so * @param parent Parent configuration object to load from @@ -175,60 +184,60 @@ public Config() { public String configLocation() { return config_location; } - + /** @return the auto_metric value */ public boolean auto_metric() { return auto_metric; } - + /** @return the auto_tagk value */ public boolean auto_tagk() { return auto_tagk; } - + /** @return the auto_tagv value */ public boolean auto_tagv() { return auto_tagv; } - + /** @param auto_metric whether or not to auto create metrics */ public void setAutoMetric(boolean auto_metric) { this.auto_metric = auto_metric; - properties.put("tsd.core.auto_create_metrics", + properties.put("tsd.core.auto_create_metrics", Boolean.toString(auto_metric)); } - + /** @return the enable_compaction value */ public boolean enable_compactions() { return enable_compactions; } - + /** @return whether or not to write data in the append format */ public boolean enable_appends() { return enable_appends; } - + /** @return whether or not to re-write appends with duplicates or out of order * data when queried. */ public boolean repair_appends() { return repair_appends; } - + /** @return whether or not to record new TSMeta objects in real time */ - public boolean enable_realtime_ts() { + public boolean enable_realtime_ts() { return enable_realtime_ts; } - + /** @return whether or not record new UIDMeta objects in real time */ - public boolean enable_realtime_uid() { + public boolean enable_realtime_uid() { return enable_realtime_uid; } - + /** @return whether or not to increment TSUID counters */ - public boolean enable_tsuid_incrementing() { + public boolean enable_tsuid_incrementing() { return enable_tsuid_incrementing; } - + /** @return whether or not to record a 1 for every TSUID */ public boolean enable_tsuid_tracking() { return enable_tsuid_tracking; @@ -238,17 +247,17 @@ public boolean enable_tsuid_tracking() { public int scanner_maxNumRows() { return scanner_max_num_rows; } - + /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { return enable_chunked_requests; } - + /** @return max incoming chunk size in bytes */ public int max_chunked_requests() { return max_chunked_requests; } - + /** @return true if duplicate values should be fixed */ public boolean fix_duplicates() { return fix_duplicates; @@ -263,7 +272,7 @@ public void setFixDuplicates(final boolean fix_duplicates) { public boolean enable_tree_processing() { return enable_tree_processing; } - + public int mul_get_batch_size() { return mul_get_batch_size; } @@ -271,14 +280,22 @@ public int mul_get_batch_size() { public int mul_get_concurrency_number() { return mul_get_cocurrency_number; } - + + public boolean use_otsdb_timestamp() { + return use_otsdb_timestamp; + } + + public boolean use_max_value() { + return use_max_value; + } + /** * Allows for modifying properties after creation or loading. - * - * WARNING: This should only be used on initialization and is meant for - * command line overrides. Also note that it will reset all static config + * + * WARNING: This should only be used on initialization and is meant for + * command line overrides. Also note that it will reset all static config * variables when called. - * + * * @param property The name of the property to override * @param value The value to store */ @@ -309,10 +326,10 @@ public final int getInt(final String property) { } /** - * Returns the given string trimed or null if is null - * @param string The string be trimmed of + * Returns the given string trimed or null if is null + * @param string The string be trimmed of * @return The string trimed or null - */ + */ private final String sanitize(final String string) { if (string == null) { return null; @@ -367,12 +384,12 @@ public final double getDouble(final String property) { /** * Returns the given property as a boolean - * + * * Property values are case insensitive and the following values will result * in a True return value: - 1 - True - Yes - * + * * Any other values, including an empty string, will result in a False - * + * * @param property The property to load * @return A parsed boolean * @throws NullPointerException if the property was not found @@ -402,7 +419,7 @@ public final String getDirectoryName(final String property) { if (IS_WINDOWS) { // Windows swings both ways. If a forward slash was already used, we'll // add one at the end if missing. Otherwise use the windows default of \ - if (directory.charAt(directory.length() - 1) == '\\' || + if (directory.charAt(directory.length() - 1) == '\\' || directory.charAt(directory.length() - 1) == '/') { return directory; } @@ -415,17 +432,17 @@ public final String getDirectoryName(final String property) { throw new IllegalArgumentException( "Unix path names cannot contain a back slash"); } - + if (directory == null || directory.isEmpty()){ return null; } - + if (directory.charAt(directory.length() - 1) == '/') { return directory; } return directory + "/"; } - + /** * Determines if the given propery is in the map * @param property The property to search for @@ -485,10 +502,10 @@ public final void enableCompactions() { public final void disableCompactions() { this.enable_compactions = false; } - + /** * Loads default entries that were not provided by a file or command line - * + * * This should be called in the constructor */ protected void setDefaults() { @@ -561,7 +578,7 @@ protected void setDefaults() { default_map.put("tsd.storage.compaction.flush_speed", "2"); default_map.put("tsd.timeseriesfilter.enable", "false"); default_map.put("tsd.uidfilter.enable", "false"); - default_map.put("tsd.core.stats_with_port", "false"); + default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.query.allow_delete", "false"); default_map.put("tsd.http.request.enable_chunked", "false"); @@ -573,6 +590,8 @@ protected void setDefaults() { default_map.put("tsd.query.timeout", "0"); default_map.put("tsd.core.mul_get_batch_size", "1024"); default_map.put("tsd.core.mul_get_cocurrency_number", "20"); + default_map.put("tsd.storage.use_otsdb_timestamp", "true"); + default_map.put("tsd.storage.use_max_value", "true"); for (Map.Entry entry : default_map.entrySet()) { if (!properties.containsKey(entry.getKey())) @@ -584,14 +603,14 @@ protected void setDefaults() { /** * Searches a list of locations for a valid opentsdb.conf file - * + * * The config file must be a standard JAVA properties formatted file. If none * of the locations have a config file, then the defaults or command line * arguments will be used for the configuration - * + * * Defaults for Linux based systems are: ./opentsdb.conf /etc/opentsdb.conf * /etc/opentsdb/opentdsb.conf /opt/opentsdb/opentsdb.conf - * + * * @throws IOException Thrown if there was an issue reading a file */ protected void loadConfig() throws IOException { @@ -620,9 +639,9 @@ protected void loadConfig() throws IOException { FileInputStream file_stream = new FileInputStream(file); Properties props = new Properties(); props.load(file_stream); - + // load the hash map - loadHashMap(props); + loadHashMap(props); } catch (Exception e) { // don't do anything, the file may be missing and that's fine LOG.debug("Unable to find or load " + file, e); @@ -650,10 +669,10 @@ protected void loadConfig(final String file) throws FileNotFoundException, try { final Properties props = new Properties(); props.load(file_stream); - + // load the hash map loadHashMap(props); - + // no exceptions thrown, so save the valid path and exit LOG.info("Successfully loaded configuration file: " + file); config_location = file; @@ -676,9 +695,9 @@ public void loadStaticVariables() { enable_chunked_requests = this.getBoolean("tsd.http.request.enable_chunked"); enable_realtime_ts = this.getBoolean("tsd.core.meta.enable_realtime_ts"); enable_realtime_uid = this.getBoolean("tsd.core.meta.enable_realtime_uid"); - enable_tsuid_incrementing = + enable_tsuid_incrementing = this.getBoolean("tsd.core.meta.enable_tsuid_incrementing"); - enable_tsuid_tracking = + enable_tsuid_tracking = this.getBoolean("tsd.core.meta.enable_tsuid_tracking"); if (this.hasProperty("tsd.http.request.max_chunk")) { max_chunked_requests = this.getInt("tsd.http.request.max_chunk"); @@ -688,18 +707,20 @@ public void loadStaticVariables() { scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); mul_get_batch_size = this.getInt("tsd.core.mul_get_batch_size"); mul_get_cocurrency_number = this.getInt("tsd.core.mul_get_cocurrency_number"); + use_otsdb_timestamp = this.getBoolean("tsd.storage.use_otsdb_timestamp"); + use_max_value = this.getBoolean("tsd.storage.use_max_value"); } - + /** * Called from {@link #loadConfig} to copy the properties into the hash map * Tsuna points out that the Properties class is much slower than a hash * map so if we'll be looking up config values more than once, a hash map - * is the way to go + * is the way to go * @param props The loaded Properties object to copy */ private void loadHashMap(final Properties props) { properties.clear(); - + @SuppressWarnings("rawtypes") Enumeration e = props.propertyNames(); while (e.hasMoreElements()) { diff --git a/test/core/TestCompactionQueue.java b/test/core/TestCompactionQueue.java index 368f1d270b..1e41ee0e10 100644 --- a/test/core/TestCompactionQueue.java +++ b/test/core/TestCompactionQueue.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + import org.hbase.async.Bytes; import org.hbase.async.KeyValue; @@ -47,6 +49,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.anyLong; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; @@ -72,9 +75,9 @@ public final class TestCompactionQueue { private static final byte[] FAMILY = { 't' }; private static final byte[] ZERO = { 0 }; private static final byte[] MIXED_FLAG = { Const.MS_MIXED_COMPACT }; - private static final String annotation = - "{\"tsuid\":\"ABCD\",\"description\":\"Description\"," + - "\"notes\":\"Notes\",\"custom\":null,\"endTime\":1328140801,\"startTime" + + private static final String annotation = + "{\"tsuid\":\"ABCD\",\"description\":\"Description\"," + + "\"notes\":\"Notes\",\"custom\":null,\"endTime\":1328140801,\"startTime" + "\":1328140800}"; private static final byte[] note = annotation.getBytes(Charset.forName("UTF-8")); private static final byte[] note_qual = { 1, 0, 0 }; @@ -96,22 +99,46 @@ public void before() throws Exception { PowerMockito.when(config.fix_duplicates()).thenReturn(true); compactionq = new CompactionQueue(tsdb); - when(tsdb.put(anyBytes(), anyBytes(), anyBytes())) + when(tsdb.put(anyBytes(), anyBytes(), anyBytes(), anyLong())) .thenAnswer(newDeferred()); when(tsdb.delete(anyBytes(), any(byte[][].class))) .thenAnswer(newDeferred()); } + @Test + public void useMaxTsWhileCompacting() throws Exception { + ArrayList kvs = new ArrayList(2); + ArrayList annotations = new ArrayList(0); + long ts1 = Math.abs(ThreadLocalRandom.current().nextLong()); + final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + kvs.add(makekvWithTs(qual1, ts1, val1)); + long ts2 = Math.abs(ThreadLocalRandom.current().nextLong()); + final byte[] qual2 = { (byte) 0xF0, 0x00, 0x01, 0x07 }; + final byte[] val2 = Bytes.fromLong(5L); + kvs.add(makekvWithTs(qual2, ts2, val2)); + long ts3 = Math.abs(ThreadLocalRandom.current().nextLong()); + final byte[] qual3 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; + final byte[] val3 = Bytes.fromLong(2L); + kvs.add(makekvWithTs(qual3, ts3, val3)); + + when(tsdb.getConfig().getBoolean("tsd.storage.use_otsdb_timestamp")).thenReturn(true); + final KeyValue kv = compactionq.compact(kvs, annotations); + assertArrayEquals(MockBase.concatByteArrays(qual1, qual2, qual3), kv.qualifier()); + assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, ZERO), kv.value()); + assert(kv.timestamp() == Math.max(ts1, Math.max(ts2, ts3))); + } + @Test public void emptyRow() throws Exception { ArrayList kvs = new ArrayList(0); ArrayList annotations = new ArrayList(0); final KeyValue kv = compactionq.compact(kvs, annotations); assertNull(kv); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } @@ -126,33 +153,33 @@ public void oneCellRow() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void oneCellAppend() throws Exception { ArrayList kvs = new ArrayList(1); ArrayList annotations = new ArrayList(0); final byte[] qual = { 0x00, 0x07 }; final byte[] val = Bytes.fromLong(42L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void oneCellRowWAnnotation() throws Exception { ArrayList kvs = new ArrayList(1); @@ -165,14 +192,14 @@ public void oneCellRowWAnnotation() throws Exception { assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); assertEquals(1, annotations.size()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void oneCellAppendWAnnotiation() throws Exception { ArrayList kvs = new ArrayList(1); @@ -180,20 +207,20 @@ public void oneCellAppendWAnnotiation() throws Exception { kvs.add(makekv(note_qual, note)); final byte[] qual = { 0x00, 0x07 }; final byte[] val = Bytes.fromLong(42L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); assertEquals(1, annotations.size()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void oneCellRowWAnnotationMS() throws Exception { ArrayList kvs = new ArrayList(1); @@ -206,10 +233,10 @@ public void oneCellRowWAnnotationMS() throws Exception { assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); assertEquals(1, annotations.size()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } @@ -227,7 +254,7 @@ public void oneCellRowBadLength() throws Exception { assertArrayEquals(val, kv.value()); // The old one needed the length fixed up, so verify that we wrote the new one - verify(tsdb, times(1)).put(KEY, cqual, val); + verify(tsdb, times(1)).put(KEY, cqual, val, kvCount - 1); // ... and deleted the old one verify(tsdb, times(1)).delete(KEY, new byte[][] { qual }); } @@ -242,10 +269,10 @@ public void oneCellRowMS() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } @@ -264,14 +291,14 @@ public void twoCellRow() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, ZERO)); + MockBase.concatByteArrays(val1, val2, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } - + @Test public void twoCellAppend() throws Exception { ArrayList kvs = new ArrayList(1); @@ -280,19 +307,19 @@ public void twoCellAppend() throws Exception { final byte[] val = Bytes.fromLong(42L); final byte[] qual2 = { 0x00, 0x17 }; final byte[] val2 = Bytes.fromLong(5L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void twoCellRowWAnnotation() throws Exception { ArrayList kvs = new ArrayList(2); @@ -309,10 +336,10 @@ public void twoCellRowWAnnotation() throws Exception { assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); assertEquals(1, annotations.size()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, ZERO)); + MockBase.concatByteArrays(val1, val2, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } @@ -326,32 +353,32 @@ public void twoCellAppendWAnnotations() throws Exception { final byte[] val = Bytes.fromLong(42L); final byte[] qual2 = { 0x00, 0x17 }; final byte[] val2 = Bytes.fromLong(5L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); assertEquals(1, annotations.size()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void fullRowSeconds() throws Exception { ArrayList kvs = new ArrayList(3600); ArrayList annotations = new ArrayList(0); - + byte[] qualifiers = new byte[] {}; byte[] values = new byte[] {}; - + for (int i = 0; i < 3600; i++) { final short qualifier = (short) (i << Const.FLAG_BITS | 0x07); kvs.add(makekv(Bytes.fromShort(qualifier), Bytes.fromLong(i))); - qualifiers = MockBase.concatByteArrays(qualifiers, + qualifiers = MockBase.concatByteArrays(qualifiers, Bytes.fromShort(qualifier)); values = MockBase.concatByteArrays(values, Bytes.fromLong(i)); } @@ -359,14 +386,14 @@ public void fullRowSeconds() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qualifiers), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(values, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, qualifiers, - MockBase.concatByteArrays(values, ZERO)); + MockBase.concatByteArrays(values, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete((byte[])any(), (byte[][])any()); } - + @Test public void bigRowMs() throws Exception { ArrayList kvs = new ArrayList(3599999); @@ -377,7 +404,7 @@ public void bigRowMs() throws Exception { for (int i = 0; i < 3599999; i++) { final int qualifier = (((i << Const.MS_FLAG_BITS ) | 0x07) | 0xF0000000); kvs.add(makekv(Bytes.fromInt(qualifier), Bytes.fromLong(i))); - qualifiers = MockBase.concatByteArrays(qualifiers, + qualifiers = MockBase.concatByteArrays(qualifiers, Bytes.fromInt(qualifier)); values = MockBase.concatByteArrays(values, Bytes.fromLong(i)); i += 100; @@ -385,14 +412,14 @@ public void bigRowMs() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qualifiers), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(values, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, qualifiers, - MockBase.concatByteArrays(values, ZERO)); + MockBase.concatByteArrays(values, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete((byte[])any(), (byte[][])any()); } - + @Test public void twoCellRowMS() throws Exception { ArrayList kvs = new ArrayList(2); @@ -407,14 +434,14 @@ public void twoCellRowMS() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, ZERO)); + MockBase.concatByteArrays(val1, val2, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } - + @Test public void sortMsAndS() throws Exception { ArrayList kvs = new ArrayList(2); @@ -428,21 +455,21 @@ public void sortMsAndS() throws Exception { final byte[] qual3 = { (byte) 0xF0, 0x00, 0x01, 0x07 }; final byte[] val3 = Bytes.fromLong(5L); kvs.add(makekv(qual3, val3)); - + final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, new byte[] { 1 }), kv.value()); // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, new byte[] { 1 })); + MockBase.concatByteArrays(val1, val3, val2, new byte[] { 1 }), kvCount - 1); // And we had to delete individual cells. - verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, + verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2, qual3 })); } - + @Test public void secondsOutOfOrder() throws Exception { ArrayList kvs = new ArrayList(3); @@ -458,22 +485,22 @@ public void secondsOutOfOrder() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual2, qual3, qual1), + assertArrayEquals(MockBase.concatByteArrays(qual2, qual3, qual1), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val2, val3, val1, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val2, val3, val1, ZERO), kv.value()); // We compacted all columns to one, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual2, qual3, qual1), - MockBase.concatByteArrays(val2, val3, val1, ZERO)); + MockBase.concatByteArrays(val2, val3, val1, ZERO), kvCount - 1); // And we had to delete individual cells. - verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, + verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2, qual3 })); } - + @Test public void msOutOfOrder() throws Exception { - // all rows with an ms qualifier will go through the compaction + // all rows with an ms qualifier will go through the compaction // process and they'll be sorted ArrayList kvs = new ArrayList(3); ArrayList annotations = new ArrayList(0); @@ -488,18 +515,18 @@ public void msOutOfOrder() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual2, qual3, qual1), + assertArrayEquals(MockBase.concatByteArrays(qual2, qual3, qual1), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val2, val3, val1, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val2, val3, val1, ZERO), kv.value()); // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual2, qual3, qual1), - MockBase.concatByteArrays(val2, val3, val1, ZERO)); + MockBase.concatByteArrays(val2, val3, val1, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2, qual3 })); } - + @Test public void secondAndMs() throws Exception { ArrayList kvs = new ArrayList(2); @@ -513,16 +540,16 @@ public void secondAndMs() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), + assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), kv.value()); // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, new byte[] { 1 })); + MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } - + @Test public void secondAndMsWAnnotation() throws Exception { ArrayList kvs = new ArrayList(2); @@ -537,13 +564,13 @@ public void secondAndMsWAnnotation() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), + assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), kv.value()); assertEquals(1, annotations.size()); // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, val2, new byte[] { 1 })); + MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } @@ -577,13 +604,13 @@ public void msSameAsSecondFix() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qual2, kv.qualifier()); assertArrayEquals(val2, kv.value()); - + // no compacted row - verify(tsdb, never()).put(KEY, qual2, val2); + verify(tsdb, never()).put(KEY, qual2, val2, kvCount - 1); // And we had to delete the earlier entry. verify(tsdb, times(1)).delete(KEY, new byte[][] {qual1}); } - + @Test public void fixQualifierFlags() throws Exception { ArrayList kvs = new ArrayList(2); @@ -604,7 +631,7 @@ public void fixQualifierFlags() throws Exception { // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(cqual1, qual2), - MockBase.concatByteArrays(val1, val2, ZERO)); + MockBase.concatByteArrays(val1, val2, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } @@ -628,10 +655,10 @@ public void fixFloatingPoint() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, cval2, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2), - MockBase.concatByteArrays(val1, cval2, ZERO)); + MockBase.concatByteArrays(val1, cval2, ZERO), kvCount - 1); // And we had to delete individual cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2, })); } @@ -651,7 +678,7 @@ public void overlappingDataPoints() throws Exception { compactionq.compact(kvs, annotations); } - + @Test public void overlappingDataPointsFix() throws Exception { ArrayList kvs = new ArrayList(2); @@ -669,7 +696,7 @@ public void overlappingDataPointsFix() throws Exception { assertArrayEquals(val2, kv.value()); // We didn't have anything to write. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // We had to delete the first entry as it was older. verify(tsdb, times(1)).delete(KEY, new byte[][] {qual1}); } @@ -694,9 +721,9 @@ public void failedCompactNoop() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qualcompact, kv.qualifier()); assertArrayEquals(valcompact, kv.value()); - + // We didn't have anything to write. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // We had to delete stuff in 1 row. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual2 })); } @@ -712,11 +739,11 @@ public void annotationOnly() throws Exception { assertEquals(1, annotations.size()); // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void annotationsOnly() throws Exception { // Two annotations in a row only (the value of the second isn't parsed at @@ -731,11 +758,11 @@ public void annotationsOnly() throws Exception { assertEquals(2, annotations.size()); // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void secondCompact() throws Exception { // In this test the row has already been compacted, and another data @@ -756,18 +783,18 @@ public void secondCompact() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), kv.value()); // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, ZERO)); + MockBase.concatByteArrays(val1, val3, val2, ZERO), kvCount - 1); // And we had to delete the individual cell + pre-existing compacted cell. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual3 })); } - + @Test public void secondCompactWAnnotation() throws Exception { // In this test the row has already been compacted, and another data @@ -789,15 +816,15 @@ public void secondCompactWAnnotation() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), kv.value()); assertEquals(1, annotations.size()); // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, ZERO)); + MockBase.concatByteArrays(val1, val3, val2, ZERO), kvCount - 1); // And we had to delete the individual cell + pre-existing compacted cell. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual3 })); } @@ -822,18 +849,18 @@ public void secondCompactMS() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, ZERO)); + MockBase.concatByteArrays(val1, val3, val2, ZERO), kvCount - 1); // And we had to delete the individual cell + pre-existing compacted cell. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual3 })); } - + @Test public void secondCompactMixedSecond() throws Exception { // In this test the row has already been compacted, and another data @@ -846,7 +873,7 @@ public void secondCompactMixedSecond() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x0A, 0x41, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, + kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, new byte[] { 1 }))); // This data point came late. Note that its time delta falls in between // that of the two data points above. @@ -855,19 +882,19 @@ public void secondCompactMixedSecond() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, new byte[] { 1 }), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, - new byte[] { 1 })); + MockBase.concatByteArrays(val1, val3, val2, + new byte[] { 1 }), kvCount - 1); // And we had to delete the individual cell + pre-existing compacted cell. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual3 })); } - + @Test public void secondCompactMixedMS() throws Exception { // In this test the row has already been compacted, and another data @@ -880,7 +907,7 @@ public void secondCompactMixedMS() throws Exception { final byte[] qual2 = { (byte) 0xF0, 0x0A, 0x41, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, + kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, new byte[] { 1 }))); // This data point came late. Note that its time delta falls in between // that of the two data points above. @@ -889,19 +916,19 @@ public void secondCompactMixedMS() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, new byte[] { 1 }), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, - new byte[] { 1 })); + MockBase.concatByteArrays(val1, val3, val2, + new byte[] { 1 }), kvCount - 1); // And we had to delete the individual cell + pre-existing compacted cell. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual3 })); } - + @Test public void secondCompactMixedMSAndS() throws Exception { // In this test the row has already been compacted with a ms flag as the @@ -915,7 +942,7 @@ public void secondCompactMixedMSAndS() throws Exception { final byte[] qual2 = { 0x00, (byte) 0xF7 }; final byte[] val2 = Bytes.fromLong(5L); final byte[] qual12 = MockBase.concatByteArrays(qual1, qual2); - kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, + kvs.add(makekv(qual12, MockBase.concatByteArrays(val1, val2, new byte[] { 1 }))); // This data point came late. Note that its time delta falls in between // that of the two data points above. @@ -924,19 +951,19 @@ public void secondCompactMixedMSAndS() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual3, qual1, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual3, qual1, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val3, val1, val2, + assertArrayEquals(MockBase.concatByteArrays(val3, val1, val2, new byte[] { 1 }), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual3, qual1, qual2), - MockBase.concatByteArrays(val3, val1, val2, - new byte[] { 1 })); + MockBase.concatByteArrays(val3, val1, val2, + new byte[] { 1 }), kvCount - 1); // And we had to delete the individual cell + pre-existing compacted cell. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual3 })); } - + @Test (expected = IllegalDataException.class) public void secondCompactOverwrite() throws Exception { PowerMockito.when(config.fix_duplicates()).thenReturn(false); @@ -959,7 +986,7 @@ public void secondCompactOverwrite() throws Exception { compactionq.compact(kvs, annotations); } - + @Test public void secondCompactOverwriteFix() throws Exception { // In this test the row has already been compacted, and a new value for an @@ -980,19 +1007,19 @@ public void secondCompactOverwriteFix() throws Exception { kvs.add(makekv(qual3, val3)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val3, val2, new byte[] { 0 }), + assertArrayEquals(MockBase.concatByteArrays(val3, val2, new byte[] { 0 }), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual3, qual2), - MockBase.concatByteArrays(val3, val2, new byte[] { 0 })); + MockBase.concatByteArrays(val3, val2, new byte[] { 0 }), kvCount - 1); // And we had to delete the individual cell, but we overwrite the pre-existing compacted cell // rather than delete it. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] {qual3})); } - + @Test public void doubleFailedCompactNoop() throws Exception { // In this test the row has already been compacted once, but we didn't @@ -1021,15 +1048,15 @@ public void doubleFailedCompactNoop() throws Exception { final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(qual132, kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), kv.value()); - + // We didn't have anything to write, the last cell is already the correct // compacted version of the row. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // And we had to delete the 3 individual cells + the first pre-existing // compacted cell. - verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, + verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual12, qual3, qual2 })); } @@ -1058,14 +1085,14 @@ public void weirdOverlappingCompactedCells() throws Exception { kvs.add(makekv(qual2, val2)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), + assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), kv.value()); - + // We had one row to compact, so one put to do. verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual3, qual2), - MockBase.concatByteArrays(val1, val3, val2, ZERO)); + MockBase.concatByteArrays(val1, val3, val2, ZERO), kvCount - 1); // And we had to delete the 3 individual cells + 2 pre-existing // compacted cells. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual1, qual12, qual13, qual3, qual2 })); @@ -1103,17 +1130,17 @@ public void tripleCompacted() throws Exception { assertArrayEquals( MockBase.concatByteArrays(qual12, qual34, qual56), kv.qualifier()); assertArrayEquals( - MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO), + MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO), kv.value()); // We wrote only the combined column. - verify(tsdb, times(1)).put(KEY, + verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2, qual3, qual4, qual5, qual6), - MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO)); + MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO), kvCount - 1); // And we had to delete the 3 partially compacted columns. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual34, qual56 })); } - + @Test public void tripleCompactedOutOfOrder() throws Exception { // Here we have a row with #kvs > scanner.maxNumKeyValues and the result @@ -1146,17 +1173,17 @@ public void tripleCompactedOutOfOrder() throws Exception { assertArrayEquals( MockBase.concatByteArrays(qual12, qual34, qual56), kv.qualifier()); assertArrayEquals( - MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO), - kv.value()); - + MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO), + kv.value()); + // We wrote only the combined column. - verify(tsdb, times(1)).put(KEY, + verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2, qual3, qual4, qual5, qual6), - MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO)); + MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, ZERO), kvCount - 1); // And we had to delete the 3 partially compacted columns. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual56, qual34 })); } - + @Test public void tripleCompactedSecondsAndMs() throws Exception { // Here we have a row with #kvs > scanner.maxNumKeyValues and the result @@ -1195,13 +1222,13 @@ public void tripleCompactedSecondsAndMs() throws Exception { kv.value()); // We wrote only the combined column. - verify(tsdb, times(1)).put(KEY, + verify(tsdb, times(1)).put(KEY, MockBase.concatByteArrays(qual1, qual2, qual3, qual4, qual5, qual6), - MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, MIXED_FLAG)); + MockBase.concatByteArrays(val1, val2, val3, val4, val5, val6, MIXED_FLAG), kvCount - 1); // And we had to delete the 3 partially compacted columns. verify(tsdb, times(1)).delete(eq(KEY), eqAnyOrder(new byte[][] { qual12, qual34, qual56 })); } - + @Test public void appendsAndLaterPuts() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1214,23 +1241,23 @@ public void appendsAndLaterPuts() throws Exception { final byte[] val3 = Bytes.fromLong(3L); final byte[] qual4 = { 0x00, 0x37 }; final byte[] val4 = Bytes.fromLong(2L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(qual3, val3)); kvs.add(makekv(qual4, val4)); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void appendsAndEarlierPuts() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1245,21 +1272,21 @@ public void appendsAndEarlierPuts() throws Exception { final byte[] val4 = Bytes.fromLong(2L); kvs.add(makekv(qual, val)); kvs.add(makekv(qual2, val2)); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual3, val3, qual4, val4))); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void appendsAndInterspersedPuts() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1274,21 +1301,21 @@ public void appendsAndInterspersedPuts() throws Exception { final byte[] val4 = Bytes.fromLong(2L); kvs.add(makekv(qual, val)); kvs.add(makekv(qual3, val3)); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual2, val2, qual4, val4))); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void doubleAppends() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1301,23 +1328,23 @@ public void doubleAppends() throws Exception { final byte[] val3 = Bytes.fromLong(3L); final byte[] qual4 = { 0x00, 0x37 }; final byte[] val4 = Bytes.fromLong(2L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual3, val3, qual4, val4))); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void tripleAppends() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1334,25 +1361,25 @@ public void tripleAppends() throws Exception { final byte[] val5 = Bytes.fromLong(1L); final byte[] qual6 = { 0x00, 0x57 }; final byte[] val6 = Bytes.fromLong(0L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual3, val3, qual4, val4))); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual5, val5, qual6, val6))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays( qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays( val, val2, val3, val4, val5, val6, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void doubleAppendsAndPuts() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1369,25 +1396,25 @@ public void doubleAppendsAndPuts() throws Exception { final byte[] val5 = Bytes.fromLong(1L); final byte[] qual6 = { 0x00, 0x57 }; final byte[] val6 = Bytes.fromLong(0L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(qual3, val3)); kvs.add(makekv(qual4, val4)); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual5, val5, qual6, val6))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays( qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays( val, val2, val3, val4, val5, val6, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void appendsAndCompacted() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1400,23 +1427,23 @@ public void appendsAndCompacted() throws Exception { final byte[] val3 = Bytes.fromLong(3L); final byte[] qual4 = { 0x00, 0x37 }; final byte[] val4 = Bytes.fromLong(2L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), + kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), MockBase.concatByteArrays(val3, val4, ZERO))); final KeyValue kv = compactionq.compact(kvs, annotations); - assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), + assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); - assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), + assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void appendsAndCompactedAndPuts() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1433,9 +1460,9 @@ public void appendsAndCompactedAndPuts() throws Exception { final byte[] val5 = Bytes.fromLong(1L); final byte[] qual6 = { 0x00, 0x57 }; final byte[] val6 = Bytes.fromLong(0L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), + kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), MockBase.concatByteArrays(val3, val4, ZERO))); kvs.add(makekv(qual5, val5)); kvs.add(makekv(qual6, val6)); @@ -1444,14 +1471,14 @@ public void appendsAndCompactedAndPuts() throws Exception { qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays( val, val2, val3, val4, val5, val6, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void appendsDuplicatePuts() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1460,21 +1487,21 @@ public void appendsDuplicatePuts() throws Exception { final byte[] val = Bytes.fromLong(42L); final byte[] qual2 = { 0x00, 0x17 }; final byte[] val2 = Bytes.fromLong(5L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(qual, val)); kvs.add(makekv(qual2, val2)); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + @Test public void appendsDuplicateCompacted() throws Exception { ArrayList kvs = new ArrayList(1); @@ -1483,21 +1510,21 @@ public void appendsDuplicateCompacted() throws Exception { final byte[] val = Bytes.fromLong(42L); final byte[] qual2 = { 0x00, 0x17 }; final byte[] val2 = Bytes.fromLong(5L); - kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, + kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - kvs.add(makekv(MockBase.concatByteArrays(qual, qual2), + kvs.add(makekv(MockBase.concatByteArrays(qual, qual2), MockBase.concatByteArrays(val, val2, ZERO))); final KeyValue kv = compactionq.compact(kvs, annotations); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); - + // We had nothing to do so... // ... verify there were no put. - verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes()); + verify(tsdb, never()).put(anyBytes(), anyBytes(), anyBytes(), anyLong()); // ... verify there were no delete. verify(tsdb, never()).delete(anyBytes(), any(byte[][].class)); } - + // ----------------- // // Helper functions. // // ----------------- // @@ -1510,6 +1537,10 @@ private static KeyValue makekv(final byte[] qualifier, final byte[] value) { return new KeyValue(KEY, FAMILY, qualifier, kvCount++, value); } + private static KeyValue makekvWithTs(final byte[] qualifier, long ts, final byte[] value) { + return new KeyValue(KEY, FAMILY, qualifier, ts, value); + } + private static byte[] anyBytes() { return any(byte[].class); } diff --git a/test/core/TestTSDBAddPoint.java b/test/core/TestTSDBAddPoint.java index 1974b42d4e..fd8b5cbfb2 100644 --- a/test/core/TestTSDBAddPoint.java +++ b/test/core/TestTSDBAddPoint.java @@ -27,6 +27,8 @@ import static org.mockito.Mockito.when; import java.util.HashMap; +import java.util.Map.Entry; +import java.util.TreeMap; import org.hbase.async.Bytes; import org.junit.Before; @@ -559,4 +561,30 @@ public void uidFilterThrowsException() throws Exception { verify(filter, times(1)).allowDataPoint(eq(METRIC_STRING), anyLong(), any(byte[].class), eq(tags), anyShort()); } -} + + @Test + public void addPointWithOTSDBTimeStamp() throws Exception { + long ts = 1356998400; + tsdb.getConfig().overrideConfig("tsd.storage.use_otsdb_timestamp", "true"); + tsdb.addPoint(METRIC_STRING, ts, 42, tags).joinUninterruptibly(); + TreeMap result = storage.getFullColumn(tsdb.dataTable(), row, tsdb.FAMILY(), new byte[] { 0, 0 }); + assert (result != null); + for (Entry e : result.entrySet()) { + long retrievedTs = e.getKey(); + assert ((ts * 1000) == retrievedTs); + } + } + + @Test + public void addPointWithoutOTSDBTimeStamp() throws Exception { + long ts = 1356998400; + tsdb.getConfig().overrideConfig("tsd.storage.use_otsdb_timestamp", "false"); + tsdb.addPoint(METRIC_STRING, ts, 42, tags).joinUninterruptibly(); + TreeMap result = storage.getFullColumn(tsdb.dataTable(), row, tsdb.FAMILY(), new byte[] { 0, 0 }); + assert(result != null); + for (Entry e : result.entrySet()) { + long retrievedTs = e.getKey(); + assert((ts * 1000) != retrievedTs); + } + } +} \ No newline at end of file diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index d69ec1376b..190c2a7b1c 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -43,7 +43,7 @@ /** * 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 - * queries and validating the group by and aggregation logic, see + * queries and validating the group by and aggregation logic, see * {@link TestTsdbQueryQueries} */ @RunWith(PowerMockRunner.class) @@ -55,80 +55,80 @@ public final class TestTsdbQuery extends BaseTsdbTest { public void beforeLocal() throws Exception { query = new TsdbQuery(tsdb); } - + @Test public void setStartTime() throws Exception { query.setStartTime(1356998400L); assertEquals(1356998400L, query.getStartTime()); } - + @Test public void setStartTimeZero() throws Exception { query.setStartTime(0L); } - + @Test (expected = IllegalArgumentException.class) public void setStartTimeInvalidNegative() throws Exception { query.setStartTime(-1L); } - + @Test (expected = IllegalArgumentException.class) public void setStartTimeInvalidTooBig() throws Exception { query.setStartTime(17592186044416L); } - + @Test (expected = IllegalArgumentException.class) public void setStartTimeEqualtoEndTime() throws Exception { query.setEndTime(1356998400L); query.setStartTime(1356998400L); } - + @Test (expected = IllegalArgumentException.class) public void setStartTimeGreaterThanEndTime() throws Exception { query.setEndTime(1356998400L); query.setStartTime(1356998460L); } - + @Test public void setEndTime() throws Exception { query.setEndTime(1356998400L); assertEquals(1356998400L, query.getEndTime()); } - + @Test (expected = IllegalStateException.class) public void getStartTimeNotSet() throws Exception { query.getStartTime(); } - + @Test (expected = IllegalArgumentException.class) public void setEndTimeInvalidNegative() throws Exception { query.setEndTime(-1L); } - + @Test (expected = IllegalArgumentException.class) public void setEndTimeInvalidTooBig() throws Exception { query.setEndTime(17592186044416L); } - + @Test (expected = IllegalArgumentException.class) public void setEndTimeEqualtoEndTime() throws Exception { query.setStartTime(1356998400L); query.setEndTime(1356998400L); } - + @Test (expected = IllegalArgumentException.class) public void setEndTimeGreaterThanEndTime() throws Exception { query.setStartTime(1356998460L); query.setEndTime(1356998400L); } - + @Test public void getEndTimeNotSet() throws Exception { PowerMockito.mockStatic(DateTime.class); PowerMockito.when(DateTime.currentTimeMillis()).thenReturn(1357300800000L); assertEquals(1357300800000L, query.getEndTime()); } - + @Test public void setTimeSeries() throws Exception { query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); @@ -139,34 +139,34 @@ public void setTimeSeries() throws Exception { assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); assertEquals(1, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES).length); - assertArrayEquals(TAGV_BYTES, + assertArrayEquals(TAGV_BYTES, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)[0]); } - + @Test (expected = NullPointerException.class) public void setTimeSeriesNullTags() throws Exception { query.setTimeSeries(METRIC_STRING, null, Aggregators.SUM, false); } - + @Test public void setTimeSeriesEmptyTags() throws Exception { tags.clear(); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); assertNotNull(query); } - + @Test (expected = NoSuchUniqueName.class) public void setTimeSeriesNosuchMetric() throws Exception { query.setTimeSeries(NSUN_METRIC, tags, Aggregators.SUM, false); } - + @Test (expected = NoSuchUniqueName.class) public void setTimeSeriesNosuchTagk() throws Exception { tags.clear(); tags.put(NSUN_TAGK, TAGV_STRING); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); } - + @Test (expected = NoSuchUniqueName.class) public void setTimeSeriesNosuchTagv() throws Exception { tags.put(TAGK_STRING, NSUN_TAGV); @@ -181,18 +181,18 @@ public void setTimeSeriesTS() throws Exception { query.setTimeSeries(tsuids, Aggregators.SUM, false); assertNotNull(query); } - + @Test (expected = IllegalArgumentException.class) public void setTimeSeriesTSNullList() throws Exception { query.setTimeSeries(null, Aggregators.SUM, false); } - + @Test (expected = IllegalArgumentException.class) public void setTimeSeriesTSEmptyList() throws Exception { final List tsuids = new ArrayList(); query.setTimeSeries(tsuids, Aggregators.SUM, false); } - + @Test (expected = IllegalArgumentException.class) public void setTimeSeriesTSDifferentMetrics() throws Exception { final List tsuids = new ArrayList(2); @@ -200,7 +200,7 @@ public void setTimeSeriesTSDifferentMetrics() throws Exception { tsuids.add("000002000001000002"); query.setTimeSeries(tsuids, Aggregators.SUM, false); } - + @Test public void configureFromQuery() throws Exception { setDataPointStorage(); @@ -208,14 +208,14 @@ public void configureFromQuery() throws Exception { ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + 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 public void configureFromQueryWithRate() throws Exception { setDataPointStorage(); @@ -226,29 +226,30 @@ public void configureFromQueryWithRate() throws Exception { ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + 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()); assertTrue(rate_options == ForTesting.getRateOptions(query)); } - + @Test public void configureFromQueryNoTags() throws Exception { + setDataPointStorage(); final TSQuery ts_query = getTSQuery(); ts_query.getQueries().get(0).setTags(Collections.EMPTY_MAP); ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(0, ForTesting.getFilters(query).size()); assertNull(ForTesting.getGroupBys(query)); assertNull(ForTesting.getRowKeyLiterals(query)); } - + @Test public void configureFromQueryGroupByAll() throws Exception { setDataPointStorage(); @@ -259,16 +260,16 @@ public void configureFromQueryGroupByAll() throws Exception { ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); - assertArrayEquals(TAGK_BYTES, + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); assertNull(ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)); } - + @Test public void configureFromQueryGroupByPipe() throws Exception { setDataPointStorage(); @@ -279,19 +280,19 @@ public void configureFromQueryGroupByPipe() throws Exception { ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); assertEquals(1, ForTesting.getRowKeyLiterals(query).size()); assertEquals(2, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES).length); - assertArrayEquals(TAGV_BYTES, + assertArrayEquals(TAGV_BYTES, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)[0]); - assertArrayEquals(TAGV_B_BYTES, + assertArrayEquals(TAGV_B_BYTES, ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)[1]); } - + @Test public void configureFromQueryWithGroupByFilter() throws Exception { setDataPointStorage(); @@ -302,7 +303,7 @@ public void configureFromQueryWithGroupByFilter() throws Exception { ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); @@ -322,7 +323,7 @@ public void configureFromQueryWithFilter() throws Exception { ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(1, ForTesting.getFilters(query).size()); assertNull(ForTesting.getGroupBys(query)); @@ -330,7 +331,7 @@ public void configureFromQueryWithFilter() throws Exception { assertNull(ForTesting.getRowKeyLiterals(query).get(TAGV_BYTES)); assertNotNull(ForTesting.getRateOptions(query)); } - + @Test public void configureFromQueryWithGroupByAndRegularFilters() throws Exception { setDataPointStorage(); @@ -352,32 +353,32 @@ public void configureFromQueryWithGroupByAndRegularFilters() throws Exception { assertNull(ForTesting.getRowKeyLiterals(query).get(TAGK_BYTES)); assertNotNull(ForTesting.getRateOptions(query)); } - + @Test (expected = IllegalArgumentException.class) public void configureFromQueryNullSubs() throws Exception { final TSQuery ts_query = new TSQuery(); new TsdbQuery(tsdb).configureFromQuery(ts_query, 0); } - + @Test (expected = IllegalArgumentException.class) public void configureFromQueryEmptySubs() throws Exception { final TSQuery ts_query = new TSQuery(); ts_query.setQueries(new ArrayList(0)); new TsdbQuery(tsdb).configureFromQuery(ts_query, 0); } - + @Test (expected = IllegalArgumentException.class) public void configureFromQueryNegativeIndex() throws Exception { final TSQuery ts_query = getTSQuery(); new TsdbQuery(tsdb).configureFromQuery(ts_query, -1); } - + @Test (expected = IllegalArgumentException.class) public void configureFromQueryIndexOutOfBounds() throws Exception { final TSQuery ts_query = getTSQuery(); new TsdbQuery(tsdb).configureFromQuery(ts_query, 2); } - + @Test (expected = NoSuchUniqueName.class) public void configureFromQueryNSUMetric() throws Exception { setDataPointStorage(); @@ -387,7 +388,7 @@ public void configureFromQueryNSUMetric() throws Exception { query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); } - + @Test (expected = DeferredGroupException.class) public void configureFromQueryNSUTagk() throws Exception { setDataPointStorage(); @@ -399,7 +400,7 @@ public void configureFromQueryNSUTagk() throws Exception { query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); } - + @Test (expected = DeferredGroupException.class) public void configureFromQueryNSUTagv() throws Exception { setDataPointStorage(); @@ -411,7 +412,7 @@ public void configureFromQueryNSUTagv() throws Exception { query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); } - + @Test (expected = DeferredGroupException.class) public void configureFromQueryGroupByPipeNSUTagk() throws Exception { setDataPointStorage(); @@ -423,7 +424,7 @@ public void configureFromQueryGroupByPipeNSUTagk() throws Exception { query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); } - + @Test (expected = DeferredGroupException.class) public void configureFromQueryGroupByPipeNSUTagv() throws Exception { setDataPointStorage(); @@ -435,9 +436,9 @@ public void configureFromQueryGroupByPipeNSUTagv() throws Exception { query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); } - + @Test - public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() + public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() throws Exception { config.overrideConfig("tsd.query.skip_unresolved_tagvs", "true"); setDataPointStorage(); @@ -448,22 +449,22 @@ public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() ts_query.validateAndSetQuery(); query = new TsdbQuery(tsdb); query.configureFromQuery(ts_query, 0).joinUninterruptibly(); - + assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(1, ForTesting.getFilters(query).size()); assertEquals(1, ForTesting.getGroupBys(query).size()); - assertArrayEquals(TAGK_BYTES, + assertArrayEquals(TAGK_BYTES, ForTesting.getGroupBys(query).get(0)); } - + @Test public void deleteDatapoints() throws Exception { setDataPointStorage(); - + tsdb.addPoint(METRIC_STRING, 1356998400, 42, tags).joinUninterruptibly(); query.setStartTime(1356998400); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + query.setDelete(true); final DataPoints[] dps1 = query.run(); assertEquals(1, dps1.length); @@ -471,14 +472,14 @@ public void deleteDatapoints() throws Exception { final DataPoints[] dps2 = query.run(); assertEquals(0, dps2.length); } - + @Test public void scannerException() throws Exception { storeLongTimeSeriesSeconds(true, false); final RuntimeException ex = new RuntimeException("Boo!"); storage.throwException(MockBase.stringToBytes( "00000150E22700000001000001"), ex, true); - + storage.dumpToSystemOut(); query.setStartTime(1356998400); query.setEndTime(1357041600); @@ -490,21 +491,21 @@ public void scannerException() throws Exception { assertSame(ex, e); } } - + /** @return a simple TSQuery object for testing */ private TSQuery getTSQuery() { final TSQuery ts_query = new TSQuery(); ts_query.setStart("1356998400"); - + 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); - + ts_query.setQueries(sub_queries); return ts_query; } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index aeffbc946c..17da164615 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -33,11 +33,6 @@ import net.opentsdb.rollup.RollupConfig; import net.opentsdb.rollup.RollupInterval; -import net.opentsdb.storage.MockBase; -import net.opentsdb.storage.MockBase.MockScanner; -import net.opentsdb.uid.NoSuchUniqueId; -import net.opentsdb.utils.Config; - import org.hbase.async.Bytes; import org.hbase.async.FilterList; import org.hbase.async.Scanner; @@ -50,9 +45,14 @@ import com.stumbleupon.async.Deferred; +import net.opentsdb.storage.MockBase; +import net.opentsdb.storage.MockBase.MockScanner; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.utils.Config; + /** * An integration test class that makes sure our query path is up to snuff. - * This class should have tests for different data point types, rates, + * This class should have tests for different data point types, rates, * compactions, etc. Other files can cover salting, aggregation and downsampling. */ @RunWith(PowerMockRunner.class) @@ -64,7 +64,7 @@ public class TestTsdbQueryQueries extends BaseTsdbTest { public void beforeLocal() throws Exception { query = new TsdbQuery(tsdb); } - + @Test public void runLongSingleTS() throws Exception { storeLongTimeSeriesSeconds(true, false); @@ -75,7 +75,7 @@ public void runLongSingleTS() throws Exception { final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998430000L; verify(tag_values, times(1)).getNameAsync(TAGV_BYTES); @@ -99,7 +99,7 @@ public void runLongSingleTSMs() throws Exception { final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998400500L; for (DataPoint dp : dps[0]) { @@ -110,7 +110,7 @@ public void runLongSingleTSMs() throws Exception { } assertEquals(300, dps[0].aggregatedSize()); } - + @Test public void runLongSingleTSNoData() throws Exception { setDataPointStorage(); @@ -118,24 +118,24 @@ public void runLongSingleTSNoData() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertNotNull(dps); assertEquals(0, dps.length); } - + @Test public void runLongTwoAggSum() throws Exception { storeLongTimeSeriesSeconds(true, false); - + tags.clear(); query.setStartTime(1356998400L); query.setEndTime(1357041600L); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { assertEquals(301, dp.longValue()); @@ -144,19 +144,19 @@ public void runLongTwoAggSum() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runLongTwoAggSumMs() throws Exception { storeLongTimeSeriesMs(); - + tags.clear(); query.setStartTime(1356998400L); query.setEndTime(1357041600L); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long timestamp = 1356998400500L; for (DataPoint dp : dps[0]) { assertEquals(301, dp.longValue()); @@ -165,22 +165,22 @@ public void runLongTwoAggSumMs() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runLongTwoGroup() throws Exception { storeLongTimeSeriesSeconds(true, false); - + tags.clear(); tags.put(TAGK_STRING , "*"); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); assertMeta(dps, 1, false); assertEquals(2, dps.length); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -190,7 +190,7 @@ public void runLongTwoGroup() throws Exception { timestamp += 30000; } assertEquals(300, dps[0].size()); - + value = 300; timestamp = 1356998430000L; for (DataPoint dp : dps[1]) { @@ -201,7 +201,7 @@ public void runLongTwoGroup() throws Exception { } assertEquals(300, dps[1].size()); } - + @Test public void runLongSingleTSRate() throws Exception { storeLongTimeSeriesSeconds(true, false); @@ -209,10 +209,10 @@ public void runLongSingleTSRate() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998460000L; for (DataPoint dp : dps[0]) { assertEquals(0.033F, dp.doubleValue(), 0.001); @@ -221,7 +221,7 @@ public void runLongSingleTSRate() throws Exception { } assertEquals(299, dps[0].size()); } - + @Test public void runLongSingleTSRateMs() throws Exception { storeLongTimeSeriesMs(); @@ -229,10 +229,10 @@ public void runLongSingleTSRateMs() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998401000L; for (DataPoint dp : dps[0]) { assertEquals(2.0F, dp.doubleValue(), 0.001); @@ -249,10 +249,10 @@ public void runFloatSingleTS() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + double value = 1.25D; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -263,7 +263,7 @@ public void runFloatSingleTS() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runFloatSingleTSMs() throws Exception { storeFloatTimeSeriesMs(); @@ -271,10 +271,10 @@ public void runFloatSingleTSMs() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + double value = 1.25D; long timestamp = 1356998400500L; for (DataPoint dp : dps[0]) { @@ -285,19 +285,19 @@ public void runFloatSingleTSMs() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runFloatTwoAggSum() throws Exception { storeFloatTimeSeriesSeconds(true, false); - + tags.clear(); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { assertEquals(76.25, dp.doubleValue(), 0.00001); @@ -306,16 +306,16 @@ public void runFloatTwoAggSum() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runFloatTwoAggNoneAgg() throws Exception { storeFloatTimeSeriesSeconds(true, false); - + tags.clear(); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.NONE, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); assertMeta(dps, 1, false); @@ -330,7 +330,7 @@ public void runFloatTwoAggNoneAgg() throws Exception { timestamp += 30000; } assertEquals(300, dps[0].size()); - + value = 75D; timestamp = 1356998430000L; for (DataPoint dp : dps[1]) { @@ -341,7 +341,7 @@ public void runFloatTwoAggNoneAgg() throws Exception { } assertEquals(300, dps[1].size()); } - + @Test public void runFloatTwoAggSumMs() throws Exception { storeFloatTimeSeriesMs(); @@ -350,10 +350,10 @@ public void runFloatTwoAggSumMs() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long timestamp = 1356998400500L; for (DataPoint dp : dps[0]) { assertEquals(76.25, dp.doubleValue(), 0.00001); @@ -362,7 +362,7 @@ public void runFloatTwoAggSumMs() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runFloatTwoGroup() throws Exception { storeFloatTimeSeriesSeconds(true, false); @@ -371,7 +371,7 @@ public void runFloatTwoGroup() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); assertMeta(dps, 1, false); @@ -386,7 +386,7 @@ public void runFloatTwoGroup() throws Exception { timestamp += 30000; } assertEquals(300, dps[0].size()); - + value = 75D; timestamp = 1356998430000L; for (DataPoint dp : dps[1]) { @@ -397,7 +397,7 @@ public void runFloatTwoGroup() throws Exception { } assertEquals(300, dps[1].size()); } - + @Test public void runFloatSingleTSRate() throws Exception { storeFloatTimeSeriesSeconds(true, false); @@ -405,10 +405,10 @@ public void runFloatSingleTSRate() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998460000L; for (DataPoint dp : dps[0]) { assertEquals(0.00833F, dp.doubleValue(), 0.00001); @@ -417,7 +417,7 @@ public void runFloatSingleTSRate() throws Exception { } assertEquals(299, dps[0].size()); } - + @Test public void runFloatSingleTSRateMs() throws Exception { storeFloatTimeSeriesMs(); @@ -425,10 +425,10 @@ public void runFloatSingleTSRateMs() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998401000L; for (DataPoint dp : dps[0]) { assertEquals(0.5F, dp.doubleValue(), 0.00001); @@ -446,10 +446,10 @@ public void runFloatSingleTSCompacted() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998430000L; double value = 1.25D; for (DataPoint dp : dps[0]) { @@ -460,7 +460,7 @@ public void runFloatSingleTSCompacted() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runMixedSingleTS() throws Exception { storeMixedTimeSeriesSeconds(); @@ -468,10 +468,10 @@ public void runMixedSingleTS() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998430000L; double float_value = 1.25D; int int_value = 76; @@ -492,7 +492,7 @@ public void runMixedSingleTS() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runMixedSingleTSMsAndS() throws Exception { storeMixedTimeSeriesMsAndS(); @@ -500,10 +500,10 @@ public void runMixedSingleTSMsAndS() throws Exception { query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.AVG, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998400500L; double float_value = 1.25D; int int_value = 76; @@ -524,11 +524,11 @@ public void runMixedSingleTSMsAndS() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runMixedSingleTSPostCompaction() throws Exception { storeMixedTimeSeriesSeconds(); - + final Field compact = Config.class.getDeclaredField("enable_compactions"); compact.setAccessible(true); compact.set(config, true); @@ -539,24 +539,24 @@ public void runMixedSingleTSPostCompaction() throws Exception { // this should only compact the rows for the time series that we fetched and // leave the others alone - - final byte[] key = + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key); - System.arraycopy(Bytes.fromInt(1356998400), 0, key, + System.arraycopy(Bytes.fromInt(1356998400), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key)); - System.arraycopy(Bytes.fromInt(1357002000), 0, key, + System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key)); - System.arraycopy(Bytes.fromInt(1357005600), 0, key, + System.arraycopy(Bytes.fromInt(1357005600), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key)); // run it again to verify the compacted data uncompacts properly final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + long timestamp = 1356998430000L; double float_value = 1.25D; int int_value = 76; @@ -577,7 +577,7 @@ public void runMixedSingleTSPostCompaction() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runEndTime() throws Exception { storeLongTimeSeriesSeconds(true, false); @@ -587,7 +587,7 @@ public void runEndTime() throws Exception { query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -598,11 +598,11 @@ public void runEndTime() throws Exception { } assertEquals(119, dps[0].size()); } - + @Test public void runCompactPostQuery() throws Exception { storeLongTimeSeriesSeconds(true, false); - + final Field compact = Config.class.getDeclaredField("enable_compactions"); compact.setAccessible(true); compact.set(config, true); @@ -612,47 +612,47 @@ public void runCompactPostQuery() throws Exception { query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + // this should only compact the rows for the time series that we fetched and // leave the others alone - final byte[] key_a = + final byte[] key_a = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key_a); final Map tags_copy = new HashMap(tags); tags_copy.put(TAGK_STRING, TAGV_B_STRING); - final byte[] key_b = + final byte[] key_b = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); RowKey.prefixKeyWithSalt(key_b); - - System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key_a)); - - System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (config.enable_appends()) { assertEquals(1, storage.numColumns(key_b)); } else { assertEquals(119, storage.numColumns(key_b)); } - - System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key_a)); - - System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (config.enable_appends()) { assertEquals(1, storage.numColumns(key_b)); } else { assertEquals(120, storage.numColumns(key_b)); } - - System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key_a)); - - System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (config.enable_appends()) { assertEquals(1, storage.numColumns(key_b)); @@ -663,7 +663,7 @@ public void runCompactPostQuery() throws Exception { // run it again to verify the compacted data uncompacts properly dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -674,7 +674,7 @@ public void runCompactPostQuery() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test (expected = IllegalStateException.class) public void runStartNotSet() throws Exception { query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); @@ -683,21 +683,25 @@ public void runStartNotSet() throws Exception { @Test public void runFloatAndIntSameTSNoFix() throws Exception { + tsdb.config.overrideConfig("tsd.storage.use_otsdb_timestamp", "true"); // if a row has an integer and a float for the same timestamp, there will be - // two different qualifiers that will resolve to the same offset. This no - // will throw the IllegalDataException as querytime fixes are disabled by - // default + // two different qualifiers that will resolve to the same offset. With DTCS enabled + // the conflicts are auto resolved i.e. either the maximum or the minimum value of + // data is returned depending on the configuration tsd.storage.use_max_value + // If DTCS is disabled, it will fall throw the exception. + // DTCS -> Date Tiered Compaction Strategy (tsd.storage.use_otsdb_timestamp config parameter) + storeLongTimeSeriesSeconds(true, false); tsdb.addPoint(METRIC_STRING, 1356998430, 42.5F, tags).joinUninterruptibly(); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - - if (config.enable_appends()) { + + if (config.enable_appends() || config.use_otsdb_timestamp()) { DataPoints[] dps = query.run(); assertMeta(dps, 0, false, false); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -712,29 +716,33 @@ public void runFloatAndIntSameTSNoFix() throws Exception { timestamp += 30000; } assertEquals(300, dps[0].size()); - } else { - try { + } + + tsdb.config.overrideConfig("tsd.storage.use_otsdb_timestamp", "false"); + try { query.run(); fail("Expected an IllegalDataException"); } catch (IllegalDataException ide) { } - } + } - + @Test public void runFloatAndIntSameTSFix() throws Exception { config.setFixDuplicates(true); // if a row has an integer and a float for the same timestamp, there will be - // two different qualifiers that will resolve to the same offset. This no - // longer tosses an exception, and keeps the last value + // two different qualifiers that owill resolve to the same offset. This no + // longer tosses an exception, and keeps the maximum of all values + // This can also be configured via tsdb.storage.use_max_value config storeLongTimeSeriesSeconds(true, false); tsdb.addPoint(METRIC_STRING, 1356998430, 42.5F, tags).joinUninterruptibly(); + query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -749,7 +757,61 @@ public void runFloatAndIntSameTSFix() throws Exception { } assertEquals(300, dps[0].aggregatedSize()); } - + + @Test + public void multipleValuesAtSameTimestampShouldReturnMaxValueDefault() throws Exception { + tsdb.config.overrideConfig("tsd.storage.use_otsdb_timestamp", "true"); + config.setFixDuplicates(true); + // if a row has an integer and a float for the same timestamp, there will be + // two different qualifiers that will resolve to the same offset. This no + // longer tosses an exception, and keeps the maximum of all values + // This can also be configured via tsdb.storage.use_max_value config + setDataPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998430, 69755263, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 62500.52F, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 2533, tags).joinUninterruptibly(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(69755263, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + } + assertEquals(1, dps[0].aggregatedSize()); + } + + @Test + public void multipleValuesAtSameTimestampShouldReturnMinValueIfConfigured() throws Exception { + tsdb.config.overrideConfig("tsd.storage.use_otsdb_timestamp", "true"); + // This test explicitly configures the use_max_value as false thus when different data type values + // are written at same timestamp, the minimum of all will be provided in output + tsdb.getConfig().overrideConfig("tsd.storage.use_max_value", "false"); + config.setFixDuplicates(true); + setDataPointStorage(); + + tsdb.addPoint(METRIC_STRING, 1356998430, 69755263, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 62500.52F, tags).joinUninterruptibly(); + tsdb.addPoint(METRIC_STRING, 1356998430, 2533, tags).joinUninterruptibly(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); + final DataPoints[] dps = query.run(); + assertMeta(dps, 0, false); + + long timestamp = 1356998430000L; + for (DataPoint dp : dps[0]) { + assertEquals(2533, dp.longValue()); + assertEquals(timestamp, dp.timestamp()); + } + assertEquals(1, dps[0].aggregatedSize()); + } + + @Test public void runWithAnnotation() throws Exception { storeLongTimeSeriesSeconds(true, false); @@ -761,7 +823,7 @@ public void runWithAnnotation() throws Exception { final DataPoints[] dps = query.run(); assertMeta(dps, 0, false, true); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -772,7 +834,7 @@ public void runWithAnnotation() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runWithAnnotationPostCompact() throws Exception { storeLongTimeSeriesSeconds(true, false); @@ -790,32 +852,32 @@ public void runWithAnnotationPostCompact() throws Exception { // this should only compact the rows for the time series that we fetched and // leave the others alone - final byte[] key_a = + final byte[] key_a = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key_a); final Map tags_copy = new HashMap(tags); tags_copy.put(TAGK_STRING, TAGV_B_STRING); - final byte[] key_b = + final byte[] key_b = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags_copy); RowKey.prefixKeyWithSalt(key_b); - System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, + System.arraycopy(Bytes.fromInt(1356998400), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(2, storage.numColumns(key_a)); - - System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, + + System.arraycopy(Bytes.fromInt(1356998400), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (config.enable_appends()) { assertEquals(1, storage.numColumns(key_b)); } else { assertEquals(119, storage.numColumns(key_b)); } - - System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key_a)); - - System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, + + System.arraycopy(Bytes.fromInt(1357002000), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (config.enable_appends()) { assertEquals(1, storage.numColumns(key_b)); @@ -823,21 +885,21 @@ public void runWithAnnotationPostCompact() throws Exception { assertEquals(120, storage.numColumns(key_b)); } - System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, + System.arraycopy(Bytes.fromInt(1357005600), 0, key_a, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); assertEquals(1, storage.numColumns(key_a)); - - System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, + + System.arraycopy(Bytes.fromInt(1357005600), 0, key_b, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); if (config.enable_appends()) { assertEquals(1, storage.numColumns(key_b)); } else { assertEquals(61, storage.numColumns(key_b)); } - + dps = query.run(); assertMeta(dps, 0, false, true); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -855,15 +917,15 @@ public void runWithOnlyAnnotation() throws Exception { // verifies that we can pickup an annotation stored all by it's lonesome // in a row without any data - final byte[] key = + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key); - System.arraycopy(Bytes.fromInt(1357002000), 0, key, + System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); storage.flushRow(key); - + storeAnnotation(1357002090); - + query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); @@ -896,13 +958,13 @@ public void runWithSingleAnnotation() throws Exception { // verifies that we can pickup an annotation stored all by it's lonesome // in a row without any data - final byte[] key = + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key); - System.arraycopy(Bytes.fromInt(1357002000), 0, key, + System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); storage.flushRow(key); - + storeAnnotation(1357002090); query.setStartTime(1356998400); @@ -910,7 +972,7 @@ public void runWithSingleAnnotation() throws Exception { query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - // TODO - apparently if you only fetch annotations, the metric and tags + // TODO - apparently if you only fetch annotations, the metric and tags // may not be set. Check this //assertMeta(dps, 0, false, true); assertEquals(1, dps[0].getAnnotations().size()); @@ -925,14 +987,14 @@ public void runSingleDataPoint() throws Exception { setDataPointStorage(); long timestamp = 1356998410; tsdb.addPoint(METRIC_STRING, timestamp, 42, tags).joinUninterruptibly(); - + query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); storage.dumpToSystemOut(); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + assertEquals(1, dps[0].size()); assertEquals(42, dps[0].longValue(0)); assertEquals(1356998410000L, dps[0].timestamp(0)); @@ -943,23 +1005,23 @@ public void runSingleDataPointWithAnnotation() throws Exception { setDataPointStorage(); long timestamp = 1356998410; tsdb.addPoint(METRIC_STRING, timestamp, 42, tags).joinUninterruptibly(); - - final byte[] key = + + final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key); - System.arraycopy(Bytes.fromInt(1357002000), 0, key, + System.arraycopy(Bytes.fromInt(1357002000), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); storage.flushRow(key); - + storeAnnotation(1357002090); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false, true); - + assertEquals(1, dps[0].size()); assertEquals(42, dps[0].longValue(0)); assertEquals(1356998410000L, dps[0].timestamp(0)); @@ -968,16 +1030,16 @@ public void runSingleDataPointWithAnnotation() throws Exception { @Test public void runTSUIDQuery() throws Exception { storeLongTimeSeriesSeconds(true, false); - + query.setStartTime(1356998400); query.setEndTime(1357041600); final List tsuids = new ArrayList(1); tsuids.add("000001000001000001"); query.setTimeSeries(tsuids, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { @@ -988,21 +1050,21 @@ public void runTSUIDQuery() throws Exception { } assertEquals(300, dps[0].aggregatedSize()); } - + @Test public void runTSUIDsAggSum() throws Exception { storeLongTimeSeriesSeconds(true, false); - + query.setStartTime(1356998400); query.setEndTime(1357041600); final List tsuids = new ArrayList(1); tsuids.add("000001000001000001"); tsuids.add("000001000001000002"); query.setTimeSeries(tsuids, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long timestamp = 1356998430000L; for (DataPoint dp : dps[0]) { assertEquals(301, dp.longValue()); @@ -1011,57 +1073,57 @@ public void runTSUIDsAggSum() throws Exception { } assertEquals(300, dps[0].size()); } - + @Test public void runTSUIDQueryNoData() throws Exception { setDataPointStorage(); - + query.setStartTime(1356998400); query.setEndTime(1357041600); - + final List tsuids = new ArrayList(1); tsuids.add("000001000001000001"); query.setTimeSeries(tsuids, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertNotNull(dps); assertEquals(0, dps.length); } - + @Test public void runTSUIDQueryNoDataForTSUID() throws Exception { // this doesn't throw an exception since the UIDs are only looked for when // the query completes. setDataPointStorage(); - + query.setStartTime(1356998400); query.setEndTime(1357041600); final List tsuids = new ArrayList(1); tsuids.add("000001000001000005"); query.setTimeSeries(tsuids, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertNotNull(dps); assertEquals(0, dps.length); } - + @Test (expected = NoSuchUniqueId.class) public void runTSUIDQueryNSU() throws Exception { when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) .thenThrow(new NoSuchUniqueId("metrics", new byte[] { 0, 0, 1 })); storeLongTimeSeriesSeconds(true, false); - + query.setStartTime(1356998400); query.setEndTime(1357041600); final List tsuids = new ArrayList(1); tsuids.add("000001000001000001"); query.setTimeSeries(tsuids, Aggregators.SUM, false); - + final DataPoints[] dps = query.run(); assertNotNull(dps); dps[0].metricName(); } - + @Test public void runRateCounterDefault() throws Exception { setDataPointStorage(); @@ -1071,14 +1133,14 @@ public void runRateCounterDefault() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 30, Long.MAX_VALUE - 25, tags) .joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 5, tags).joinUninterruptibly(); - + final RateOptions ro = new RateOptions(true, Long.MAX_VALUE, 0); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + timestamp = 1356998460000L; for (DataPoint dp : dps[0]) { assertEquals(1.0, dp.doubleValue(), 0.001); @@ -1087,7 +1149,7 @@ public void runRateCounterDefault() throws Exception { } assertEquals(2, dps[0].size()); } - + @Test public void runRateCounterDefaultNoOp() throws Exception { setDataPointStorage(); @@ -1095,14 +1157,14 @@ public void runRateCounterDefaultNoOp() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 30, 30, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 60, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 90, tags).joinUninterruptibly(); - + final RateOptions ro = new RateOptions(true, Long.MAX_VALUE, 0); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, true, ro); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + timestamp = 1356998460000L; for (DataPoint dp : dps[0]) { assertEquals(1.0, dp.doubleValue(), 0.001); @@ -1111,7 +1173,7 @@ public void runRateCounterDefaultNoOp() throws Exception { } assertEquals(2, dps[0].size()); } - + @Test public void runRateCounterMaxSet() throws Exception { setDataPointStorage(); @@ -1119,7 +1181,7 @@ public void runRateCounterMaxSet() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 30, 45, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 75, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 5, tags).joinUninterruptibly(); - + final RateOptions ro = new RateOptions(true, 100, 0); query.setStartTime(1356998400); query.setEndTime(1357041600); @@ -1135,7 +1197,7 @@ public void runRateCounterMaxSet() throws Exception { } assertEquals(2, dps[0].size()); } - + @Test public void runRateCounterAnomally() throws Exception { setDataPointStorage(); @@ -1143,7 +1205,7 @@ public void runRateCounterAnomally() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 30, 45, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 75, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 25, tags).joinUninterruptibly(); - + final RateOptions ro = new RateOptions(true, 10000, 35); query.setStartTime(1356998400); query.setEndTime(1357041600); @@ -1166,7 +1228,7 @@ public void runRateCounterAnomallyDrop() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 30, 75, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 25, tags).joinUninterruptibly(); tsdb.addPoint(METRIC_STRING, timestamp += 30, 55, tags).joinUninterruptibly(); - + final RateOptions ro = new RateOptions(true, 10000, 35, true); query.setStartTime(1356998400); query.setEndTime(1357041600); @@ -1180,7 +1242,7 @@ public void runRateCounterAnomallyDrop() throws Exception { assertEquals(1356998520000L, dps[0].timestamp(1)); assertEquals(2, dps[0].size()); } - + @Test public void runMultiCompact() throws Exception { final byte[] qual1 = { 0x00, 0x17 }; @@ -1202,20 +1264,20 @@ public void runMultiCompact() throws Exception { final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key); - System.arraycopy(Bytes.fromInt(1356998400), 0, key, + System.arraycopy(Bytes.fromInt(1356998400), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - + setDataPointStorage(); - storage.addColumn(key, - MockBase.concatByteArrays(qual1, qual2), + storage.addColumn(key, + MockBase.concatByteArrays(qual1, qual2), MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); - storage.addColumn(key, - MockBase.concatByteArrays(qual3, qual4), + storage.addColumn(key, + MockBase.concatByteArrays(qual3, qual4), MockBase.concatByteArrays(val3, val4, new byte[] { 0 })); - storage.addColumn(key, - MockBase.concatByteArrays(qual5, qual6), + storage.addColumn(key, + MockBase.concatByteArrays(qual5, qual6), MockBase.concatByteArrays(val5, val6, new byte[] { 0 })); - + HashMap tags = new HashMap(1); tags.put(TAGK_STRING , TAGV_STRING ); query.setStartTime(1356998400); @@ -1224,7 +1286,7 @@ public void runMultiCompact() throws Exception { final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998401000L; for (DataPoint dp : dps[0]) { @@ -1257,26 +1319,26 @@ public void runMultiCompactAndSingles() throws Exception { final byte[] key = IncomingDataPoints.rowKeyTemplate(tsdb, METRIC_STRING, tags); RowKey.prefixKeyWithSalt(key); - System.arraycopy(Bytes.fromInt(1356998400), 0, key, + System.arraycopy(Bytes.fromInt(1356998400), 0, key, Const.SALT_WIDTH() + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - + setDataPointStorage(); - storage.addColumn(key, - MockBase.concatByteArrays(qual1, qual2), + storage.addColumn(key, + MockBase.concatByteArrays(qual1, qual2), MockBase.concatByteArrays(val1, val2, new byte[] { 0 })); storage.addColumn(key, qual3, val3); storage.addColumn(key, qual4, val4); - storage.addColumn(key, - MockBase.concatByteArrays(qual5, qual6), + storage.addColumn(key, + MockBase.concatByteArrays(qual5, qual6), MockBase.concatByteArrays(val5, val6, new byte[] { 0 })); - + query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); assertMeta(dps, 0, false); - + int value = 1; long timestamp = 1356998401000L; for (DataPoint dp : dps[0]) { @@ -1287,7 +1349,7 @@ public void runMultiCompactAndSingles() throws Exception { } assertEquals(6, dps[0].aggregatedSize()); } - + @Test public void runInterpolationSeconds() throws Exception { setDataPointStorage(); @@ -1303,21 +1365,21 @@ public void runInterpolationSeconds() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 30, i, tags) .joinUninterruptibly(); } - + tags.clear(); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long v = 1; long ts = 1356998430000L; for (DataPoint dp : dps[0]) { assertEquals(ts, dp.timestamp()); ts += 15000; assertEquals(v, dp.longValue()); - + if (dp.timestamp() == 1357007400000L) { v = 1; } else if (v == 1 || v == 302) { @@ -1328,7 +1390,7 @@ public void runInterpolationSeconds() throws Exception { } assertEquals(600, dps[0].size()); } - + @Test public void runInterpolationMs() throws Exception { setDataPointStorage(); @@ -1344,21 +1406,21 @@ public void runInterpolationMs() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags) .joinUninterruptibly(); } - + tags.clear(); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); - + long v = 1; long ts = 1356998400500L; for (DataPoint dp : dps[0]) { assertEquals(ts, dp.timestamp()); ts += 250; assertEquals(v, dp.longValue()); - + if (dp.timestamp() == 1356998550000L) { v = 1; } else if (v == 1 || v == 302) { @@ -1369,7 +1431,7 @@ public void runInterpolationMs() throws Exception { } assertEquals(600, dps[0].size()); } - + @Test public void runInterpolationMsDownsampled() throws Exception { setDataPointStorage(); @@ -1392,7 +1454,7 @@ public void runInterpolationMsDownsampled() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp, i, tags) .joinUninterruptibly(); } - + // ts = 1356998400750, v = 300 // ts = 1356998401250, v = 299 // ts = 1356998401750, v = 298 @@ -1409,13 +1471,13 @@ public void runInterpolationMsDownsampled() throws Exception { tsdb.addPoint(METRIC_STRING, timestamp += 500, i, tags) .joinUninterruptibly(); } - + tags.clear(); query.setStartTime(1356998400); query.setEndTime(1357041600); query.setTimeSeries(METRIC_STRING, tags, Aggregators.SUM, false); query.downsample(1000, Aggregators.SUM); - + final DataPoints[] dps = query.run(); assertMeta(dps, 0, true); @@ -1476,7 +1538,7 @@ public void runRegexp() throws Exception { } assertEquals(300, dps[0].aggregatedSize()); } - + @Test public void runRegexpNoMatch() throws Exception { storeLongTimeSeriesSeconds(true, false); @@ -1552,13 +1614,13 @@ public void filterExplicitTagsOK() throws Exception { query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - + assertNotNull(dps); assertEquals("sys.cpu.user", dps[0].metricName()); assertTrue(dps[0].getAggregatedTags().isEmpty()); assertNull(dps[0].getAnnotations()); assertEquals("web01", dps[0].getTags().get("host")); - + int value = 1; for (DataPoint dp : dps[0]) { assertEquals(value, dp.longValue()); @@ -1570,7 +1632,7 @@ public void filterExplicitTagsOK() throws Exception { assertTrue(scanner.getFilter() instanceof FilterList); } } - + @Test public void filterExplicitTagsGroupByOK() throws Exception { tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); @@ -1583,13 +1645,13 @@ public void filterExplicitTagsGroupByOK() throws Exception { query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - + assertNotNull(dps); assertEquals("sys.cpu.user", dps[0].metricName()); assertTrue(dps[0].getAggregatedTags().isEmpty()); assertNull(dps[0].getAnnotations()); assertEquals("web01", dps[0].getTags().get("host")); - + int value = 1; for (DataPoint dp : dps[0]) { assertEquals(value, dp.longValue()); @@ -1601,7 +1663,7 @@ public void filterExplicitTagsGroupByOK() throws Exception { assertTrue(scanner.getFilter() instanceof FilterList); } } - + @Test public void filterExplicitTagsMissing() throws Exception { tsdb.getConfig().overrideConfig("tsd.query.enable_fuzzy", "true"); @@ -1619,7 +1681,7 @@ public void filterExplicitTagsMissing() throws Exception { query.setTimeSeries("sys.cpu.user", tags, Aggregators.SUM, false); final DataPoints[] dps = query.run(); - + assertNotNull(dps); assertEquals(0, dps.length); // assert fuzzy @@ -1627,5 +1689,5 @@ public void filterExplicitTagsMissing() throws Exception { assertTrue(scanner.getFilter() instanceof FilterList); } } - + } diff --git a/test/core/TestTsdbTSConfig.java b/test/core/TestTsdbTSConfig.java new file mode 100644 index 0000000000..1010b83512 --- /dev/null +++ b/test/core/TestTsdbTSConfig.java @@ -0,0 +1,189 @@ +// 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 static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.hbase.async.HBaseClient; +import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; +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.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +/** + * Sets up a real TSDB with mocked client, compaction queue and timer along + * with mocked UID assignment, fetches for common unit tests. + */ +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, + HashedWheelTimer.class, Scanner.class, Const.class }) +public class TestTsdbTSConfig { + /** A list of UIDs from A to Z for unit testing UIDs values */ + public static final Map METRIC_UIDS = + new HashMap(26); + public static final Map TAGK_UIDS = + new HashMap(26); + public static final Map TAGV_UIDS = + new HashMap(26); + static { + char letter = 'A'; + int uid = 10; + for (int i = 0; i < 26; i++) { + METRIC_UIDS.put(Character.toString(letter), + UniqueId.longToUID(uid, TSDB.metrics_width())); + TAGK_UIDS.put(Character.toString(letter), + UniqueId.longToUID(uid, TSDB.tagk_width())); + TAGV_UIDS.put(Character.toString(letter++), + UniqueId.longToUID(uid++, TSDB.tagv_width())); + } + } + + public static final String METRIC_STRING = "sys.cpu.user"; + public static final byte[] METRIC_BYTES = new byte[] { 0, 0, 1 }; + + public static final String TAGK_STRING = "host"; + public static final byte[] TAGK_BYTES = new byte[] { 0, 0, 1 }; + + public static final String TAGV_STRING = "web01"; + public static final byte[] TAGV_BYTES = new byte[] { 0, 0, 1 }; + + protected Config config; + protected TSDB tsdb; + protected HBaseClient client = mock(HBaseClient.class); + protected UniqueId metrics = mock(UniqueId.class); + protected UniqueId tag_names = mock(UniqueId.class); + protected UniqueId tag_values = mock(UniqueId.class); + protected Map tags = new HashMap(1); + protected MockBase storage; + + @Before + public void before() throws Exception { + + config = new Config(false); + config.overrideConfig("tsd.storage.enable_compaction", "false"); + tsdb = PowerMockito.spy(new TSDB(config)); + + config.setAutoMetric(true); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + setupMetricMaps(); + setupTagkMaps(); + setupTagvMaps(); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + tags.put(TAGK_STRING, TAGV_STRING); + } + + /** Adds the static UIDs to the metrics UID mock object */ + void setupMetricMaps() { + when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); + when(metrics.getIdAsync(METRIC_STRING)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(METRIC_BYTES); + } + }); + when(metrics.getOrCreateId(METRIC_STRING)) + .thenReturn(METRIC_BYTES); + } + + /** Adds the static UIDs to the tag keys UID mock object */ + void setupTagkMaps() { + when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); + when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); + when(tag_names.getIdAsync(TAGK_STRING)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGK_BYTES); + } + }); + when(tag_names.getOrCreateIdAsync(TAGK_STRING)) + .thenReturn(Deferred.fromResult(TAGK_BYTES)); + + } + + /** Adds the static UIDs to the tag values UID mock object */ + void setupTagvMaps() { + when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); + when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); + when(tag_values.getIdAsync(TAGV_STRING)) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGV_BYTES); + } + }); + when(tag_values.getOrCreateIdAsync(TAGV_STRING)) + .thenReturn(Deferred.fromResult(TAGV_BYTES)); + + } + + @Test + public void scannerTimestampsEqualToQueryTimestamps() { + + TSQuery q = new TSQuery(); + q.setStart("1h-ago"); + + TSSubQuery subQuery = new TSSubQuery(); + subQuery.setMetric(METRIC_STRING); + subQuery.setAggregator("none"); + + ArrayList list = new ArrayList(); + list.add(subQuery); + q.setQueries(list); + q.validateAndSetQuery(); + + TsdbQuery query = new TsdbQuery(tsdb); + query.configureFromQuery(q, 0); + + Scanner scanner = query.getScanner(); + long minTs = scanner.getMinTimestamp(); + long maxTs = scanner.getMaxTimestamp(); + // For 1h - ago, the TSDB will create a window of 2 hrs, For example if the current time is 2:45 PM, the window will be 1 PM to 3 PM + assert((maxTs - minTs) == (2 * 3600000)); + } + +} \ No newline at end of file diff --git a/test/meta/TestAnnotation.java b/test/meta/TestAnnotation.java index 3da5487744..6d2cbe720f 100644 --- a/test/meta/TestAnnotation.java +++ b/test/meta/TestAnnotation.java @@ -665,12 +665,12 @@ private void setupStorage(final boolean salted) { new byte[] { 1, 0, 0 }, ("{\"startTime\":1328140800,\"endTime\":1328140801,\"description\":" + "\"Description\",\"notes\":\"Notes\",\"custom\":{\"owner\":" + - "\"ops\"}}").getBytes(MockBase.ASCII())); + "\"ops\"}}").getBytes(MockBase.ASCII()), 1328140799972L); storage.addColumn(global_row_key, new byte[] { 1, 0, 1 }, ("{\"startTime\":1328140801,\"endTime\":1328140803,\"description\":" + - "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII())); + "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII()), 1328140799973L); // add a local storage.addColumn(tsuid_row_key, @@ -678,20 +678,20 @@ private void setupStorage(final boolean salted) { ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450562," + "\"endTime\":1419984000,\"description\":\"Hello!\",\"notes\":" + "\"My Notes\",\"custom\":{\"owner\":\"ops\"}}") - .getBytes(MockBase.ASCII())); + .getBytes(MockBase.ASCII()), 1388448000003L); storage.addColumn(tsuid_row_key, new byte[] { 1, 0x0A, 0x03 }, ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450563," + "\"endTime\":1419984000,\"description\":\"Note2\",\"notes\":" + "\"Nothing\"}") - .getBytes(MockBase.ASCII())); + .getBytes(MockBase.ASCII()), 1388448000004L); // add some data points too storage.addColumn(tsuid_row_key, - new byte[] { 0x50, 0x10 }, new byte[] { 1 }); + new byte[] { 0x50, 0x10 }, new byte[] { 1 }, 1388448000005L); storage.addColumn(tsuid_row_key, - new byte[] { 0x50, 0x18 }, new byte[] { 2 }); + new byte[] { 0x50, 0x18 }, new byte[] { 2 }, 1388448000006L); } } diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 903070f047..6aeb38ddd6 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -66,23 +66,23 @@ /** * Mock HBase implementation useful in testing calls to and from storage with * actual pretend data. The underlying data store is an incredibly ugly nesting - * of ByteMaps from AsyncHbase so it stores and orders byte arrays similar to - * HBase. It supports tables and column families along with timestamps but + * of ByteMaps from AsyncHbase so it stores and orders byte arrays similar to + * HBase. It supports tables and column families along with timestamps but * doesn't deal with TTLs or other features. *

    - * By default we configure the "'tsdb', {NAME => 't'}" and + * By default we configure the "'tsdb', {NAME => 't'}" and * "'tsdb-uid', {NAME => 'id'}, {NAME => 'name'}" tables. If you need more, just * add em. - * + * *

    * It's not a perfect mock but is useful for the majority of unit tests. Gets, * puts, cas, deletes and scans are currently supported. See notes for each * inner class below about what does and doesn't work. *

    - * Regarding timestamps, whenever you execute an RPC request, the + * Regarding timestamps, whenever you execute an RPC request, the * {@code current_timestamp} will be incremented by one millisecond. By default * the timestamp starts at 1/1/2014 00:00:00 but you can set it to any value - * at any time. If a PutRequest comes in with a specific time, that time will + * at any time. If a PutRequest comes in with a specific time, that time will * be stored and the timestamp will not be incremented. *

    * Warning: To use this class, you need to prepare the classes for testing @@ -102,27 +102,27 @@ public final class MockBase { private static final Charset ASCII = Charset.forName("ISO-8859-1"); private TSDB tsdb; - + /** Gross huh? >>>> * Why is CF before row? Because we want to throw exceptions if a CF hasn't * been "configured" */ - private ByteMap>>>> + private ByteMap>>>> storage = new ByteMap>>>>(); private HashSet scanners = new HashSet(2); - + /** The default family for shortcuts */ private byte[] default_family; - + /** The default table for shortcuts */ private byte[] default_table; - + /** Incremented every time a new value is stored (without a timestamp) */ private long current_timestamp = 1388534400000L; - + /** A list of exceptions that can be thrown when working with a row key */ private ByteMap> exceptions; - + /** * Setups up mock intercepts for all of the calls. Depending on the given * flags, some mocks may not be enabled, allowing local unit tests to setup @@ -139,7 +139,7 @@ public final class MockBase { @SuppressWarnings("unchecked") public MockBase( final TSDB tsdb, final HBaseClient client, - final boolean default_get, + final boolean default_get, final boolean default_put, final boolean default_delete, final boolean default_scan) { @@ -148,7 +148,7 @@ public MockBase( default_family = "t".getBytes(ASCII); default_table = "tsdb".getBytes(ASCII); setupDefaultTables(); - + // replace the "real" field objects with mocks Whitebox.setInternalState(tsdb, "client", client); @@ -156,7 +156,7 @@ public MockBase( if (default_get) { when(client.get((GetRequest)any())).thenAnswer(new MockGet()); } - + when(client.get(any(List.class))).thenAnswer(new MockMultiGet(client)); // Default put answer will store the given values in the proper location. @@ -169,7 +169,7 @@ public MockBase( if (default_delete) { when(client.delete((DeleteRequest)any())).thenAnswer(new MockDelete()); } - + if (default_scan) { // to facilitate unit tests where more than one scanner is used (i.e. in a // callback chain) we have to provide a new mock scanner for each new @@ -184,11 +184,11 @@ public Scanner answer(InvocationOnMock arg0) throws Throwable { scanners.add(new MockScanner(scanner, table)); return scanner; } - - }); + + }); } - + when(client.atomicIncrement((AtomicIncrementRequest)any())) .then(new MockAtomicIncrement()); when(client.bufferAtomicIncrement((AtomicIncrementRequest)any())) @@ -214,7 +214,7 @@ public void addTable(final byte[] table, final List families) { } } } - + /** * Pops the table out of the map * @param table The table to pop @@ -223,45 +223,62 @@ public void addTable(final byte[] table, final List families) { public boolean deleteTable(final byte[] table) { return storage.remove(table) != null; } - + /** @param family Sets the default family for calls that need it */ public void setFamily(final byte[] family) { default_family = family; } - + /** @param table Sets the default table for calls that need it */ public void setDefaultTable(final byte[] table) { default_table = table; } - + /** @param timestamp The timestamp to use for further storage increments */ public void setCurrentTimestamp(final long timestamp) { this.current_timestamp = timestamp; } - + /** @return the incrementing timestamp */ public long getCurrentTimestamp() { return current_timestamp; } - + + + /** + * Add a column to the hash table using the default column family. + * The proper row will be created if it doesn't exist. If the column already + * exists, the original value will be overwritten with the new data. + * Uses the default table and family + * @param key The row key + * @param qualifier The qualifier + * @param value The value to store + * @param timestamp The timestamp of cell + */ + public void addColumn(final byte[] key, final byte[] qualifier, + final byte[] value, final long timestamp) { + addColumn(default_table, key, default_family, qualifier, value, + timestamp); + } + /** - * Add a column to the hash table using the default column family. - * The proper row will be created if it doesn't exist. If the column already + * Add a column to the hash table using the default column family. + * The proper row will be created if it doesn't exist. If the column already * exists, the original value will be overwritten with the new data. * Uses the default table and family * @param key The row key * @param qualifier The qualifier * @param value The value to store */ - public void addColumn(final byte[] key, final byte[] qualifier, + public void addColumn(final byte[] key, final byte[] qualifier, final byte[] value) { - addColumn(default_table, key, default_family, qualifier, value, + addColumn(default_table, key, default_family, qualifier, value, current_timestamp++); } - + /** - * Add a column to the hash table - * The proper row will be created if it doesn't exist. If the column already + * Add a column to the hash table + * The proper row will be created if it doesn't exist. If the column already * exists, the original value will be overwritten with the new data. * Uses the default table. * @param key The row key @@ -269,14 +286,14 @@ public void addColumn(final byte[] key, final byte[] qualifier, * @param qualifier The qualifier * @param value The value to store */ - public void addColumn(final byte[] key, final byte[] family, + public void addColumn(final byte[] key, final byte[] family, final byte[] qualifier, final byte[] value) { addColumn(default_table, key, family, qualifier, value, current_timestamp++); } - + /** - * Add a column to the hash table - * The proper row will be created if it doesn't exist. If the column already + * Add a column to the hash table + * The proper row will be created if it doesn't exist. If the column already * exists, the original value will be overwritten with the new data * @param table The table * @param key The row key @@ -284,14 +301,14 @@ public void addColumn(final byte[] key, final byte[] family, * @param qualifier The qualifier * @param value The value to store */ - public void addColumn(final byte[] table, final byte[] key, final byte[] family, + public void addColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier, final byte[] value) { addColumn(table, key, family, qualifier, value, current_timestamp++); } - + /** - * Add a column to the hash table - * The proper row will be created if it doesn't exist. If the column already + * Add a column to the hash table + * The proper row will be created if it doesn't exist. If the column already * exists, the original value will be overwritten with the new data * @param table The table * @param key The row key @@ -300,7 +317,7 @@ public void addColumn(final byte[] table, final byte[] key, final byte[] family, * @param value The value to store * @param timestamp The timestamp to store */ - public void addColumn(final byte[] table, final byte[] key, final byte[] family, + public void addColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier, final byte[] value, final long timestamp) { // AsyncHBase will throw an NPE if the user tries to write a NULL value // so we better do the same. An empty value is ok though, i.e. new byte[] {} @@ -317,7 +334,7 @@ public void addColumn(final byte[] table, final byte[] key, final byte[] family, throw new RuntimeException( "No such CF " + Bytes.pretty(family)); } - + ByteMap> row = cf.get(key); if (row == null) { row = new ByteMap>(); @@ -332,7 +349,7 @@ public void addColumn(final byte[] table, final byte[] key, final byte[] family, } column.put(timestamp, value); } - + /** * Stores an exception so that any operation on the given key will cause it * to be thrown. @@ -342,7 +359,7 @@ public void addColumn(final byte[] table, final byte[] key, final byte[] family, public void throwException(final byte[] key, final RuntimeException exception) { throwException(key, exception, true); } - + /** * Stores an exception so that any operation on the given key will cause it * to be thrown. @@ -351,37 +368,37 @@ public void throwException(final byte[] key, final RuntimeException exception) { * @param as_result Whether or not to return the exception in the deferred * result or throw it outright. */ - public void throwException(final byte[] key, final RuntimeException exception, + public void throwException(final byte[] key, final RuntimeException exception, final boolean as_result) { if (exceptions == null) { exceptions = new ByteMap>(); } exceptions.put(key, new Pair(exception, as_result)); } - + /** Removes all exceptions from the exception list */ public void clearExceptions() { exceptions.clear(); } - - /** @return Total number of unique rows in the default table. Returns 0 if the + + /** @return Total number of unique rows in the default table. Returns 0 if the * default table does not exist */ public int numRows() { return numRows(default_table); } - + /** * Total number of rows in the given table. Returns 0 if the table does not exit. * @param table The table to scan * @return The number of rows */ public int numRows(final byte[] table) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return 0; } - final ByteMap unique_rows = new ByteMap(); + final ByteMap unique_rows = new ByteMap(); for (final ByteMap>> cf : map.values()) { for (final byte[] key : cf.keySet()) { unique_rows.put(key, null); @@ -389,26 +406,26 @@ public int numRows(final byte[] table) { } return unique_rows.size(); } - + /** * Return the total number of column families for the row in the default table * @param key The row to search for - * @return -1 if the table or row did not exist, otherwise the number of + * @return -1 if the table or row did not exist, otherwise the number of * column families. */ public int numColumnFamilies(final byte[] key) { return numColumnFamilies(default_table, key); } - + /** * Return the number of column families for the given row key in the given table. * @param table The table to iterate over - * @param key The row to search for - * @return -1 if the table or row did not exist, otherwise the number of + * @param key The row to search for + * @return -1 if the table or row did not exist, otherwise the number of * column families. */ public int numColumnFamilies(final byte[] table, final byte[] key) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return -1; @@ -421,7 +438,7 @@ public int numColumnFamilies(final byte[] table, final byte[] key) { } return sum == 0 ? -1 : sum; } - + /** * Total number of columns in the given row across all column families in the * default table @@ -431,7 +448,7 @@ public int numColumnFamilies(final byte[] table, final byte[] key) { public long numColumns(final byte[] key) { return numColumns(default_table, key); } - + /** * Total number of columns in the given row across all column families in the * default table @@ -440,7 +457,7 @@ public long numColumns(final byte[] key) { * @return -1 if the row did not exist, otherwise the number of columns. */ public long numColumns(final byte[] table, final byte[] key) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return -1; @@ -454,7 +471,7 @@ public long numColumns(final byte[] table, final byte[] key) { } return size == 0 ? -1 : size; } - + /** * Return the total number of columns for a specific row and family in the * default table @@ -465,16 +482,16 @@ public long numColumns(final byte[] table, final byte[] key) { public int numColumnsInFamily(final byte[] key, final byte[] family) { return numColumnsInFamily(default_table, key, family); } - + /** * Return the total number of columns for a specific row and family * @param key The row to search for * @param family The column family to search for * @return -1 if the row did not exist, otherwise the number of columns. */ - public int numColumnsInFamily(final byte[] table, final byte[] key, + public int numColumnsInFamily(final byte[] table, final byte[] key, final byte[] family) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return -1; @@ -496,7 +513,7 @@ public int numColumnsInFamily(final byte[] table, final byte[] key, public byte[] getColumn(final byte[] key, final byte[] qualifier) { return getColumn(default_table, key, default_family, qualifier); } - + /** * Retrieve the most recent contents of a single column with the default table * @param key The row key of the column @@ -504,11 +521,11 @@ public byte[] getColumn(final byte[] key, final byte[] qualifier) { * @param qualifier The column qualifier * @return The byte array of data or null if not found */ - public byte[] getColumn(final byte[] key, final byte[] family, + public byte[] getColumn(final byte[] key, final byte[] family, final byte[] qualifier) { return getColumn(default_table, key, family, qualifier); } - + /** * Retrieve the most recent contents of a single column * @param table The table to fetch from @@ -517,9 +534,9 @@ public byte[] getColumn(final byte[] key, final byte[] family, * @param qualifier The column qualifier * @return The byte array of data or null if not found */ - public byte[] getColumn(final byte[] table, final byte[] key, + public byte[] getColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return null; @@ -540,17 +557,17 @@ public byte[] getColumn(final byte[] table, final byte[] key, } /** - * Retrieve the full map of timestamps and values of a single column with + * Retrieve the full map of timestamps and values of a single column with * the default family and default table * @param key The row key of the column * @param qualifier The column qualifier * @return The byte array of data or null if not found */ - public TreeMap getFullColumn(final byte[] key, + public TreeMap getFullColumn(final byte[] key, final byte[] qualifier) { return getFullColumn(default_table, key, default_family, qualifier); } - + /** * Retrieve the full map of timestamps and values of a single column * @param table The table to fetch from @@ -559,9 +576,9 @@ public TreeMap getFullColumn(final byte[] key, * @param qualifier The column qualifier * @return The tree map of timestamps and values or null if not found */ - public TreeMap getFullColumn(final byte[] table, final byte[] key, + public TreeMap getFullColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return null; @@ -576,7 +593,7 @@ public TreeMap getFullColumn(final byte[] table, final byte[] key, } return row.get(qualifier); } - + /** * Returns the most recent value from all columns for a given column family * in the default table @@ -584,11 +601,11 @@ public TreeMap getFullColumn(final byte[] table, final byte[] key, * @param family The column family ID * @return A map of columns if the CF was found, null if no such CF */ - public ByteMap getColumnFamily(final byte[] key, + public ByteMap getColumnFamily(final byte[] key, final byte[] family) { return getColumnFamily(default_table, key , family); } - + /** * Returns the most recent value from all columns for a given column family * @param table The table to fetch from @@ -596,9 +613,9 @@ public ByteMap getColumnFamily(final byte[] key, * @param family The column family ID * @return A map of columns if the CF was found, null if no such CF */ - public ByteMap getColumnFamily(final byte[] table, final byte[] key, + public ByteMap getColumnFamily(final byte[] table, final byte[] key, final byte[] family) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return null; @@ -619,24 +636,24 @@ public ByteMap getColumnFamily(final byte[] table, final byte[] key, } return columns; } - + /** @return the list of keys stored in the default table for all CFs */ public Set getKeys() { return getKeys(default_table); } - + /** * Return the list of unique keys in the given table for all CFs * @param table The table to pull from * @return A list of keys. May be null if the table doesn't exist */ public Set getKeys(final byte[] table) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return null; } - final ByteMap unique_rows = new ByteMap(); + final ByteMap unique_rows = new ByteMap(); for (final ByteMap>> cf : map.values()) { for (final byte[] key : cf.keySet()) { unique_rows.put(key, null); @@ -644,12 +661,12 @@ public Set getKeys(final byte[] table) { } return unique_rows.keySet(); } - + /** @return The set of scanners configured by the caller */ public HashSet getScanners() { return scanners; } - + /** * Return the mocked TSDB object to use for HBaseClient access * @return @@ -657,17 +674,17 @@ public HashSet getScanners() { public TSDB getTSDB() { return tsdb; } - + /** - * Runs through all rows in the "tsdb" table and compacts them by making a - * call to the {@link TSDB.compact} method. It will delete any columns - * that were compacted and leave others untouched, just as the normal + * Runs through all rows in the "tsdb" table and compacts them by making a + * call to the {@link TSDB.compact} method. It will delete any columns + * that were compacted and leave others untouched, just as the normal * method does. * And only iterates over the 't' family. * @throws Exception if Whitebox couldn't access the compact method */ public void tsdbCompactAllRows() throws Exception { - final ByteMap>>> map = + final ByteMap>>> map = storage.get("tsdb".getBytes(ASCII)); if (map == null) { return; @@ -676,16 +693,16 @@ public void tsdbCompactAllRows() throws Exception { if (cf == null) { return; } - + for (Entry>> entry : cf.entrySet()) { final byte[] key = entry.getKey(); - + final ByteMap> row = entry.getValue(); ArrayList kvs = new ArrayList(row.size()); final Set deletes = new HashSet(); for (Map.Entry> column : row.entrySet()) { if (column.getKey().length % 2 == 0) { - kvs.add(new KeyValue(key, default_family, column.getKey(), + kvs.add(new KeyValue(key, default_family, column.getKey(), column.getValue().firstKey(), column.getValue().firstEntry().getValue())); deletes.add(column.getKey()); @@ -695,7 +712,7 @@ public void tsdbCompactAllRows() throws Exception { for (final byte[] k : deletes) { row.remove(k); } - final KeyValue compacted = + final KeyValue compacted = Whitebox.invokeMethod(tsdb, "compact", kvs, Collections.EMPTY_LIST); final TreeMap compacted_value = new TreeMap(); compacted_value.put(current_timestamp++, compacted.value()); @@ -703,12 +720,12 @@ public void tsdbCompactAllRows() throws Exception { } } } - + /** * Clears out all rows from storage but doesn't delete the tables or families. */ public void flushStorage() { - for (final ByteMap>>> table : + for (final ByteMap>>> table : storage.values()) { for (final ByteMap>> cf : table.values()) { cf.clear(); @@ -729,7 +746,7 @@ public void flushStorage(final byte[] table) { cf.clear(); } } - + /** * Removes the entire row from the default table for all column families * @param key The row to remove @@ -737,7 +754,7 @@ public void flushStorage(final byte[] table) { public void flushRow(final byte[] key) { flushRow(default_table, key); } - + /** * Removes the entire row from the table for all column families * @param table The table to purge @@ -752,7 +769,7 @@ public void flushRow(final byte[] table, final byte[] key) { cf.remove(key); } } - + /** * Removes all rows from the default table for the given column family * @param family The family to remove @@ -760,7 +777,7 @@ public void flushRow(final byte[] table, final byte[] key) { public void flushFamily(final byte[] family) { flushFamily(default_table, family); } - + /** * Removes all rows from the default table for the given column family * @param table The table to purge from @@ -776,18 +793,18 @@ public void flushFamily(final byte[] table, final byte[] family) { cf.clear(); } } - + /** - * Removes the given column from the default table + * Removes the given column from the default table * @param key Row key * @param family Column family * @param qualifier Column qualifier */ - public void flushColumn(final byte[] key, final byte[] family, + public void flushColumn(final byte[] key, final byte[] family, final byte[] qualifier) { flushColumn(default_table, key, family, qualifier); } - + /** * Removes the given column from the table * @param table The table to purge from @@ -795,7 +812,7 @@ public void flushColumn(final byte[] key, final byte[] family, * @param family Column family * @param qualifier Column qualifier */ - public void flushColumn(final byte[] table, final byte[] key, + public void flushColumn(final byte[] table, final byte[] key, final byte[] family, final byte[] qualifier) { final ByteMap>>> map = storage.get(table); if (map == null) { @@ -811,7 +828,7 @@ public void flushColumn(final byte[] table, final byte[] key, } row.remove(qualifier); } - + /** * Dumps the entire storage hash to stdout in a sort of tree style format with * all byte arrays hex encoded @@ -819,7 +836,7 @@ public void flushColumn(final byte[] table, final byte[] key, public void dumpToSystemOut() { dumpToSystemOut(false); } - + /** * Dumps the entire storage hash to stdout in a sort of tree style format * @param ascii Whether or not the values should be converted to ascii @@ -829,27 +846,27 @@ public void dumpToSystemOut(final boolean ascii) { System.out.println("Storage is Empty"); return; } - - for (Entry>>>> table : + + for (Entry>>>> table : storage.entrySet()) { System.out.println("[Table] " + new String(table.getKey(), ASCII)); - - for (Entry>>> cf : + + for (Entry>>> cf : table.getValue().entrySet()) { System.out.println(" [CF] " + new String(cf.getKey(), ASCII)); - for (Entry>> row : + for (Entry>> row : cf.getValue().entrySet()) { - System.out.println(" [Row] " + (ascii ? + System.out.println(" [Row] " + (ascii ? new String(row.getKey(), ASCII) : bytesToString(row.getKey()))); - + for (Map.Entry> column : row.getValue().entrySet()) { System.out.println(" [Qual] " + (ascii ? "\"" + new String(column.getKey(), ASCII) + "\"" : bytesToString(column.getKey()))); for (Map.Entry cell : column.getValue().entrySet()) { - System.out.println(" [TS] " + cell.getKey() + " [Value] " + - (ascii ? new String(cell.getValue(), ASCII) + System.out.println(" [TS] " + cell.getKey() + " [Value] " + + (ascii ? new String(cell.getValue(), ASCII) : bytesToString(cell.getValue()))); } } @@ -857,7 +874,7 @@ public void dumpToSystemOut(final boolean ascii) { } } } - + /** * Helper to convert an array of bytes to a hexadecimal encoded string. * @param bytes The byte array to convert @@ -866,7 +883,7 @@ public void dumpToSystemOut(final boolean ascii) { public static String bytesToString(final byte[] bytes) { return DatatypeConverter.printHexBinary(bytes); } - + /** * Helper to convert a hex encoded string into a byte array. * Warning: This method won't pad the string to make sure it's an @@ -879,12 +896,12 @@ public static String bytesToString(final byte[] bytes) { public static byte[] stringToBytes(final String bytes) { return DatatypeConverter.parseHexBinary(bytes); } - + /** @return Returns the ASCII character set */ public static Charset ASCII() { return ASCII; } - + /** * Concatenates byte arrays into one big array * @param arrays Any number of arrays to concatenate @@ -903,26 +920,26 @@ public static byte[] concatByteArrays(final byte[]... arrays) { } return result; } - + /** Creates the TSDB and UID tables */ private void setupDefaultTables() { final ByteMap>>> tsdb = new ByteMap>>>(); tsdb.put("t".getBytes(ASCII), new ByteMap>>()); storage.put("tsdb".getBytes(ASCII), tsdb); - + final ByteMap>>> tsdb_uid = new ByteMap>>>(); - tsdb_uid.put("name".getBytes(ASCII), + tsdb_uid.put("name".getBytes(ASCII), new ByteMap>>()); - tsdb_uid.put("id".getBytes(ASCII), + tsdb_uid.put("id".getBytes(ASCII), new ByteMap>>()); storage.put("tsdb-uid".getBytes(ASCII), tsdb_uid); } - + /** * Gets one or more columns from a row. If the row does not exist, a null is - * returned. If no qualifiers are given, the entire row is returned. + * returned. If no qualifiers are given, the entire row is returned. * NOTE: all timestamp, value pairs are returned. */ private class MockGet implements Answer>> { @@ -931,7 +948,7 @@ public Deferred> answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final GetRequest get = (GetRequest)args[0]; - + if (exceptions != null) { final Pair ex = exceptions.get(get.key()); if (ex != null) { @@ -942,40 +959,40 @@ public Deferred> answer(InvocationOnMock invocation) } } } - - final ByteMap>>> map = + + final ByteMap>>> map = storage.get(get.table()); if (map == null) { return Deferred.fromError(new RuntimeException( "No such table " + Bytes.pretty(get.table()))); } - + // compile a set of qualifiers to use as a filter if necessary final ByteMap qualifiers = new ByteMap(); - if (get.qualifiers() != null && get.qualifiers().length > 0) { + if (get.qualifiers() != null && get.qualifiers().length > 0) { for (byte[] q : get.qualifiers()) { qualifiers.put(q, null); } } - + final ArrayList kvs = new ArrayList(); - for (final Entry>>> cf : + for (final Entry>>> cf : map.entrySet()) { if (get.family() != null && Bytes.memcmp(get.family(), cf.getKey()) != 0) { continue; } - + final ByteMap> row = cf.getValue().get(get.key()); if (row == null) { continue; } - + for (Entry> column : row.entrySet()) { if (!qualifiers.isEmpty() && !qualifiers.containsKey(column.getKey())) { continue; } - - // TODO - if we want to support multiple values, iterate over the + + // TODO - if we want to support multiple values, iterate over the // tree map. Otherwise Get returns just the latest value. kvs.add(new KeyValue(get.key(), cf.getKey(), column.getKey(), column.getValue().firstKey(), @@ -988,7 +1005,7 @@ public Deferred> answer(InvocationOnMock invocation) return Deferred.fromResult(kvs); } } - + /** * Handles a multi-get call by routing individual requests to the MockGet */ @@ -1025,11 +1042,11 @@ public Deferred> answer( */ private class MockPut implements Answer> { @Override - public Deferred answer(final InvocationOnMock invocation) + public Deferred answer(final InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final PutRequest put = (PutRequest)args[0]; - + if (exceptions != null) { final Pair ex = exceptions.get(put.key()); if (ex != null) { @@ -1040,20 +1057,20 @@ public Deferred answer(final InvocationOnMock invocation) } } } - - final ByteMap>>> map = + + final ByteMap>>> map = storage.get(put.table()); if (map == null) { return Deferred.fromError(new RuntimeException( "No such table " + Bytes.pretty(put.table()))); } - + final ByteMap>> cf = map.get(put.family()); if (cf == null) { return Deferred.fromError(new RuntimeException( - "No such CF " + Bytes.pretty(put.table()))); + "No such CF " + Bytes.pretty(put.table()))); } - + ByteMap> row = cf.get(put.key()); if (row == null) { row = new ByteMap>(); @@ -1065,23 +1082,33 @@ public Deferred answer(final InvocationOnMock invocation) if (column == null) { column = new TreeMap(Collections.reverseOrder()); row.put(put.qualifiers()[i], column); + } else { + long storedTs = column.firstKey(); + if(put.timestamp() >= storedTs) { + column.clear(); + } else { + continue; + } } - - column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : + + column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : current_timestamp++, put.values()[i]); + assert column.size() == 1 : "Since max versions allowed is 1, there can " + + "never be two entries at similar timestamp. To resolve change the " + + "code to only keep the entry with higher timestamp"; } - + return Deferred.fromResult(true); } } - + /** * Stores one or more columns in a row. If the row does not exist, it's * created. */ private class MockAppend implements Answer> { @Override - public Deferred answer(final InvocationOnMock invocation) + public Deferred answer(final InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final AppendRequest append = (AppendRequest)args[0]; @@ -1096,48 +1123,48 @@ public Deferred answer(final InvocationOnMock invocation) } } } - - final ByteMap>>> map = + + final ByteMap>>> map = storage.get(append.table()); if (map == null) { return Deferred.fromError(new RuntimeException( "No such table " + Bytes.pretty(append.table()))); } - + final ByteMap>> cf = map.get(append.family()); if (cf == null) { return Deferred.fromError(new RuntimeException( - "No such CF " + Bytes.pretty(append.table()))); + "No such CF " + Bytes.pretty(append.table()))); } - + ByteMap> row = cf.get(append.key()); if (row == null) { row = new ByteMap>(); cf.put(append.key(), row); } - + for (int i = 0; i < append.qualifiers().length; i++) { TreeMap column = row.get(append.qualifiers()[i]); if (column == null) { column = new TreeMap(Collections.reverseOrder()); row.put(append.qualifiers()[i], column); } - + final byte[] values; long column_timestamp = 0; - if (append.timestamp() != Long.MAX_VALUE) { - values = column.get(append.timestamp()); - column_timestamp = append.timestamp(); - } else { - if (column.isEmpty()) { - values = null; - } else { - values = column.firstEntry().getValue(); - column_timestamp = column.firstKey(); - } - } - if (column_timestamp == 0) { + /* + * If there is no Map for Timestamp -> Value, then we create one, add the value with current timestamp + * else we fetch the top most KeyValue from the column map and append the value in the request with the value + * already present. The timestamp is also updated. The larger of either current time or the first timestamp from + * the map is incremented by 1 and used for the updated KeyValue. + */ + + if (column.isEmpty()) { + values = null; column_timestamp = current_timestamp++; + } else { + values = column.firstEntry().getValue(); + column_timestamp = current_timestamp > column.firstKey() ? current_timestamp++ : column.firstKey() + 1; } final int current_len = values != null ? values.length : 0; @@ -1145,34 +1172,38 @@ public Deferred answer(final InvocationOnMock invocation) if (current_len > 0) { System.arraycopy(values, 0, append_value, 0, values.length); } - - System.arraycopy(append.value(), 0, append_value, current_len, + + System.arraycopy(append.value(), 0, append_value, current_len, append.values()[i].length); + // Remove all the old KeyValues, if any, since we need to have only 1 version of data + // Column_timestamp will hold the new timestamp and append_value holds the corresponding new data + column.clear(); column.put(column_timestamp, append_value); + assert column.size() == 1 : "MockBase is designed to store only a single version of cell since OpenTSDB table schema for HBase is designed in that manner"; } - + return Deferred.fromResult(true); } } - + /** * Imitates the compareAndSet client call where a {@code PutRequest} is passed * along with a byte array to compared the stored value against. If the stored - * value doesn't match, the put is ignored and a "false" is returned. If the + * value doesn't match, the put is ignored and a "false" is returned. If the * comparator matches, the new put is recorded. * Warning: While a put works on multiple qualifiers, CAS only works * with one. So if the put includes more than one qualifier, only the first - * one will be processed in this CAS call. + * one will be processed in this CAS call. */ private class MockCAS implements Answer> { - + @Override - public Deferred answer(final InvocationOnMock invocation) + public Deferred answer(final InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final PutRequest put = (PutRequest)args[0]; final byte[] expected = (byte[])args[1]; - + if (exceptions != null) { final Pair ex = exceptions.get(put.key()); if (ex != null) { @@ -1183,20 +1214,20 @@ public Deferred answer(final InvocationOnMock invocation) } } } - - final ByteMap>>> map = + + final ByteMap>>> map = storage.get(put.table()); if (map == null) { return Deferred.fromError(new RuntimeException( "No such table " + Bytes.pretty(put.table()))); } - + final ByteMap>> cf = map.get(put.family()); if (cf == null) { return Deferred.fromError(new RuntimeException( - "No such CF " + Bytes.pretty(put.table()))); + "No such CF " + Bytes.pretty(put.table()))); } - + ByteMap> row = cf.get(put.key()); if (row == null) { if (expected != null && expected.length > 0) { @@ -1205,53 +1236,65 @@ public Deferred answer(final InvocationOnMock invocation) row = new ByteMap>(); cf.put(put.key(), row); } - - // CAS can only operate on one cell, so if the put request has more than + + // CAS can only operate on one cell, so if the put request has more than // one, we ignore any but the first TreeMap column = row.get(put.qualifiers()[0]); if (column == null && (expected != null && expected.length > 0)) { return Deferred.fromResult(false); } - // if a timestamp was specified, maybe we're CASing against a specific - // cell. Otherwise we deal with the latest value - final byte[] stored = column == null ? null : - put.timestamp() != Long.MAX_VALUE ? column.get(put.timestamp()) : - column.firstEntry().getValue(); + + // HBase CAS doesn't use Timestamps for comparison + // Since OpenTSDB uses an HBase Table with single version, the final TreeMap 'column' should always + // contain a single entry, the one with higher timestamp + + final byte[] stored = column == null ? null : column.firstEntry().getValue(); + if (stored == null && (expected != null && expected.length > 0)) { return Deferred.fromResult(false); } if (stored != null && (expected == null || expected.length < 1)) { return Deferred.fromResult(false); } - if (stored != null && expected != null && + if (stored != null && expected != null && Bytes.memcmp(stored, expected) != 0) { return Deferred.fromResult(false); } - + // passed CAS! if (column == null) { column = new TreeMap(Collections.reverseOrder()); row.put(put.qualifiers()[0], column); + } else { + long storedTs = column.firstKey(); + if(put.timestamp() >= storedTs) { + column.clear(); + } else { + return Deferred.fromResult(true); + } } - column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : + + column.put(put.timestamp() != Long.MAX_VALUE ? put.timestamp() : current_timestamp++, put.value()); + + assert column.size() == 1 : "MockBase is designed to store only a single version of cell since OpenTSDB table schema for HBase is designed in that manner"; return Deferred.fromResult(true); } - + } - + /** * Deletes one or more columns. If a row no longer has any valid columns, the * entire row will be removed. */ private class MockDelete implements Answer> { - + @Override public Deferred answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final DeleteRequest delete = (DeleteRequest)args[0]; - + if (exceptions != null) { final Pair ex = exceptions.get(delete.key()); if (ex != null) { @@ -1262,44 +1305,44 @@ public Deferred answer(InvocationOnMock invocation) } } } - - final ByteMap>>> map = + + final ByteMap>>> map = storage.get(delete.table()); if (map == null) { return Deferred.fromError(new RuntimeException( "No such table " + Bytes.pretty(delete.table()))); } - + // if no qualifiers or family, then delete the row from all families - if ((delete.qualifiers() == null || delete.qualifiers().length < 1 || - delete.qualifiers()[0].length < 1) && (delete.family() == null || + if ((delete.qualifiers() == null || delete.qualifiers().length < 1 || + delete.qualifiers()[0].length < 1) && (delete.family() == null || delete.family().length < 1)) { - for (final Entry>>> cf : + for (final Entry>>> cf : map.entrySet()) { cf.getValue().remove(delete.key()); } return Deferred.fromResult(new Object()); } - + final byte[] family = delete.family(); if (family != null && family.length > 0) { if (!map.containsKey(family)) { return Deferred.fromError(new RuntimeException( - "No such CF " + Bytes.pretty(family))); + "No such CF " + Bytes.pretty(family))); } } - + // compile a set of qualifiers ByteMap qualifiers = new ByteMap(); - if (delete.qualifiers() != null || delete.qualifiers().length > 0) { + if (delete.qualifiers() != null || delete.qualifiers().length > 0) { for (byte[] q : delete.qualifiers()) { qualifiers.put(q, null); } } - + // TODO - validate the assumption that a delete with a row key and qual // but without a family would delete the columns in ALL families - + // if the request only has a column family and no qualifiers, we delete // the row from the entire family if (family != null && qualifiers.isEmpty()) { @@ -1308,27 +1351,27 @@ public Deferred answer(InvocationOnMock invocation) cf.remove(delete.key()); return Deferred.fromResult(new Object()); } - - for (final Entry>>> cf : + + for (final Entry>>> cf : map.entrySet()) { - + // column family filter - if (family != null && family.length > 0 && + if (family != null && family.length > 0 && !Bytes.equals(family, cf.getKey())) { continue; } - + ByteMap> row = cf.getValue().get(delete.key()); if (row == null) { continue; } - + for (byte[] qualifier : qualifiers.keySet()) { final TreeMap column = row.get(qualifier); if (column == null) { continue; } - + // with this flag we delete a single timestamp if (delete.deleteAtTimestampOnly()) { if (column != null) { @@ -1338,7 +1381,7 @@ public Deferred answer(InvocationOnMock invocation) } } } else { - // otherwise we delete everything less than or equal to the + // otherwise we delete everything less than or equal to the // delete timestamp List column_removals = new ArrayList(column.size()); for (Map.Entry cell : column.entrySet()) { @@ -1354,16 +1397,16 @@ public Deferred answer(InvocationOnMock invocation) } } } - + if (row.isEmpty()) { cf.getValue().remove(delete.key()); } } return Deferred.fromResult(new Object()); } - + } - + /** * This is a limited implementation of the scanner object. The only fields * caputred and acted on are: @@ -1377,10 +1420,10 @@ public Deferred answer(InvocationOnMock invocation) * call. The second {@code nextRows} call will always return null. Multiple * qualifiers are supported for matching. *

    - * The KeyRegexp can be set and it will run against the hex value of the + * The KeyRegexp can be set and it will run against the hex value of the * row key. In testing it seems to work nicely even with byte patterns. */ - public class MockScanner implements + public class MockScanner implements Answer>>> { private final Scanner mock_scanner; @@ -1391,11 +1434,11 @@ public class MockScanner implements private byte[] family = null; private ScanFilter filter = null; private int max_num_rows = Scanner.DEFAULT_MAX_NUM_ROWS; - private ByteMap>>>> + private ByteMap>>>> cursors; private ByteMap>>> cf_rows; private byte[] last_row; - + /** * Default ctor * @param mock_scanner The scanner we're using @@ -1414,7 +1457,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return null; } }).when(mock_scanner).setKeyRegexp(anyString()); - + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1423,34 +1466,43 @@ public Object answer(InvocationOnMock invocation) throws Throwable { return null; } }).when(mock_scanner).setKeyRegexp(anyString(), (Charset)any()); - + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + final Object[] args = invocation.getArguments(); + filter = (ScanFilter)args[0]; + return null; + } + }).when(mock_scanner).setFilter(any(ScanFilter.class)); + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); start = (byte[])args[0]; return null; - } + } }).when(mock_scanner).setStartKey((byte[])any()); - + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); stop = (byte[])args[0]; return null; - } + } }).when(mock_scanner).setStopKey((byte[])any()); - + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); family = (byte[])args[0]; return null; - } + } }).when(mock_scanner).setFamily((byte[])any()); - + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1458,9 +1510,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { scnr_qualifiers = new HashSet(1); scnr_qualifiers.add(bytesToString((byte[])args[0])); return null; - } + } }).when(mock_scanner).setQualifier((byte[])any()); - + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1471,9 +1523,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { scnr_qualifiers.add(bytesToString(qualifier)); } return null; - } + } }).when(mock_scanner).setQualifiers((byte[][])any()); - + doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -1489,9 +1541,9 @@ public byte[] answer(InvocationOnMock invocation) throws Throwable { return start; } }).when(mock_scanner).getCurrentKey(); - + when(mock_scanner.nextRows()).thenAnswer(this); - + doAnswer(new Answer() { @Override public ScanFilter answer(InvocationOnMock invocation) throws Throwable { @@ -1506,7 +1558,7 @@ public String answer(final InvocationOnMock ignored) throws Throwable { } }).when(mock_scanner).toString(); } - + @Override public String toString() { final StringBuilder buf = new StringBuilder(); @@ -1526,21 +1578,22 @@ public String toString() { @Override public Deferred>> answer( final InvocationOnMock invocation) throws Throwable { + if (cursors == null) { - final ByteMap>>> map = + final ByteMap>>> map = storage.get(table); if (map == null) { return Deferred.fromError( new RuntimeException( "No such table " + Bytes.pretty(table))); } - - cursors = new ByteMap>>>>(); cf_rows = new ByteMap>>>(); - + if (family == null || family.length < 1) { for (final Entry>>> cf : map) { - final Iterator>>> + final Iterator>>> cursor = cf.getValue().iterator(); cursors.put(cf.getKey(), cursor); cf_rows.put(cf.getKey(), null); @@ -1551,25 +1604,26 @@ public Deferred>> answer( return Deferred.fromError(new RuntimeException( "No such CF " + Bytes.pretty(family))); } - final Iterator>>> + final Iterator>>> cursor = cf.iterator(); cursors.put(family, cursor); cf_rows.put(family, null); } } - // If we're out of rows to scan, then you HAVE to return null as the + // If we're out of rows to scan, then you HAVE to return null as the // HBase client does. if (!hasNext()) { return Deferred.fromResult(null); } - + // TODO - fuzzy filter support + // TODO - fix the regex comparator Pattern pattern = null; Charset regex_charset = null; if (filter != null) { KeyRegexpFilter regex_filter = null; - + if (filter instanceof KeyRegexpFilter) { regex_filter = (KeyRegexpFilter)filter; } else if (filter instanceof FilterList) { @@ -1579,7 +1633,7 @@ public Deferred>> answer( } } } - + if (regex_filter != null) { try { // key regex filter uses Bytes.UTF8() @@ -1592,14 +1646,14 @@ public Deferred>> answer( } } } - + // return all matches - final ArrayList> results = + final ArrayList> results = new ArrayList>(); int rows_read = 0; while (hasNext()) { advance(); - + // if it's before the start row, after the end row or doesn't // match the given regex, continue on to the next row if (start != null && Bytes.memcmp(last_row, start) < 0) { @@ -1607,11 +1661,11 @@ public Deferred>> answer( } // asynchbase Scanner's logic: // - start_key is inclusive, stop key is exclusive - // - when start key is equal to the stop key, + // - when start key is equal to the stop key, // include the key in scan result // - if stop key is empty, scan till the end - if (stop != null && stop.length > 0 && - Bytes.memcmp(last_row, stop) >= 0 && + if (stop != null && stop.length > 0 && + Bytes.memcmp(last_row, stop) >= 0 && Bytes.memcmp(start, stop) != 0) { continue; } @@ -1621,7 +1675,7 @@ public Deferred>> answer( continue; } } - + // throws AFTER we match on a row key if (exceptions != null) { final Pair ex = exceptions.get(last_row); @@ -1636,21 +1690,22 @@ public Deferred>> answer( // loop over the column family rows to see if they match final ArrayList kvs = new ArrayList(); - for (final Entry>>> row : + for (final Entry>>> row : cf_rows.entrySet()) { - if (row.getValue() == null || + if (row.getValue() == null || Bytes.memcmp(last_row, row.getValue().getKey()) != 0) { continue; } - - for (final Entry> column : + + for (final Entry> column : row.getValue().getValue().entrySet()) { // if the qualifier isn't in the set, continue - if (scnr_qualifiers != null && + if (scnr_qualifiers != null && !scnr_qualifiers.contains(bytesToString(column.getKey()))) { continue; } - + + // handle qualifier filters. Just regexp for now if (filter != null) { List qfs = Lists.newArrayList(); @@ -1700,32 +1755,32 @@ public Deferred>> answer( } } - kvs.add(new KeyValue(row.getValue().getKey(), row.getKey(), + kvs.add(new KeyValue(row.getValue().getKey(), row.getKey(), column.getKey(), column.getValue().firstKey(), column.getValue().firstEntry().getValue())); } } - + if (!kvs.isEmpty()) { results.add(kvs); } rows_read++; - + if (rows_read >= max_num_rows) { Thread.sleep(10); // this is here for time based unit tests break; } } - + if (results.isEmpty()) { return Deferred.fromResult(null); } return Deferred.fromResult(results); } - + /** @return Returns true if any of the CF iterators have another value */ private boolean hasNext() { - for (final Iterator>>> cursor : + for (final Iterator>>> cursor : cursors.values()) { if (cursor.hasNext()) { return true; @@ -1733,15 +1788,15 @@ private boolean hasNext() { } return false; } - + /** Insanely inefficient and ugly way of advancing the cursors */ private void advance() { // first time to get the ceiling if (last_row == null) { - for (final Entry>>>> iterator : + for (final Entry>>>> iterator : cursors.entrySet()) { - final Entry>> row = + final Entry>> row = iterator.getValue().hasNext() ? iterator.getValue().next() : null; cf_rows.put(iterator.getKey(), row); if (last_row == null) { @@ -1754,14 +1809,14 @@ private void advance() { } return; } - - for (final Entry>>> cf : + + for (final Entry>>> cf : cf_rows.entrySet()) { final Entry>> row = cf.getValue(); if (row == null) { continue; } - + if (Bytes.memcmp(last_row, row.getKey()) == 0) { if (!cursors.get(cf.getKey()).hasNext()) { cf_rows.put(cf.getKey(), null); // EX? @@ -1770,14 +1825,14 @@ private void advance() { } } } - + last_row = null; - for (final Entry>> row : + for (final Entry>> row : cf_rows.values()) { if (row == null) { continue; } - + if (last_row == null) { last_row = row.getKey(); } else { @@ -1787,18 +1842,18 @@ private void advance() { } } } - + /** @return The scanner for this mock */ public Scanner getScanner() { return mock_scanner; } - + /** @return The filter for this mock */ public ScanFilter getFilter() { return filter; } } - + /** * Creates or increments (possibly decrements) a Long in the hash table at the * given location. @@ -1811,7 +1866,7 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { final Object[] args = invocation.getArguments(); final AtomicIncrementRequest air = (AtomicIncrementRequest)args[0]; final long amount = air.getAmount(); - + if (exceptions != null) { final Pair ex = exceptions.get(air.key()); if (ex != null) { @@ -1822,26 +1877,26 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { } } } - - final ByteMap>>> map = + + final ByteMap>>> map = storage.get(air.table()); if (map == null) { return Deferred.fromError(new RuntimeException( "No such table " + Bytes.pretty(air.table()))); } - + final ByteMap>> cf = map.get(air.family()); if (cf == null) { return Deferred.fromError(new RuntimeException( - "No such CF " + Bytes.pretty(air.table()))); + "No such CF " + Bytes.pretty(air.table()))); } - + ByteMap> row = cf.get(air.key()); if (row == null) { row = new ByteMap>(); cf.put(air.key(), row); } - + TreeMap column = row.get(air.qualifier()); if (column == null) { column = new TreeMap(Collections.reverseOrder()); @@ -1849,13 +1904,13 @@ public Deferred answer(InvocationOnMock invocation) throws Throwable { column.put(current_timestamp++, Bytes.fromLong(amount)); return Deferred.fromResult(amount); } - + long incremented_value = Bytes.getLong(column.firstEntry().getValue()); incremented_value += amount; column.put(column.firstKey(), Bytes.fromLong(incremented_value)); return Deferred.fromResult(incremented_value); } - + } } diff --git a/test/tsd/TestAnnotationRpc.java b/test/tsd/TestAnnotationRpc.java index 596f7acbe9..fc80a399d5 100644 --- a/test/tsd/TestAnnotationRpc.java +++ b/test/tsd/TestAnnotationRpc.java @@ -70,12 +70,12 @@ public void before() throws Exception { new byte[] { 1, 0, 0 }, ("{\"startTime\":1328140800,\"endTime\":1328140801,\"description\":" + "\"Description\",\"notes\":\"Notes\",\"custom\":{\"owner\":" + - "\"ops\"}}").getBytes(MockBase.ASCII())); + "\"ops\"}}").getBytes(MockBase.ASCII()), 1328140799972L); storage.addColumn(global_row_key, new byte[] { 1, 0, 1 }, ("{\"startTime\":1328140801,\"endTime\":1328140803,\"description\":" + - "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII())); + "\"Global 2\",\"notes\":\"Nothing\"}").getBytes(MockBase.ASCII()), 1328140799973L); // add a local storage.addColumn(tsuid_row_key, @@ -83,21 +83,21 @@ public void before() throws Exception { ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450562," + "\"endTime\":1419984000,\"description\":\"Hello!\",\"notes\":" + "\"My Notes\",\"custom\":{\"owner\":\"ops\"}}") - .getBytes(MockBase.ASCII())); + .getBytes(MockBase.ASCII()), 1388448000003L); storage.addColumn(tsuid_row_key, new byte[] { 1, 0x0A, 0x03 }, ("{\"tsuid\":\"000001000001000001\",\"startTime\":1388450563," + "\"endTime\":1419984000,\"description\":\"Note2\",\"notes\":" + "\"Nothing\"}") - .getBytes(MockBase.ASCII())); + .getBytes(MockBase.ASCII()), 1388448000004L); // add some data points too storage.addColumn(tsuid_row_key, - new byte[] { 0x50, 0x10 }, new byte[] { 1 }); + new byte[] { 0x50, 0x10 }, new byte[] { 1 }, 1388448000005L); storage.addColumn(tsuid_row_key, - new byte[] { 0x50, 0x18 }, new byte[] { 2 }); + new byte[] { 0x50, 0x18 }, new byte[] { 2 }, 1388448000006L); } @Test From 9b2e4db7bbdca50037cc0b16e3f11e8957bacf30 Mon Sep 17 00:00:00 2001 From: HiramJ Date: Fri, 19 May 2017 16:18:37 -0700 Subject: [PATCH 627/826] Add the start of histogram storage. Signed-off-by: Chris Larsen --- src/core/HistogramAggregation.java | 20 + src/core/HistogramAggregator.java | 27 ++ src/core/HistogramDataPoint.java | 163 ++++++++ src/core/HistogramDataPointDecoder.java | 30 ++ .../HistogramDataPointDecoderManager.java | 72 ++++ src/core/HistogramDataPoints.java | 169 ++++++++ src/core/HistogramRowSeq.java | 375 ++++++++++++++++++ src/core/HistogramSeekableView.java | 56 +++ src/core/iHistogramRowSeq.java | 32 ++ 9 files changed, 944 insertions(+) create mode 100644 src/core/HistogramAggregation.java create mode 100644 src/core/HistogramAggregator.java create mode 100644 src/core/HistogramDataPoint.java create mode 100644 src/core/HistogramDataPointDecoder.java create mode 100644 src/core/HistogramDataPointDecoderManager.java create mode 100644 src/core/HistogramDataPoints.java create mode 100644 src/core/HistogramRowSeq.java create mode 100644 src/core/HistogramSeekableView.java create mode 100644 src/core/iHistogramRowSeq.java diff --git a/src/core/HistogramAggregation.java b/src/core/HistogramAggregation.java new file mode 100644 index 0000000000..87414ddad6 --- /dev/null +++ b/src/core/HistogramAggregation.java @@ -0,0 +1,20 @@ +// 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; + +/** + * Aggregator functions for histogram data points. + */ +public enum HistogramAggregation { + SUM; +} diff --git a/src/core/HistogramAggregator.java b/src/core/HistogramAggregator.java new file mode 100644 index 0000000000..fcf749408a --- /dev/null +++ b/src/core/HistogramAggregator.java @@ -0,0 +1,27 @@ +// 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; + + +/** + * Aggregator for histogram data points. + */ +public class HistogramAggregator { + + + public interface Histograms { + boolean hasNextValue(); + + HistogramDataPoint nextHistogramValue(); + } +} diff --git a/src/core/HistogramDataPoint.java b/src/core/HistogramDataPoint.java new file mode 100644 index 0000000000..83b623af75 --- /dev/null +++ b/src/core/HistogramDataPoint.java @@ -0,0 +1,163 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents a single histogram data point. + * + */ +public interface HistogramDataPoint extends Cloneable { + byte PREFIX = 0x6; + + /** + * Returns the timestamp (in milliseconds) associated with this data point. + * @return A strictly positive, 32 bit integer. + */ + long timestamp(); + + /** + * Get the encoded value of this histogram. + * NOTE: implementation should store the serialize information + * in the byte array so latter it can decide how to deserialize it back + * @return The encoded value os this histogram data point + */ + byte[] getRawData(); + + /** + * Decode the raw data and reset the current histogram data point to the + * decoded value + * @param raw_data The encoded value of the histogram data point + */ + void resetFromRawData(final byte[] raw_data); + + /** + * Calculate percentile of this histogram data point + * @param p the distribution threshold + * @return The percentile value + */ + double percentile(final double p); + + /** + * Calculate percentile values of this histogram data point + * @param p the distribution threshold list + * @return A list of the percentile values + */ + List percentile(final List p); + + void aggregate(HistogramDataPoint histo, HistogramAggregation func); + + /** + * Create and return a copy of this object + * + * @return A deep copy object {@link HistogramDataPoint} + */ + HistogramDataPoint clone(); + + + HistogramDataPoint cloneAndSetTimestamp(final long timestamp); + + + ///////////////////////////////////////////////////////////////////////////////////////////// + // A nested class to present the bucket information + //////////////////////////////////////////////////////////////////////////////////////////// + public class HistogramBucket implements Comparable { + public enum BucketType { + UNDERFLOW, REGULAR, OVERFLOW + } + + private final BucketType type; + private final float lower_bound; + private final float upper_bound; + + public HistogramBucket(final BucketType type, final float lower_bound, + final float uper_bound) { + this.type = type; + this.lower_bound = lower_bound; + this.upper_bound = uper_bound; + } + + public BucketType bucketType() { + return this.type; + } + + public float getLowerBound() { + return this.lower_bound; + } + + public float getUpperBound() { + return this.upper_bound; + } + + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + + if (that == null || getClass() != that.getClass()) { + return false; + } + + HistogramBucket bk = (HistogramBucket)that; + if (bucketType() != bk.bucketType()) { + return false; + } + + if ((BucketType.UNDERFLOW == bucketType() && BucketType.UNDERFLOW == bk.bucketType()) || + (BucketType.OVERFLOW == bucketType() && BucketType.OVERFLOW == bk.bucketType())) { + return true; + } + + if (Float.compare(getLowerBound(), bk.getLowerBound()) != 0) { + return false; + } + + return (Float.compare(getUpperBound(), bk.getUpperBound()) == 0); + } + + @Override + public int compareTo(HistogramBucket that) { + if (this.equals(that)) { + return 0; + } else if (BucketType.UNDERFLOW == type) { + return -1; + } else if (BucketType.REGULAR == type) { + int lower_bound_compare = Float.compare(getLowerBound(), that.getLowerBound()); + if (lower_bound_compare != 0) { + return lower_bound_compare; + } else { + return Float.compare(getUpperBound(), that.getUpperBound()); + } + } else if (BucketType.OVERFLOW == type) { + return +1; + } + + return 0; + } + } + + /** + * Get buckets from this histogram data point + * @return + */ + Map getHistogramBucketsIfHas(); + + /** + void aggregate(List histos, HistoAggregation func); + */ +} diff --git a/src/core/HistogramDataPointDecoder.java b/src/core/HistogramDataPointDecoder.java new file mode 100644 index 0000000000..4965ac7f3c --- /dev/null +++ b/src/core/HistogramDataPointDecoder.java @@ -0,0 +1,30 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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; + +/** + * Creates {@code HistogramDataPoint} from raw data and timestamp. + * + * NOTE: Implementation of this interface should be thread safe. + * @see HistogramDataPointDecoderManager + */ +public interface HistogramDataPointDecoder { + + /** + * Creates {@code HistogramDataPoint} from raw data and timestamp. + * @param raw_data The encoded byte array of the histogram data + * @param timestamp The timestamp of this data point + * @return The decoded histogram data point instance + */ + HistogramDataPoint decode(final byte[] raw_data, final long timestamp); +} diff --git a/src/core/HistogramDataPointDecoderManager.java b/src/core/HistogramDataPointDecoderManager.java new file mode 100644 index 0000000000..08f1d7c54b --- /dev/null +++ b/src/core/HistogramDataPointDecoderManager.java @@ -0,0 +1,72 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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.HashMap; +import java.util.Map; + +/** + *

    + * Manages the histogram decoder singletons. + *

    + *

    + * This manage accepts the full class name of the decoder, use reflection to create the decoder, + * and it ensures each type of the decoder will be created only once, after that, the cached decoder + * instance will be returned. + *

    + *

    + * This behavior actually makes each decoder a singleton. + *

    + * + *

    This class is thread safe

    + */ +public class HistogramDataPointDecoderManager { + + private static final Map decoders = + new HashMap(); + + /** + * Return the singleton instance of the given decoder. + * @param decoder_name The full class name of the decoder + * @return The singleton instance of the given decoder + * + * @throws RuntimeException If failed to create the decoder + */ + public static HistogramDataPointDecoder getDecoder(final String decoder_name) { + HistogramDataPointDecoder decoder = decoders.get(decoder_name); + if (decoder == null) { + synchronized(decoders) { + decoder = decoders.get(decoder_name); + if (decoder == null) { + decoder = createInstance(decoder_name); + decoders.put(decoder_name, decoder); + } + } + } + + return decoder; + } + + private static HistogramDataPointDecoder createInstance(final String decoder_name) { + try { + Class c = Class.forName(decoder_name); + return (HistogramDataPointDecoder) c.newInstance(); + } catch (Exception exp) { + throw new RuntimeException("Failed to create the decoder instance of " + + decoder_name, exp); + } + } + + private HistogramDataPointDecoderManager() { + } +} diff --git a/src/core/HistogramDataPoints.java b/src/core/HistogramDataPoints.java new file mode 100644 index 0000000000..9d8ba3b783 --- /dev/null +++ b/src/core/HistogramDataPoints.java @@ -0,0 +1,169 @@ +package net.opentsdb.core; + +import com.stumbleupon.async.Deferred; +import net.opentsdb.meta.Annotation; +import org.hbase.async.Bytes; + +import java.util.List; +import java.util.Map; + +/** + * Created by haiyang on 6/15/16. + */ +public interface HistogramDataPoints extends Iterable { + + /** + * Returns the name of the series. + * @return The name of the metric as a string. + */ + String metricName(); + + /** + * Returns the name of the series. + * @return The name of the metric in a deferred (may contain an exception). + * @since 1.2 + */ + Deferred metricNameAsync(); + + /** + * @return the metric UID + * @return The metric UID as an array of bytes. + * @since 2.3 + */ + byte[] metricUID(); + + /** + * Returns the tags associated with these data points. + * @return A non-{@code null} map of tag names (keys), tag values (values). + */ + Map getTags(); + + /** + * Returns the tags associated with these data points. + * @return A non-{@code null} map of tag names (keys), tag values (values). + * @since 1.2 + */ + Deferred> getTagsAsync(); + + /** + * 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 + */ + Bytes.ByteMap getTagUids(); + + /** + * 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. + */ + List getAggregatedTags(); + + /** + * 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 + */ + Deferred> getAggregatedTagsAsync(); + + /** + * Returns the tagk UIDs associated with some but not all of the data points. + * @return a non-{@code null} list of tagk UIDs. + */ + List getAggregatedTagUids(); + + /** + * Returns a list of unique TSUIDs contained in the results + * @return an empty list if there were no results, otherwise a list of TSUIDs + */ + public List getTSUIDs(); + + /** + * 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 + */ + public List getAnnotations(); + + /** + * Returns a warning about the query, i.e if it terminated prematurely + * @return A null if no warning, otherwise a string to return to the user + */ + public String getWarning(); + + /** + * Returns the number of histogram data points. + *

    + * This method must be implemented in {@code O(1)} or {@code O(n)} + * where n = {@link #aggregatedSize} > 0. + * @return A positive integer. + */ + int 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. + */ + int 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. + * @return An iterator over the data points. + */ + HistogramSeekableView iterator(); + + /** + * 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 The index to fetch a timestamp for + * @return A strictly positive integer. + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * [0, {@link #size} - 1] + */ + long timestamp(int i); + + /** + * Return the query index that maps this datapoints to the original subquery + * @return index of the query in the TSQuery class + */ + int getQueryIndex(); +} diff --git a/src/core/HistogramRowSeq.java b/src/core/HistogramRowSeq.java new file mode 100644 index 0000000000..11a8b1bbbe --- /dev/null +++ b/src/core/HistogramRowSeq.java @@ -0,0 +1,375 @@ +// 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 com.stumbleupon.async.Deferred; + +import net.opentsdb.core.HistogramDataPoint.HistogramBucket; +import net.opentsdb.meta.Annotation; +import org.hbase.async.Bytes; + +import java.util.*; + +/** + * Represents a read-only sequence of continuous HBase rows. + *

    + * This class stores in memory the data of one or more continuous HBase rows for + * a given time series. To consolidate memory, the data points are stored in two + * byte arrays: one for the time offsets/flags and another for the values. + * Access is granted via pointers. + */ +public class HistogramRowSeq implements iHistogramRowSeq { + + /** The {@link TSDB} instance we belong to. */ + private final TSDB tsdb; + + /** First row key. */ + protected byte[] key; + + protected List rowSeq; + + public HistogramRowSeq(final TSDB tsdb) { + this.tsdb = tsdb; + } + + @Override + public void setRow(final byte[] key, final List row) { + if (this.key != null) { + throw new IllegalStateException("setRow was already called on " + this); + } + + this.key = key; + this.rowSeq = row; + } + + @Override + public void addRow(final List row) { + if (null == this.key) { + throw new IllegalStateException("setRow was never called on " + this); + } + + int index_local = 0; + int index_remote = 0; + List combinedRows = new ArrayList(this.rowSeq.size() + row.size()); + while (index_local < this.rowSeq.size() && index_remote < row.size()) { + HistogramDataPoint hdp_local = this.rowSeq.get(index_local); + HistogramDataPoint hdp_remote = row.get(index_remote); + + long sort = hdp_remote.timestamp() - hdp_local.timestamp(); + if (0 == sort) { + // duplicate histogram data point with the same timestamp, pick the local one + combinedRows.add(hdp_local); + ++index_local; + ++index_remote; + } else if (sort > 0) { + // the remote one has a bigger timestamp, pick the local one + combinedRows.add(hdp_local); + ++index_local; + } else { + // the local one has a bigger timestamp, pick the remote one + combinedRows.add(hdp_remote); + ++index_remote; + } + } // end while + + if (index_local < this.rowSeq.size()) { + // add the left elements of local to the combined list + combinedRows.addAll(this.rowSeq.subList(index_local, this.rowSeq.size())); + } else if (index_remote < row.size()) { + // add the left elements of remote to the combined list + combinedRows.addAll(row.subList(index_remote, row.size())); + } + + this.rowSeq = combinedRows; + } + + @Override + public byte[] key() { + return key; + } + + @Override + public long baseTime() { + return Internal.baseTime(key); + } + + @Override + public Iterator internalIterator() { + return new Iterator(); + } + + @Override + public String metricName() { + try { + return metricNameAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the metric name call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred metricNameAsync() { + if (key == null) { + throw new IllegalStateException("the row key is null!"); + } + return RowKey.metricNameAsync(tsdb, key); + } + + @Override + public byte[] metricUID() { + return Arrays.copyOfRange(key, Const.SALT_WIDTH(), Const.SALT_WIDTH() + TSDB.metrics_width()); + } + + @Override + public Map getTags() { + try { + return getTagsAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the tags call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + @Override + public Deferred> getTagsAsync() { + return Tags.getTagsAsync(tsdb, key); + } + + @Override + public Bytes.ByteMap getTagUids() { + return Tags.getTagUids(key); + } + + @Override + public List getAggregatedTags() { + return Collections.emptyList(); + } + + @Override + public Deferred> getAggregatedTagsAsync() { + final List empty = Collections.emptyList(); + return Deferred.fromResult(empty); + } + + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } + + @Override + public List getTSUIDs() { + return Collections.emptyList(); + } + + @Override + public List getAnnotations() { + return Collections.emptyList(); + } + + @Override + public String getWarning() { + return null; + } + + @Override + public int size() { + return rowSeq.size(); + } + + @Override + public int aggregatedSize() { + return 0; + } + + @Override + public HistogramSeekableView iterator() { + return internalIterator(); + } + + @Override + public long timestamp(int i) { + checkIndex(i); + + return rowSeq.get(i).timestamp(); + } + + @Override + public int getQueryIndex() { + return 0; + } + + /** + * @throws IndexOutOfBoundsException + * if {@code i} is out of bounds. + */ + private void checkIndex(final int i) { + if (i >= size()) { + throw new IndexOutOfBoundsException("index " + i + " >= " + size() + " for this=" + this); + } + if (i < 0) { + throw new IndexOutOfBoundsException("negative index " + i + " for this=" + this); + } + } + + /** Returns a human readable string representation of the object. */ + @Override + public String toString() { + final String metric = metricName(); + final int sz = size(); + + // The argument passed to StringBuilder is an estimate = number of data points * 125 + final StringBuilder buf = new StringBuilder(sz * 125); + final long base_time = baseTime(); + buf.append("RowSeq(") + .append(key == null ? "" : Arrays.toString(key)) + .append(" (metric=") + .append(metric) + .append("), base_time=") + .append(base_time) + .append(" (") + .append(base_time > 0 ? new Date(base_time * 1000) : "no date") + .append(")"); + + for (short i = 0; i < sz; ++i) { + buf.append('+').append(rowSeq.get(i).timestamp()); + buf.append(":histogram(").append(Arrays.toString(rowSeq.get(i).getRawData())); + buf.append(')'); + if (i != sz -1) { + buf.append(", "); + } + } // end for + + buf.append(")"); + return buf.toString(); + } + + /** + * Used to compare two RowSeq objects when sorting a {@link Span}. Compares on + * the {@code RowSeq#baseTime()} + * + * @since 2.0 + */ + public static final class HistogramRowSeqComparator implements Comparator { + public int compare(final iHistogramRowSeq a, final iHistogramRowSeq b) { + if (null == a || null == b) { + if (a == b) { + return 0; + } else { + return (null == a) ? -1 : 1; + } + } + if (a.baseTime() == b.baseTime()) { + return 0; + } + return a.baseTime() < b.baseTime() ? -1 : 1; + } + } + + final class Iterator implements iHistogramRowSeq.Iterator { + private int next_index; + + Iterator() { + } + + @Override + public long timestamp() { + return getCurrent().timestamp(); + } + + @Override + public byte[] getRawData() { + return getCurrent().getRawData(); + } + + @Override + public void resetFromRawData(byte[] raw_data) { + getCurrent().resetFromRawData(raw_data); + } + + @Override + public double percentile(double p) { + return getCurrent().percentile(p); + } + + @Override + public List percentile(List p) { + return getCurrent().percentile(p); + } + + @Override + public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + getCurrent().aggregate(histo, func); + } + + @Override + public boolean hasNext() { + return next_index < rowSeq.size(); + } + + @Override + public HistogramDataPoint next() { + if (!hasNext()) { + throw new NoSuchElementException("no more elements"); + } + + ++next_index; + return this; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void seek(long timestamp) { + if ((timestamp & Const.MILLISECOND_MASK) != 0) { // negative or not 48 bits + throw new IllegalArgumentException("invalid timestamp: " + timestamp); + } + + // TODO: this can be optimized to O(nlogn) + next_index = 0; + while (next_index < rowSeq.size() && rowSeq.get(next_index).timestamp() < timestamp) { + ++next_index; + } + } + + @Override + public HistogramDataPoint clone() { + return getCurrent().clone(); + } + + @Override + public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { + return getCurrent().cloneAndSetTimestamp(timestamp); + } + + private HistogramDataPoint getCurrent() { + assert next_index > 0 : "not initialized: " + this; + return rowSeq.get(next_index - 1); + } + + @Override + public Map getHistogramBucketsIfHas() { + return getCurrent().getHistogramBucketsIfHas(); + } + } +} diff --git a/src/core/HistogramSeekableView.java b/src/core/HistogramSeekableView.java new file mode 100644 index 0000000000..5532486a6d --- /dev/null +++ b/src/core/HistogramSeekableView.java @@ -0,0 +1,56 @@ +// 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.Iterator; +import java.util.NoSuchElementException; + +/** + * Created by haiyang on 6/15/16. + */ +public interface HistogramSeekableView extends Iterator { + + /** + * Returns {@code true} if this view has more elements. + */ + boolean 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}. + */ + HistogramDataPoint next(); + + /** + * Unsupported operation. + * @throws UnsupportedOperationException always. + */ + void remove(); + + /** + * 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!). + */ + void seek(long timestamp); + +} diff --git a/src/core/iHistogramRowSeq.java b/src/core/iHistogramRowSeq.java new file mode 100644 index 0000000000..ecd1680e0d --- /dev/null +++ b/src/core/iHistogramRowSeq.java @@ -0,0 +1,32 @@ +// 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; + + +public interface iHistogramRowSeq extends HistogramDataPoints { + void setRow(final byte[] key, final List row); + + void addRow(final List row); + + byte[] key(); + + long baseTime(); + + Iterator internalIterator(); + + interface Iterator extends HistogramSeekableView, HistogramDataPoint { + + } +} From f241ab90e17d6c93f517856dab6f7abbd47d4279 Mon Sep 17 00:00:00 2001 From: HiramJ Date: Sat, 20 May 2017 14:54:49 -0700 Subject: [PATCH 628/826] Add Histogram utility methods toe the Internal class. Add the Histo aggregation iterator, downsampler and span and set an aggregation function in the downsampling specifier. Signed-off-by: Chris Larsen --- src/core/DownsamplingSpecification.java | 16 + src/core/HistogramAggregationIterator.java | 308 +++++++++++ src/core/HistogramDownsampler.java | 377 ++++++++++++++ src/core/HistogramSpan.java | 571 +++++++++++++++++++++ src/core/Internal.java | 57 ++ src/utils/Config.java | 8 + 6 files changed, 1337 insertions(+) create mode 100644 src/core/HistogramAggregationIterator.java create mode 100644 src/core/HistogramDownsampler.java create mode 100644 src/core/HistogramSpan.java diff --git a/src/core/DownsamplingSpecification.java b/src/core/DownsamplingSpecification.java index b64cceb157..0d38404adb 100644 --- a/src/core/DownsamplingSpecification.java +++ b/src/core/DownsamplingSpecification.java @@ -36,6 +36,8 @@ public final class DownsamplingSpecification { /** The default fill policy. */ public static final FillPolicy DEFAULT_FILL_POLICY = FillPolicy.NONE; + public static final HistogramAggregation NO_HIST_AGG = null; + // Parsed downsample interval. private final long interval; @@ -54,6 +56,8 @@ public final class DownsamplingSpecification { // The user provided timezone for calendar alignment (defaults to UTC) private TimeZone timezone; + private final HistogramAggregation hist_agg; + /** * A specification indicating no downsampling is requested. */ @@ -64,6 +68,7 @@ private DownsamplingSpecification() { string_interval = null; use_calendar = false; timezone = DateTime.timezones.get(DateTime.UTC_ID); + hist_agg = NO_HIST_AGG; } /** @@ -96,6 +101,7 @@ public DownsamplingSpecification(final long interval, string_interval = null; use_calendar = false; timezone = DateTime.timezones.get(DateTime.UTC_ID); + hist_agg = NO_HIST_AGG; } /** @@ -144,6 +150,12 @@ public DownsamplingSpecification(final String specification) { string_interval = parts[0]; } + if (parts[1].toLowerCase().equals("sum")) { + hist_agg = HistogramAggregation.SUM; + } else { + hist_agg = null; + } + // FUNCTION. try { function = Aggregators.get(parts[1]); @@ -236,6 +248,10 @@ public TimeZone getTimezone() { return timezone; } + public HistogramAggregation getHistogramAggregation() { + return hist_agg; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/src/core/HistogramAggregationIterator.java b/src/core/HistogramAggregationIterator.java new file mode 100644 index 0000000000..6ee3aa7714 --- /dev/null +++ b/src/core/HistogramAggregationIterator.java @@ -0,0 +1,308 @@ +// 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.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.opentsdb.core.HistogramDataPoint.HistogramBucket; + +/** + * + * This is where the real business of @{link HistogramSpanGroup}. It provides a merged and aggregated + * view of the data points. It will apply the following processing: + *

      + *
    • Down sampling + *
    • Aggregation + *
    + * + * The following processing is inapplicable: + *
      + *
    • Interpolation + *
    • Rate Calculation + *
    + * + */ +public class HistogramAggregationIterator implements HistogramSeekableView, HistogramDataPoint { + private static final Logger LOG = LoggerFactory.getLogger(HistogramAggregationIterator.class); + + /** Aggregator to use to aggregate histogram data points from different HistogramSpans. */ + private final HistogramAggregation aggregation; + + /** + * Where we are in each {@link HistogramSpan} in the group. + */ + private final HistogramSeekableView[] iterators; + + /** + * Start time (UNIX timestamp in seconds or ms) on 32 bits ("unsigned" int). + */ + private final long start_time; + + /** End time (UNIX timestamp in seconds or ms) on 32 bits ("unsigned" int). */ + private final long end_time; + + /** + * The next timestamps for the data points being used + */ + private final long[] timestamps; + + /** + * The next values for the data points being used. + */ + private final HistogramDataPoint[] values; + + /** + * The current value + */ + private HistogramDataPoint value; + + /** + * Cotr. + * + * @param spans The spans that join the aggregation + * @param start_time Any data point strictly before this timestamp will be ignored. + * @param end_time Any data point strictly after this timestamp will be ignored. + * @param aggregation The aggregation will be applied on the spans + * @param downsampler The downsamper will be applied on each span + * @param query_start The start time of the actual query + * @param query_end The end time of the actual query + * @param is_rollup Whether we are handling the rollup data points + * @return + */ + public static HistogramAggregationIterator create(final List spans, + final long start_time, + final long end_time, + final HistogramAggregation aggregation, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final boolean is_rollup) { + final int size = spans.size(); + final HistogramSeekableView[] iterators = new HistogramSeekableView[size]; + for (int i = 0; i < size; i++) { + HistogramSeekableView it; + if (downsampler == DownsamplingSpecification.NO_DOWNSAMPLER) { + it = spans.get(i).spanIterator(); + } else { + it = spans.get(i).downsampler(start_time, end_time, downsampler, is_rollup, query_start, query_end); + } + iterators[i] = it; + } + return new HistogramAggregationIterator(iterators, start_time, end_time, aggregation); + } + + private HistogramAggregationIterator(final HistogramSeekableView[] iterators, + final long start_time, + final long end_time, + final HistogramAggregation aggregation) { + this.iterators = iterators; + this.start_time = start_time; + this.end_time = end_time; + this.aggregation = aggregation; + + timestamps = new long[this.iterators.length]; + values = new HistogramDataPoint[this.iterators.length]; + + // Initialize every Iterator, fetch their first values that fall + // within our time range. + int num_empty_spans = 0; + for (int i = 0; i < this.iterators.length; i++) { + HistogramSeekableView it = iterators[i]; + it.seek(this.start_time); + + final HistogramDataPoint dp; + if (!it.hasNext()) { + ++num_empty_spans; + endReached(i); + continue; + } + + dp = it.next(); + if (dp.timestamp() >= this.start_time) { + putDataPoint(i, dp); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("No DP in range for #%d: %d < %d", i, dp.timestamp(), start_time)); + } + endReached(i); + continue; + } + } // end for + + if (num_empty_spans > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("%d out of %d spans are empty!", num_empty_spans, this.iterators.length)); + } + } + } + + /** + * Indicates that an iterator in {@link #iterators} has reached the end. + * + * @param i The index in {@link #iterators} of the iterator. + */ + private void endReached(final int i) { + timestamps[i] = 0; + iterators[i] = null; // We won't use it anymore, so free() it. + } + + /** + * Puts the next data point of an iterator in the internal buffer. + * + * @param i The index in {@link #iterators} of the iterator. + * @param dp The last data point returned by that iterator. + */ + private void putDataPoint(final int i, final HistogramDataPoint dp) { + timestamps[i] = dp.timestamp(); + values[i] = dp.clone(); + } + + @Override + public long timestamp() { + return value.timestamp(); + } + + @Override + public byte[] getRawData() { + return value.getRawData(); + } + + @Override + public void resetFromRawData(byte[] raw_data) { + value.resetFromRawData(raw_data); + } + + @Override + public double percentile(double p) { + return value.percentile(p); + } + + @Override + public List percentile(List p) { + return value.percentile(p); + } + + @Override + public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + value.aggregate(histo, func); + } + + @Override + public HistogramDataPoint clone() { + return value.clone(); + } + + @Override + public HistogramDataPoint cloneAndSetTimestamp(long timestamp) { + return value.cloneAndSetTimestamp(timestamp); + } + + @Override + public boolean hasNext() { + for (int i = 0; i < iterators.length; ++i) { + if (0 != this.timestamps[i] && this.timestamps[i] <= this.end_time) { + return true; + } + } + + return false; + } + + @Override + public HistogramDataPoint next() { + if (!hasNext()) { + throw new NoSuchElementException("no more elements"); + } + + long min_ts = Long.MAX_VALUE; + int fist_min_ts_index = -1; + boolean is_multiple = false; + + // find out the data points with the smallest timestamp + for (int i = 0; i < this.iterators.length; ++i) { + if (0 == this.timestamps[i]) { + continue; + } + + if (this.timestamps[i] > this.end_time) { + continue; + } + + if (this.timestamps[i] < min_ts) { + min_ts = this.timestamps[i]; + fist_min_ts_index = i; + is_multiple = false; + } else if (this.timestamps[i] == min_ts) { + is_multiple = true; + } + } // end for + + if (fist_min_ts_index < 0) { + throw new NoSuchElementException("no more elements"); + } + + // do the aggregation on the data points with the smallest timestamp + this.value = this.values[fist_min_ts_index]; + if (is_multiple) { + for (int i = fist_min_ts_index + 1; i < this.iterators.length; ++i) { + if (this.timestamps[i] == min_ts) { + this.value.aggregate(this.values[i], this.aggregation); + + // move to next data point on this span + moveToNext(i); + } + } // end for + } + + moveToNext(fist_min_ts_index); + return this; + } + + /** + * Makes iterator number {@code i} move forward to the next data point. + * + * @param i The index in {@link #iterators} of the iterator. + */ + private void moveToNext(final int i) { + final HistogramSeekableView it = iterators[i]; + if (it.hasNext()) { + putDataPoint(i, it.next()); + } else { + endReached(i); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void seek(long timestamp) { + for (final HistogramSeekableView it : iterators) { + it.seek(timestamp); + } + } + + @Override + public Map getHistogramBucketsIfHas() { + return this.value.getHistogramBucketsIfHas(); + } +} diff --git a/src/core/HistogramDownsampler.java b/src/core/HistogramDownsampler.java new file mode 100644 index 0000000000..b0349ee0ea --- /dev/null +++ b/src/core/HistogramDownsampler.java @@ -0,0 +1,377 @@ +// 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.core.HistogramDataPoint.HistogramBucket; +import net.opentsdb.utils.DateTime; + +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + + +/** + * Iterator that downsamples histogram data points using an + * {@link HistogramAggregation}. + */ +public class HistogramDownsampler implements HistogramSeekableView, HistogramDataPoint { + + /** Matches the weekly downsampler as it requires special handling. */ + protected final static int WEEK_UNIT = DateTime.unitsToCalendarType("w"); + protected final static int DAY_UNIT = DateTime.unitsToCalendarType("d"); + protected final static int WEEK_LENGTH = 7; + + protected final HistogramSeekableView source; + /** Iterator to iterate the values of the current interval. */ + protected final HistogramDownsampler.ValuesInInterval values_in_interval; + /** The downsampling specification when provided */ + protected final DownsamplingSpecification specification; + /** The start timestamp of the actual query for use with "all" */ + protected final long query_start; + /** The end timestamp of the actual query for use with "all" */ + protected final long query_end; + /** Last normalized timestamp */ + protected long timestamp; + /** Last value */ + protected HistogramDataPoint value; + /** The interval to use with a calendar */ + protected final int interval; + /** The unit to use with a calendar as a Calendar integer */ + protected final int unit; + /** Whether or not to merge all DPs in the source into one value */ + protected final boolean run_all; + + /** + * Ctor. + * + * @param source The iterator to access the underlying data. + * @param specification The downsampling spec to use + * @param query_start The start timestamp of the actual query for use with "all" + * @param query_end The end timestamp of the actual query for use with "all" + * @since 2.3 + */ + HistogramDownsampler(final HistogramSeekableView source, + final DownsamplingSpecification specification, + final long query_start, + final long query_end) { + this.source = source; + this.specification = specification; + this.values_in_interval = new ValuesInInterval(); + this.query_start = query_start; + this.query_end = query_end; + + final String s = specification.getStringInterval(); + if (s != null && s.toLowerCase().contains("all")) { + run_all = true; + interval = 0; + unit = 0; + } else if (s != null && specification.useCalendar()) { + if (s.toLowerCase().contains("ms")) { + interval = Integer.parseInt(s.substring(0, s.length() - 2)); + unit = DateTime.unitsToCalendarType(s.substring(s.length() - 2)); + } else { + interval = Integer.parseInt(s.substring(0, s.length() - 1)); + unit = DateTime.unitsToCalendarType(s.substring(s.length() - 1)); + } + run_all = false; + } else { + run_all = false; + interval = 0; + unit = 0; + } + } + + @Override + public long timestamp() { + if (run_all) { + return query_start; + } + return timestamp; + } + + @Override + public byte[] getRawData() { + return value.getRawData(); + } + + @Override + public void resetFromRawData(byte[] raw_data) { + throw new UnsupportedOperationException(); + } + + @Override + public double percentile(double p) { + return value.percentile(p); + } + + @Override + public List percentile(List p) { + return value.percentile(p); + } + + @Override + public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + value.aggregate(histo, func); + } + + @Override + public boolean hasNext() { + return values_in_interval.hasNextValue(); + } + + @Override + public HistogramDataPoint next() { + if (hasNext()) { + value = values_in_interval.nextHistogramValue(); + while (values_in_interval.hasNextValue()) { + // this call will change the data in @{code value} + value.aggregate(values_in_interval.nextHistogramValue(), specification.getHistogramAggregation()); + } + timestamp = values_in_interval.getIntervalTimestamp(); + + values_in_interval.moveToNextInterval(); + return this; + } + + throw new NoSuchElementException("no more data points in " + this); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void seek(long timestamp) { + values_in_interval.seekInterval(timestamp); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("HistogramDownsampler: ").append(", downsampler=").append(specification).append(", query_start=") + .append(query_start).append(", current data=(timestamp=").append(timestamp).append(", value=").append(value) + .append("), values_in_interval=").append(values_in_interval); + return buf.toString(); + } + + @Override + public HistogramDataPoint clone() { + // make sure give the right timestamp here, because value has a timestamp + // from the underlaid data point, not the aligned the downsampled timestamp + return this.value.cloneAndSetTimestamp(timestamp); + } + + @Override + public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { + return this.value.cloneAndSetTimestamp(timestamp); + } + + class ValuesInInterval implements HistogramAggregator.Histograms { + + /** An optional calendar set to the current timestamp for the data point */ + private Calendar previous_calendar; + /** An optional calendar set to the end of the interval timestamp */ + private Calendar next_calendar; + /** The end of the current interval. */ + private long timestamp_end_interval = Long.MIN_VALUE; + /** True if the last value was successfully extracted from the source. */ + private boolean has_next_value_from_source = false; + /** The last data point extracted from the source. */ + private HistogramDataPoint next_dp = null; + /** True if it is initialized for iterating intervals. */ + private boolean initialized = false; + + protected ValuesInInterval() { + if (run_all) { + timestamp_end_interval = query_end; + } else if (!specification.useCalendar()) { + timestamp_end_interval = specification.getInterval(); + } + } + + /** Initializes to iterate intervals. */ + protected void initializeIfNotDone() { + // NOTE: Delay initialization is required to not access any data point + // from the source until a user requests it explicitly to avoid the severe + // performance penalty by accessing the unnecessary first data of a span. + if (!initialized) { + initialized = true; + if (source.hasNext()) { + moveToNextValue(); + if (!run_all) { + if (specification.useCalendar()) { + previous_calendar = DateTime.previousInterval(next_dp.timestamp(), interval, unit, + specification.getTimezone()); + next_calendar = DateTime.previousInterval(next_dp.timestamp(), interval, unit, + specification.getTimezone()); + if (unit == WEEK_UNIT) { + next_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + next_calendar.add(unit, interval); + } + timestamp_end_interval = next_calendar.getTimeInMillis(); + } else { + timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + specification.getInterval(); + } + } + } + } + } + + /** Extracts the next value from the source. */ + private void moveToNextValue() { + if (source.hasNext()) { + has_next_value_from_source = true; + // filter out dps that don't match start and end for run_alls + if (run_all) { + while (source.hasNext()) { + next_dp = source.next(); + if (next_dp.timestamp() < query_start) { + next_dp = null; + continue; + } + if (next_dp.timestamp() >= query_end) { + has_next_value_from_source = false; + } + break; + } + if (next_dp == null) { + has_next_value_from_source = false; + } + } else { + next_dp = source.next(); + } + } else { + has_next_value_from_source = false; + } + } + + /** + * Resets the current interval with the interval of the timestamp of the + * next value read from source. It is the first value of the next interval. + */ + private void resetEndOfInterval() { + if (has_next_value_from_source && !run_all) { + if (specification.useCalendar()) { + while (next_dp.timestamp() >= timestamp_end_interval) { + if (unit == WEEK_UNIT) { + previous_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + next_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + previous_calendar.add(unit, interval); + next_calendar.add(unit, interval); + } + timestamp_end_interval = next_calendar.getTimeInMillis(); + } + } else { + timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + specification.getInterval(); + } + } + } + + /** Moves to the next available interval. */ + void moveToNextInterval() { + initializeIfNotDone(); + resetEndOfInterval(); + } + + /** Advances the interval iterator to the given timestamp. */ + void seekInterval(final long timestamp) { + // To make sure that the interval of the given timestamp is fully filled, + // rounds up the seeking timestamp to the smallest timestamp that is + // a multiple of the interval and is greater than or equal to the given + // timestamp.. + if (run_all) { + source.seek(timestamp); + } else if (specification.useCalendar()) { + final Calendar seek_calendar = DateTime.previousInterval(timestamp, interval, unit, + specification.getTimezone()); + if (timestamp > seek_calendar.getTimeInMillis()) { + if (unit == WEEK_UNIT) { + seek_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); + } else { + seek_calendar.add(unit, interval); + } + } + source.seek(seek_calendar.getTimeInMillis()); + } else { + source.seek(alignTimestamp(timestamp + specification.getInterval() - 1)); + } + initialized = false; + } + + /** Returns the representative timestamp of the current interval. */ + protected long getIntervalTimestamp() { + // NOTE: It is well-known practice taking the start time of + // a downsample interval as a representative timestamp of it. It also + // provides the correct context for seek. + if (run_all) { + return timestamp_end_interval; + } else if (specification.useCalendar()) { + return previous_calendar.getTimeInMillis(); + } else { + return alignTimestamp(timestamp_end_interval - specification.getInterval()); + } + } + + /** Returns timestamp aligned by interval. */ + protected long alignTimestamp(final long timestamp) { + return timestamp - (timestamp % specification.getInterval()); + } + + @Override + public boolean hasNextValue() { + initializeIfNotDone(); + if (run_all) { + return has_next_value_from_source; + } + return has_next_value_from_source && next_dp.timestamp() < timestamp_end_interval; + } + + @Override + public HistogramDataPoint nextHistogramValue() { + if (hasNextValue()) { + if (next_dp != null) { + HistogramDataPoint value = null; + // we have to clone the object, else when moveToNextValue in the next step will + // also change the @{code next_dp} and @{code value} here + value = next_dp.clone(); + moveToNextValue(); + return value; + } + } + throw new NoSuchElementException("no more values in interval of " + timestamp_end_interval); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("ValuesInInterval{").append(", timestamp_end_interval=").append(timestamp_end_interval) + .append(", unit=").append(unit).append(", interval=").append(interval).append(", has_next_value_from_source=") + .append(has_next_value_from_source); + if (has_next_value_from_source) { + buf.append(", nextValue=(").append(next_dp).append(')'); + } + buf.append(", source=").append(source).append("}"); + return buf.toString(); + } + } + + @Override + public Map getHistogramBucketsIfHas() { + return this.value.getHistogramBucketsIfHas(); + } +} diff --git a/src/core/HistogramSpan.java b/src/core/HistogramSpan.java new file mode 100644 index 0000000000..f4c740d376 --- /dev/null +++ b/src/core/HistogramSpan.java @@ -0,0 +1,571 @@ +// 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 com.stumbleupon.async.Deferred; +import net.opentsdb.meta.Annotation; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.UniqueId; +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; + +import java.util.*; + +/** + * Represents a read-only sequence of continuous histogram data points. + *

    + * This class stores a continuous sequence of {@link HistogramRowSeq}s in memory. + */ +public class HistogramSpan implements HistogramDataPoints { + + /** + * The {@link TSDB} instance we belong to. + */ + protected final TSDB tsdb; + + /** + * All the rows in this span. + */ + protected List rows = new ArrayList(); + + /** + * A list of annotations for this span. We can't lazily initialize since we + * have to pass a collection to the compaction queue + */ + private List annotations = new ArrayList(0); + + /** + * Whether or not the rows have been sorted. This should be toggled by the + * first call to an iterator method + */ + private boolean sorted; + + /** + * Stores a warning about a prematurely terminated query + */ + private String warning; + + /** + * Default constructor. + * + * @param tsdb The TSDB to which we belong + */ + protected HistogramSpan(final TSDB tsdb) { + this.tsdb = tsdb; + } + + public String getWarning() { + return warning; + } + + /** + * Store a warning about the query + * + * @param warning The warning to store + */ + public void setWarning(final String warning) { + this.warning = warning; + } + + /** + * @throws IllegalStateException if the span doesn't have any rows + */ + private void checkNotEmpty() { + if (rows.size() == 0) { + throw new IllegalStateException("empty Span"); + } + } + + /** + * @return the name of the metric associated with the rows in this span + * @throws IllegalStateException if the span was empty + * @throws NoSuchUniqueId if the row key UID did not exist + */ + public String metricName() { + try { + return metricNameAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the metric name call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred metricNameAsync() { + checkNotEmpty(); + return rows.get(0).metricNameAsync(); + } + + public byte[] metricUID() { + return rows.get(0).metricUID(); + } + + /** + * @return the list of tag pairs for the rows in this span + * @throws IllegalStateException if the span was empty + * @throws NoSuchUniqueId if the any of the tagk/v UIDs did not exist + */ + public Map getTags() { + try { + return getTagsAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the tags call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred> getTagsAsync() { + checkNotEmpty(); + return rows.get(0).getTagsAsync(); + } + + @Override + public Bytes.ByteMap getTagUids() { + checkNotEmpty(); + return rows.get(0).getTagUids(); + } + + /** + * @return an empty list since aggregated tags cannot exist on a single span + */ + public List getAggregatedTags() { + return Collections.emptyList(); + } + + public Deferred> getAggregatedTagsAsync() { + final List empty = Collections.emptyList(); + return Deferred.fromResult(empty); + } + + @Override + public List getAggregatedTagUids() { + return Collections.emptyList(); + } + + /** + * @return the number of data points in this span, O(n) Unfortunately we must + * walk the entire array for every row as there may be a mix of second + * and millisecond timestamps + */ + public int size() { + int size = 0; + for (final iHistogramRowSeq row : rows) { + size += row.size(); + } + return size; + } + + /** + * @return 0 since aggregation cannot happen at the span level + */ + public int aggregatedSize() { + return 0; + } + + public List getTSUIDs() { + if (rows.size() < 1) { + return null; + } + final byte[] tsuid = UniqueId.getTSUIDFromKey(rows.get(0).key(), TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + final List tsuids = new ArrayList(1); + tsuids.add(UniqueId.uidToString(tsuid)); + return tsuids; + } + + /** + * @return a list of annotations associated with this span. May be empty + */ + public List getAnnotations() { + return annotations; + } + + /** + * Adds a row of histogram data points to the span, merging with an existing + * RowSeq or creating a new one if necessary. + *

    + * Take the following rows as an example. Salt A has a row T0 at the base time T0, + * a row T2 at the base time T2...; Salt B has a row T1 at the base time T1, + * a row T3 at the base time T3... + * + * Since the salt is generated basing the following hash code: + *

    +   * {@code 
    +   *  int modulo = (new String(Arrays.copyOfRange(rowKey,  Const.SALT_WIDTH(),
    +   *  rowKey.length))).hashCode() % Const.SALT_BUCKETS();
    +   *  byte[] salt = Internal.getSalt(modulo);
    +   * }
    +   * 
    + * This hash scheme guarantees that all the data points with the same base time will + * has the same salt. + *
    +   * |---A---|       |---B---|
    +   * |  T0   |       |   T1  |
    +   * |-------|       |-------|
    +   * |  T2   |       |   T3  |
    +   * |-------|       |-------|
    +   * |  T4   |       |   T5  |
    +   * |-------|       |-------|
    +   * 
    + * + * This method expects the caller adds the rows from the same salt in the timestamp order. + * When the caller adds the rows from salt A, the result rows will be as below: + *
    +   * |-------|       
    +   * |  T0   |      
    +   * |-------|      
    +   * |  T2   |      
    +   * |-------|       
    +   * |  T4   |       
    +   * |-------|
    +   * 
    + * + * After the caller adds the rows from salt B, the result rows will be as below: + *
    +   * |-------|       
    +   * |  T0   |      
    +   * |-------|      
    +   * |  T2   |      
    +   * |-------|       
    +   * |  T4   |       
    +   * |-------|
    +   * |  T1   |
    +   * |-------|
    +   * |  T3   |
    +   * |-------|
    +   * |  T5   |
    +   * |-------|
    +   * 
    + * When the caller iterates the data points in the Span, the Span will firstly sort the rows. + * Then the final result rows will be as below: + *
    +   * |-------|       
    +   * |  T0   |      
    +   * |-------|      
    +   * |  T1   |      
    +   * |-------|       
    +   * |  T2   |       
    +   * |-------|
    +   * |  T3   |
    +   * |-------|
    +   * |  T4   |
    +   * |-------|
    +   * |  T5   |
    +   * |-------|
    +   * 
    + *

    + * @param key The row key of the row that want to add in the span + * @param data_points histogram data points in the row + * @throws IllegalArgumentException if the argument and this span are for two different time series. + */ + protected void addRow(final byte[] key, final List data_points) { + if (null == key || null == data_points) { + throw new NullPointerException("row key and histogram data points can't be null"); + } + + long last_ts = 0; + if (rows.size() != 0) { + // Verify that we have the same metric id and tags. + final iHistogramRowSeq last = rows.get(rows.size() - 1); + final short metric_width = tsdb.metrics.width(); + final short tags_offset = (short) (Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES); + final short tags_bytes = (short) (key.length - tags_offset); + String error = null; + if (key.length != last.key().length) { + error = "row key length mismatch"; + } else if (Bytes.memcmp(key, last.key(), Const.SALT_WIDTH(), metric_width) != 0) { + error = "metric ID mismatch"; + } else if (Bytes.memcmp(key, last.key(), tags_offset, tags_bytes) != 0) { + error = "tags mismatch"; + } + if (error != null) { + throw new IllegalArgumentException(error + ". " + "This Span's last row key is " + Arrays.toString(last.key()) + + " whereas the row key being added is " + Arrays.toString(key) + " and metric_width=" + metric_width); + } + last_ts = last.timestamp(last.size() - 1); // O(n) + } + + final iHistogramRowSeq rowseq = createRowSequence(tsdb); + rowseq.setRow(key, data_points); + sorted = false; + if (last_ts >= rowseq.timestamp(0)) { + // scan to see if we need to merge into an existing row + for (final iHistogramRowSeq rs : rows) { + if ((rs.key().length == key.length) + && (Bytes.memcmp(rs.key(), key, Const.SALT_WIDTH(), (rs.key().length - Const.SALT_WIDTH())) == 0)) { + rs.addRow(data_points); + return; + } + } + } + + rows.add(rowseq); + } + + /** + * Package private helper to access the last timestamp in an HBase row. + * + * @param metric_width The number of bytes on which metric IDs are stored. + * @param row A compacted HBase row. + * @return A strictly positive timestamp in seconds or ms. + * @throws IllegalArgumentException if {@code row} doesn't contain any cell. + */ + static long lastTimestampInRow(final short metric_width, final KeyValue row) { + final long base_time = Internal.baseTime(row.key()); + final byte[] qual = row.qualifier(); + return Internal.getTimeStampFromNonDP(base_time, qual); + } + + /** + * @return an iterator to run over the list of data points + */ + public HistogramSeekableView iterator() { + checkRowOrder(); + return spanIterator(); + } + + /** + * Finds the index of the row of the ith data point and the offset in the row. + * + * @param i The index of the data point to find. + * @return two ints packed in a long. The first int is the index of the row in + * {@code rows} and the second is offset in that {@link RowSeq} + * instance. + */ + private long getIdxOffsetFor(final int i) { + checkRowOrder(); + int idx = 0; + int offset = 0; + for (final iHistogramRowSeq row : rows) { + final int sz = row.size(); + if (offset + sz > i) { + break; + } + offset += sz; + idx++; + } + return ((long) idx << 32) | (i - offset); + } + + /** + * Returns the timestamp for a data point at index {@code i} if it exists. + * Note: To get to a timestamp this method must walk the entire byte + * array, i.e. O(n) so call this sparingly. Use the iterator instead. + * + * @param i A 0 based index incremented per the number of data points in the + * span. + * @return A Unix epoch timestamp in milliseconds + * @throws IndexOutOfBoundsException + * if the index would be out of bounds + */ + public long timestamp(final int i) { + checkRowOrder(); + final long idxoffset = getIdxOffsetFor(i); + final int idx = (int) (idxoffset >>> 32); + final int offset = (int) (idxoffset & 0x00000000FFFFFFFF); + return rows.get(idx).timestamp(offset); + } + + /** + * Returns a human readable string representation of the object. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("HistogramSpan(").append(rows.size()).append(" rows, ["); + for (int i = 0; i < rows.size(); i++) { + if (i != 0) { + buf.append(", "); + } + buf.append(rows.get(i).toString()); + } + buf.append("])"); + return buf.toString(); + } + + /** + * Finds the index of the row in which the given timestamp should be. + * + * @param timestamp A strictly positive 32-bit integer. + * @return A strictly positive index in the {@code rows} array. + */ + private int seekRow(final long timestamp) { + checkRowOrder(); + int row_index = 0; + iHistogramRowSeq row = null; + final int nrows = rows.size(); + for (int i = 0; i < nrows; i++) { + row = rows.get(i); + final int sz = row.size(); + if (sz < 1) { + row_index++; + } else if (row.timestamp(sz - 1) < timestamp) { + row_index++; // The last DP in this row is before 'timestamp'. + } else { + break; + } + } + if (row_index == nrows) { // If this timestamp was too large for the + --row_index; // last row, return the last row. + } + return row_index; + } + + /** + * Checks the sorted flag and sorts the rows if necessary. Should be called by + * any iteration method. Since 2.0 + */ + private void checkRowOrder() { + if (!sorted) { + Collections.sort(rows, new HistogramRowSeq.HistogramRowSeqComparator()); + sorted = true; + } + } + + /** + * Package private iterator method to access it as a Span.Iterator. + */ + HistogramSpan.Iterator spanIterator() { + checkRowOrder(); + return new HistogramSpan.Iterator(); + } + + /** + * Iterator for {@link HistogramSpan}s. + */ + final class Iterator implements HistogramSeekableView { + + /** + * Index of the {@link HistogramRowSeq} we're currently at, in {@code rows}. + */ + private int row_index; + + /** + * Iterator on the current row. + */ + private iHistogramRowSeq.Iterator current_row; + + Iterator() { + current_row = rows.get(0).internalIterator(); + } + + // ------------------ // + // Iterator interface // + // ------------------ // + + @Override + public boolean hasNext() { + if (current_row.hasNext()) { + return true; + } + // handle situations where a row in the middle may be empty due to some + // kind of logic kicking out data points + while (row_index < rows.size() - 1) { + row_index++; + current_row = rows.get(row_index).internalIterator(); + if (current_row.hasNext()) { + return true; + } + } + return false; + } + + @Override + public HistogramDataPoint next() { + if (current_row.hasNext()) { + return current_row.next(); + } + // handle situations where a row in the middle may be empty due to some + // kind of logic kicking out data points + while (row_index < rows.size() - 1) { + row_index++; + current_row = rows.get(row_index).internalIterator(); + if (current_row.hasNext()) { + return current_row.next(); + } + } + throw new NoSuchElementException("no more elements"); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + // ---------------------- // + // SeekableView interface // + // ---------------------- // + + @Override + public void seek(final long timestamp) { + int row_index = seekRow(timestamp); + if (row_index != this.row_index) { + this.row_index = row_index; + current_row = rows.get(row_index).internalIterator(); + } + current_row.seek(timestamp); + } + + @Override + public String toString() { + return "HistogramSpan.Iterator(row_index=" + row_index + ", current_row=" + current_row + ", span=" + + HistogramSpan.this + ')'; + } + + } + + /** + * + * @param start_time The time in milliseconds at which the data begins. + * @param end_time The time in milliseconds at which the data ends. + * @param downsampler The downsampling specification to use + * @param is_rollup Whether or not the query is handling rollup data + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @return A new downsampler. + * @since 2.3 + */ + HistogramDownsampler downsampler(final long start_time, + final long end_time, + final DownsamplingSpecification downsampler, + final boolean is_rollup, + final long query_start, + final long query_end) { + // ignore the fill policy + return new HistogramDownsampler(spanIterator(), downsampler, query_start, query_end); + } + + /** + * Return the query index that maps this datapoints to the original subquery + * + * @return index of the query in the TSQuery class + */ + public int getQueryIndex() { + throw new UnsupportedOperationException("Span.java: getQueryIndex not supported"); + } + + /** + * RowSeq abstract factory API implementation + * + * @param tsdb The TSDB to which we belong + * @return RowSeq object which stores read-only sequence of continuous HBase + * rows + */ + protected iHistogramRowSeq createRowSequence(TSDB tsdb) { + return new HistogramRowSeq(tsdb); + } +} diff --git a/src/core/Internal.java b/src/core/Internal.java index 229ddba072..442aade1fe 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -21,6 +21,7 @@ import java.util.Map; import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; import org.hbase.async.Bytes; import org.hbase.async.KeyValue; @@ -986,4 +987,60 @@ public static boolean rowKeyMatchsTSUID(final byte[] tsuid, return true; } + + /** + * Get timestamp from base time and quantifier for non datapoints. The returned time + * will always be in ms. + * @param base_time the base time of the point + * @param quantifier the quantifier of the point, it is expected to be either length of + * 3 or length of 5 (the first byte represents the type of the point) + * @return The timestamp in ms + */ + public static long getTimeStampFromNonDP(final long base_time, byte[] quantifier) { + long ret = base_time; + if (quantifier.length == 3) { + ret += quantifier[1] << 8 | (quantifier[2] & 0xFF); + ret *= 1000; + } else if (quantifier.length == 5) { + ret *= 1000; + ret += (quantifier[1] & 0xFF) << 24 | (quantifier[2] & 0xFF) << 16 + | (quantifier[3] & 0xFF) << 8 | quantifier[4] & 0xFF; + } else { + throw new IllegalArgumentException("Quantifier is not valid: " + Bytes.pretty(quantifier)); + } + + return ret; + + } + + /** + * Decode the histogram point from the given key value + * @param kv the key value that contains a histogram + * @param config config object of TSDB, will use {@code "tsd.core.hist_decoder"} + * to get the decoder + * @return the decoded {@code HistogramDataPoint} + */ + public static HistogramDataPoint decodeHistogramDataPoint(final KeyValue kv, final Config config) { + long timestamp = Internal.baseTime(kv.key()); + return decodeHistogramDataPoint(timestamp, kv.qualifier(), kv.value(), config); + } + + /** + * Decode the histogram point from the given key and values + * @param base_time the base time of the histogram + * @param qualifier the qualifier used to store the histogram + * @param value the encoded value of the histogram + * @param config config object of TSDB, will use {@code "tsd.core.hist_decoder"} + * to get the decoder + * @return the decoded {@code HistogramDataPoint} + */ + public static HistogramDataPoint decodeHistogramDataPoint(final long base_time, final byte[] qualifier, + final byte[] value, final Config config) { + final String decoder_name = config.hist_decoder_name(); + final HistogramDataPointDecoder decoder = + HistogramDataPointDecoderManager.getDecoder(decoder_name); + long timestamp = getTimeStampFromNonDP(base_time, qualifier); + return decoder.decode(value, timestamp); + } + } diff --git a/src/utils/Config.java b/src/utils/Config.java index 8ce9a97852..66e4c6cece 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -119,6 +119,8 @@ public class Config { /** If set to true, the maximum value will be returned, minimum */ private boolean use_max_value = true; + private String hist_decoder_name; + /** * The list of properties configured to their defaults or modified by users */ @@ -289,6 +291,11 @@ public boolean use_max_value() { return use_max_value; } + /** @return The full class name of the decoder for histogram data points */ + public String hist_decoder_name() { + return hist_decoder_name; + } + /** * Allows for modifying properties after creation or loading. * @@ -709,6 +716,7 @@ public void loadStaticVariables() { mul_get_cocurrency_number = this.getInt("tsd.core.mul_get_cocurrency_number"); use_otsdb_timestamp = this.getBoolean("tsd.storage.use_otsdb_timestamp"); use_max_value = this.getBoolean("tsd.storage.use_max_value"); + hist_decoder_name = this.getString("tsd.core.hist_decoder"); } /** From ca68dc728f27d674764f525a7451183d515f11b1 Mon Sep 17 00:00:00 2001 From: qiubz Date: Sat, 27 May 2017 12:33:10 -0700 Subject: [PATCH 629/826] Add percentiles to the DataPoints interface and add the histo bucket adaptor. Signed-off-by: Chris Larsen --- src/core/BatchedDataPoints.java | 9 + src/core/DataPoints.java | 17 ++ .../HistogramBucketDataPointsAdaptor.java | 251 ++++++++++++++++++ src/core/IncomingDataPoints.java | 10 + src/core/RowSeq.java | 9 + src/core/Span.java | 9 + src/core/SpanGroup.java | 10 + src/query/expression/EDPtoDPS.java | 10 + .../expression/PostAggregatedDataPoints.java | 10 + src/rollup/RollupSeq.java | 10 + 10 files changed, 345 insertions(+) create mode 100644 src/core/HistogramBucketDataPointsAdaptor.java diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 82c32063dd..12d990204d 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -511,4 +511,13 @@ public String toString() { public int getQueryIndex() { throw new UnsupportedOperationException("Not mapped to a query"); } + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } } diff --git a/src/core/DataPoints.java b/src/core/DataPoints.java index c9a4930992..a728a158b9 100644 --- a/src/core/DataPoints.java +++ b/src/core/DataPoints.java @@ -218,4 +218,21 @@ public interface DataPoints extends Iterable { * @since 2.2 */ int 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 + */ + boolean isPercentile(); + + /** + * Return the percentile calculation parameter. This interface and {@code isPercentile} are used + * to convert {@code HistogramDataPoints} to {@code DataPoints} + * + * @return the percentile parameter + */ + float getPercentile(); } diff --git a/src/core/HistogramBucketDataPointsAdaptor.java b/src/core/HistogramBucketDataPointsAdaptor.java new file mode 100644 index 0000000000..35252be9fc --- /dev/null +++ b/src/core/HistogramBucketDataPointsAdaptor.java @@ -0,0 +1,251 @@ +package net.opentsdb.core; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hbase.async.Bytes.ByteMap; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; +import net.opentsdb.meta.Annotation; + +public class HistogramBucketDataPointsAdaptor implements DataPoints { + private final HistogramDataPoints hist_data_points; + private final HistogramDataPoint.HistogramBucket bucket; + + HistogramBucketDataPointsAdaptor(final HistogramDataPoints hists, final HistogramDataPoint.HistogramBucket bucket) { + this.hist_data_points = hists; + this.bucket = bucket; + } + + @Override + public String metricName() { + return (this.hist_data_points.metricName() + metricNamePostfix()); + } + + @Override + public Deferred metricNameAsync() { + return this.hist_data_points.metricNameAsync().addCallback(new Callback() { + public String call(final String name) { + return name + metricNamePostfix(); + } + }); + } + + @Override + public byte[] metricUID() { + return this.hist_data_points.metricUID(); + } + + @Override + public Map getTags() { + return this.hist_data_points.getTags(); + } + + @Override + public Deferred> getTagsAsync() { + return this.hist_data_points.getTagsAsync(); + } + + @Override + public ByteMap getTagUids() { + return this.hist_data_points.getTagUids(); + } + + @Override + public List getAggregatedTags() { + return this.hist_data_points.getAggregatedTags(); + } + + @Override + public Deferred> getAggregatedTagsAsync() { + return this.hist_data_points.getAggregatedTagsAsync(); + } + + @Override + public List getAggregatedTagUids() { + return this.hist_data_points.getAggregatedTagUids(); + } + + @Override + public List getTSUIDs() { + return this.hist_data_points.getTSUIDs(); + } + + @Override + public List getAnnotations() { + return this.hist_data_points.getAnnotations(); + } + + @Override + public int size() { + return this.hist_data_points.size(); + } + + @Override + public int aggregatedSize() { + return this.hist_data_points.aggregatedSize(); + } + + @Override + public SeekableView iterator() { + return internalIterator(); + } + + private HistogramDataPoint getHistogramDataPoint(int i) { + if (i < 0) { + throw new IndexOutOfBoundsException("negative index: " + i); + } + final int saved_i = i; + final HistogramSeekableView it = this.hist_data_points.iterator(); + HistogramDataPoint 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; + } + + @Override + public long timestamp(int i) { + return getHistogramDataPoint(i).timestamp(); + } + + @Override + public boolean isInteger(int i) { + return true; + } + + @Override + public long longValue(int i) { + HistogramDataPoint hdp = this.getHistogramDataPoint(i); + + try { + Map buckets = hdp.getHistogramBucketsIfHas(); + if (null != buckets && buckets.containsKey(bucket)) { + return buckets.get(bucket).longValue(); + } + } catch (UnsupportedOperationException e) { + // Just ignore + } + + return 0; + } + + @Override + public double doubleValue(int i) { + return this.longValue(i); + } + + @Override + public int getQueryIndex() { + return this.hist_data_points.getQueryIndex(); + } + + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + return 0; + } + + private String metricNamePostfix() { + if (this.bucket.bucketType() == HistogramDataPoint.HistogramBucket.BucketType.UNDERFLOW) { + return "_UNDERFLOW"; + } else if (this.bucket.bucketType() == HistogramDataPoint.HistogramBucket.BucketType.OVERFLOW) { + return "_OVERFLOW"; + } else { + StringBuilder sb = new StringBuilder(); + sb.append("_").append(this.bucket.getLowerBound()).append("_") + .append(this.bucket.getUpperBound()); + return sb.toString(); + } + } + + private Iterator internalIterator() { + return new Iterator(); + } + + ////////////////////////////////////////////////////////////////////////////////// + // internal iterator + ///////////////////////////////////////////////////////////////////////////////// + final class Iterator implements SeekableView, DataPoint { + final private HistogramSeekableView source; + private long value; + private long timestamp; + + public Iterator() { + this.source = hist_data_points.iterator(); + } + + @Override + public boolean hasNext() { + return this.source.hasNext(); + } + + @Override + public DataPoint next() { + HistogramDataPoint hdp = this.source.next(); + + this.value = 0; + try { + Map buckets = hdp.getHistogramBucketsIfHas(); + if (null != buckets && buckets.containsKey(bucket)) { + this.value = buckets.get(bucket).longValue(); + } + } catch (UnsupportedOperationException e) { + // Just ignore + } + + this.timestamp = hdp.timestamp(); + return this; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove is not supported here"); + } + + @Override + public void seek(long timestamp) { + this.source.seek(timestamp); + } + + @Override + public long timestamp() { + return this.timestamp; + } + + @Override + public boolean isInteger() { + return true; + } + + @Override + public long longValue() { + return this.value; + } + + @Override + public double doubleValue() { + throw new ClassCastException("value #" + " is not a long in " + this); + } + + @Override + public double toDouble() { + return this.value; + } + + @Override + public long valueCount() { + return 0; + } + } +} diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 14394fc6b3..0456e47be2 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -604,4 +604,14 @@ public Deferred persist() { public int getQueryIndex() { throw new UnsupportedOperationException("Not mapped to a query"); } + + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } } diff --git a/src/core/RowSeq.java b/src/core/RowSeq.java index 0434db0618..e614ff40b9 100644 --- a/src/core/RowSeq.java +++ b/src/core/RowSeq.java @@ -687,4 +687,13 @@ public long valueCount() { public int getQueryIndex() { throw new UnsupportedOperationException("Not mapped to a query"); } + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } } diff --git a/src/core/Span.java b/src/core/Span.java index 8384ecb742..02a16e07f8 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -571,4 +571,13 @@ protected iRowSeq createRowSequence(TSDB tsdb) { public int getQueryIndex() { throw new UnsupportedOperationException("Not mapped to a query"); } + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } } diff --git a/src/core/SpanGroup.java b/src/core/SpanGroup.java index 5baed6edeb..94bed154d0 100644 --- a/src/core/SpanGroup.java +++ b/src/core/SpanGroup.java @@ -592,7 +592,17 @@ private String toStringSharedAttributes() { public int getQueryIndex() { return query_index; } + + @Override + public boolean isPercentile() { + return false; + } + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } + /** * Resolves the set of tag keys to their string names. * @param tagks The set of unique tag names diff --git a/src/query/expression/EDPtoDPS.java b/src/query/expression/EDPtoDPS.java index 4c17ff70e6..5666ce34e4 100644 --- a/src/query/expression/EDPtoDPS.java +++ b/src/query/expression/EDPtoDPS.java @@ -212,6 +212,16 @@ public int getQueryIndex() { return 0; } + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } + /** * Simple class that fills the local data point while iterating through the * expression data points at the proper index. diff --git a/src/query/expression/PostAggregatedDataPoints.java b/src/query/expression/PostAggregatedDataPoints.java index 2af4bf47ff..c54a1c2c00 100644 --- a/src/query/expression/PostAggregatedDataPoints.java +++ b/src/query/expression/PostAggregatedDataPoints.java @@ -200,6 +200,16 @@ public double doubleValue(int i) { return points[i].doubleValue(); } + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } + /** * An iterator working over the data points resulting from the expression * calculation. diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java index 6ecbea2c5a..478d6a1571 100644 --- a/src/rollup/RollupSeq.java +++ b/src/rollup/RollupSeq.java @@ -461,6 +461,16 @@ public Iterator internalIterator() { return new RollupIterator(); } + @Override + public boolean isPercentile() { + return false; + } + + @Override + public float getPercentile() { + throw new UnsupportedOperationException("getPercentile not supported"); + } + /** Iterator for {@link RowSeq}s. */ public final class RollupIterator implements iRowSeq.Iterator { From 1689fee9062209c3e397622d398fa7022d2a1424 Mon Sep 17 00:00:00 2001 From: qiubz Date: Sat, 27 May 2017 12:42:40 -0700 Subject: [PATCH 630/826] Add the Histo span group and data points to points adaptor. Add a bunch of unit tests and utilities for the tests. Signed-off-by: Chris Larsen --- ...istogramDataPointsToDataPointsAdaptor.java | 220 +++ src/core/HistogramSpanGroup.java | 521 +++++++ test/core/BaseTsdbTest.java | 144 +- test/core/HistogramSeekableViewForTest.java | 232 +++ test/core/LongHistogramDataPointForTest.java | 106 ++ .../LongHistogramDataPointForTestDecoder.java | 10 + .../TestHistogramAggregationIterator.java | 512 +++++++ ...istogramDataPointsToDataPointsAdaptor.java | 395 +++++ test/core/TestHistogramDownsampler.java | 1334 +++++++++++++++++ test/core/TestHistogramRowSeq.java | 427 ++++++ test/core/TestHistogramSpan.java | 266 ++++ test/core/TestHistogramSpanGroup.java | 230 +++ 12 files changed, 4392 insertions(+), 5 deletions(-) create mode 100644 src/core/HistogramDataPointsToDataPointsAdaptor.java create mode 100644 src/core/HistogramSpanGroup.java create mode 100644 test/core/HistogramSeekableViewForTest.java create mode 100644 test/core/LongHistogramDataPointForTest.java create mode 100644 test/core/LongHistogramDataPointForTestDecoder.java create mode 100644 test/core/TestHistogramAggregationIterator.java create mode 100644 test/core/TestHistogramDataPointsToDataPointsAdaptor.java create mode 100644 test/core/TestHistogramDownsampler.java create mode 100644 test/core/TestHistogramRowSeq.java create mode 100644 test/core/TestHistogramSpan.java create mode 100644 test/core/TestHistogramSpanGroup.java diff --git a/src/core/HistogramDataPointsToDataPointsAdaptor.java b/src/core/HistogramDataPointsToDataPointsAdaptor.java new file mode 100644 index 0000000000..b87162b544 --- /dev/null +++ b/src/core/HistogramDataPointsToDataPointsAdaptor.java @@ -0,0 +1,220 @@ +package net.opentsdb.core; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.hbase.async.Bytes.ByteMap; + +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.meta.Annotation; + +public class HistogramDataPointsToDataPointsAdaptor implements DataPoints { + final private HistogramDataPoints hist_data_points; + final private float percentile; + + + public HistogramDataPointsToDataPointsAdaptor(final HistogramDataPoints hdps, final float percentile) { + this.hist_data_points = hdps; + this.percentile = percentile; + } + + @Override + public String metricName() { + return this.hist_data_points.metricName() + "_pct_" + Float.toString(this.percentile); + } + + @Override + public Deferred metricNameAsync() { + return this.hist_data_points.metricNameAsync().addCallback(new Callback() { + public String call(final String name) { + return name + "_pct_" + Float.toString(percentile); + } + }); + } + + @Override + public byte[] metricUID() { + return this.hist_data_points.metricUID(); + } + + @Override + public Map getTags() { + return this.hist_data_points.getTags(); + } + + @Override + public Deferred> getTagsAsync() { + return this.hist_data_points.getTagsAsync(); + } + + @Override + public ByteMap getTagUids() { + return this.hist_data_points.getTagUids(); + } + + @Override + public List getAggregatedTags() { + return this.hist_data_points.getAggregatedTags(); + } + + @Override + public Deferred> getAggregatedTagsAsync() { + return this.hist_data_points.getAggregatedTagsAsync(); + } + + @Override + public List getAggregatedTagUids() { + return this.hist_data_points.getAggregatedTagUids(); + } + + @Override + public List getTSUIDs() { + return this.hist_data_points.getTSUIDs(); + } + + @Override + public List getAnnotations() { + return this.hist_data_points.getAnnotations(); + } + + @Override + public int size() { + return this.hist_data_points.size(); + } + + @Override + public int aggregatedSize() { + return this.hist_data_points.aggregatedSize(); + } + + @Override + public SeekableView iterator() { + return internalIterator(); + } + + private HistogramDataPoint getHistogramDataPoint(int i) { + if (i < 0) { + throw new IndexOutOfBoundsException("negative index: " + i); + } + final int saved_i = i; + final HistogramSeekableView it = this.hist_data_points.iterator(); + HistogramDataPoint 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; + } + + @Override + public long timestamp(int i) { + return getHistogramDataPoint(i).timestamp(); + } + + @Override + public boolean isInteger(int i) { + return false; + } + + @Override + public long longValue(int i) { + throw new ClassCastException("value #" + i + " is not a long in " + this); + } + + @Override + public double doubleValue(int i) { + return getHistogramDataPoint(i).percentile(this.percentile); + } + + @Override + public int getQueryIndex() { + return this.hist_data_points.getQueryIndex(); + } + + @Override + public boolean isPercentile() { + return true; + } + + @Override + public float getPercentile() { + return this.percentile; + } + + private Iterator internalIterator() { + return new Iterator(); + } + + ////////////////////////////////////////////////////////////////////////////////// + // internal iterator + ///////////////////////////////////////////////////////////////////////////////// + final class Iterator implements SeekableView, DataPoint { + final private HistogramSeekableView source; + private double value; + private long timestamp; + + public Iterator() { + this.source = hist_data_points.iterator(); + } + + @Override + public boolean hasNext() { + return this.source.hasNext(); + } + + @Override + public DataPoint next() { + HistogramDataPoint hdp = this.source.next(); + this.value = hdp.percentile(percentile); + this.timestamp = hdp.timestamp(); + return this; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove is not supported here"); + } + + @Override + public void seek(long timestamp) { + this.source.seek(timestamp); + } + + @Override + public long timestamp() { + return this.timestamp; + } + + @Override + public boolean isInteger() { + return false; + } + + @Override + public long longValue() { + throw new ClassCastException("value #" + " is not a long in " + this); + } + + @Override + public double doubleValue() { + return this.value; + } + + @Override + public double toDouble() { + return this.value; + } + + @Override + public long valueCount() { + return 0; + } + } +} diff --git a/src/core/HistogramSpanGroup.java b/src/core/HistogramSpanGroup.java new file mode 100644 index 0000000000..9e95b0d77b --- /dev/null +++ b/src/core/HistogramSpanGroup.java @@ -0,0 +1,521 @@ +// 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.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 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.utils.ByteSet; + +final class HistogramSpanGroup implements HistogramDataPoints { + + /** Annotations */ + private final ArrayList annotations; + + /** Start time (UNIX timestamp in seconds or ms) on 32 bits ("unsigned" int). */ + private final long start_time; + + /** End time (UNIX timestamp in seconds or ms) on 32 bits ("unsigned" int). */ + private final long end_time; + + /** + * The tags of this group as names and UIDs + * This is the intersection set between the tags of all the Spans + * in this group. + * @see #computeTags + */ + private Map tags; + private ByteMap tag_uids; + + /** + * The names of the tags that aren't shared by every single data point. + * This is the symmetric difference between the tags of all the Spans + * in this group. + * @see #computeTags + */ + private List aggregated_tags; + private Set aggregated_tag_uids; + + /** Spans in this group. They must all be for the same metric. */ + private final ArrayList spans = new ArrayList(); + + /** Aggregation to use to aggregate data points from different Spans. */ + private final HistogramAggregation aggregation; + + /** + * Downsampling spec to use, if any (can be {@code null}). + */ + private final DownsamplingSpecification downsampler; + + /** Start timestamp of the query for filtering */ + private final long query_start; + + /** End timestamp of the query for filtering */ + private final long query_end; + + /** index of the query in the TSQuery class */ + private int query_index; + + /** The TSDB to which we belong, used for resolution */ + private final TSDB tsdb; + + /** If non-null, a list of tags we're grouping on in the query for determining + * whether or not a tag should be moved to the agg tags list */ + private final ByteSet query_tags; + + /** whether we are handling rollup data points*/ + private final boolean is_rollup; + + + /** + * Ctor. + * @param tsdb The TSDB we belong to. + * @param start_time Any data point strictly before this timestamp will be ignored. + * @param end_time Any data point strictly after this timestamp will be ignored. + * @param spans A sequence of initial {@link HistogramSpan} to add to this group. + * Ignored if {@code null}. Additional spans can be added with {@link #add}. + * @param aggregation The aggregation function to use. + * @param downsampler The downsampling specification to use + * @param query_start Start of the actual query + * @param query_end End of the actual query + * @param query_index Index of the sub query + */ + HistogramSpanGroup(final TSDB tsdb, + final long start_time, + final long end_time, + final Iterable spans, + final HistogramAggregation aggregation, + final DownsamplingSpecification downsampler, + final long query_start, + final long query_end, + final int query_index, + final boolean is_rollup, + final ByteSet query_tags) { + annotations = new ArrayList(); + this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; + this.end_time = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; + if (spans != null) { + for (final HistogramSpan span : spans) { + add(span); + } + } + + this.aggregation = aggregation; + this.downsampler = downsampler; + this.query_start = query_start; + this.query_end = query_end; + this.query_index = query_index; + this.tsdb = tsdb; + this.is_rollup = is_rollup; + this.query_tags = query_tags; + } + + public String getWarning() { + // only one of these should be set so grab the first we find. It *should* + // be the first one in the list if this was sorted properly. + for (final HistogramSpan span : spans) { + if (span.getWarning() != null) { + return span.getWarning(); + } + } + return null; + } + + /** + * Adds a span to this group, provided that it's in the right time range. + * Must not be called once {@link #getTags} or + * {@link #getAggregatedTags} has been called on this instance. + * @param span The span to add to this group. If none of the data points + * fall within our time range, this method will silently ignore that span. + */ + void add(final HistogramSpan span) { + if (tags != null) { + throw new AssertionError("The set of tags has already been computed" + ", you can't add more Spans to " + this); + } + + // normalize timestamps to milliseconds for proper comparison + final long start = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; + final long end = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; + + if (span.size() == 0) { + // copy annotations that are in the time range + for (Annotation annot : span.getAnnotations()) { + long annot_start = annot.getStartTime(); + if ((annot_start & Const.SECOND_MASK) == 0) { + annot_start *= 1000; + } + long annot_end = annot.getStartTime(); + if ((annot_end & Const.SECOND_MASK) == 0) { + annot_end *= 1000; + } + if (annot_end >= start && annot_start <= end) { + annotations.add(annot); + } + } + } else { + long first_dp = span.timestamp(0); + if ((first_dp & Const.SECOND_MASK) == 0) { + first_dp *= 1000; + } + // The following call to timestamp() will throw an + // IndexOutOfBoundsException if size == 0, which is OK since it would + // be a programming error. + long last_dp = span.timestamp(span.size() - 1); + if ((last_dp & Const.SECOND_MASK) == 0) { + last_dp *= 1000; + } + if (first_dp <= end && last_dp >= start) { + this.spans.add(span); + annotations.addAll(span.getAnnotations()); + } + } + } + + public String metricName() { + try { + return metricNameAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the metric name call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred metricNameAsync() { + return spans.isEmpty() ? Deferred.fromResult("") : + spans.get(0).metricNameAsync(); + } + + public byte[] metricUID() { + return spans.isEmpty() ? new byte[] {} : spans.get(0).metricUID(); + } + + public Map getTags() { + try { + return getTagsAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the tags call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred> getTagsAsync() { + if (tags != null) { + return Deferred.fromResult(tags); + } + + if (spans.isEmpty()) { + tags = new HashMap(0); + return Deferred.fromResult(tags); + } + + if (tag_uids == null) { + computeTags(); + } + + return resolveTags(tag_uids); + } + + @Override + public ByteMap getTagUids() { + if (tag_uids == null) { + computeTags(); + } + return tag_uids; + } + + public List getAggregatedTags() { + try { + return getAggregatedTagsAsync().join(); + } catch (InterruptedException iex) { + throw new RuntimeException("Interrupted the aggregated tags call", iex); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Should never be here", e); + } + } + + public Deferred> getAggregatedTagsAsync() { + if (aggregated_tags != null) { + return Deferred.fromResult(aggregated_tags); + } + + if (spans.isEmpty()) { + aggregated_tags = new ArrayList(0); + return Deferred.fromResult(aggregated_tags); + } + + if (aggregated_tag_uids == null) { + computeTags(); + } + + return resolveAggTags(aggregated_tag_uids); + } + + @Override + public List getAggregatedTagUids() { + if (aggregated_tag_uids != null) { + return new ArrayList(aggregated_tag_uids); + } + + if (spans.isEmpty()) { + return Collections.emptyList(); + } + + if (aggregated_tag_uids == null) { + computeTags(); + } + return new ArrayList(aggregated_tag_uids); + } + + public List getTSUIDs() { + List tsuids = new ArrayList(spans.size()); + for (HistogramSpan hsp : spans) { + tsuids.addAll(hsp.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 + */ + public List getAnnotations() { + return annotations.isEmpty() ? null : annotations; + } + + public int size() { + // TODO(tsuna): There is a way of doing this way more efficiently by + // inspecting the Spans and counting only data points that fall in + // our time range. + final HistogramSeekableView it = iterator(); + int size = 0; + while (it.hasNext()) { + it.next(); + size++; + } + return size; + } + + public int aggregatedSize() { + int size = 0; + for (final HistogramSpan span : spans) { + size += span.size(); + } + return size; + } + + public HistogramSeekableView iterator() { + return HistogramAggregationIterator.create(this.spans, this.start_time, this.end_time, this.aggregation, + this.downsampler, this.query_start, this.query_end, this.is_rollup); + } + + /** + * 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 HistogramDataPoint getDataPoint(int i) { + if (i < 0) { + throw new IndexOutOfBoundsException("negative index: " + i); + } + final int saved_i = i; + final HistogramSeekableView it = iterator(); + HistogramDataPoint 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(); + } + + public String toString() { + return "HistogramSpanGroup(" + toStringSharedAttributes() + + ", spans=" + spans + + ')'; + } + + private String toStringSharedAttributes() { + return "start_time=" + start_time + + ", end_time=" + end_time + + ", tags=" + tags + + ", aggregated_tags=" + aggregated_tags + + ", aggregator=" + aggregation + + ", downsampler=" + downsampler + + ')'; + } + + /** + * Return the query index that maps this datapoints to the original subquery + * @return index of the query in the TSQuery class + */ + public int getQueryIndex() { + return this.query_index; + } + + /** + * Computes the intersection set + symmetric difference of tags in all spans. + * This method loads the UID aggregated list and tag pair maps with byte arrays + * but does not actually resolve the UIDs to strings. + * On the first run, it will initialize the UID collections (which may be empty) + * and subsequent calls will skip processing. + */ + private void computeTags() { + if (tag_uids != null && aggregated_tag_uids != null) { + return; + } + if (spans.isEmpty()) { + tag_uids = new ByteMap(); + aggregated_tag_uids = new HashSet(); + return; + } + + // local tag uids + final ByteMap tag_set = new ByteMap(); + + // value is always null, we just want the set of unique keys + final ByteMap discards = new ByteMap(); + final Iterator it = spans.iterator(); + while (it.hasNext()) { + final HistogramSpan span = it.next(); + final ByteMap uids = span.getTagUids(); + + for (final Map.Entry tag_pair : uids.entrySet()) { + // we already know it's an aggregated tag + if (discards.containsKey(tag_pair.getKey())) { + continue; + } else if (query_tags != null && !query_tags.contains(tag_pair.getKey())) { + discards.put(tag_pair.getKey(), null); + continue; + } + + final byte[] tag_value = tag_set.get(tag_pair.getKey()); + if (tag_value == null) { + tag_set.put(tag_pair.getKey(), tag_pair.getValue()); + } else if (Bytes.memcmp(tag_value, tag_pair.getValue()) != 0) { + // bump to aggregated tags + discards.put(tag_pair.getKey(), null); + tag_set.remove(tag_pair.getKey()); + } + } + } + + aggregated_tag_uids = discards.keySet(); + tag_uids = tag_set; + } + + /** + * Resolves the set of tag keys to their string names. + * @param tagks The set of unique tag names + * @return a deferred to wait on for all of the tag keys to be resolved. The + * result should be null. + */ + private Deferred> resolveAggTags(final Set tagks) { + if (aggregated_tags != null) { + return Deferred.fromResult(null); + } + aggregated_tags = new ArrayList(tagks.size()); + + final List> names = + new ArrayList>(tagks.size()); + for (final byte[] tagk : tagks) { + names.add(tsdb.tag_names.getNameAsync(tagk)); + } + + /** Adds the names to the aggregated_tags list */ + final class ResolveCB implements Callback, ArrayList> { + @Override + public List call(final ArrayList names) throws Exception { + for (final String name : names) { + aggregated_tags.add(name); + } + return aggregated_tags; + } + } + + return Deferred.group(names).addCallback(new ResolveCB()); + } + + /** + * Resolves the tags to their names, loading them into {@link tags} after + * initializing that map. + * @param tag_uids The tag UIDs + * @return A defeferred to wait on for resolution to complete, the result + * should be null. + */ + private Deferred> resolveTags(final ByteMap tag_uids) { + if (tags != null) { + return Deferred.fromResult(null); + } + tags = new HashMap(tag_uids.size()); + + final List> deferreds = + new ArrayList>(tag_uids.size()); + + /** Dumps the pairs into the map in the correct order */ + final class PairCB implements Callback> { + @Override + public Object call(final ArrayList pair) throws Exception { + tags.put(pair.get(0), pair.get(1)); + return null; + } + } + + /** Callback executed once all of the pairs are resolved and stored in the map */ + final class GroupCB implements Callback, ArrayList> { + @Override + public Map call(final ArrayList group) + throws Exception { + return tags; + } + } + + for (Map.Entry tag_pair : tag_uids.entrySet()) { + final List> resolve_pair = + new ArrayList>(2); + resolve_pair.add(tsdb.tag_names.getNameAsync(tag_pair.getKey())); + resolve_pair.add(tsdb.tag_values.getNameAsync(tag_pair.getValue())); + deferreds.add(Deferred.groupInOrder(resolve_pair).addCallback(new PairCB())); + } + + return Deferred.group(deferreds).addCallback(new GroupCB()); + } +} diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 912639a919..c50ecf96e4 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -371,16 +371,23 @@ protected byte[] getRowKeyTemplate() { * @param tags A non-null list of tag key/value pairs as UIDs. * @return A row key to check mock storage for. */ - protected byte[] getRowKey(final byte[] metric, final int base_time, - final byte[] tags) { + public static byte[] getRowKey(final byte[] metric, final int base_time, + final byte[]... tags) { + int tags_length = 0; + for (final byte[] tag : tags) { + tags_length += tag.length; + } final byte[] key = new byte[Const.SALT_WIDTH() + metric.length + - Const.TIMESTAMP_BYTES + tags.length]; + Const.TIMESTAMP_BYTES + tags_length]; System.arraycopy(metric, 0, key, Const.SALT_WIDTH(), metric.length); System.arraycopy(Bytes.fromInt(base_time), 0, key, Const.SALT_WIDTH() + metric.length, Const.TIMESTAMP_BYTES); - System.arraycopy(tags, 0, key, Const.SALT_WIDTH() + metric.length + - Const.TIMESTAMP_BYTES, tags.length); + int offset = Const.SALT_WIDTH() + metric.length + Const.TIMESTAMP_BYTES; + for (final byte[] tag : tags) { + System.arraycopy(tag, 0, key, offset, tag.length); + offset += tag.length; + } RowKey.prefixKeyWithSalt(key); return key; } @@ -446,6 +453,133 @@ protected byte[] getRowKey(final String metric, final int base_time, return key; } + /** + * Generates a TSUID given the metric and tag UIDs. + * @param metric A metric UID. + * @param tags A set of UIDs + * @return A TSUID byte array + */ + public static byte[] getTSUID(final byte[] metric, final byte[]... tags) { + int tags_length = 0; + for (final byte[] tag : tags) { + tags_length += tag.length; + } + final byte[] tsuid = new byte[metric.length + tags_length]; + System.arraycopy(metric, 0, tsuid, 0, metric.length); + int offset = metric.length; + for (final byte[] tag : tags) { + System.arraycopy(tag, 0, tsuid, offset, tag.length); + offset += tag.length; + } + RowKey.prefixKeyWithSalt(tsuid); + return tsuid; + } + + /** + * Generates a UID of the proper length given a type and ID. + * @param type The type of UID. + * @param id The ID to set (just tweaks the last byte) + * @return A Unique ID of the proper width. + */ + public static byte[] generateUID(final UniqueIdType type, byte id) { + final byte[] uid; + switch (type) { + case METRIC: + uid = new byte[TSDB.metrics_width()]; + break; + case TAGK: + uid = new byte[TSDB.tagk_width()]; + break; + case TAGV: + uid = new byte[TSDB.tagv_width()]; + break; + default: + throw new IllegalArgumentException("Yo! You have to mock out " + type + "!"); + } + uid[uid.length - 1] = id; + return uid; + } + + /** + * Generates a UID of the proper length given a type and ID. + * @param type The type of UID. + * @param id The ID to set (just tweaks the last byte) + * @return A Unique ID of the proper width. + */ + public static String generateUIDString(final UniqueIdType type, byte id) { + return UniqueId.uidToString(generateUID(type, id)); + } + + /** + * Generates a TSUID given the metric and tag UIDs. + * @param metric A metric UID. + * @param tags A set of UIDs + * @return A TSUID as a hex string + */ + public static String getTSUIDString(final byte[] metric, final byte[]... tags) { + return UniqueId.uidToString(getTSUID(metric, tags)); + } + + /** + * Generates a TSUID given the mocked UID strings. + * @param metric A mocked metric name. + * @param tags A set of mocked tag key and values. + * @return A TSUID byte array + */ + protected byte[] getTSUID(final String metric, final String... tags) { + final int m = TSDB.metrics_width(); + final int tk = TSDB.tagk_width(); + final int tv = TSDB.tagv_width(); + + final byte[] tsuid = new byte[m + (tags.length / 2) * tk + (tags.length / 2) * tv]; + byte[] uid = uid_map.get(metric); + + // metrics first + if (uid != null) { + System.arraycopy(uid, 0, tsuid, 0, m); + } else { + throw new IllegalArgumentException("No METRIC UID was mocked for: " + metric); + } + + int ctr = 0; + int offset = 0; + for (final String tag : tags) { + uid = uid_map.get(tag); + + if (ctr % 2 == 0) { + // TAGK + if (uid != null) { + System.arraycopy(uid, 0, tsuid, m + offset, tk); + } else { + throw new IllegalArgumentException("No TAGK UID was mocked for: " + tag); + } + offset += tk; + } else { + // TAGV + if (uid != null) { + System.arraycopy(uid, 0, tsuid, m + offset, tv); + } else { + throw new IllegalArgumentException("No TAGK UID was mocked for: " + tag); + } + offset += tv; + } + + ctr++; + } + + return tsuid; + } + + /** + * Generates a TSUID given the mocked UID strings. + * @param metric A mocked metric name. + * @param tags A set of mocked tag key and values. + * @return A TSUID hex string. + */ + protected String getTSUIDString(final String metric, final String... tags) { + return UniqueId.uidToString(getTSUID(metric, tags)); + } + protected void setDataPointStorage() throws Exception { storage = new MockBase(tsdb, client, true, true, true, true); storage.setFamily("t".getBytes(MockBase.ASCII())); diff --git a/test/core/HistogramSeekableViewForTest.java b/test/core/HistogramSeekableViewForTest.java new file mode 100644 index 0000000000..dd376c66e4 --- /dev/null +++ b/test/core/HistogramSeekableViewForTest.java @@ -0,0 +1,232 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.NoSuchElementException; + +import org.hbase.async.Bytes; +import org.junit.Ignore; +import org.junit.Test; + +/** Helper class to mock HistogramSeekableView. */ +@Ignore +public class HistogramSeekableViewForTest { + + /** + * Creates a {@link HistogramSeekableView} object to iterate the given data points. + * @param data_points Test data. + * @return A {@link HistogramSeekableView} object + */ + public static HistogramSeekableView fromArray(final HistogramDataPoint[] data_points) { + return new MockHistogramSeekableView(data_points); + } + + /** + * Creates a {@link HistogramSeekableView} that generates a sequence of data points. + * @param start_time Starting timestamp + * @param sample_period Average sample period of data points + * @param num_data_points Total number of data points to generate + * @return A {@link HistogramSeekableView} object + */ + public static HistogramSeekableView generator(final long start_time, + final long sample_period, + final int num_data_points) { + return new DataPointGenerator(start_time, sample_period, num_data_points); + } + + /** Iterates an array of data points. */ + public static class MockHistogramSeekableView implements HistogramSeekableView { + + private final HistogramDataPoint[] data_points; + private int index = 0; + + MockHistogramSeekableView(final HistogramDataPoint[] data_points2) { + this.data_points = data_points2; + } + + @Override + public boolean hasNext() { + return data_points.length > index; + } + + @Override + public HistogramDataPoint next() { + if (hasNext()) { + return data_points[index++]; + } + throw new NoSuchElementException("no more values"); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void seek(long timestamp) { + for (index = 0; index < data_points.length; ++index) { + if (data_points[index].timestamp() >= timestamp) { + break; + } + } + } + + public void resetIndex() { + index = 0; + } + } + + /** Generates a sequence of data points. */ + private static class DataPointGenerator implements HistogramSeekableView { + + private final long start_time_ms; + private final long sample_period_ms; + private final int num_data_points; + private final LongHistogramDataPointForTest current_data = new LongHistogramDataPointForTest(100L, Bytes.fromLong(0L)); + private int current = 0; + + DataPointGenerator(final long start_time_ms, final long sample_period_ms, + final int num_data_points) { + this.start_time_ms = start_time_ms; + this.sample_period_ms = sample_period_ms; + this.num_data_points = num_data_points; + rewind(); + } + + @Override + public boolean hasNext() { + return current < num_data_points; + } + + @Override + public HistogramDataPoint next() { + if (hasNext()) { + generateData(); + ++current; + return current_data; + } + throw new NoSuchElementException("no more values"); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void seek(long timestamp) { + rewind(); + current = (int)((timestamp -1 - start_time_ms) / sample_period_ms); + if (current < 0) { + current = 0; + } + while (generateTimestamp() < timestamp) { + ++current; + } + } + + private void rewind() { + current = 0; + generateData(); + } + + private void generateData() { + current_data.setTimeStamp(generateTimestamp()); + current_data.setRawData(Bytes.fromLong(current)); + } + + private long generateTimestamp() { + long timestamp = start_time_ms + sample_period_ms * current; + return timestamp + (((current % 2) == 0) ? -1000 : 1000); + } + } + + @Test + public void testDataPointGenerator() { + DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); + HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { + new LongHistogramDataPointForTest(99000, Bytes.fromLong(0L)), + new LongHistogramDataPointForTest(111000, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + }; + for (HistogramDataPoint expected: expected_data_points) { + assertTrue(hdpg.hasNext()); + HistogramDataPoint dp = hdpg.next(); + assertEquals(expected.timestamp(), dp.timestamp()); + assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(dp.getRawData())); + } + assertFalse(hdpg.hasNext()); + } + + @Test + public void testDataPointGenerator_seek() { + DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); + hdpg.seek(119000); + HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { + new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + }; + for (HistogramDataPoint expected: expected_data_points) { + assertTrue(hdpg.hasNext()); + HistogramDataPoint hdp = hdpg.next(); + assertEquals(expected.timestamp(), hdp.timestamp()); + assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(hdp.getRawData())); + } + assertFalse(hdpg.hasNext()); + } + + @Test + public void testDataPointGenerator_seekToFirst() { + DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); + hdpg.seek(100000); + HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { + new LongHistogramDataPointForTest(111000, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + }; + for (HistogramDataPoint expected: expected_data_points) { + assertTrue(hdpg.hasNext()); + HistogramDataPoint hdp = hdpg.next(); + assertEquals(expected.timestamp(), hdp.timestamp()); + assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(hdp.getRawData())); + } + assertFalse(hdpg.hasNext()); + } + + @Test + public void testDataPointGenerator_seekToSecond() { + DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); + hdpg.seek(100001); + HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { + new LongHistogramDataPointForTest(111000, Bytes.fromLong(1)), + new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + }; + for (HistogramDataPoint expected: expected_data_points) { + assertTrue(hdpg.hasNext()); + HistogramDataPoint hdp = hdpg.next(); + assertEquals(expected.timestamp(), hdp.timestamp()); + assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(hdp.getRawData())); + } + assertFalse(hdpg.hasNext()); + } +} diff --git a/test/core/LongHistogramDataPointForTest.java b/test/core/LongHistogramDataPointForTest.java new file mode 100644 index 0000000000..80dc2aa1a8 --- /dev/null +++ b/test/core/LongHistogramDataPointForTest.java @@ -0,0 +1,106 @@ +// 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 java.util.Map; +import java.util.Set; + +import org.hbase.async.Bytes; +import net.opentsdb.core.HistogramDataPoint.HistogramBucket; + +public class LongHistogramDataPointForTest implements HistogramDataPoint { + private long timestamp; + private long data; + + LongHistogramDataPointForTest(final long timestamp, final byte[] raw_data) { + this.timestamp = timestamp; + this.data = Bytes.getLong(raw_data); + } + + protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs) { + this.timestamp = rhs.timestamp; + this.data = rhs.data; + } + + protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs, final long timestamp) { + this.data = rhs.data; + this.timestamp = timestamp; + } + + @Override + public long timestamp() { + return this.timestamp; + } + + public void setTimeStamp(final long timestamp) { + this.timestamp = timestamp; + } + + @Override + public byte[] getRawData() { + return Bytes.fromLong(this.data); + } + + public void setRawData(final byte[] data) { + this.data = Bytes.getLong(data); + } + + @Override + public void resetFromRawData(byte[] raw_data) { + // TODO Auto-generated method stub + + } + + @Override + public double percentile(double p) { + return data * p; + } + + @Override + public List percentile(List p) { + List rs = new ArrayList(); + for (Double d : p) { + rs.add(d.doubleValue() * data); + } + return rs; + } + + @Override + public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + if (!(histo instanceof LongHistogramDataPointForTest)) { + throw new IllegalArgumentException("The object must be an instance of the " + "LongHistogramDataPointForTest"); + } + + long agg = this.data + Bytes.getLong(histo.getRawData()); + this.data = agg; + } + + @Override + public HistogramDataPoint clone() { + return new LongHistogramDataPointForTest(this); + } + + @Override + public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { + return new LongHistogramDataPointForTest(this, timestamp); + } + + @Override + public Map getHistogramBucketsIfHas() { + throw new UnsupportedOperationException( + "LongHistogramDataPointForTest doesn't support getHistogramBuckets operation"); + } +} diff --git a/test/core/LongHistogramDataPointForTestDecoder.java b/test/core/LongHistogramDataPointForTestDecoder.java new file mode 100644 index 0000000000..3740704f36 --- /dev/null +++ b/test/core/LongHistogramDataPointForTestDecoder.java @@ -0,0 +1,10 @@ +package net.opentsdb.core; + +public class LongHistogramDataPointForTestDecoder implements HistogramDataPointDecoder { + + @Override + public HistogramDataPoint decode(byte[] raw_data, long timestamp) { + return new LongHistogramDataPointForTest(timestamp, raw_data); + } + +} diff --git a/test/core/TestHistogramAggregationIterator.java b/test/core/TestHistogramAggregationIterator.java new file mode 100644 index 0000000000..8a20b8e24f --- /dev/null +++ b/test/core/TestHistogramAggregationIterator.java @@ -0,0 +1,512 @@ +// 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 static org.junit.Assert.*; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({ "javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*" }) +@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, Config.class, + RowKey.class }) + +public class TestHistogramAggregationIterator { + private TSDB tsdb = mock(TSDB.class); + private static final long BASE_TIME = 1356998400000L; + public static final byte[] KEY = + new byte[] { 0, 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 2 }; + + @Test + public void testOneHistogramSpanWithNoDownsampler() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps.add(hdp.timestamp()); + } // end while + + assertEquals(10, values.size()); + for (int i = 0; i < 10; ++i) { + assertEquals(i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps.get(i).longValue()); + } // end for + } // end testOneHistogramSpanWithNoDownsampler() + + @Test + public void testOneHistogramSpanWithDownsampler_10secs() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + DownsamplingSpecification specification = new DownsamplingSpecification("10s-sum"); + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, specification, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(5, values.size()); + // 0 + 1 + assertEquals(1, values.get(0).longValue()); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + + // 2 + 3 + assertEquals(5, values.get(1).longValue()); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + + // 4 + 5 + assertEquals(9, values.get(2).longValue()); + assertEquals(BASE_TIME + 20000L, timestamps_in_millis.get(2).longValue()); + + // 6 + 7 + assertEquals(13, values.get(3).longValue()); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(3).longValue()); + + // 8 + 9 + assertEquals(17, values.get(4).longValue()); + assertEquals(BASE_TIME + 40000L, timestamps_in_millis.get(4).longValue()); + } // end testOneHistogramSpanWithDownsampler_10secs() + + @Test + public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME + 5000L, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(9, values.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(i + 1, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * (i + 1), timestamps_in_millis.get(i).longValue()); + } + } // end testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() + + @Test + public void testOneHistogramSpanNoDownSamplerOutofRange() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME + 5000L * 10, + BASE_TIME + 5000L * 20, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + assertFalse(histAggIt.hasNext()); + } // end testOneHistogramSpanNoDownSamplerOutofRange() + + @Test + public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 5, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(6, values.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } + } // end testOneHistogramSpanNoDownSamplerLaterDataPoints() + + @Test + public void testOneHistogramSpanDownSamplerLaterDataPoints() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + DownsamplingSpecification specification = new DownsamplingSpecification("10s-sum"); + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 5, HistogramAggregation.SUM, specification, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(3, values.size()); + // 0 + 1 + assertEquals(1, values.get(0).longValue()); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + + // 2 + 3 + assertEquals(5, values.get(1).longValue()); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + + // 4 + 5 + assertEquals(9, values.get(2).longValue()); + assertEquals(BASE_TIME + 20000L, timestamps_in_millis.get(2).longValue()); + } // end testOneHistogramSpanWithLaterDataPoints() + + @Test + public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + List row2 = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan2 = new HistogramSpan(tsdb); + hspan2.addRow(KEY, row2); + spans.add(hspan2); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(10, values.size()); + for (int i = 0; i < 10; ++i) { + assertEquals(i * 2, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } + } // end testTwoHistogramSpanNoDownSamplerSameTimestamp() + + @Test + public void testTwoHistogramSpanDownSamplerSameTimestamp() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + List row2 = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan2 = new HistogramSpan(tsdb); + hspan2.addRow(KEY, row2); + spans.add(hspan2); + + DownsamplingSpecification specification = new DownsamplingSpecification("10s-sum"); + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, specification, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(5, values.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(2 * (i * 2 + i * 2 + 1), values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * 2 * i, timestamps_in_millis.get(i).longValue()); + } + } // end testTwoHistogramSpanDownSamplerSameTimestamp() + + @Test + public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { + List row = new ArrayList(); + // 0, 2, 4... + for (int i = 0; i < 10; ) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + i += 2; + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + List row2 = new ArrayList(); + // 1, 3, 5... + for (int i = 1; i < 10; ) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + i += 2; + } + + final HistogramSpan hspan2 = new HistogramSpan(tsdb); + hspan2.addRow(KEY, row2); + spans.add(hspan2); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(10, values.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } + } // end testTwoHistogramSpanNoDownSamplerDiffTimestamp() + + @Test + public void testTwoHistogramSpanNoDownSamplerMergeSome() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + List row2 = new ArrayList(); + for (int i = 1; i < 5; ++i) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + for (int i = 5; i < 10; ++i) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * (5 + i), Bytes.fromLong(5 + i))); + } + + final HistogramSpan hspan2 = new HistogramSpan(tsdb); + hspan2.addRow(KEY, row2); + spans.add(hspan2); + + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 20, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(15, values.size()); + for (int i = 0; i < 5; ++i) { + assertEquals(2 * i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } + + for (int i = 5; i < 10; ++i) { + assertEquals(i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } + + for (int i = 10; i < 15; ++i) { + assertEquals(i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } + } // end testTwoHistogramSpanNoDownSamplerOverlap() + + @Test + public void testTwoHistogramSpanNoDownSamplerOneHasMore() { + // span 1 has 10 data points + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + // span 2 has 5 data points + List row2 = new ArrayList(); + for (int i = 1; i < 5; ++i) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan2 = new HistogramSpan(tsdb); + hspan2.addRow(KEY, row2); + spans.add(hspan2); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 20, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(10, values.size()); + for (int i = 0; i < 5; ++i) { + assertEquals(2 * i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } // end for + + for (int i = 5; i < 10; ++i) { + assertEquals(i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * i, timestamps_in_millis.get(i).longValue()); + } // end for + } // end testTwoHistogramSpanNoDownSamplerOneHasMore() + + @Test + public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { + // span1 has 10 data points + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + // span 2 has 5 data points + List row2 = new ArrayList(); + for (int i = 1; i < 5; ++i) { + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan2 = new HistogramSpan(tsdb); + hspan2.addRow(KEY, row2); + spans.add(hspan2); + + HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME + 5000L * 5, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + + List values = new ArrayList(); + List timestamps_in_millis = new ArrayList(); + while (histAggIt.hasNext()) { + HistogramDataPoint hdp = histAggIt.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } // end while + + assertEquals(5, values.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(5 + i, values.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * (5 + i), timestamps_in_millis.get(i).longValue()); + } // end for + } // end testTwoHistogramSpanNoDownSamplerOneOutofRange() +} diff --git a/test/core/TestHistogramDataPointsToDataPointsAdaptor.java b/test/core/TestHistogramDataPointsToDataPointsAdaptor.java new file mode 100644 index 0000000000..11c263c01b --- /dev/null +++ b/test/core/TestHistogramDataPointsToDataPointsAdaptor.java @@ -0,0 +1,395 @@ +// 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 static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.hbase.async.Bytes.ByteMap; +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 org.powermock.reflect.Whitebox; + +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.ByteSet; +import net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ HistogramSpanGroup.class, HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, +Config.class, RowKey.class }) +public final class TestHistogramDataPointsToDataPointsAdaptor { + private static final long BASE_TIME = 1356998400000L; + public static final byte[] KEY = + new byte[] { 0, 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 2 }; + + private static long start_ts = 1356998400L; + private static long end_ts = 1356998600L; + + private TSDB tsdb; + + @Before + public void before() { + tsdb = PowerMockito.mock(TSDB.class); + } + + @Test + public void getTagUids() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final ByteMap uids_read = dps_ada.getTagUids(); + + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 2 }, + uids_read.firstEntry().getValue())); + } + + @Test + public void getTagUidsAggedOut() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + final ByteMap uids2 = new ByteMap(); + uids2.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 3 }); + final HistogramSpan span2 = mock(HistogramSpan.class); + when(span2.getTagUids()).thenReturn(uids2); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + spans.add(span2); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final ByteMap uids_read = dps_ada.getTagUids(); + + assertEquals(0, uids_read.size()); + } + + @Test + public void getTagUidsNoSpans() throws Exception { + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final ByteMap uids_read = dps_ada.getTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getAggregatedTagUidsNotAgged() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final List uids_read = dps_ada.getAggregatedTagUids(); + + assertEquals(0, uids_read.size()); + } + + @Test + public void getAggregatedTagUids() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + final ByteMap uids2 = new ByteMap(); + uids2.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 3 }); + final HistogramSpan span2 = mock(HistogramSpan.class); + when(span2.getTagUids()).thenReturn(uids2); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + spans.add(span2); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final List uids_read = dps_ada.getAggregatedTagUids(); + + assertEquals(1, uids_read.size()); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, uids_read.get(0)); + } + + @Test + public void getAggregatedTagUidsNoSpans() throws Exception { + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final List uids_read = dps_ada.getAggregatedTagUids(); + + assertEquals(0, uids_read.size()); + } + + @Test + public void getTagUidsAggedNotInQuery() throws Exception { + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 3 }); + + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final ByteMap uids_read = dps_ada.getTagUids(); + assertEquals(0, uids_read.size()); + + final List agg_tags = dps_ada.getAggregatedTagUids(); + assertEquals(1, agg_tags.size()); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, agg_tags.get(0)); + } + + @Test + public void getTagUidsInQueryTags() throws Exception { + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 1 }); + + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final ByteMap uids_read = dps_ada.getTagUids(); + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 2 }, + uids_read.firstEntry().getValue())); + assertEquals(0, dps_ada.getAggregatedTagUids().size()); + } + + @Test + public void getTagUidsNullQueryTags() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + final ByteMap uids_read = dps_ada.getTagUids(); + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 2 }, + uids_read.firstEntry().getValue())); + assertEquals(0, dps_ada.getAggregatedTagUids().size()); + } + + @Test + public void iteratorAllItems() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 1 }); + HistogramSpanGroup hist_span_group = new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, + HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); + List values = new ArrayList(); + List timestamp_in_ms = new ArrayList(); + for (DataPoint dp : dps_ada) { + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + timestamp_in_ms.add(dp.timestamp()); + } // end for + + assertTrue(dps_ada.isPercentile()); + List to_checks = new ArrayList(); + for (int i = 0; i < 10; ++i) { + to_checks.add(i * 0.98); + } // end for + + assertEquals(values.size(), to_checks.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), 0.0001); + assertEquals(timestamp_in_ms.get(i).longValue(), BASE_TIME + 5000L * i); + } // end for + } + + @Test + public void doubleIteratorAllItems() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 1 }); + HistogramSpanGroup hist_span_group = new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, + HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); + + HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); + List values = new ArrayList(); + for (DataPoint dp : dps_ada) { + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + } // end for + + List values2 = new ArrayList(); + for (DataPoint dp : dps_ada) { + values2.add(dp.doubleValue()); + } // end for + + assertTrue(dps_ada.isPercentile()); + List to_checks = new ArrayList(); + for (int i = 0; i < 10; ++i) { + to_checks.add(i * 0.98); + } // end for + + assertEquals(values.size(), to_checks.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), 0.0001); + assertEquals(values.get(i).doubleValue(), values2.get(i).doubleValue(), 0.0001); + } // end for + } + + @Test + public void iteratorAllItemsWithDiffPercentile() { + List row = new ArrayList(); + for (int i = 0; i < 10; ++i) { + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + } + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + List spans = new ArrayList(); + spans.add(hspan); + + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 1 }); + HistogramSpanGroup hist_span_group = new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, + HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); + + // 98 percentile + HistogramDataPointsToDataPointsAdaptor dps_ada_98 = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); + List values = new ArrayList(); + for (DataPoint dp : dps_ada_98) { + assertFalse(dp.isInteger()); + values.add(dp.doubleValue()); + } // end for + + assertTrue(dps_ada_98.isPercentile()); + List to_checks = new ArrayList(); + for (int i = 0; i < 10; ++i) { + to_checks.add(i * 0.98); + } // end for + + assertEquals(values.size(), to_checks.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), 0.0001); + } // end for + + // 95 percentile + HistogramDataPointsToDataPointsAdaptor dps_ada_95 = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.95f); + List values_95 = new ArrayList(); + for (DataPoint dp : dps_ada_95) { + assertFalse(dp.isInteger()); + values_95.add(dp.doubleValue()); + } // end for + + assertTrue(dps_ada_95.isPercentile()); + List to_checks_95 = new ArrayList(); + for (int i = 0; i < 10; ++i) { + to_checks_95.add(i * 0.95); + } // end for + + assertEquals(values_95.size(), to_checks_95.size()); + for (int i = 0; i < values.size(); ++i) { + assertEquals(values_95.get(i).doubleValue(), to_checks_95.get(i).doubleValue(), 0.0001); + } // end for + } +} diff --git a/test/core/TestHistogramDownsampler.java b/test/core/TestHistogramDownsampler.java new file mode 100644 index 0000000000..80f0fdea48 --- /dev/null +++ b/test/core/TestHistogramDownsampler.java @@ -0,0 +1,1334 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; + +import com.google.common.collect.Lists; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.SeekableViewsForTest.MockSeekableView; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.core.HistogramSeekableViewForTest.MockHistogramSeekableView; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.DateTime; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +@RunWith(PowerMockRunner.class) +// "Classloader hell"... It's real. Tell PowerMock to ignore these classes +// because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({ "javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*" }) +@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, Config.class, + RowKey.class }) +public class TestHistogramDownsampler { + private TSDB tsdb = mock(TSDB.class); + private Config config = mock(Config.class); + private UniqueId metrics = mock(UniqueId.class); + + private static final long BASE_TIME = 1356998400000L; + + private static final HistogramDataPoint[] HIST_DATA_POINTS = new HistogramDataPoint[] { + // timestamp = 1,356,998,400,000 ms + new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(40L)), + // timestamp = 1,357,000,400,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 2000000, Bytes.fromLong(50L)), + // timestamp = 1,357,002,000,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 3600000, Bytes.fromLong(40L)), + // timestamp = 1,357,002,005,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 3605000, Bytes.fromLong(50L)), + // timestamp = 1,357,005,600,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 7200000, Bytes.fromLong(40L)), + // timestamp = 1,357,007,600,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 9200000, Bytes.fromLong(50L)) }; + + public static final byte[] KEY = + new byte[] { 0, 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 2 }; + + private static final int THOUSAND_SEC_INTERVAL = (int) DateTime.parseDuration("1000s"); + private static final int TEN_SEC_INTERVAL = (int) DateTime.parseDuration("10s"); + + // 30 minute offset + final static TimeZone AF = DateTime.timezones.get("Asia/Kabul"); + + // 12h offset w/o DST + final static TimeZone TV = DateTime.timezones.get("Pacific/Funafuti"); + + // 12h offset w DST + final static TimeZone FJ = DateTime.timezones.get("Pacific/Fiji"); + + // Tue, 15 Dec 2015 04:02:25.123 UTC + final static long DST_TS = 1450137600000L; + + private HistogramSeekableView source; + private HistogramDownsampler downsampler; + private DownsamplingSpecification specification; + + @Before + public void before() { + // Inject the attributes we need into the "tsdb" object. + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "config", config); + when(tsdb.getConfig()).thenReturn(config); + when(tsdb.metrics.width()).thenReturn((short)4); + + source = spy(HistogramSeekableViewForTest.fromArray(HIST_DATA_POINTS)); + } + + @Test + public void testDownsampler() { + specification = new DownsamplingSpecification("1000s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(5, values.size()); + assertEquals(40L, values.get(0).longValue()); + assertEquals(BASE_TIME - 400000L, timestamps_in_millis.get(0).longValue()); + assertEquals(50, values.get(1).longValue()); + assertEquals(BASE_TIME + 1600000, timestamps_in_millis.get(1).longValue()); + assertEquals(90, values.get(2).longValue()); + assertEquals(BASE_TIME + 3600000L, timestamps_in_millis.get(2).longValue()); + assertEquals(40, values.get(3).longValue()); + assertEquals(BASE_TIME + 6600000L, timestamps_in_millis.get(3).longValue()); + assertEquals(50, values.get(4).longValue()); + assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(4).longValue()); + } + + @Test + public void testDownsampler_10seconds() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 0, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 1, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 2, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 3, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 4, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 5, Bytes.fromLong(32L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 6, Bytes.fromLong(64L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 7, Bytes.fromLong(128L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 8, Bytes.fromLong(256L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 9, Bytes.fromLong(512L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 10, Bytes.fromLong(1024L)) })); + + specification = new DownsamplingSpecification("10s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(6, values.size()); + assertEquals(3, values.get(0).longValue()); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(12, values.get(1).longValue()); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + assertEquals(48, values.get(2).longValue()); + assertEquals(BASE_TIME + 20000L, timestamps_in_millis.get(2).longValue()); + assertEquals(192, values.get(3).longValue()); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(3).longValue()); + assertEquals(768, values.get(4).longValue()); + assertEquals(BASE_TIME + 40000L, timestamps_in_millis.get(4).longValue()); + assertEquals(1024, values.get(5).longValue()); + assertEquals(BASE_TIME + 50000L, timestamps_in_millis.get(5).longValue()); + } + + @Test + public void testDownsampler_15seconds() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + specification = new DownsamplingSpecification("15s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(4, values.size()); + assertEquals(1, values.get(0).longValue()); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(6, values.get(1).longValue()); + assertEquals(BASE_TIME + 15000L, timestamps_in_millis.get(1).longValue()); + assertEquals(8, values.get(2).longValue()); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(2).longValue()); + assertEquals(48, values.get(3).longValue()); + assertEquals(BASE_TIME + 45000L, timestamps_in_millis.get(3).longValue()); + } + + @Test + public void testDownsampler_allFullRange() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(63, values.get(0).longValue()); + assertEquals(0L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testDownsampler_allFilterOnQuery() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new HistogramDownsampler(source, specification, BASE_TIME + 15000L, BASE_TIME + 45000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(14, values.get(0).longValue()); + assertEquals(BASE_TIME + 15000L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new HistogramDownsampler(source, specification, BASE_TIME + 65000L, BASE_TIME + 75000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(0, values.size()); + } + + @Test + public void testDownsampler_allFilterOnQueryOutOfRangeLate() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = new HistogramDownsampler(source, specification, BASE_TIME - 15000L, BASE_TIME - 5000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(0, values.size()); + } + + @Test + public void testDownsampler_calendarHour() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 1800000, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 3599000L, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(BASE_TIME + 3600000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 5400000L, Bytes.fromLong(5L)), + new LongHistogramDataPointForTest(BASE_TIME + 7199000L, Bytes.fromLong(6L)) })); + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(TV); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = BASE_TIME; + long value = 6; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + ts += 3600000; + value = 15; + } + + // hour offset by 30m + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1356996600000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + ts += 3600000; + if (value == 1) { + value = 9; + } else { + value = 11; + } + } + + // multiple hours + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("4hc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1356996600000L; + value = 21; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + } + } + + @Test + public void testDownsampler_calendarDay() { + // UTC + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(DST_TS, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(DST_TS + 86399000, Bytes.fromLong(2L)), + // falls to the next in FJ + new LongHistogramDataPointForTest(DST_TS + 126001000L, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(DST_TS + 172799000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(DST_TS + 172800000L, Bytes.fromLong(5L)), + // falls within 30m offset + new LongHistogramDataPointForTest(DST_TS + 242999000L, Bytes.fromLong(6L)) })); + + // control + specification = new DownsamplingSpecification("1d-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = DST_TS; + long value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 3) { + value = 7; + } else if (value == 7) { + value = 11; + } + } + + + // 12 hour offset from UTC + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(TV); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450094400000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 9; + } else { + value = 6; + } + } + + + // 11 hour offset from UTC + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(FJ); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450090800000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 1) { + value = 2; + } else if (value == 2) { + value = 12; + } else { + value = 6; + } + } + + + // 30m offset + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450121400000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 15; + } + } + + // multiple days + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("3dc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1450121400000L; + value = 21; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + } + } + + @Test + public void testDownsampler_calendarWeek() { + source = HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + // a Tuesday in UTC land + new LongHistogramDataPointForTest(DST_TS, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(DST_TS + (86400000L * 7), Bytes.fromLong(2L)), + // falls to the next in FJ + new LongHistogramDataPointForTest(1451129400000L, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(DST_TS + (86400000L * 21), Bytes.fromLong(4L)), + // falls within 30m offset + new LongHistogramDataPointForTest(1452367799000L, Bytes.fromLong(5L)) + }); + // control + specification = new DownsamplingSpecification("1wc-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = 1449964800000L; + long value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + if (ts == 1450569600000L) { + ts = 1451779200000L; // skips a week + } else { + ts += 86400000L * 7; + } + if (value == 1) { + value = 5; + } else { + value = 9; + } + } + + // 12 hour offset from UTC + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(TV); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449921600000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + if (ts == 1450526400000L) { + ts = 1451736000000L; // skip a week + } else { + ts += 86400000L * 7; + } + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 4; + } else { + value = 5; + } + } + + // 11 hour offset from UTC + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(FJ); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449918000000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000L * 7; + value++; + } + + // 30m offset + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1wc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449948600000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + if (ts == 1449948600000L) { + ts = 1450553400000L; + } else { + ts = 1451763000000L; + } + if (value == 1) { + value = 5; + } else { + value = 9; + } + } + + // multiple weeks + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("2wc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1449948600000L; + value = 6; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts = 1451158200000L; + value = 9; + } + } + + @Test + public void testDownsampler_calendarMonth() { + final long dec_1st = 1448928000000L; + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(dec_1st, Bytes.fromLong(1L)), + // falls to the next in FJ + new LongHistogramDataPointForTest(1451559600000L, Bytes.fromLong(2L)), + // jan 1st + new LongHistogramDataPointForTest(1451606400000L, Bytes.fromLong(3L)), + // feb 1st + new LongHistogramDataPointForTest(1454284800000L, Bytes.fromLong(4L)), + // feb 29th (leap year) + new LongHistogramDataPointForTest(1456704000000L, Bytes.fromLong(5L)), + // falls within 30m offset AT + new LongHistogramDataPointForTest(1456772400000L, Bytes.fromLong(6L)) + })); + + // control + specification = new DownsamplingSpecification("1nc-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = dec_1st; + long value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + if (ts == 1448928000000L) { + ts = 1451606400000L; + } else { + ts = 1454284800000L; + value = 15; + } + } + + // 12h offset + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(TV); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1448884800000L; + value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + if (ts == 1448884800000L) { + ts = 1451563200000L; + } else if (ts == 1451563200000L) { + value = 9; + ts = 1454241600000L; + } else { + ts = 1456747200000L; + value = 6; + } + } + + // 11h offset + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(FJ); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1448881200000L; + value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + if (ts == 1448881200000L) { + ts = 1451559600000L; + value = 5; + } else if (ts == 1451559600000L) { + ts = 1454241600000L; + value = 9; + } else { + ts = 1456747200000L; + value = 6; + } + } + + // 30m offset + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1nc-sum"); + specification.setTimezone(AF); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1448911800000L; + value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + if (ts == 1448911800000L) { + ts = 1451590200000L; + } else { + ts = 1454268600000L; + value = 15; + } + } + + // multiple months + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("3nc-sum"); + specification.setTimezone(TV); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + ts = 1443614400000L; + value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + ts = 1451563200000L; + value = 18; + } + } + + @Test + public void testDownsampler_calendarSkipSomePoints() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 1800000, Bytes.fromLong(2L)), + // skip an hour + new LongHistogramDataPointForTest(BASE_TIME + 7200000, Bytes.fromLong(6L)) })); + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(TV); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + long ts = BASE_TIME; + long value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + ts = 1357005600000L; + value = 6; + } + } + + @Test + public void testDownsampler_noData() { + source = spy(HistogramSeekableViewForTest.fromArray(new HistogramDataPoint[] {})); + specification = new DownsamplingSpecification("1d-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + verify(source, never()).next(); + assertFalse(downsampler.hasNext()); + } + + @Test + public void testDownsampler_noDataCalendar() { + source = spy(HistogramSeekableViewForTest.fromArray(new HistogramDataPoint[] {})); + specification = new DownsamplingSpecification("1mc-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + verify(source, never()).next(); + assertFalse(downsampler.hasNext()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testRemove() { + specification = new DownsamplingSpecification("1d-sum"); + new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE).remove(); + } + + @Test + public void testSeek() { + specification = new DownsamplingSpecification("1000s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + downsampler.seek(BASE_TIME + 3600000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + values.add(Bytes.getLong(dp.getRawData())); + timestamps_in_millis.add(dp.timestamp()); + } + + assertEquals(3, values.size()); + assertEquals(90, values.get(0).longValue()); + assertEquals(BASE_TIME + 3600000L, timestamps_in_millis.get(0).longValue()); + assertEquals(40, values.get(1).longValue()); + assertEquals(BASE_TIME + 6600000L, timestamps_in_millis.get(1).longValue()); + assertEquals(50, values.get(2).longValue()); + assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(2).longValue()); + } + + @Test + public void testSeek_skipPartialInterval() { + specification = new DownsamplingSpecification("1000s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + downsampler.seek(BASE_TIME + 3800000L); + verify(source, never()).next(); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + values.add(Bytes.getLong(dp.getRawData())); + timestamps_in_millis.add(dp.timestamp()); + } + + // seek timestamp was BASE_TIME + 3800000L or 1,357,002,200,000 ms. + // The interval that has the timestamp began at 1,357,002,000,000 ms. It + // had two data points but was abandoned because the requested timestamp + // was not aligned. The next two intervals at 1,357,003,000,000 and + // at 1,357,004,000,000 did not have data points. The first interval that + // had a data point began at 1,357,002,005,000 ms or BASE_TIME + 6600000L. + assertEquals(2, values.size()); + assertEquals(40, values.get(0).longValue()); + assertEquals(BASE_TIME + 6600000L, timestamps_in_millis.get(0).longValue()); + assertEquals(50, values.get(1).longValue()); + assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(1).longValue()); + } + + @Test + public void testSeek_doubleIteration() { + specification = new DownsamplingSpecification("1000s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + while (downsampler.hasNext()) { + downsampler.next(); + } + downsampler.seek(BASE_TIME + 3600000L); + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(3, values.size()); + assertEquals(90, values.get(0).longValue()); + assertEquals(BASE_TIME + 3600000L, timestamps_in_millis.get(0).longValue()); + assertEquals(40, values.get(1).longValue()); + assertEquals(BASE_TIME + 6600000L, timestamps_in_millis.get(1).longValue()); + assertEquals(50, values.get(2).longValue()); + assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(2).longValue()); + } + + @Test + public void testSeek_abandoningIncompleteInterval() { + source = HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 1100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 2100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 3100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 4100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 5100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 6100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 7100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 8100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 9100L, Bytes.fromLong(40L)), + new LongHistogramDataPointForTest(BASE_TIME + 10100L, Bytes.fromLong(40L)) }); + specification = new DownsamplingSpecification("10s-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + // The seek is aligned by the downsampling window. + downsampler.seek(BASE_TIME); + assertTrue("seek(BASE_TIME)", downsampler.hasNext()); + HistogramDataPoint first_dp = downsampler.next(); + assertEquals("seek(1356998400000)", BASE_TIME, first_dp.timestamp()); + assertEquals("seek(1356998400000)", 400, Bytes.getLong(first_dp.getRawData())); + + // No seeks but the last one is aligned by the downsampling window. + for (long seek_timestamp = BASE_TIME + 1000L; seek_timestamp < BASE_TIME + 10100L; seek_timestamp += 1000) { + downsampler.seek(seek_timestamp); + assertTrue("ts = " + seek_timestamp, downsampler.hasNext()); + HistogramDataPoint dp = downsampler.next(); + // Timestamp should be greater than or equal to the seek timestamp. + assertTrue(String.format("%d >= %d", dp.timestamp(), seek_timestamp), dp.timestamp() >= seek_timestamp); + assertEquals(String.format("seek(%d)", seek_timestamp), BASE_TIME + 10000L, dp.timestamp()); + assertEquals(String.format("seek(%d)", seek_timestamp), 40, Bytes.getLong(dp.getRawData())); + } + } + + @Test + public void testSeek_useCalendar() { + source = spy(HistogramSeekableViewForTest + .fromArray(new HistogramDataPoint[] { new LongHistogramDataPointForTest(1356998400000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(1388534400000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(1420070400000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(1451606400000L, Bytes.fromLong(8L)) })); + + specification = new DownsamplingSpecification("1yc-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); + + downsampler.seek(1420070400000L); + verify(source, never()).next(); + + long timestamp = 1420070400000L; + long value = 4; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + System.out.println(dp.timestamp() + " " + Bytes.getLong(dp.getRawData())); + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + timestamp = 1451606400000L; + value = 8; + } + + ((MockHistogramSeekableView) source).resetIndex(); + specification = new DownsamplingSpecification("1yc-sum"); + downsampler = new HistogramDownsampler(source, specification, 0, 0); + downsampler.seek(1420070400001L); + + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(timestamp, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + } + } + + @Test + public void testHistogramSpanDownSampler() { + List row = Arrays.asList(HIST_DATA_POINTS); + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + // check the data points using iterator + List it_values = Lists.newArrayList(); + HistogramSpan.Iterator it = hspan.spanIterator(); + while (it.hasNext()) { + HistogramDataPoint hdp = it.next(); + it_values.add(Bytes.getLong(hdp.getRawData())); + } + assertEquals(6, it_values.size()); + assertEquals(40L, it_values.get(0).longValue()); + assertEquals(50L, it_values.get(1).longValue()); + assertEquals(40L, it_values.get(2).longValue()); + assertEquals(50L, it_values.get(3).longValue()); + assertEquals(40L, it_values.get(4).longValue()); + assertEquals(50L, it_values.get(5).longValue()); + + + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + specification = new DownsamplingSpecification("1000s-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, 0, 0); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(5, values.size()); + assertEquals(40L, values.get(0).longValue()); + assertEquals(BASE_TIME - 400000L, timestamps_in_millis.get(0).longValue()); + assertEquals(50, values.get(1).longValue()); + assertEquals(BASE_TIME + 1600000, timestamps_in_millis.get(1).longValue()); + assertEquals(90, values.get(2).longValue()); + assertEquals(BASE_TIME + 3600000L, timestamps_in_millis.get(2).longValue()); + assertEquals(40, values.get(3).longValue()); + assertEquals(BASE_TIME + 6600000L, timestamps_in_millis.get(3).longValue()); + assertEquals(50, values.get(4).longValue()); + assertEquals(BASE_TIME + 8600000L, timestamps_in_millis.get(4).longValue()); + } + + @Test + public void testHistogramSpanDownSampler_10seconds() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 0, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 1, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 2, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 3, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 4, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 5, Bytes.fromLong(32L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 6, Bytes.fromLong(64L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 7, Bytes.fromLong(128L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 8, Bytes.fromLong(256L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 9, Bytes.fromLong(512L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 10, Bytes.fromLong(1024L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + // downsample iterator the span + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + specification = new DownsamplingSpecification("10s-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, 0, 0); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(6, values.size()); + assertEquals(3, values.get(0).longValue()); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(12, values.get(1).longValue()); + assertEquals(BASE_TIME + 10000L, timestamps_in_millis.get(1).longValue()); + assertEquals(48, values.get(2).longValue()); + assertEquals(BASE_TIME + 20000L, timestamps_in_millis.get(2).longValue()); + assertEquals(192, values.get(3).longValue()); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(3).longValue()); + assertEquals(768, values.get(4).longValue()); + assertEquals(BASE_TIME + 40000L, timestamps_in_millis.get(4).longValue()); + assertEquals(1024, values.get(5).longValue()); + assertEquals(BASE_TIME + 50000L, timestamps_in_millis.get(5).longValue()); + } + + @Test + public void testHistogramSpanDownSampler_15seconds() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + // downsample iterator the span + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + specification = new DownsamplingSpecification("15s-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, 0, 0); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(4, values.size()); + assertEquals(1, values.get(0).longValue()); + assertEquals(BASE_TIME + 00000L, timestamps_in_millis.get(0).longValue()); + assertEquals(6, values.get(1).longValue()); + assertEquals(BASE_TIME + 15000L, timestamps_in_millis.get(1).longValue()); + assertEquals(8, values.get(2).longValue()); + assertEquals(BASE_TIME + 30000L, timestamps_in_millis.get(2).longValue()); + assertEquals(48, values.get(3).longValue()); + assertEquals(BASE_TIME + 45000L, timestamps_in_millis.get(3).longValue()); + } + + @Test + public void testHistogramSpanDownsampler_allFullRange() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + specification = new DownsamplingSpecification("0all-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(63, values.get(0).longValue()); + assertEquals(0L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testHistogramSpanDownsampler_allFilterOnQuery() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + specification = new DownsamplingSpecification("0all-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, BASE_TIME + 15000L, BASE_TIME + 45000L); + + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(1, values.size()); + assertEquals(14, values.get(0).longValue()); + assertEquals(BASE_TIME + 15000L, timestamps_in_millis.get(0).longValue()); + } + + @Test + public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeEarly() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + specification = new DownsamplingSpecification("0all-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, BASE_TIME + 65000L, BASE_TIME + 75000L); + + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(0, values.size()); + } + + @Test + public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeLate() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + specification = new DownsamplingSpecification("0all-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, BASE_TIME - 15000L, BASE_TIME - 5000L); + + List values = Lists.newArrayList(); + List timestamps_in_millis = Lists.newArrayList(); + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + values.add(Bytes.getLong(hdp.getRawData())); + timestamps_in_millis.add(hdp.timestamp()); + } + + assertEquals(0, values.size()); + } + + @Test + public void testHistogramSpanDownsampler_calendarHour() { + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 1800000, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 3599000L, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(BASE_TIME + 3600000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 5400000L, Bytes.fromLong(5L)), + new LongHistogramDataPointForTest(BASE_TIME + 7199000L, Bytes.fromLong(6L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + { + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(TV); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = BASE_TIME; + long value = 6; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + ts += 3600000; + value = 15; + } + } + + // hour offset by 30m + { + specification = new DownsamplingSpecification("1hc-sum"); + specification.setTimezone(AF); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = 1356996600000L; + long value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint dp = downsampler.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(value, Bytes.getLong(dp.getRawData())); + ts += 3600000; + if (value == 1) { + value = 9; + } else { + value = 11; + } + } + + } + + + // multiple hours + { + specification = new DownsamplingSpecification("4hc-sum"); + specification.setTimezone(AF); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = 1356996600000L; + long value = 21; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + } + } + } + + @Test + public void testHistogramSpanDownsampler_calendarDay() { + // UTC + List row = Arrays.asList(new HistogramDataPoint[] { + new LongHistogramDataPointForTest(DST_TS, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(DST_TS + 86399000, Bytes.fromLong(2L)), + // falls to the next in FJ + new LongHistogramDataPointForTest(DST_TS + 126001000L, Bytes.fromLong(3L)), + new LongHistogramDataPointForTest(DST_TS + 172799000L, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(DST_TS + 172800000L, Bytes.fromLong(5L)), + // falls within 30m offset + new LongHistogramDataPointForTest(DST_TS + 242999000L, Bytes.fromLong(6L)) }); + + final HistogramSpan hspan = new HistogramSpan(tsdb); + hspan.addRow(KEY, row); + + // control + { + specification = new DownsamplingSpecification("1d-sum"); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = DST_TS; + long value = 3; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 3) { + value = 7; + } else if (value == 7) { + value = 11; + } + } + } + + // 12 hour offset from UTC + { + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(TV); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = 1450094400000L; + long value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 9; + } else { + value = 6; + } + } + } + + // 11 hour offset from UTC + { + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(FJ); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = 1450090800000L; + long value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 1) { + value = 2; + } else if (value == 2) { + value = 12; + } else { + value = 6; + } + } + } + + { + // 30m offset + specification = new DownsamplingSpecification("1dc-sum"); + specification.setTimezone(AF); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = 1450121400000L; + long value = 1; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + ts += 86400000; + if (value == 1) { + value = 5; + } else if (value == 5) { + value = 15; + } + } + } + + { + // multiple days + specification = new DownsamplingSpecification("3dc-sum"); + specification.setTimezone(AF); + downsampler = hspan.downsampler(0, 0, specification, false, 0, Long.MAX_VALUE); + + long ts = 1450121400000L; + long value = 21; + while (downsampler.hasNext()) { + HistogramDataPoint hdp = downsampler.next(); + assertEquals(ts, hdp.timestamp()); + assertEquals(value, Bytes.getLong(hdp.getRawData())); + } + } + } +} diff --git a/test/core/TestHistogramRowSeq.java b/test/core/TestHistogramRowSeq.java new file mode 100644 index 0000000000..1e702acab0 --- /dev/null +++ b/test/core/TestHistogramRowSeq.java @@ -0,0 +1,427 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.util.NoSuchElementException; + +import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.KeyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, + Config.class, RowKey.class }) +public final class TestHistogramRowSeq { + private TSDB tsdb = mock(TSDB.class); + private Config config = mock(Config.class); + private UniqueId metrics = mock(UniqueId.class); + private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; + public byte[] key = null; + public static final byte[] FAMILY = { 't' }; + public static final byte[] ZERO = { 0 }; + + @Before + public void before() throws Exception { + // Inject the attributes we need into the "tsdb" object. + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "table", TABLE); + Whitebox.setInternalState(tsdb, "config", config); + when(tsdb.getConfig()).thenReturn(config); + when(tsdb.metrics.width()).thenReturn((short) 3); + key = BaseTsdbTest.getRowKey( + BaseTsdbTest.generateUID(UniqueIdType.METRIC, (byte) 1), + 1356998400, + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), + BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1)); + when(RowKey.metricNameAsync(tsdb, key)) + .thenReturn(Deferred.fromResult("in.rps.latency")); + } + + @Test + public void setRow() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(2, hrs.size()); + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + } + + @Test (expected = IllegalStateException.class) + public void setRowAlreadySet() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + hrs.setRow(key, hdps); + } + + @Test + public void addRowMergeLater() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + assertEquals(2, hrs.size()); + + + List hdps2 = new ArrayList(); + hdps2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + hrs.addRow(hdps2); + assertEquals(4, hrs.size()); + + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + assertEquals(110L, hrs.timestamp(2)); + assertEquals(115L, hrs.timestamp(3)); + } + + @Test + public void addRowMergeEarlier() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + assertEquals(2, hrs.size()); + + + List hdps2 = new ArrayList(); + hdps2.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps2.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hrs.addRow(hdps2); + assertEquals(4, hrs.size()); + + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + assertEquals(110L, hrs.timestamp(2)); + assertEquals(115L, hrs.timestamp(3)); + } + + @Test + public void addRowMergeMiddle() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(120L, Bytes.fromLong(4))); + hdps.add(new LongHistogramDataPointForTest(125L, Bytes.fromLong(5))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + assertEquals(2, hrs.size()); + + + List hdps2 = new ArrayList(); + hdps2.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps2.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hrs.addRow(hdps2); + assertEquals(4, hrs.size()); + + List hdps3 = new ArrayList(); + hdps3.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps3.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + hrs.addRow(hdps3); + assertEquals(6, hrs.size()); + + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + assertEquals(110L, hrs.timestamp(2)); + assertEquals(115L, hrs.timestamp(3)); + assertEquals(120L, hrs.timestamp(4)); + assertEquals(125L, hrs.timestamp(5)); + } + + @Test + public void addRowMergeDuplicateLater() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + assertEquals(2, hrs.size()); + + + List hdps2 = new ArrayList(); + hdps2.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(100))); + hdps2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hrs.addRow(hdps2); + assertEquals(3, hrs.size()); + + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + assertEquals(110L, hrs.timestamp(2)); + } + + @Test + public void timestamp() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(2, hrs.size()); + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + } + + @Test (expected = IndexOutOfBoundsException.class) + public void timestampOutofBounds() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(2, hrs.size()); + assertEquals(100L, hrs.timestamp(0)); + assertEquals(105L, hrs.timestamp(1)); + hrs.timestamp(2); + } + + @Test + public void iterateAllItems() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(2, hrs.size()); + + final HistogramSeekableView it = hrs.iterator(); + HistogramDataPoint hdp = it.next(); + + assertEquals(100L, hdp.timestamp()); + assertEquals(0L, Bytes.getLong(hdp.getRawData())); + + hdp = it.next(); + assertEquals(105L, hdp.timestamp()); + assertEquals(1L, Bytes.getLong(hdp.getRawData())); + + assertFalse(it.hasNext()); + } + + @Test + public void iterateAfterMergeDuplicate() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(2, hrs.size()); + + List hdps2 = new ArrayList(); + hdps2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(20))); + hdps2.add(new LongHistogramDataPointForTest(120L, Bytes.fromLong(4))); + + final HistogramSeekableView it = hrs.iterator(); + HistogramDataPoint hdp = it.next(); + + assertEquals(110L, hdp.timestamp()); + assertEquals(2L, Bytes.getLong(hdp.getRawData())); + + hdp = it.next(); + assertEquals(115L, hdp.timestamp()); + assertEquals(3L, Bytes.getLong(hdp.getRawData())); + + assertFalse(it.hasNext()); + } + + @Test + public void iterateLarge() throws Exception { + long ts = 100L; + final int limit = 64 * 1000; + List hdps = new ArrayList(); + for (int i = 0; i < limit; ++i) { + hdps.add(new LongHistogramDataPointForTest(ts + 5 * i, Bytes.fromLong(i))); + } + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + final HistogramSeekableView it = hrs.iterator(); + while (it.hasNext()) { + assertEquals(ts, it.next().timestamp()); + ts += 5; + } + assertFalse(it.hasNext()); + } + + @Test + public void seekStart() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(3, hrs.size()); + + final HistogramSeekableView it = hrs.iterator(); + it.seek(100L); + HistogramDataPoint hdp = it.next(); + assertEquals(100L, hdp.timestamp()); + assertEquals(0, Bytes.getLong(hdp.getRawData())); + + assertTrue(it.hasNext()); + } + + @Test + public void seekMsBetween() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(3, hrs.size()); + + final HistogramSeekableView it = hrs.iterator(); + it.seek(105L); + HistogramDataPoint hdp = it.next(); + assertEquals(105L, hdp.timestamp()); + assertEquals(1, Bytes.getLong(hdp.getRawData())); + + assertTrue(it.hasNext()); + } + + @Test + public void seekMsEnd() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(3, hrs.size()); + + final HistogramSeekableView it = hrs.iterator(); + it.seek(110L); + HistogramDataPoint hdp = it.next(); + assertEquals(110L, hdp.timestamp()); + assertEquals(2, Bytes.getLong(hdp.getRawData())); + + assertFalse(it.hasNext()); + } + + @Test + public void seekMsTooEarly() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(2, hrs.size()); + + final HistogramSeekableView it = hrs.iterator(); + it.seek(100L); + HistogramDataPoint hdp = it.next(); + assertEquals(105L, hdp.timestamp()); + assertEquals(1, Bytes.getLong(hdp.getRawData())); + + assertTrue(it.hasNext()); + } + + @Test (expected = NoSuchElementException.class) + public void seekMsPastLastDp() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + assertEquals(3, hrs.size()); + + final HistogramSeekableView it = hrs.iterator(); + it.seek(200L); + + it.next(); + } + + @Test + public void getTagUids() throws Exception { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.setRow(key, hdps); + + final ByteMap uids = hrs.getTagUids(); + assertEquals(1, uids.size()); + assertEquals(0, Bytes.memcmp( + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), uids.firstKey())); + assertEquals(0, Bytes.memcmp( + BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1), + uids.firstEntry().getValue())); + } + + @Test (expected = NullPointerException.class) + public void getTagUidsNotSet() throws Exception { + final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); + hrs.getTagUids(); + } +} diff --git a/test/core/TestHistogramSpan.java b/test/core/TestHistogramSpan.java new file mode 100644 index 0000000000..dc674cc3e0 --- /dev/null +++ b/test/core/TestHistogramSpan.java @@ -0,0 +1,266 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.KeyValue; +import org.hbase.async.Bytes.ByteMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, +Config.class, RowKey.class }) +public final class TestHistogramSpan { + protected TSDB tsdb = mock(TSDB.class); + protected Config config = mock(Config.class); + protected UniqueId metrics = mock(UniqueId.class); + protected static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; + protected static final byte[] FAMILY = { 't' }; + protected static final byte[] ZERO = { 0 }; + + protected byte[] hour1 = null; + protected byte[] hour2 = null; + protected byte[] hour3 = null; + + @Before + public void before() throws Exception { + // Inject the attributes we need into the "tsdb" object. + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "table", TABLE); + Whitebox.setInternalState(tsdb, "config", config); + when(tsdb.getConfig()).thenReturn(config); + when(tsdb.metrics.width()).thenReturn((short) 3); + hour1 = BaseTsdbTest.getRowKey( + BaseTsdbTest.generateUID(UniqueIdType.METRIC, (byte) 1), + 1356998400, + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), + BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1)); + hour2 = BaseTsdbTest.getRowKey( + BaseTsdbTest.generateUID(UniqueIdType.METRIC, (byte) 1), + 1357002000, + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), + BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1)); + hour3 = BaseTsdbTest.getRowKey( + BaseTsdbTest.generateUID(UniqueIdType.METRIC, (byte) 1), + 1357005600, + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), + BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1)); + + when(RowKey.metricNameAsync(tsdb, hour1)) + .thenReturn(Deferred.fromResult("in.rps.latency")); + } + + @Test + public void addRow() { + List hdps = new ArrayList(); + hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, hdps); + + assertEquals(2, histSpan.size()); + } + + @Test (expected = NullPointerException.class) + public void addRowNull() { + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, null); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowBadKeyLength() { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + List row2 = new ArrayList(); + row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + final byte[] bad_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x43, 0x20, 0, 0, 0, 1 }; + histSpan.addRow(bad_key, row2); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMissMatchedMetric() { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + List row2 = new ArrayList(); + row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + final byte[] not_matched_mitric_key = new byte[] { 0, 0, 0, 2, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 2 }; + histSpan.addRow(not_matched_mitric_key, row2); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMissMatchedTagk() { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + List row2 = new ArrayList(); + row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + final byte[] not_matched_tagk_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 2, 0, 0, 0, 2 }; + histSpan.addRow(not_matched_tagk_key, row2); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMissMatchedTagv() { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + List row2 = new ArrayList(); + row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + final byte[] not_matched_tagv_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 3 }; + histSpan.addRow(not_matched_tagv_key, row2); + } + + @Test + public void addRowOutOfOrder() { + List row2 = new ArrayList(); + row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour2, row2); + + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + histSpan.addRow(hour1, row1); + + assertEquals(4, histSpan.size()); + + assertEquals(100L, histSpan.timestamp(0)); + assertEquals(105L, histSpan.timestamp(1)); + assertEquals(110L, histSpan.timestamp(2)); + assertEquals(115L, histSpan.timestamp(3)); + } + + @Test (expected = IllegalArgumentException.class) + public void addDifferentKey() throws Exception { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + final byte[] hour1_with_diff_key = Arrays.copyOf(hour1, hour1.length); + hour1_with_diff_key[hour1_with_diff_key.length - 1] = 3; + + List row2 = new ArrayList(); + row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + + histSpan.addRow(hour1_with_diff_key, row2); + } + + @Test + public void getTagUids() { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + final ByteMap uids = histSpan.getTagUids(); + assertEquals(1, uids.size()); + assertEquals(0, Bytes.memcmp( + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), uids.firstKey())); + assertEquals(0, Bytes.memcmp( + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), + uids.firstEntry().getValue())); + } + + @Test (expected = IllegalStateException.class) + public void getTagUidsNotSet() { + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.getTagUids(); + } + + @Test + public void getAggregatedTagUids() { + List row1 = new ArrayList(); + row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); + row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + + final HistogramSpan histSpan = new HistogramSpan(tsdb); + histSpan.addRow(hour1, row1); + + final List uids = histSpan.getAggregatedTagUids(); + assertEquals(0, uids.size()); + } + + @Test + public void getAggregatedTagUidsNotSet() { + final HistogramSpan histSpan = new HistogramSpan(tsdb); + assertTrue(histSpan.getAggregatedTagUids().isEmpty()); + } + + @Test (expected = IndexOutOfBoundsException.class) + public void iteratorEmpty() { + final HistogramSpan histSpan = new HistogramSpan(tsdb); + assertFalse(histSpan.spanIterator().hasNext()); + } + +} diff --git a/test/core/TestHistogramSpanGroup.java b/test/core/TestHistogramSpanGroup.java new file mode 100644 index 0000000000..f47f792a51 --- /dev/null +++ b/test/core/TestHistogramSpanGroup.java @@ -0,0 +1,230 @@ +// 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import net.opentsdb.utils.ByteSet; +import net.opentsdb.utils.Config; + +import org.hbase.async.Bytes; +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.HBaseClient; +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; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, HBaseClient.class, Config.class, HistogramSpanGroup.class, + HistogramSpan.class }) +public final class TestHistogramSpanGroup { + private static long start_ts = 1356998400L; + private static long end_ts = 1356998600L; + + private TSDB tsdb; + + @Before + public void before() { + tsdb = PowerMockito.mock(TSDB.class); + } + + @Test + public void getTagUids() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 2 }, + uids_read.firstEntry().getValue())); + } + + @Test + public void getTagUidsAggedOut() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + final ByteMap uids2 = new ByteMap(); + uids2.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 3 }); + final HistogramSpan span2 = mock(HistogramSpan.class); + when(span2.getTagUids()).thenReturn(uids2); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + spans.add(span2); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getTagUidsNoSpans() throws Exception { + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getAggregatedTagUidsNotAgged() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final List uids_read = group.getAggregatedTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getAggregatedTagUids() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + final ByteMap uids2 = new ByteMap(); + uids2.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 3 }); + final HistogramSpan span2 = mock(HistogramSpan.class); + when(span2.getTagUids()).thenReturn(uids2); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + spans.add(span2); + + final List uids_read = group.getAggregatedTagUids(); + assertEquals(1, uids_read.size()); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, uids_read.get(0)); + } + + @Test + public void getAggregatedTagUidsNoSpans() throws Exception { + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final List uids_read = group.getAggregatedTagUids(); + assertEquals(0, uids_read.size()); + } + + @Test + public void getTagUidsAggedNotInQuery() throws Exception { + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 3 }); + + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(0, uids_read.size()); + final List agg_tags = group.getAggregatedTagUids(); + assertEquals(1, agg_tags.size()); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, agg_tags.get(0)); + } + + @Test + public void getTagUidsInQueryTags() throws Exception { + final ByteSet query_tags = new ByteSet(); + query_tags.add(new byte[] { 0, 0, 0, 1 }); + + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 2 }, + uids_read.firstEntry().getValue())); + assertEquals(0, group.getAggregatedTagUids().size()); + } + + @Test + public void getTagUidsNullQueryTags() throws Exception { + final ByteMap uids = new ByteMap(); + uids.put(new byte[] { 0, 0, 0, 1 }, new byte[] { 0, 0, 0, 2 }); + final HistogramSpan span = mock(HistogramSpan.class); + when(span.getTagUids()).thenReturn(uids); + + DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); + + final ArrayList spans = Whitebox.getInternalState(group, "spans"); + spans.add(span); + + final ByteMap uids_read = group.getTagUids(); + assertEquals(1, uids_read.size()); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); + assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 2 }, + uids_read.firstEntry().getValue())); + assertEquals(0, group.getAggregatedTagUids().size()); + } +} From 8209a5ac7869d44b8f5856e96f1ce31a1d09e5f7 Mon Sep 17 00:00:00 2001 From: HiramJ Date: Sat, 27 May 2017 14:44:33 -0700 Subject: [PATCH 631/826] Add method to write histograms in the TSDB class. Also add methods to the RTPublisher plugin and Data Point filter plugin for handling histograms. Signed-off-by: Chris Larsen --- src/core/Internal.java | 36 ++++++ src/core/TSDB.java | 112 +++++++++++++++---- src/core/WriteableDataPointFilterPlugin.java | 19 ++++ src/tsd/RTPublisher.java | 18 +++ test/core/TestTSDBAddHistogramPoint.java | 77 +++++++++++++ 5 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 test/core/TestTSDBAddHistogramPoint.java diff --git a/src/core/Internal.java b/src/core/Internal.java index 442aade1fe..6f64a6b4b2 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -988,6 +988,42 @@ public static boolean rowKeyMatchsTSUID(final byte[] tsuid, return true; } + /** + * Calculates and returns the column qualifier. The qualifier is the offset + * of the {@code #timestamp} from the row key's base time stamp in seconds + * with a prefix of {@code #PREFIX}. Thus if the offset is 0 and the prefix is + * 1 and the timestamp is in seconds, the qualifier would be [1, 0, 0]. + * Millisecond timestamps will have a 5 byte qualifier. + * @param timestamp The base timestamp. + * @param prefix The prefix to set at the start of the array. + * @return The column qualifier as a byte array + * @throws IllegalArgumentException if the start_time has not been set + * @since 2.4 + */ + public static byte[] getQualifier(final long timestamp, final byte prefix) { + if (timestamp < 1) { + throw new IllegalArgumentException("The start timestamp has not been set"); + } + + final long base_time; + final byte[] qualifier; + 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)); + qualifier = new byte[5]; + final int offset = (int) (timestamp - (base_time * 1000)); + System.arraycopy(Bytes.fromInt(offset), 0, qualifier, 1, 4); + } else { + base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN)); + qualifier = new byte[3]; + final short offset = (short) (timestamp - base_time); + System.arraycopy(Bytes.fromShort(offset), 0, qualifier, 1, 2); + } + qualifier[0] = prefix; + return qualifier; + } + /** * Get timestamp from base time and quantifier for non datapoints. The returned time * will always be in ms. diff --git a/src/core/TSDB.java b/src/core/TSDB.java index df3754051d..7d0aed0098 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -91,6 +91,7 @@ public final class TSDB { private static short TAG_NAME_WIDTH = 3; private static final String TAG_VALUE_QUAL = "tagv"; private static short TAG_VALUE_WIDTH = 3; + private static final int MIN_HISTOGRAM_BYTES = 2; /** Client for the HBase cluster to use. */ final HBaseClient client; @@ -1045,24 +1046,65 @@ public Deferred addPoint(final String metric, tags, flags); } - Deferred addPointInternal(final String metric, - final long timestamp, - final byte[] value, - final Map tags, - final short flags) { - // 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); + /** + * Adds an encoded Histogram data point in the TSDB. + * @param metric A non-empty string. + * @param timestamp The timestamp associated with the value. + * @param raw_data The encoded data blob of the Histogram 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 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 addHistogramPoint(final String metric, + final long timestamp, + final byte[] raw_data, + final Map tags) { + if (raw_data == null || raw_data.length < MIN_HISTOGRAM_BYTES) { + throw new IllegalArgumentException("The histogram raw data is invalid: " + Bytes.pretty(raw_data)); } - IncomingDataPoints.checkMetricAndTags(metric, tags); + + checkTimestampAndTags(metric, timestamp, raw_data, tags, (short) 0); final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); - final long base_time; + + final byte[] qualifier = Internal.getQualifier(timestamp, HistogramDataPoint.PREFIX); + + return storeIntoDB(metric, timestamp, raw_data, tags, (short) 0, row, qualifier); + } + + final Deferred addPointInternal(final String metric, + final long timestamp, + final byte[] value, + final Map tags, + final short flags) { + + checkTimestampAndTags(metric, timestamp, value, tags, flags); + final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); + final byte[] qualifier = Internal.buildQualifier(timestamp, flags); + return storeIntoDB(metric, timestamp, value, tags, flags, row, qualifier); + } + + private final Deferred storeIntoDB(final String metric, + final long timestamp, + final byte[] value, + final Map tags, + final short flags, + final byte[] row, + final byte[] qualifier) { + final long base_time; + if ((timestamp & Const.SECOND_MASK) != 0) { // drop the ms timestamp to seconds to calculate the base timestamp base_time = ((timestamp / 1000) - @@ -1085,7 +1127,7 @@ public Deferred call(final Boolean allowed) throws Exception { RowKey.prefixKeyWithSalt(row); Deferred result = null; - if (config.enable_appends()) { + if (!isHistogram(qualifier) && config.enable_appends()) { if(config.use_otsdb_timestamp()) { LOG.error("Cannot use Date Tiered Compaction with AppendPoints. Please turn off either of them."); } @@ -1093,10 +1135,14 @@ public Deferred call(final Boolean allowed) throws Exception { final AppendRequest point = new AppendRequest(table, row, FAMILY, AppendDataPoints.APPEND_COLUMN_QUALIFIER, kv.getBytes()); result = client.append(point); - } else { + } else if (!isHistogram(qualifier)) { scheduleForCompaction(row, (int) base_time); final PutRequest point = RequestBuilder.buildPutRequest(config, table, row, FAMILY, qualifier, value, timestamp); result = client.put(point); + } else { + scheduleForCompaction(row, (int) base_time); + final PutRequest histo_point = new PutRequest(table, row, FAMILY, qualifier, value); + result = client.put(histo_point); } // Count all added datapoints, not just those that came in through PUT rpc @@ -1134,7 +1180,11 @@ public Deferred call(final Boolean allowed) throws Exception { } if (rt_publisher != null) { - rt_publisher.sinkDataPoint(metric, timestamp, value, tags, tsuid, flags); + if (isHistogram(qualifier)) { + rt_publisher.publishHistogramPoint(metric, timestamp, value, tags, tsuid); + } else { + rt_publisher.sinkDataPoint(metric, timestamp, value, tags, tsuid, flags); + } } return result; } @@ -1145,11 +1195,31 @@ public String toString() { } if (ts_filter != null && ts_filter.filterDataPoints()) { - return ts_filter.allowDataPoint(metric, timestamp, value, tags, flags) - .addCallbackDeferring(new WriteCB()); + if (isHistogram(qualifier)) { + return ts_filter.allowHistogramPoint(metric, timestamp, value, tags) + .addCallbackDeferring(new WriteCB()); + } else { + return ts_filter.allowDataPoint(metric, timestamp, value, tags, flags) + .addCallbackDeferring(new WriteCB()); + } } return Deferred.fromResult(true).addCallbackDeferring(new WriteCB()); } + + private final void checkTimestampAndTags(final String metric, final long timestamp, + final byte[] value, + final Map tags, final short flags) { + // 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); + } /** * Adds a rolled up and/or groupby/pre-agged data point to the proper table. @@ -1988,6 +2058,10 @@ public String getRawTagValue() { return raw_agg_tag_value; } + private final boolean isHistogram(final byte[] qualifier) { + return (qualifier.length & 0x1) == 1; + } + // ------------------ // // Compaction helpers // // ------------------ // diff --git a/src/core/WriteableDataPointFilterPlugin.java b/src/core/WriteableDataPointFilterPlugin.java index 8a01159667..0d97dd2bc9 100644 --- a/src/core/WriteableDataPointFilterPlugin.java +++ b/src/core/WriteableDataPointFilterPlugin.java @@ -90,6 +90,25 @@ public abstract Deferred allowDataPoint( final Map tags, final short flags); + /** + * Determine whether or not the data point should be stored. + * If the data should not be stored, the implementation can return false or an + * exception in the deferred object. Otherwise it should return true and the + * data point will be written to storage. + * @param metric The metric name for the data point + * @param timestamp The timestamp of the data + * @param value The value encoded as either an integer or floating point value + * @param tags The tags associated with the data point + * @return True if the data should be written, false if it should be rejected. + */ + public Deferred allowHistogramPoint( + final String metric, + final long timestamp, + final byte[] value, + final Map tags) { + throw new UnsupportedOperationException("Not yet implemented."); + } + /** * Whether or not the filter should process data points. * @return False if {@link #allowDataPoint(String, long, byte[], Map, short)} diff --git a/src/tsd/RTPublisher.java b/src/tsd/RTPublisher.java index 551267e62d..877c14d784 100644 --- a/src/tsd/RTPublisher.java +++ b/src/tsd/RTPublisher.java @@ -145,4 +145,22 @@ public abstract Deferred publishDataPoint(final String metric, */ public abstract Deferred publishAnnotation(Annotation annotation); + /** + * Called any time a new histogram point is published + * @param metric The name of the metric associated with the data point + * @param timestamp Timestamp as a Unix epoch in seconds or milliseconds + * (depending on the TSD's configuration) + * @param value Encoded raw data blob for the histogram point + * @param tags Tagk/v pairs + * @param tsuid Time series UID for the value + * @return A deferred without special meaning to wait on if necessary. The + * value may be null but a Deferred must be returned. + */ + public Deferred publishHistogramPoint(final String metric, + final long timestamp, final byte[] value, + final Map tags, + final byte[] tsuid) { + throw new UnsupportedOperationException("Not yet implemented"); + } + } diff --git a/test/core/TestTSDBAddHistogramPoint.java b/test/core/TestTSDBAddHistogramPoint.java new file mode 100644 index 0000000000..31f4de3e74 --- /dev/null +++ b/test/core/TestTSDBAddHistogramPoint.java @@ -0,0 +1,77 @@ +package net.opentsdb.core; + +import com.stumbleupon.async.Deferred; +import net.opentsdb.storage.MockBase; +import net.opentsdb.tsd.RTPublisher; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +public class TestTSDBAddHistogramPoint extends BaseTsdbTest { + + private static final byte HISTOGRAM_PREFIX = 0x6; + + @Before + public void beforeLocal() throws Exception { + storage = new MockBase(tsdb, client, true, true, true, true); + } + + @Test + public void addHistogramPoint() throws Exception { + byte[] testRawValue = "Test Raw Value".getBytes(); + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags).joinUninterruptibly(); + final byte[] row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + byte[] qualifier = Internal.getQualifier(1356998400, HISTOGRAM_PREFIX); + final byte[] value = storage.getColumn(row, qualifier); + assertNotNull(value); + assertArrayEquals(testRawValue, value); + } + + @Test (expected = IllegalArgumentException.class) + public void addHistogramPointShortRawData() throws Exception { + byte[] testRawValue = new byte[1]; + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags).joinUninterruptibly(); + } + + @Test (expected = IllegalArgumentException.class) + public void addHistogramPointNullRawData() throws Exception { + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, null, tags).joinUninterruptibly(); + } + + @Test + public void addHistogramPointCallRTPublisher() throws Exception { + byte[] raw_data = new byte[5]; + RTPublisher rt_publisher = mock(RTPublisher.class); + setField(tsdb, "rt_publisher", rt_publisher); + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags).joinUninterruptibly(); + + byte[] tsuid = new byte[]{ 0, 0, 1, 0, 0, 1, 0, 0, 1}; + verify(rt_publisher, times(1)).publishHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags, tsuid); + } + + @Test + public void addHistogramPointTSFilterPlugin() throws Exception { + byte[] raw_data = new byte[5]; + WriteableDataPointFilterPlugin ts_filter = mock(WriteableDataPointFilterPlugin.class); + when(ts_filter.filterDataPoints()).thenReturn(true); + when(ts_filter.allowHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags)). + thenReturn(Deferred.fromResult(true)); + setField(tsdb, "ts_filter", ts_filter); + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags).joinUninterruptibly(); + + verify(ts_filter, times(1)).allowHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags); + } + + private static void setField(TSDB tsdb, String field_name, Object field_value) + throws NoSuchFieldException, IllegalAccessException { + Field field = TSDB.class.getDeclaredField(field_name); + field.setAccessible(true); + field.set(tsdb, field_value); + } + +} From 796c11ffbc1728d36e130bf880421c1cd23fed93 Mon Sep 17 00:00:00 2001 From: qiubz Date: Sun, 28 May 2017 12:52:47 -0700 Subject: [PATCH 632/826] Add histogram scanning to the salt scanner and processing to the compaction queue. Also new Query methods for histograms/percentiles. Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 23 +- src/core/Query.java | 40 ++ src/core/SaltMultiGetter.java | 2 +- src/core/SaltScanner.java | 190 +++++++-- src/core/TSDB.java | 5 +- src/core/TSSubQuery.java | 109 ++++- src/core/TsdbQuery.java | 367 +++++++++++++++- test/core/BaseTsdbTest.java | 58 +++ test/core/TestCompactionQueue.java | 106 ++--- test/core/TestSaltScanner.java | 4 +- test/core/TestTsdbQueryHistogramQueries.java | 427 +++++++++++++++++++ test/storage/MockBase.java | 3 +- 12 files changed, 1240 insertions(+), 94 deletions(-) create mode 100644 test/core/TestTsdbQueryHistogramQueries.java diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 886d583825..7c7fcdb541 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -235,7 +235,7 @@ public String toString() { private final class CompactCB implements Callback> { @Override public Object call(final ArrayList row) { - return compact(row, null); + return compact(row, null, null, null); } @Override public String toString() { @@ -250,9 +250,10 @@ public String toString() { * @return A compacted version of this row. */ KeyValue compact(final ArrayList row, - List annotations) { + List annotations, + List histograms) { final KeyValue[] compacted = { null }; - compact(row, compacted, annotations); + compact(row, compacted, annotations, histograms); return compacted[0]; } @@ -269,6 +270,7 @@ private class Compaction { private final ArrayList row; private final KeyValue[] compacted; private final List annotations; + private final List histograms; private long compactedKVTimestamp; private final int nkvs; @@ -293,11 +295,12 @@ private class Compaction { // and if we only had a single column with a single value, we return this. private KeyValue last_append_column; - public Compaction(ArrayList row, KeyValue[] compacted, List annotations) { + public Compaction(ArrayList row, KeyValue[] compacted, List annotations, List histograms) { nkvs = row.size(); this.row = row; this.compacted = compacted; this.annotations = annotations; + this.histograms = histograms; to_delete = new ArrayList(nkvs); compactedKVTimestamp = Long.MIN_VALUE; } @@ -439,6 +442,13 @@ private int buildHeapProcessAnnotations() { // process annotations and other extended formats if (qual[0] == Annotation.PREFIX()) { annotations.add(JSON.parseToObject(kv.value(), Annotation.class)); + } else if (qual[0] == HistogramDataPoint.PREFIX) { + try { + HistogramDataPoint histogram = Internal.decodeHistogramDataPoint(kv, tsdb.getConfig()); + histograms.add(histogram); + } catch (Throwable t) { + LOG.error("Failed to decode histogram data point", t); + } } else if (qual[0] == AppendDataPoints.APPEND_COLUMN_PREFIX){ compactedKVTimestamp = Math.max(compactedKVTimestamp, kv.timestamp()); final AppendDataPoints adp = new AppendDataPoints(); @@ -669,8 +679,9 @@ protected static boolean isDatapoint(KeyValue kv) { */ Deferred compact(final ArrayList row, final KeyValue[] compacted, - List annotations) { - return new Compaction(row, compacted, annotations).compact(); + List annotations, + List histograms) { + return new Compaction(row, compacted, annotations, histograms).compact(); } /** diff --git a/src/core/Query.java b/src/core/Query.java index 32e94efd66..3d95834004 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -211,6 +211,19 @@ public Deferred configureFromQuery(final TSQuery query, */ DataPoints[] run() throws HBaseException; + /** + * 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 + */ + DataPoints[] runHistogram() throws HBaseException; + /** * Executes the query asynchronously * @return The data points matched by this query. @@ -224,10 +237,37 @@ public Deferred configureFromQuery(final TSQuery query, */ public Deferred runAsync() throws HBaseException; + /** + * 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 + */ + Deferred runHistogramAsync() throws HBaseException; + /** * Returns an index for this sub-query in the original set of queries. * @return A zero based index. * @since 2.4 */ public int getQueryIdx(); + + /** + * Check this is a histogram query or not + * @return + */ + public boolean isHistogramQuery(); + + /** + * Set the percentile calculation parameters for this query if this is + * a histogram query + * + * @param percentiles + */ + public void setPercentiles(List percentiles); } diff --git a/src/core/SaltMultiGetter.java b/src/core/SaltMultiGetter.java index 913ff86a72..e225894f76 100644 --- a/src/core/SaltMultiGetter.java +++ b/src/core/SaltMultiGetter.java @@ -347,7 +347,7 @@ private void processNotRollupQuery(final byte[] key, KeyValue compacted = null; try { final long compaction_start = DateTime.nanoTime(); - compacted = tsdb.compact(row, notes); + compacted = tsdb.compact(row, notes, null); mul_get_compaction_time += (DateTime.nanoTime() - compaction_start); if (compacted != null) { diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 5215d62858..f1c7a97167 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.core; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -66,6 +67,8 @@ public class SaltScanner { * WARNING: The salted row comparator should be applied to this map. */ private final TreeMap spans; + private final TreeMap histSpans; + /** The list of pre-configured scanners. One scanner should be created per * salt bucket. */ private final List scanners; @@ -80,10 +83,16 @@ public class SaltScanner { Collections.synchronizedMap( new TreeMap>(new RowKey.SaltCmp())); + private final Map>>> + histMap = new ConcurrentHashMap>>>(); + /** A deferred to call with the spans on completion */ private final Deferred> results = 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. */ private final byte[] metric; @@ -134,7 +143,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List scanners, final TreeMap spans, final List filters) { - this(tsdb, metric, scanners, spans, filters, false, null, null, 0); + this(tsdb, metric, scanners, spans, filters, false, null, null, 0, null); } /** @@ -149,6 +158,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, * @param filters A list of filters for processing * @param query_stats A stats object for tracking timing * @param query_index The index of the sub query in the main query list + * @param histogramSpans The histo map to populate. * @throws IllegalArgumentException if any required data was missing or * we had invalid parameters. */ @@ -159,7 +169,8 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final boolean delete, final RollupQuery rollup_query, final QueryStats query_stats, - final int query_index) { + final int query_index, + final TreeMap histogramSpans) { if (Const.SALT_WIDTH() < 1) { throw new IllegalArgumentException( "Salting is disabled. Use the regular scanner"); @@ -167,17 +178,20 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, if (tsdb == null) { throw new IllegalArgumentException("The TSDB argument was null."); } - if (spans == null) { - throw new IllegalArgumentException("Span map cannot be null."); + if (spans == null && histogramSpans == null) { + throw new IllegalArgumentException("Both Span map and HistogramSpan map were null."); } - if (!spans.isEmpty()) { + if (spans != null && !spans.isEmpty()) { throw new IllegalArgumentException("The span map should be empty."); } + if (histogramSpans != null && !histogramSpans.isEmpty()) { + throw new IllegalArgumentException("The histogram span map should be empty."); + } if (scanners == null || scanners.isEmpty()) { throw new IllegalArgumentException("Missing or empty scanners list. " + "Please provide a list of scanners for each salt."); } - if (scanners.size() != Const.SALT_BUCKETS()) { + if (Const.SALT_WIDTH() > 0 && scanners.size() != Const.SALT_BUCKETS()) { throw new IllegalArgumentException("Not enough or too many scanners " + scanners.size() + " when the salt bucket count is " + Const.SALT_BUCKETS()); @@ -192,6 +206,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.scanners = scanners; this.spans = spans; + this.histSpans = histogramSpans; this.metric = metric; this.tsdb = tsdb; this.filters = filters; @@ -217,16 +232,27 @@ public Deferred> scan() { return results; } + public Deferred> scanHistogram() { + start_time = DateTime.currentTimeMillis(); + + int index = 0; + for (Scanner scanner: scanners) { + ScannerCB scnr = new ScannerCB(scanner, index++); + scnr.scan(); + } + + return histogramResults; + } + /** * Called once all of the scanners have reported back in to record our * latency and merge the results into the spans map. If there was an exception * stored then we'll return that instead. */ private void mergeAndReturnResults() { - final long hbase_time = System.currentTimeMillis(); + final long hbase_time = DateTime.currentTimeMillis(); TsdbQuery.scanlatency.add((int)(hbase_time - start_time)); - long rows = 0; - + if (exception != null) { LOG.error("After all of the scanners finished, at " + "least one threw an exception", exception); @@ -234,6 +260,99 @@ private void mergeAndReturnResults() { return; } + final long merge_start = DateTime.nanoTime(); + //Merge sorted spans together + if (!isHistogramScan()) { + mergeDataPoints(); + } else { + // Merge histogram data points + mergeHistogramDataPoints(); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("It took " + (DateTime.currentTimeMillis() - hbase_time) + " ms, " + + " to merge and sort the rows into a tree map"); + } + + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, + (DateTime.nanoTime() - merge_start)); + } + + if (!isHistogramScan()) { + results.callback(spans); + } else { + histogramResults.callback(histSpans); + } + } + + private boolean isHistogramScan() { + return histSpans != null; + } + + private void mergeHistogramDataPoints() { + if (histSpans != null) { + for (List>> rows : histMap.values()) { + if (null == rows || rows.isEmpty()) { + LOG.error("Found a histogram rows list that was null or empty"); + continue; + } + + // for all the rows with the same salt in the scann order - timestamp order + for (final SimpleEntry> row : rows) { + if (null == row) { + LOG.error("Found a histogram row item that was null"); + continue; + } + + HistogramSpan histSpan = null; + try { + histSpan = histSpans.get(row.getKey()); + } catch (RuntimeException e) { + LOG.error("Failed to fetch the histogram span", e); + } + + if (histSpan == null) { + histSpan = new HistogramSpan(tsdb); + histSpans.put(row.getKey(), histSpan); + } + + if (annotation_map.containsKey(row.getKey())) { + histSpan.getAnnotations().addAll(annotation_map.get(row.getKey())); + annotation_map.remove(row.getKey()); + } + + try { + histSpan.addRow(row.getKey(), row.getValue()); + } catch (RuntimeException e) { + LOG.error("Exception adding row to histogram span", e); + } + } // end for + } // end for + + histMap.clear(); + + for (byte[] key : annotation_map.keySet()) { + HistogramSpan histSpan = histSpans.get(key); + + if (histSpan == null) { + histSpan = new HistogramSpan(tsdb); + histSpans.put(key, histSpan); + } + + histSpan.getAnnotations().addAll(annotation_map.get(key)); + } + + annotation_map.clear(); + } + } + + /** + * Called once all of the scanners have reported back in to record our + * latency and merge the results into the spans map. If there was an exception + * stored then we'll return that instead. + */ + private void mergeDataPoints() { // Merge sorted spans together final long merge_start = DateTime.nanoTime(); for (final List kvs : kv_map.values()) { @@ -267,7 +386,6 @@ private void mergeAndReturnResults() { } try { datapoints.addRow(kv); - rows++; } catch (RuntimeException e) { LOG.error("Exception adding row to span", e); throw e; @@ -288,19 +406,8 @@ private void mergeAndReturnResults() { datapoints.getAnnotations().add(note); } } - - if (query_stats != null) { - query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, - (DateTime.nanoTime() - merge_start)); - } - if (LOG.isDebugEnabled()) { - LOG.debug("Scanning completed in " + (hbase_time - start_time) + " ms, " + - rows + " rows, and stored in " + spans.size() + " spans"); - LOG.debug("It took " + (System.currentTimeMillis() - hbase_time) + " ms, " - + " to merge and sort the rows into a tree map"); - } - - results.callback(spans); + + annotation_map.clear(); } /** @@ -321,6 +428,12 @@ final class ScannerCB implements Callback()); private final Set keepers = Collections.newSetFromMap( new ConcurrentHashMap()); + + // use list here because we want to keep the rows in the scan order - timestamp order. + // i don't want to define an additional class to store the information of the row key and + // the histogram data points in the row, then use {@link SimpleEntry} + private List>> histograms = + Collections.synchronizedList(Lists.>>newArrayList()); private long scanner_start = -1; /** nanosecond timestamps */ @@ -538,6 +651,8 @@ void processRow(final byte[] key, final ArrayList row) { tsdb.getClient().delete(del); } + List hists = new ArrayList(); + //TODO rollup doesn't use the column qualifier prefix right now //Please move this logic to @CompactionQueue.compact API, if the //qualifier prefix is set for rollup. Right now there is no way to @@ -566,6 +681,13 @@ void processRow(final byte[] key, final ArrayList row) { } map_notes.add(note); } + } else if (qual[0] == HistogramDataPoint.PREFIX) { + try { + HistogramDataPoint histogram = Internal.decodeHistogramDataPoint(kv, tsdb.getConfig()); + hists.add(histogram); + } catch (Throwable t) { + LOG.error("Failed to decode histogram data point", t); + } } else { if (rollup_query.getGroupBy() == Aggregators.AVG || rollup_query.getGroupBy() == Aggregators.DEV) { @@ -580,6 +702,11 @@ void processRow(final byte[] key, final ArrayList row) { } } } // end for + + // histogram row + if (hists.size() > 0) { + this.histograms.add(new SimpleEntry>(key, hists)); + } } else { // calculate estimated data point count. We don't want to deserialize // the byte arrays so we'll just get a rough estimate of compacted @@ -617,7 +744,13 @@ void processRow(final byte[] key, final ArrayList row) { final long compaction_start = DateTime.nanoTime(); try { final List notes = Lists.newArrayList(); - compacted = tsdb.compact(row, notes); + compacted = tsdb.compact(row, notes, hists); + + // histogram row + if (hists.size() > 0) { + this.histograms.add(new SimpleEntry>(key, hists)); + } + if (!notes.isEmpty()) { synchronized (annotations) { List map_notes = annotations.get(key); @@ -685,7 +818,7 @@ void close(final boolean ok) { } if (ok && exception == null) { - validateAndTriggerCallback(kvs, annotations); + validateAndTriggerCallback(kvs, annotations, histograms); } else { completed_tasks.incrementAndGet(); } @@ -698,7 +831,8 @@ void close(final boolean ok) { * @param annotations The annotations fetched by the scanners */ private void validateAndTriggerCallback(final List kvs, - final Map> annotations) { + final Map> annotations, + final List>> histograms) { final int tasks = completed_tasks.incrementAndGet(); if (kvs.size() > 0) { @@ -713,6 +847,10 @@ private void validateAndTriggerCallback(final List kvs, } } + if (histograms.size() > 0) { + histMap.put(tasks, histograms); + } + if (tasks >= Const.SALT_BUCKETS()) { try { mergeAndReturnResults(); diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 7d0aed0098..be00334796 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -2067,8 +2067,9 @@ private final boolean isHistogram(final byte[] qualifier) { // ------------------ // final KeyValue compact(final ArrayList row, - List annotations) { - return compactionq.compact(row, annotations); + List annotations, + List histograms) { + return compactionq.compact(row, annotations, histograms); } /** diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 664bb0ac39..4adf11ff8c 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -88,6 +88,18 @@ public final class TSSubQuery { /** Whether or not to match series with ONLY the given tags */ private boolean explicit_tags; + /** Whether or not to enable fuzzy scanning if explicit tags is set */ + private boolean use_fuzzy_filter; + + /** List of percentiles if fetching histogram data */ + private List percentiles; + + /** Whether or not to return the raw histogram buckets for a histo query. */ + private boolean show_histogram_buckets; + + /** Whether or not to override multi-gets for explicit tag queries */ + private boolean use_multi_gets; + /** Index of the sub query */ private int index; @@ -97,6 +109,8 @@ public final class TSSubQuery { public TSSubQuery() { // Assume no downsampling until told otherwise. downsample_specifier = DownsamplingSpecification.NO_DOWNSAMPLER; + use_fuzzy_filter = true; + use_multi_gets = true; } @Override @@ -129,7 +143,13 @@ public boolean equals(final Object obj) { && Objects.equal(rate, query.rate) && Objects.equal(rate_options, query.rate_options) && Objects.equal(filters, query.filters) - && Objects.equal(explicit_tags, query.explicit_tags); + && Objects.equal(explicit_tags, query.explicit_tags) + && Objects.equal(pre_aggregate, query.pre_aggregate) + && Objects.equal(use_fuzzy_filter, query.use_fuzzy_filter) + && Objects.equal(percentiles, query.percentiles) + && Objects.equal(show_histogram_buckets, query.show_histogram_buckets) + && Objects.equal(use_fuzzy_filter, query.use_fuzzy_filter) + && Objects.equal(use_multi_gets, query.use_multi_gets); } public String toString() { @@ -172,6 +192,10 @@ public String toString() { .append("explicit_tags") .append(", index=") .append(index) + .append(", percentiles=") + .append(percentiles) + .append(", show_histogram_buckets=") + .append(show_histogram_buckets) .append(")"); return buf.toString(); } @@ -216,8 +240,43 @@ public void validateAndSetQuery() { // no downsampler downsample_specifier = DownsamplingSpecification.NO_DOWNSAMPLER; } + checkHistogramQuery(); } + /** + * Make sure the parameters for histogram query are valid. + *

      + *
    • aggregation function: only NONE and SUM supported
    • + *
    • aggregation function in downsampling: only SUM supported
    • + *
    • percentile: only in rage (0,100)
    • + *
    + */ + private void checkHistogramQuery() { + if (!isHistogramQuery()) { + return; + } + + // only support NONE and SUM + if (agg != null && agg != Aggregators.NONE && agg != Aggregators.SUM) { + throw new IllegalArgumentException("Only NONE or SUM aggregation function supported for histogram query"); + } + + // only support SUM in downsampling + if (DownsamplingSpecification.NO_DOWNSAMPLER != downsample_specifier && + downsample_specifier.getHistogramAggregation() != HistogramAggregation.SUM) { + throw new IllegalArgumentException("Only SUM downsampling aggregation supported for histogram query"); + } + + + if (null != percentiles && percentiles.size() > 0) { + for (Float parameter : percentiles) { + if (parameter < 0 || parameter > 100) { + throw new IllegalArgumentException("Invalid percentile parameters: " + parameter); + } + } + } + } + /** @return the parsed aggregation function */ public Aggregator aggregator() { return this.agg; @@ -352,6 +411,19 @@ public void setTsuids(List tsuids) { this.tsuids = tsuids; } + /** @return The percentile parameters */ + public List getPercentiles() { + return percentiles; + } + + /** @param percentiles The percentile parameters*/ + public void setPercentiles(List percentiles) { + this.percentiles = percentiles; + if (this.percentiles != null && !this.percentiles.isEmpty()) { + Collections.sort(this.percentiles); + } + } + /** @param tags an optional list of tags for specificity or grouping * As of 2.2 this will convert the existing tags to filter * @deprecated */ @@ -391,6 +463,16 @@ public void setExplicitTags(final boolean explicit_tags) { this.explicit_tags = explicit_tags; } + /** @return Whether or not the fuzzy filter is enabled. */ + public boolean getUseFuzzyFilter() { + return use_fuzzy_filter; + } + + /** @param use_fuzzy_filter Whether or not to enable the fuzzy filter. */ + public void setUseFuzzyFilter(final boolean use_fuzzy_filter) { + this.use_fuzzy_filter = use_fuzzy_filter; + } + /** @param index the index of the sub query * @since 2.3 */ public void setIndex(final int index) { @@ -448,4 +530,29 @@ void setTsdbQuery(TsdbQuery tsdb_query) { this.tsdb_query = tsdb_query; } + /** + * Whether this query is towards histogram data points. + * @return true if this query is toward histogram data points, false otherwise. + */ + public boolean isHistogramQuery() { + return ((percentiles != null && percentiles.size() > 0) || this.show_histogram_buckets); + } + + public boolean getShowHistogramBuckets() { + return this.show_histogram_buckets; + } + + public void setShowHistogramBuckets(final boolean show_histogram_buckets) { + this.show_histogram_buckets = show_histogram_buckets; + } + + /** @return Whether or not to use multi gets for explicit tag queries */ + public boolean getUseMultiGets() { + return use_multi_gets; + } + + /** @param use_multi_gets Whether or not to use multi gets for explicit tag queries */ + public void setUseMultiGets(final boolean use_multi_gets) { + this.use_multi_gets = use_multi_gets; + } } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 0b57b22d33..ae7935ff12 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -40,6 +40,7 @@ import org.hbase.async.FilterList.Operator; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; @@ -59,6 +60,7 @@ import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.ByteSet; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSON; @@ -167,6 +169,10 @@ final class TsdbQuery implements Query { private boolean has_filter_cannot_use_get = false; + private List percentiles; + + private boolean show_histogram_buckets; + /** * Enum for rollup fallback control. * @since 2.4 @@ -210,6 +216,7 @@ public boolean fallback() { /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; + this.downsampler = DownsamplingSpecification.NO_DOWNSAMPLER; enable_fuzzy_filter = tsdb.getConfig() .getBoolean("tsd.query.enable_fuzzy_filter"); } @@ -303,6 +310,11 @@ public boolean getDelete() { return delete; } + @Override + public void setPercentiles(List percentiles) { + this.percentiles = percentiles; + } + @Override public void setTimeSeries(final String metric, final Map tags, @@ -430,6 +442,10 @@ public Deferred configureFromQuery(final TSQuery query, filters = sub_query.getFilters(); explicit_tags = sub_query.getExplicitTags(); + // set percentile options + percentiles = sub_query.getPercentiles(); + show_histogram_buckets = sub_query.getShowHistogramBuckets(); + if (rollup_usage != ROLLUP_USAGE.ROLLUP_RAW) { //Check whether the down sampler is set and rollup is enabled transformDownSamplerToRollupQuery(aggregator, sub_query.getDownsample()); @@ -619,6 +635,21 @@ public DataPoints[] run() throws HBaseException { } } + @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; @@ -635,6 +666,33 @@ public Deferred runAsync() throws HBaseException { return result; } + @Override + public Deferred runHistogramAsync() throws HBaseException { + if (!isHistogramQuery()) { + throw new RuntimeException("Should never be here"); + } + + Deferred result = null; + if (!this.has_filter_cannot_use_get && this.explicit_tags) { + result = findHistogramSpansWithMultiGetter() + .addCallback(new HistogramGroupByAndAggregateCB()); + } else { + result = findHistogramSpans() + .addCallback(new HistogramGroupByAndAggregateCB()); + } + + return result; + } + + @Override + public boolean isHistogramQuery() { + if ((this.percentiles != null && this.percentiles.size() > 0) || show_histogram_buckets) { + return true; + } + + return false; + } + /** * Finds all the {@link Span}s that match this query. * This is what actually scans the HBase table and loads the data into @@ -674,7 +732,7 @@ private Deferred> findSpans() throws HBaseException { } scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, - delete, rollup_query, query_stats, query_index).scan(); + delete, rollup_query, query_stats, query_index, null).scan(); } scan_start_time = DateTime.nanoTime(); @@ -983,7 +1041,7 @@ void processRow(final byte[] key, final ArrayList row) { } final long compaction_start = DateTime.nanoTime(); final KeyValue compacted = - tsdb.compact(row, datapoints.getAnnotations()); + tsdb.compact(row, datapoints.getAnnotations(), null); compaction_time += (DateTime.nanoTime() - compaction_start); seenAnnotation |= !datapoints.getAnnotations().isEmpty(); if (compacted != null) { // Can be null if we ignored all KVs. @@ -1055,6 +1113,68 @@ private Deferred> findSpansWithMultiGetter() throws HBaseE false).fetch(); } + /** + * Finds all the {@link HistogramSpan}s that match this query. + * This is what actually scans the HBase table and loads the data into + * {@link HistogramSpan}s. + * + * @return A map from HBase row key to the {@link HistogramSpan} for that row key. + * Since a {@link HistogramSpan} actually contains multiple HBase rows, the row key + * stored in the map has its timestamp zero'ed out. + * + * @throws HBaseException if there was a problem communicating with HBase to + * perform the search. + * @throws IllegalArgumentException if bad data was retreived from HBase. + */ + private Deferred> findHistogramSpans() throws HBaseException { + final short metric_width = tsdb.metrics.width(); + final TreeMap histSpans = new TreeMap(new SpanCmp(metric_width)); + + // Copy only the filters that should trigger a tag resolution. If this list + // is empty due to literals or a wildcard star, then we'll save a TON of + // UID lookups + final List scanner_filters; + if (filters != null) { + scanner_filters = new ArrayList(filters.size()); + for (final TagVFilter filter : filters) { + if (filter.postScan()) { + scanner_filters.add(filter); + } + } + } else { + scanner_filters = null; + } + + scan_start_time = System.nanoTime(); + final List scanners; + if (Const.SALT_WIDTH() > 0) { + scanners = new ArrayList(Const.SALT_BUCKETS()); + for (int i = 0; i < Const.SALT_BUCKETS(); i++) { + scanners.add(getScanner(i)); + } + scan_start_time = DateTime.nanoTime(); + return new SaltScanner(tsdb, metric, scanners, null, scanner_filters, + delete, rollup_query, query_stats, query_index, histSpans).scanHistogram(); + } else { + scanners = Lists.newArrayList(getScanner()); + scan_start_time = DateTime.nanoTime(); + return new SaltScanner(tsdb, metric, scanners, null, scanner_filters, + delete, rollup_query, query_stats, query_index, histSpans).scanHistogram(); + } + } + + 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)); + + scan_start_time = System.nanoTime(); + return Deferred.fromError(new UnsupportedOperationException("Not implemented yet.")); + //return new SaltMultiGetter(tsdb, metric, row_key_literals, getScanStartTimeSeconds(), getScanEndTimeSeconds(), + // tableToBeScanned(), null, histSpans, rollup_query, query_stats, query_index).fetchHistogram(); + } + + /** * Callback that should be attached the the output of * {@link TsdbQuery#findSpans} to group and sort the results. @@ -1190,6 +1310,249 @@ public DataPoints[] call(final TreeMap spans) throws Exception { } } + /** + * Callback that should be attached the the output of + * {@link TsdbQuery#findHistogramSpans} to group and sort the results. + */ + private class HistogramGroupByAndAggregateCB implements + Callback>{ + + /** + * Creates the {@link HistogramSpanGroup}s to form the final results of this query. + * @param spans The {@link HistogramSpan}s found for this query ({@link #findHistogramSpans}). + * Can be {@code null}, in which case the array returned will be empty. + * @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 { + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, + (System.nanoTime() - TsdbQuery.this.scan_start_time)); + } + + final long group_build = System.nanoTime(); + if (spans == null || spans.size() <= 0) { + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, 0); + } + return NO_RESULT; + } + final ByteSet query_tags = null; + // TODO +// if (agg_tag_promotion && group_bys != null && !group_bys.isEmpty()) { +// query_tags = new ByteSet(); +// query_tags.addAll(group_bys); +// } else { +// query_tags = null; +// } + + final ArrayList result_dp_groups = new ArrayList(); + // The raw aggregator skips group bys and ignores downsampling + if (aggregator == Aggregators.NONE) { + for (final HistogramSpan span : spans.values()) { + final HistogramSpanGroup group = new HistogramSpanGroup(tsdb, + getScanStartTimeSeconds(), + getScanEndTimeSeconds(), + null, + null, + downsampler, + getStartTime(), + getEndTime(), + query_index, + RollupQuery.isValidQuery(rollup_query), + query_tags); + group.add(span); + + // create histogram data points to data points adaptor for each percentile calculation + if (null != percentiles && percentiles.size() > 0) { + List percentile_datapoints_list = generateHistogramPercentileDataPoints(group); + if (null != percentile_datapoints_list && percentile_datapoints_list.size() > 0) + result_dp_groups.addAll(percentile_datapoints_list); + } + + + // create bucket metric + if (show_histogram_buckets) { + List bucket_datapoints_list = generateHistogramBucketDataPoints(group); + if (null != bucket_datapoints_list && bucket_datapoints_list.size() > 0) { + result_dp_groups.addAll(bucket_datapoints_list); + } + } + } // end for + + int i = 0; + DataPoints[] result = new DataPoints[result_dp_groups.size()]; + for (DataPoints item : result_dp_groups) { + result[i++] = item; + } + return result; + } + + if (group_bys == null) { + // We haven't been asked to find groups, so let's put all the spans + // together in the same group. + final HistogramSpanGroup group = new HistogramSpanGroup(tsdb, + getScanStartTimeSeconds(), + getScanEndTimeSeconds(), + spans.values(), + HistogramAggregation.SUM, // only SUM is applicable for histogram metric + downsampler, + getStartTime(), + getEndTime(), + query_index, + RollupQuery.isValidQuery(rollup_query), + query_tags); + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, + (System.nanoTime() - group_build)); + } + + // create histogram data points to data points adaptor for each percentile calculation + if (null != percentiles && percentiles.size() > 0) { + List percentile_datapoints_list = generateHistogramPercentileDataPoints(group); + if (null != percentile_datapoints_list && percentile_datapoints_list.size() > 0) + result_dp_groups.addAll(percentile_datapoints_list); + } + + // create bucket metric + if (show_histogram_buckets) { + List bucket_datapoints_list = generateHistogramBucketDataPoints(group); + if (null != bucket_datapoints_list && bucket_datapoints_list.size() > 0) { + result_dp_groups.addAll(bucket_datapoints_list); + } + } + + int i = 0; + DataPoints[] result = new DataPoints[result_dp_groups.size()]; + for (DataPoints item : result_dp_groups) { + result[i++] = item; + } + return result; + } + + // Maps group value IDs to the SpanGroup for those values. Say we've + // been asked to group by two things: foo=* bar=* Then the keys in this + // map will contain all the value IDs combinations we've seen. If the + // name IDs for `foo' and `bar' are respectively [0, 0, 7] and [0, 0, 2] + // then we'll have group_bys=[[0, 0, 2], [0, 0, 7]] (notice it's sorted + // by ID, so bar is first) and say we find foo=LOL bar=OMG as well as + // foo=LOL bar=WTF and that the IDs of the tag values are: + // LOL=[0, 0, 1] OMG=[0, 0, 4] WTF=[0, 0, 3] + // then the map will have two keys: + // - one for the LOL-OMG combination: [0, 0, 1, 0, 0, 4] and, + // - one for the LOL-WTF combination: [0, 0, 1, 0, 0, 3]. + final ByteMap groups = new ByteMap(); + final short value_width = tsdb.tag_values.width(); + final byte[] group = new byte[group_bys.size() * value_width]; + for (final Map.Entry entry : spans.entrySet()) { + final byte[] row = entry.getKey(); + byte[] value_id = null; + int i = 0; + // TODO(tsuna): The following loop has a quadratic behavior. We can + // make it much better since both the row key and group_bys are sorted. + for (final byte[] tag_id : group_bys) { + value_id = Tags.getValueId(tsdb, row, tag_id); + if (value_id == null) { + break; + } + System.arraycopy(value_id, 0, group, i, value_width); + i += value_width; + } + if (value_id == null) { + LOG.error("WTF? Dropping span for row " + Arrays.toString(row) + + " as it had no matching tag from the requested groups," + + " which is unexpected. Query=" + this); + continue; + } + + //LOG.info("Span belongs to group " + Arrays.toString(group) + ": " + Arrays.toString(row)); + HistogramSpanGroup thegroup = groups.get(group); + if (thegroup == null) { + thegroup = new HistogramSpanGroup(tsdb, + getScanStartTimeSeconds(), + getScanEndTimeSeconds(), + null, + HistogramAggregation.SUM, // only SUM is applicable for histogram metric + downsampler, + getStartTime(), + getEndTime(), + query_index, + RollupQuery.isValidQuery(rollup_query), + query_tags); + + // 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); + groups.put(group_copy, thegroup); + } + thegroup.add(entry.getValue()); + } + + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.GROUP_BY_TIME, + (System.nanoTime() - group_build)); + } + + + for (final Map.Entry entry : groups.entrySet()) { + // create histogram data points to data points adaptor for each percentile calculation + if (null != percentiles && percentiles.size() > 0) { + List percentile_datapoints_list = generateHistogramPercentileDataPoints(entry.getValue()); + if (null != percentile_datapoints_list && percentile_datapoints_list.size() > 0) + result_dp_groups.addAll(percentile_datapoints_list); + } + + // create bucket metric + if (show_histogram_buckets) { + List bucket_datapoints_list = generateHistogramBucketDataPoints(entry.getValue()); + if (null != bucket_datapoints_list && bucket_datapoints_list.size() > 0) { + result_dp_groups.addAll(bucket_datapoints_list); + } + } + } // end for + + int i = 0; + DataPoints[] result = new DataPoints[result_dp_groups.size()]; + for (DataPoints item : result_dp_groups) { + result[i++] = item; + } + return result; + } + + private List generateHistogramPercentileDataPoints(final HistogramSpanGroup group) { + ArrayList result_dp_groups = new ArrayList(); + for (final Float percentil : percentiles) { + final HistogramDataPointsToDataPointsAdaptor dp_adaptor = new HistogramDataPointsToDataPointsAdaptor(group, + percentil.floatValue()); + result_dp_groups.add(dp_adaptor); + } // end for + + return result_dp_groups; + } + + private List generateHistogramBucketDataPoints(final HistogramSpanGroup group) { + ArrayList result_dp_groups = new ArrayList(); + try { + HistogramSeekableView seek_view = group.iterator(); + if (seek_view.hasNext()) { + HistogramDataPoint hdp = seek_view.next(); + Map buckets = hdp.getHistogramBucketsIfHas(); + if (null != buckets) { + for (Map.Entry bucket : buckets.entrySet()) { + final HistogramBucketDataPointsAdaptor dp_bucket_adaptor = new HistogramBucketDataPointsAdaptor(group, bucket.getKey()); + result_dp_groups.add(dp_bucket_adaptor); + } // end for + } // end if + } + } catch (UnsupportedOperationException e) { + // Just Ignore + } + + return result_dp_groups; + } + } + /** * Scan the tables again with the next best rollup match, on empty result set */ diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index c50ecf96e4..71b94f3ec6 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -92,6 +92,10 @@ public class BaseTsdbTest { static final String NOTE_DESCRIPTION = "Hello DiscWorld!"; static final String NOTE_NOTES = "Millenium hand and shrimp"; + //histgoram metric + public static final String HISTOGRAM_METRIC_STRING = "msg.end2end.latency"; + public static final byte[] HISTOGRAM_METRIC_BYTES = new byte[] { 0, 0, 5 }; + public static final Map UIDS = new HashMap(26); static { char letter = 'A'; @@ -140,10 +144,13 @@ public void before() throws Exception { setupTagkMaps(); setupTagvMaps(); + mockUID(UniqueIdType.METRIC, HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + // add metrics and tags to the UIDs list for other functions to share uid_map.put(METRIC_STRING, METRIC_BYTES); uid_map.put(METRIC_B_STRING, METRIC_B_BYTES); uid_map.put(NSUN_METRIC, NSUI_METRIC); + uid_map.put(HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); uid_map.put(TAGK_STRING, TAGK_BYTES); uid_map.put(TAGK_B_STRING, TAGK_B_BYTES); @@ -766,6 +773,57 @@ protected void storeMixedTimeSeriesMsAndS() throws Exception { } } + + //store histogram data points of {@link LongHistogramDataPointForTest} with second timestamp + protected void storeTestHistogramTimeSeriesSeconds(final boolean offset) throws Exception { + setDataPointStorage(); + + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + HashMap tags_local = new HashMap(); + tags_local.put("host", "web01"); + + long timestamp = 1356998400; + for (int i = 1; i <= 300; i++) { + LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); + tsdb.addHistogramPoint(HISTOGRAM_METRIC_STRING, timestamp += 30, hdp.getRawData(), tags_local).joinUninterruptibly(); + } + + // dump a parallel set but invert the values + tags_local.clear(); + tags_local.put("host", "web02"); + timestamp = offset ? 1356998415 : 1356998400; + for (int i = 300; i > 0; i--) { + LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); + tsdb.addHistogramPoint(HISTOGRAM_METRIC_STRING, timestamp += 30, hdp.getRawData(), tags_local).joinUninterruptibly(); + } + } + + // store histogram data points of {@link LongHistogramDataPointForTest} with ms timestamp + protected void storeTestHistogramTimeSeriesMs() throws Exception { + setDataPointStorage(); + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + long timestamp = 1356998400000L; + for (int i = 1; i <= 300; i++) { + timestamp += 500; + LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); + tsdb.addHistogramPoint("msg.end2end.latency", timestamp, hdp.getRawData(), tags).joinUninterruptibly(); + } // end for + + // dump a parallel set but invert the values + tags.clear(); + tags.put("host", "web02"); + timestamp = 1356998400000L; + for (int i = 300; i > 0; i--) { + timestamp += 500; + LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); + tsdb.addHistogramPoint("msg.end2end.latency", timestamp, hdp.getRawData(), tags).joinUninterruptibly(); + } // end for + } + /** * Validates the metric name, tags and annotations * @param dps The datapoints array returned from the query diff --git a/test/core/TestCompactionQueue.java b/test/core/TestCompactionQueue.java index 1e41ee0e10..651150304d 100644 --- a/test/core/TestCompactionQueue.java +++ b/test/core/TestCompactionQueue.java @@ -123,7 +123,7 @@ public void useMaxTsWhileCompacting() throws Exception { kvs.add(makekvWithTs(qual3, ts3, val3)); when(tsdb.getConfig().getBoolean("tsd.storage.use_otsdb_timestamp")).thenReturn(true); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2, qual3), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, val3, ZERO), kv.value()); assert(kv.timestamp() == Math.max(ts1, Math.max(ts2, ts3))); @@ -133,7 +133,7 @@ public void useMaxTsWhileCompacting() throws Exception { public void emptyRow() throws Exception { ArrayList kvs = new ArrayList(0); ArrayList annotations = new ArrayList(0); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertNull(kv); // We had nothing to do so... @@ -150,7 +150,7 @@ public void oneCellRow() throws Exception { final byte[] qual = { 0x00, 0x07 }; final byte[] val = Bytes.fromLong(42L); kvs.add(makekv(qual, val)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); @@ -169,7 +169,7 @@ public void oneCellAppend() throws Exception { final byte[] val = Bytes.fromLong(42L); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); @@ -188,7 +188,7 @@ public void oneCellRowWAnnotation() throws Exception { final byte[] qual = { 0x00, 0x07 }; final byte[] val = Bytes.fromLong(42L); kvs.add(makekv(qual, val)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); assertEquals(1, annotations.size()); @@ -209,7 +209,7 @@ public void oneCellAppendWAnnotiation() throws Exception { final byte[] val = Bytes.fromLong(42L); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); assertEquals(1, annotations.size()); @@ -229,7 +229,7 @@ public void oneCellRowWAnnotationMS() throws Exception { final byte[] qual = { 0x00, 0x07 }; final byte[] val = Bytes.fromLong(42L); kvs.add(makekv(qual, val)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); assertEquals(1, annotations.size()); @@ -249,7 +249,7 @@ public void oneCellRowBadLength() throws Exception { final byte[] cqual = { 0x00, 0x07 }; byte[] val = Bytes.fromLong(42L); kvs.add(makekv(qual, val)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(cqual, kv.qualifier()); assertArrayEquals(val, kv.value()); @@ -266,7 +266,7 @@ public void oneCellRowMS() throws Exception { final byte[] qual = { (byte) 0xF0, 0x00, 0x00, 0x07 }; byte[] val = Bytes.fromLong(42L); kvs.add(makekv(qual, val)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual, kv.qualifier()); assertArrayEquals(val, kv.value()); @@ -288,7 +288,7 @@ public void twoCellRow() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); @@ -309,7 +309,7 @@ public void twoCellAppend() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); @@ -332,7 +332,7 @@ public void twoCellRowWAnnotation() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); assertEquals(1, annotations.size()); @@ -355,7 +355,7 @@ public void twoCellAppendWAnnotations() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual, val, qual2, val2))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); assertEquals(1, annotations.size()); @@ -383,7 +383,7 @@ public void fullRowSeconds() throws Exception { values = MockBase.concatByteArrays(values, Bytes.fromLong(i)); } - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qualifiers), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(values, ZERO), kv.value()); @@ -409,7 +409,7 @@ public void bigRowMs() throws Exception { values = MockBase.concatByteArrays(values, Bytes.fromLong(i)); i += 100; } - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qualifiers), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(values, ZERO), kv.value()); @@ -431,7 +431,7 @@ public void twoCellRowMS() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); @@ -456,7 +456,7 @@ public void sortMsAndS() throws Exception { final byte[] val3 = Bytes.fromLong(5L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, @@ -484,7 +484,7 @@ public void secondsOutOfOrder() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual2, qual3, qual1), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val2, val3, val1, ZERO), @@ -514,7 +514,7 @@ public void msOutOfOrder() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual2, qual3, qual1), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val2, val3, val1, ZERO), @@ -538,7 +538,7 @@ public void secondAndMs() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), kv.value()); @@ -562,7 +562,7 @@ public void secondAndMsWAnnotation() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, new byte[] { 1 }), kv.value()); @@ -587,7 +587,7 @@ public void msSameAsSecond() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - compactionq.compact(kvs, annotations); + compactionq.compact(kvs, annotations, null); } @Test @@ -601,7 +601,7 @@ public void msSameAsSecondFix() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual2, kv.qualifier()); assertArrayEquals(val2, kv.value()); @@ -625,7 +625,7 @@ public void fixQualifierFlags() throws Exception { final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(cqual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val2, ZERO), kv.value()); @@ -652,7 +652,7 @@ public void fixFloatingPoint() throws Exception { final byte[] cval2 = Bytes.fromInt(Float.floatToRawIntBits(4.2F)); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, cval2, ZERO), kv.value()); @@ -676,7 +676,7 @@ public void overlappingDataPoints() throws Exception { final byte[] val2 = Bytes.fromInt(4); kvs.add(makekv(qual2, val2)); - compactionq.compact(kvs, annotations); + compactionq.compact(kvs, annotations, null); } @Test @@ -691,7 +691,7 @@ public void overlappingDataPointsFix() throws Exception { final byte[] val2 = Bytes.fromInt(4); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual2, kv.qualifier()); assertArrayEquals(val2, kv.value()); @@ -718,7 +718,7 @@ public void failedCompactNoop() throws Exception { final byte[] valcompact = MockBase.concatByteArrays(val1, val2, ZERO); kvs.add(makekv(qualcompact, valcompact)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qualcompact, kv.qualifier()); assertArrayEquals(valcompact, kv.value()); @@ -734,7 +734,7 @@ public void annotationOnly() throws Exception { ArrayList annotations = new ArrayList(1); kvs.add(makekv(note_qual, note)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertNull(kv); assertEquals(1, annotations.size()); @@ -753,7 +753,7 @@ public void annotationsOnly() throws Exception { kvs.add(makekv(note_qual, note)); kvs.add(makekv(new byte[] { 1, 0, 1 }, note)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertNull(kv); assertEquals(2, annotations.size()); @@ -782,7 +782,7 @@ public void secondCompact() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), @@ -815,7 +815,7 @@ public void secondCompactWAnnotation() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), @@ -848,7 +848,7 @@ public void secondCompactMS() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), @@ -881,7 +881,7 @@ public void secondCompactMixedSecond() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, @@ -915,7 +915,7 @@ public void secondCompactMixedMS() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, @@ -950,7 +950,7 @@ public void secondCompactMixedMSAndS() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual3, qual1, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val3, val1, val2, @@ -984,7 +984,7 @@ public void secondCompactOverwrite() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - compactionq.compact(kvs, annotations); + compactionq.compact(kvs, annotations, null); } @Test @@ -1006,7 +1006,7 @@ public void secondCompactOverwriteFix() throws Exception { final byte[] val3 = Bytes.fromLong(6L); kvs.add(makekv(qual3, val3)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val3, val2, new byte[] { 0 }), @@ -1046,7 +1046,7 @@ public void doubleFailedCompactNoop() throws Exception { kvs.add(makekv(qual3, val3)); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(qual132, kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), kv.value()); @@ -1084,7 +1084,7 @@ public void weirdOverlappingCompactedCells() throws Exception { kvs.add(makekv(qual3, val3)); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual1, qual3, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val1, val3, val2, ZERO), @@ -1126,7 +1126,7 @@ public void tripleCompacted() throws Exception { kvs.add(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); kvs.add(makekv(qual56, MockBase.concatByteArrays(val5, val6, ZERO))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals( MockBase.concatByteArrays(qual12, qual34, qual56), kv.qualifier()); assertArrayEquals( @@ -1169,7 +1169,7 @@ public void tripleCompactedOutOfOrder() throws Exception { kvs.add(makekv(qual56, MockBase.concatByteArrays(val5, val6, ZERO))); kvs.add(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals( MockBase.concatByteArrays(qual12, qual34, qual56), kv.qualifier()); assertArrayEquals( @@ -1213,7 +1213,7 @@ public void tripleCompactedSecondsAndMs() throws Exception { kvs.add(makekv(qual34, MockBase.concatByteArrays(val3, val4, ZERO))); kvs.add(makekv(qual56, MockBase.concatByteArrays(val5, val6, ZERO))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals( MockBase.concatByteArrays(qual12, qual34, qual56), kv.qualifier()); // TODO(jat): metadata byte should be 0x01? @@ -1245,7 +1245,7 @@ public void appendsAndLaterPuts() throws Exception { MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(qual3, val3)); kvs.add(makekv(qual4, val4)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), @@ -1274,7 +1274,7 @@ public void appendsAndEarlierPuts() throws Exception { kvs.add(makekv(qual2, val2)); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual3, val3, qual4, val4))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), @@ -1303,7 +1303,7 @@ public void appendsAndInterspersedPuts() throws Exception { kvs.add(makekv(qual3, val3)); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual2, val2, qual4, val4))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), @@ -1332,7 +1332,7 @@ public void doubleAppends() throws Exception { MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual3, val3, qual4, val4))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), @@ -1367,7 +1367,7 @@ public void tripleAppends() throws Exception { MockBase.concatByteArrays(qual3, val3, qual4, val4))); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual5, val5, qual6, val6))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays( qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays( @@ -1402,7 +1402,7 @@ public void doubleAppendsAndPuts() throws Exception { kvs.add(makekv(qual4, val4)); kvs.add(makekv(AppendDataPoints.APPEND_COLUMN_QUALIFIER, MockBase.concatByteArrays(qual5, val5, qual6, val6))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays( qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays( @@ -1431,7 +1431,7 @@ public void appendsAndCompacted() throws Exception { MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(MockBase.concatByteArrays(qual3, qual4), MockBase.concatByteArrays(val3, val4, ZERO))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2, qual3, qual4), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, val3, val4, ZERO), @@ -1466,7 +1466,7 @@ public void appendsAndCompactedAndPuts() throws Exception { MockBase.concatByteArrays(val3, val4, ZERO))); kvs.add(makekv(qual5, val5)); kvs.add(makekv(qual6, val6)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays( qual, qual2, qual3, qual4, qual5, qual6), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays( @@ -1491,7 +1491,7 @@ public void appendsDuplicatePuts() throws Exception { MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(qual, val)); kvs.add(makekv(qual2, val2)); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); @@ -1514,7 +1514,7 @@ public void appendsDuplicateCompacted() throws Exception { MockBase.concatByteArrays(qual, val, qual2, val2))); kvs.add(makekv(MockBase.concatByteArrays(qual, qual2), MockBase.concatByteArrays(val, val2, ZERO))); - final KeyValue kv = compactionq.compact(kvs, annotations); + final KeyValue kv = compactionq.compact(kvs, annotations, null); assertArrayEquals(MockBase.concatByteArrays(qual, qual2), kv.qualifier()); assertArrayEquals(MockBase.concatByteArrays(val, val2, ZERO), kv.value()); diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java index 6992fe6a97..189708f91f 100644 --- a/test/core/TestSaltScanner.java +++ b/test/core/TestSaltScanner.java @@ -400,7 +400,7 @@ public void scanCompactionDataException() throws Exception { setupMockScanners(false); doThrow(new IllegalDataException("Boo!")).when( - tsdb).compact(any(ArrayList.class), any(List.class)); + tsdb).compact(any(ArrayList.class), any(List.class), any(List.class)); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); @@ -413,7 +413,7 @@ public void scanCompactionRuntimeException() throws Exception { setupMockScanners(false); doThrow(new RuntimeException("Boo!")).when( - tsdb).compact(any(ArrayList.class), any(List.class)); + tsdb).compact(any(ArrayList.class), any(List.class), any(List.class)); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); diff --git a/test/core/TestTsdbQueryHistogramQueries.java b/test/core/TestTsdbQueryHistogramQueries.java new file mode 100644 index 0000000000..90b1dc2ccf --- /dev/null +++ b/test/core/TestTsdbQueryHistogramQueries.java @@ -0,0 +1,427 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.reflect.Whitebox; + +import net.opentsdb.meta.Annotation; + +public class TestTsdbQueryHistogramQueries extends BaseTsdbTest { + private TsdbQuery query = null; + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + query = new TsdbQuery(tsdb); + } + + @Test + public void runSingleTsMsSinglePercentile() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + + this.storeTestHistogramTimeSeriesMs(); + storage.dumpToSystemOut(); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertTrue(dps[0].isPercentile()); + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + } // end runSingleTsMsSinglePercentile() + + @Test + public void runSingleTsMsDoulePercentile() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + + this.storeTestHistogramTimeSeriesMs(); + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + float per_95 = 0.95F; + percentiles.add(per_95); + + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertTrue(dps[0].isPercentile()); + assertTrue(dps[1].isPercentile()); + + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + assertEquals("msg.end2end.latency_pct_0.95", dps[1].metricName()); + + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + assertTrue(dps[1].getAggregatedTags().isEmpty()); + assertNull(dps[1].getAnnotations()); + assertEquals("web01", dps[1].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + + int value_95 = 1; + for (DataPoint dp : dps[1]) { + assertEquals(value_95 * 0.95, dp.doubleValue(), 0.0001); + value_95++; + } + assertEquals(300, dps[1].aggregatedSize()); + } // end runSingleTsMsSinglePercentile() + + @Test + public void runSingleTsMsTwoAggSum() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesMs(); + + HashMap tags = new HashMap(); + + query.setStartTime(1356998400L); + query.setEndTime(1357041600L); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertTrue(dps[0].isPercentile()); + + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + for (DataPoint dp : dps[0]) { + assertEquals(301 * 0.98, dp.doubleValue(), 0.0001); + } + assertEquals(300, dps[0].size()); + } // end runSingleTsMsTwoAggSum() + + @Test + public void runSingleTsMsAggNone() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesMs(); + + HashMap tags = new HashMap(); + + query.setStartTime(1356998400L); + query.setEndTime(1357041600L); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.NONE, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals(2, dps.length); + + assertTrue(dps[0].isPercentile()); + assertTrue(dps[1].isPercentile()); + + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(1, dps[0].getTags().size()); + assertEquals("web01", dps[0].getTags().get("host")); + + assertEquals("msg.end2end.latency_pct_0.98", dps[1].metricName()); + assertTrue(dps[1].getAggregatedTags().isEmpty()); + assertNull(dps[1].getAnnotations()); + assertEquals(1, dps[1].getTags().size()); + assertEquals("web02", dps[1].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + ++value; + } + assertEquals(300, dps[0].size()); + + int value_other = 300; + for (DataPoint dp : dps[1]) { + assertEquals(value_other * 0.98, dp.doubleValue(), 0.0001); + --value_other; + } + assertEquals(300, dps[1].size()); + } // end runSingleTsMsTwoAggSum() + + @Test + public void runSingleTsMsAggSumTwoGroups() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesMs(); + + HashMap tags = new HashMap(); + tags.put("host", "*"); + + query.setStartTime(1356998400L); + query.setEndTime(1357041600L); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals(2, dps.length); + + assertTrue(dps[0].isPercentile()); + assertTrue(dps[1].isPercentile()); + + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(1, dps[0].getTags().size()); + assertEquals("web01", dps[0].getTags().get("host")); + + assertEquals("msg.end2end.latency_pct_0.98", dps[1].metricName()); + assertTrue(dps[1].getAggregatedTags().isEmpty()); + assertNull(dps[1].getAnnotations()); + assertEquals(1, dps[1].getTags().size()); + assertEquals("web02", dps[1].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + ++value; + } + assertEquals(300, dps[0].size()); + + int value_other = 300; + for (DataPoint dp : dps[1]) { + assertEquals(value_other * 0.98, dp.doubleValue(), 0.0001); + --value_other; + } + assertEquals(300, dps[1].size()); + } // end runSingleTsMsTwoAggSum() + + @Test + public void runWithAnnotation() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesSeconds(false); + + final Annotation note = new Annotation(); + note.setTSUID(getTSUIDString(HISTOGRAM_METRIC_STRING, TAGK_STRING, TAGV_STRING)); + note.setStartTime(1356998490); + note.setDescription("Hello World!"); + note.syncToStorage(tsdb, false).joinUninterruptibly(); + + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals(1, dps[0].getAnnotations().size()); + assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + value++; + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runWithOnlyAnnotation() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesSeconds(false); + + byte[] key = getRowKey(HISTOGRAM_METRIC_STRING, 1357002000, TAGK_STRING, TAGV_STRING); + storage.flushRow(key); + final Annotation note = new Annotation(); + note.setTSUID(getTSUIDString(HISTOGRAM_METRIC_STRING, TAGK_STRING, TAGV_STRING)); + note.setStartTime(1357002090); + note.setDescription("Hello World!"); + note.syncToStorage(tsdb, false).joinUninterruptibly(); + + HashMap tags = new HashMap(1); + tags.put("host", "web01"); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + query.setTimeSeries("msg.end2end.latency", tags, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals(1, dps[0].getAnnotations().size()); + assertEquals("Hello World!", dps[0].getAnnotations().get(0).getDescription()); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + value++; + // account for the jump + if (value == 120) { + value = 240; + } + } + assertEquals(180, dps[0].size()); + } + + @Test + public void runTSUIDQuery() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesSeconds(false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + final List tsuids = new ArrayList(1); + tsuids.add(getTSUIDString(HISTOGRAM_METRIC_STRING, TAGK_STRING, TAGV_STRING)); + + query.setTimeSeries(tsuids, Aggregators.SUM, false); + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals("web01", dps[0].getTags().get("host")); + + int value = 1; + for (DataPoint dp : dps[0]) { + assertEquals(value * 0.98, dp.doubleValue(), 0.0001); + value++; + } + assertEquals(300, dps[0].aggregatedSize()); + } + + @Test + public void runTSUIDsAggSum() throws Exception { + Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + this.storeTestHistogramTimeSeriesSeconds(false); + + query.setStartTime(1356998400); + query.setEndTime(1357041600); + + final List tsuids = new ArrayList(1); + tsuids.add(getTSUIDString(HISTOGRAM_METRIC_STRING, TAGK_STRING, TAGV_STRING)); + tsuids.add(getTSUIDString(HISTOGRAM_METRIC_STRING, TAGK_STRING, TAGV_B_STRING)); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals("msg.end2end.latency_pct_0.98", dps[0].metricName()); + assertEquals("host", dps[0].getAggregatedTags().get(0)); + assertNull(dps[0].getAnnotations()); + assertTrue(dps[0].getTags().isEmpty()); + + for (DataPoint dp : dps[0]) { + assertEquals(301 * 0.98, dp.doubleValue(), 0.0001); + } + assertEquals(300, dps[0].size()); + } + + @Test + public void runTSUIDQueryNoData() throws Exception { + setDataPointStorage(); + query.setStartTime(1356998400); + query.setEndTime(1357041600); + final List tsuids = new ArrayList(1); + tsuids.add(getTSUIDString(HISTOGRAM_METRIC_STRING, TAGK_STRING, TAGV_STRING)); + query.setTimeSeries(tsuids, Aggregators.SUM, false); + + List percentiles = new ArrayList(); + float per_98 = 0.98F; + percentiles.add(per_98); + query.setPercentiles(percentiles); + + final DataPoints[] dps = query.runHistogram(); + + assertNotNull(dps); + assertEquals(0, dps.length); + } +} diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 6aeb38ddd6..5f0a78e863 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -713,7 +713,8 @@ public void tsdbCompactAllRows() throws Exception { row.remove(k); } final KeyValue compacted = - Whitebox.invokeMethod(tsdb, "compact", kvs, Collections.EMPTY_LIST); + Whitebox.invokeMethod(tsdb, "compact", kvs, Collections.EMPTY_LIST, + Collections.EMPTY_LIST); final TreeMap compacted_value = new TreeMap(); compacted_value.put(current_timestamp++, compacted.value()); row.put(compacted.qualifier(), compacted_value); From 929188f2301f50fbdafcbe6824b214018725883a Mon Sep 17 00:00:00 2001 From: qiubz Date: Sun, 28 May 2017 15:31:29 -0700 Subject: [PATCH 633/826] Add parsing of the percentiles to the QueryRpc class and allow it to execute the histo scan. Signed-off-by: Chris Larsen --- src/tsd/QueryRpc.java | 43 ++++++++++++++++++++++++++++- test/tsd/TestQueryRpc.java | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 1888ed3dd9..54c6ef8842 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -309,7 +309,12 @@ public Deferred call(final Query[] queries) { final ArrayList> deferreds = new ArrayList>(queries.length); for (final Query query : queries) { - deferreds.add(query.runAsync()); + // call different interfaces basing on whether it is a percentile query + if (!query.isHistogramQuery()) { + deferreds.add(query.runAsync()); + } else { + deferreds.add(query.runHistogramAsync()); + } } return Deferred.groupInOrder(deferreds).addCallback(new QueriesCB()); } @@ -705,6 +710,10 @@ private static void parseMTypeSubQuery(final String query_string, sub_query.setPreAggregate(true); } else if (parts[x].toLowerCase().startsWith("rollup_")) { sub_query.setRollupUsage(parts[x]); + } else if (parts[x].toLowerCase().startsWith("percentiles")) { + sub_query.setPercentiles(QueryRpc.parsePercentiles(parts[x])); + } else if (parts[x].toLowerCase().startsWith("show-histogram-buckets")) { + sub_query.setShowHistogramBuckets(true); } else if (parts[x].toLowerCase().startsWith("explicit_tags")) { sub_query.setExplicitTags(true); } @@ -760,6 +769,10 @@ private static void parseTsuidTypeSubQuery(final String query_string, } } else if (Character.isDigit(parts[x].charAt(0))) { sub_query.setDownsample(parts[x]); + } else if (parts[x].toLowerCase().startsWith("percentiles")) { + sub_query.setPercentiles(QueryRpc.parsePercentiles(parts[x])); + } else if (parts[x].toLowerCase().startsWith("show-histogram-buckets")) { + sub_query.setShowHistogramBuckets(true); } } @@ -871,6 +884,34 @@ private LastPointQuery parseLastPointQuery(final TSDB tsdb, return query; } + /** + * Parse the "percentile" section of the query string and returns an list of + * float that contains the percentile calculation paramters + *

    + * the format of the section: percentile[xx,yy,zz] + *

    + *

    + * xx, yy, zz are the floats + *

    + * @param spec + * @return + */ + public static final List parsePercentiles(final String spec) { + List rs = new ArrayList(); + int start_pos = spec.indexOf('['); + int end_pos = spec.indexOf(']'); + if (start_pos == -1 || end_pos == -1) { + throw new BadRequestException("Malformated percentile query paramater: " + spec); + } + + String [] floats = Tags.splitString(spec.substring(start_pos + 1, end_pos), ','); + for (String s : floats) { + String trimed = s.trim(); + rs.add(Float.valueOf(trimed)); + } + return rs; + } + /** @param collector Populates the collector with statistics */ public static void collectStats(final StatsCollector collector) { collector.record("http.query.invalid_requests", query_invalid); diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index eea877a6a7..3b4c5e71f9 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -24,6 +24,7 @@ import java.lang.reflect.Method; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.List; import net.opentsdb.auth.AuthState; @@ -679,6 +680,61 @@ public void gexpBadExpression() throws Exception { assertTrue(json.contains("factor")); } + @Test + public void testParsePercentile() { + final String s = "percentile[0.98,0.95,0.99]"; + final String ss = "percentile [0.98,0.95,0.99]"; + final String sss = "percentile[ 0.98,0.95,0.99]"; + final String ssss = "percentile[0.98,0.95,0.99 ]"; + final String sssss = "percentile[ 0.98, 0.95,0.99]"; + List strs = new ArrayList(); + strs.add(sssss); + strs.add(ssss); + strs.add(sss); + strs.add(ss); + strs.add(s); + + for (String str : strs) { + List fs = QueryRpc.parsePercentiles(str); + assertEquals(3, fs.size()); + assertEquals(0.98, fs.get(0), 0.0001); + assertEquals(0.95, fs.get(1), 0.0001); + assertEquals(0.99, fs.get(2), 0.0001); + } + } + + @Test + public void parseHistogramQueryMType() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:percentiles[0.98]:msg.end2end.latency"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + assertNotNull(tsq); + assertEquals("1h-ago", tsq.getStart()); + assertNotNull(tsq.getQueries()); + TSSubQuery sub = tsq.getQueries().get(0); + + assertNotNull(sub); + assertEquals("sum", sub.getAggregator()); + assertEquals("msg.end2end.latency", sub.getMetric()); + assertEquals(0.98f, sub.getPercentiles().get(0).floatValue(), 0.0001); + } + + @Test + public void parseHistogramQueryTSUIDType() throws Exception { + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&tsuid=sum:percentiles[0.98]:010101"); + TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); + assertNotNull(tsq); + assertEquals("1h-ago", tsq.getStart()); + assertNotNull(tsq.getQueries()); + TSSubQuery sub = tsq.getQueries().get(0); + assertNotNull(sub); + assertEquals("sum", sub.getAggregator()); + assertEquals(1, sub.getTsuids().size()); + assertEquals("010101", sub.getTsuids().get(0)); + assertEquals(0.98f, sub.getPercentiles().get(0).floatValue(), 0.0001); + } + @Test public void v1Auth() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; From 17ca1f3610381ef9e0cb1c3539901481081f2e2a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 28 May 2017 16:53:36 -0700 Subject: [PATCH 634/826] Cleanup headers and whitespace in the histo classes. Signed-off-by: Chris Larsen --- src/core/DataPoints.java | 4 +- src/core/HistogramAggregation.java | 4 +- src/core/HistogramAggregationIterator.java | 32 +- src/core/HistogramAggregator.java | 6 +- .../HistogramBucketDataPointsAdaptor.java | 44 ++- src/core/HistogramDataPoint.java | 239 +++++++------ src/core/HistogramDataPointDecoder.java | 18 +- .../HistogramDataPointDecoderManager.java | 12 +- src/core/HistogramDataPoints.java | 319 +++++++++--------- ...istogramDataPointsToDataPointsAdaptor.java | 33 +- src/core/HistogramDownsampler.java | 63 ++-- src/core/HistogramRowSeq.java | 35 +- src/core/HistogramSeekableView.java | 69 ++-- src/core/HistogramSpan.java | 46 ++- src/core/HistogramSpanGroup.java | 28 +- src/core/SaltScanner.java | 23 +- src/core/iHistogramRowSeq.java | 52 ++- src/tsd/QueryRpc.java | 2 +- test/core/HistogramSeekableViewForTest.java | 26 +- test/core/LongHistogramDataPointForTest.java | 10 +- .../LongHistogramDataPointForTestDecoder.java | 12 + .../TestHistogramAggregationIterator.java | 150 +++++--- ...istogramDataPointsToDataPointsAdaptor.java | 160 ++++++--- test/core/TestHistogramDownsampler.java | 82 ++--- test/core/TestHistogramRowSeq.java | 2 +- test/core/TestHistogramSpan.java | 18 +- test/core/TestHistogramSpanGroup.java | 83 +++-- test/core/TestTsdbQueryHistogramQueries.java | 29 +- 28 files changed, 964 insertions(+), 637 deletions(-) diff --git a/src/core/DataPoints.java b/src/core/DataPoints.java index a728a158b9..d231f9c1f7 100644 --- a/src/core/DataPoints.java +++ b/src/core/DataPoints.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// 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 @@ -225,6 +225,7 @@ public interface DataPoints extends Iterable { * the percentile calculation parameter. * * @return true or false + * @since 2.4 */ boolean isPercentile(); @@ -233,6 +234,7 @@ public interface DataPoints extends Iterable { * to convert {@code HistogramDataPoints} to {@code DataPoints} * * @return the percentile parameter + * @since 2.4 */ float getPercentile(); } diff --git a/src/core/HistogramAggregation.java b/src/core/HistogramAggregation.java index 87414ddad6..aa859af7c5 100644 --- a/src/core/HistogramAggregation.java +++ b/src/core/HistogramAggregation.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -14,6 +14,8 @@ /** * Aggregator functions for histogram data points. + * + * @since 2.4 */ public enum HistogramAggregation { SUM; diff --git a/src/core/HistogramAggregationIterator.java b/src/core/HistogramAggregationIterator.java index 6ee3aa7714..8c74465d9e 100644 --- a/src/core/HistogramAggregationIterator.java +++ b/src/core/HistogramAggregationIterator.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -16,17 +16,15 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.opentsdb.core.HistogramDataPoint.HistogramBucket; - /** * - * This is where the real business of @{link HistogramSpanGroup}. It provides a merged and aggregated - * view of the data points. It will apply the following processing: + * This is where the real business of @{link HistogramSpanGroup}. It provides a + * merged and aggregated view of the data points. It will apply the following + * processing: *
      *
    • Down sampling *
    • Aggregation @@ -38,11 +36,15 @@ *
    • Rate Calculation *
    * + * @since 2.4 */ -public class HistogramAggregationIterator implements HistogramSeekableView, HistogramDataPoint { - private static final Logger LOG = LoggerFactory.getLogger(HistogramAggregationIterator.class); +public class HistogramAggregationIterator implements + HistogramSeekableView, HistogramDataPoint { + private static final Logger LOG = LoggerFactory.getLogger( + HistogramAggregationIterator.class); - /** Aggregator to use to aggregate histogram data points from different HistogramSpans. */ + /** Aggregator to use to aggregate histogram data points from different + * HistogramSpans. */ private final HistogramAggregation aggregation; /** @@ -101,11 +103,13 @@ public static HistogramAggregationIterator create(final List span if (downsampler == DownsamplingSpecification.NO_DOWNSAMPLER) { it = spans.get(i).spanIterator(); } else { - it = spans.get(i).downsampler(start_time, end_time, downsampler, is_rollup, query_start, query_end); + it = spans.get(i).downsampler(start_time, end_time, downsampler, + is_rollup, query_start, query_end); } iterators[i] = it; } - return new HistogramAggregationIterator(iterators, start_time, end_time, aggregation); + return new HistogramAggregationIterator(iterators, start_time, + end_time, aggregation); } private HistogramAggregationIterator(final HistogramSeekableView[] iterators, @@ -139,7 +143,8 @@ private HistogramAggregationIterator(final HistogramSeekableView[] iterators, putDataPoint(i, dp); } else { if (LOG.isDebugEnabled()) { - LOG.debug(String.format("No DP in range for #%d: %d < %d", i, dp.timestamp(), start_time)); + LOG.debug(String.format("No DP in range for #%d: %d < %d", i, + dp.timestamp(), start_time)); } endReached(i); continue; @@ -148,7 +153,8 @@ private HistogramAggregationIterator(final HistogramSeekableView[] iterators, if (num_empty_spans > 0) { if (LOG.isDebugEnabled()) { - LOG.debug(String.format("%d out of %d spans are empty!", num_empty_spans, this.iterators.length)); + LOG.debug(String.format("%d out of %d spans are empty!", + num_empty_spans, this.iterators.length)); } } } diff --git a/src/core/HistogramAggregator.java b/src/core/HistogramAggregator.java index fcf749408a..4f53512d30 100644 --- a/src/core/HistogramAggregator.java +++ b/src/core/HistogramAggregator.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -12,13 +12,13 @@ // see . package net.opentsdb.core; - /** * Aggregator for histogram data points. + * + * @since 2.4 */ public class HistogramAggregator { - public interface Histograms { boolean hasNextValue(); diff --git a/src/core/HistogramBucketDataPointsAdaptor.java b/src/core/HistogramBucketDataPointsAdaptor.java index 35252be9fc..f25fb6b5f0 100644 --- a/src/core/HistogramBucketDataPointsAdaptor.java +++ b/src/core/HistogramBucketDataPointsAdaptor.java @@ -1,8 +1,19 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 java.util.List; import java.util.Map; -import java.util.Set; import org.hbase.async.Bytes.ByteMap; @@ -10,11 +21,18 @@ import com.stumbleupon.async.Deferred; import net.opentsdb.meta.Annotation; +/** + * A class for converting histograms back into DataPoints so that we can re-use + * the existing TSDB serialization when displaying percentiles. + * + * @since 2.4 + */ public class HistogramBucketDataPointsAdaptor implements DataPoints { private final HistogramDataPoints hist_data_points; private final HistogramDataPoint.HistogramBucket bucket; - HistogramBucketDataPointsAdaptor(final HistogramDataPoints hists, final HistogramDataPoint.HistogramBucket bucket) { + HistogramBucketDataPointsAdaptor(final HistogramDataPoints hists, + final HistogramDataPoint.HistogramBucket bucket) { this.hist_data_points = hists; this.bucket = bucket; } @@ -26,7 +44,8 @@ public String metricName() { @Override public Deferred metricNameAsync() { - return this.hist_data_points.metricNameAsync().addCallback(new Callback() { + return this.hist_data_points.metricNameAsync() + .addCallback(new Callback() { public String call(final String name) { return name + metricNamePostfix(); } @@ -105,7 +124,8 @@ private HistogramDataPoint getHistogramDataPoint(int i) { i--; } if (i != -1 || dp == null) { - throw new IndexOutOfBoundsException("index " + saved_i + " too large (it's >= " + size() + ") for " + this); + throw new IndexOutOfBoundsException("index " + saved_i + + " too large (it's >= " + size() + ") for " + this); } return dp; } @@ -125,7 +145,8 @@ public long longValue(int i) { HistogramDataPoint hdp = this.getHistogramDataPoint(i); try { - Map buckets = hdp.getHistogramBucketsIfHas(); + Map buckets = + hdp.getHistogramBucketsIfHas(); if (null != buckets && buckets.containsKey(bucket)) { return buckets.get(bucket).longValue(); } @@ -157,9 +178,11 @@ public float getPercentile() { } private String metricNamePostfix() { - if (this.bucket.bucketType() == HistogramDataPoint.HistogramBucket.BucketType.UNDERFLOW) { + if (this.bucket.bucketType() == + HistogramDataPoint.HistogramBucket.BucketType.UNDERFLOW) { return "_UNDERFLOW"; - } else if (this.bucket.bucketType() == HistogramDataPoint.HistogramBucket.BucketType.OVERFLOW) { + } else if (this.bucket.bucketType() == + HistogramDataPoint.HistogramBucket.BucketType.OVERFLOW) { return "_OVERFLOW"; } else { StringBuilder sb = new StringBuilder(); @@ -173,9 +196,9 @@ private Iterator internalIterator() { return new Iterator(); } - ////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// // internal iterator - ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// final class Iterator implements SeekableView, DataPoint { final private HistogramSeekableView source; private long value; @@ -196,7 +219,8 @@ public DataPoint next() { this.value = 0; try { - Map buckets = hdp.getHistogramBucketsIfHas(); + Map buckets = + hdp.getHistogramBucketsIfHas(); if (null != buckets && buckets.containsKey(bucket)) { this.value = buckets.get(bucket).longValue(); } diff --git a/src/core/HistogramDataPoint.java b/src/core/HistogramDataPoint.java index 83b623af75..59ceb2201d 100644 --- a/src/core/HistogramDataPoint.java +++ b/src/core/HistogramDataPoint.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2011-2012 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -14,150 +14,149 @@ import java.util.List; import java.util.Map; -import java.util.Set; /** - * Represents a single histogram data point. - * + * Represents a single histogram data point, e.g. all of the buckets for a + * measurement at a point in time. + * + * @since 2.4 */ public interface HistogramDataPoint extends Cloneable { - byte PREFIX = 0x6; + byte PREFIX = 0x6; - /** - * Returns the timestamp (in milliseconds) associated with this data point. - * @return A strictly positive, 32 bit integer. - */ - long timestamp(); + /** + * Returns the timestamp (in milliseconds) associated with this data point. + * @return A strictly positive, 32 bit integer. + */ + long timestamp(); - /** - * Get the encoded value of this histogram. - * NOTE: implementation should store the serialize information - * in the byte array so latter it can decide how to deserialize it back - * @return The encoded value os this histogram data point - */ - byte[] getRawData(); + /** + * Get the encoded value of this histogram. + * NOTE: implementation should store the serialize information + * in the byte array so latter it can decide how to deserialize it back + * @return The encoded value os this histogram data point + */ + byte[] getRawData(); - /** - * Decode the raw data and reset the current histogram data point to the - * decoded value - * @param raw_data The encoded value of the histogram data point - */ - void resetFromRawData(final byte[] raw_data); + /** + * Decode the raw data and reset the current histogram data point to the + * decoded value + * @param raw_data The encoded value of the histogram data point + */ + void resetFromRawData(final byte[] raw_data); - /** - * Calculate percentile of this histogram data point - * @param p the distribution threshold - * @return The percentile value - */ - double percentile(final double p); + /** + * Calculate percentile of this histogram data point + * @param p the distribution threshold + * @return The percentile value + */ + double percentile(final double p); - /** - * Calculate percentile values of this histogram data point - * @param p the distribution threshold list - * @return A list of the percentile values - */ - List percentile(final List p); + /** + * Calculate percentile values of this histogram data point + * @param p the distribution threshold list + * @return A list of the percentile values + */ + List percentile(final List p); - void aggregate(HistogramDataPoint histo, HistogramAggregation func); - - /** - * Create and return a copy of this object - * - * @return A deep copy object {@link HistogramDataPoint} - */ - HistogramDataPoint clone(); + void aggregate(HistogramDataPoint histo, HistogramAggregation func); + + /** + * Create and return a copy of this object + * + * @return A deep copy object {@link HistogramDataPoint} + */ + HistogramDataPoint clone(); + + + HistogramDataPoint cloneAndSetTimestamp(final long timestamp); + + + /////////////////////////////////////////////////////////////////////////// + // A nested class to present the bucket information + /////////////////////////////////////////////////////////////////////////// + public class HistogramBucket implements Comparable { + public enum BucketType { + UNDERFLOW, REGULAR, OVERFLOW + } + + private final BucketType type; + private final float lower_bound; + private final float upper_bound; + + public HistogramBucket(final BucketType type, final float lower_bound, + final float uper_bound) { + this.type = type; + this.lower_bound = lower_bound; + this.upper_bound = uper_bound; + } + public BucketType bucketType() { + return this.type; + } - HistogramDataPoint cloneAndSetTimestamp(final long timestamp); + public float getLowerBound() { + return this.lower_bound; + } + public float getUpperBound() { + return this.upper_bound; + } - ///////////////////////////////////////////////////////////////////////////////////////////// - // A nested class to present the bucket information - //////////////////////////////////////////////////////////////////////////////////////////// - public class HistogramBucket implements Comparable { - public enum BucketType { - UNDERFLOW, REGULAR, OVERFLOW - } - - private final BucketType type; - private final float lower_bound; - private final float upper_bound; - - public HistogramBucket(final BucketType type, final float lower_bound, - final float uper_bound) { - this.type = type; - this.lower_bound = lower_bound; - this.upper_bound = uper_bound; + @Override + public boolean equals(Object that) { + if (this == that) { + return true; } - public BucketType bucketType() { - return this.type; + if (that == null || getClass() != that.getClass()) { + return false; } - public float getLowerBound() { - return this.lower_bound; + HistogramBucket bk = (HistogramBucket)that; + if (bucketType() != bk.bucketType()) { + return false; } - public float getUpperBound() { - return this.upper_bound; + if ((BucketType.UNDERFLOW == bucketType() && + BucketType.UNDERFLOW == bk.bucketType()) || + (BucketType.OVERFLOW == bucketType() && + BucketType.OVERFLOW == bk.bucketType())) { + return true; } - - @Override - public boolean equals(Object that) { - if (this == that) { - return true; - } - - if (that == null || getClass() != that.getClass()) { - return false; - } - - HistogramBucket bk = (HistogramBucket)that; - if (bucketType() != bk.bucketType()) { - return false; - } - - if ((BucketType.UNDERFLOW == bucketType() && BucketType.UNDERFLOW == bk.bucketType()) || - (BucketType.OVERFLOW == bucketType() && BucketType.OVERFLOW == bk.bucketType())) { - return true; - } - - if (Float.compare(getLowerBound(), bk.getLowerBound()) != 0) { - return false; - } - - return (Float.compare(getUpperBound(), bk.getUpperBound()) == 0); + if (Float.compare(getLowerBound(), bk.getLowerBound()) != 0) { + return false; } + + return (Float.compare(getUpperBound(), bk.getUpperBound()) == 0); + } - @Override - public int compareTo(HistogramBucket that) { - if (this.equals(that)) { - return 0; - } else if (BucketType.UNDERFLOW == type) { - return -1; - } else if (BucketType.REGULAR == type) { - int lower_bound_compare = Float.compare(getLowerBound(), that.getLowerBound()); - if (lower_bound_compare != 0) { - return lower_bound_compare; - } else { - return Float.compare(getUpperBound(), that.getUpperBound()); - } - } else if (BucketType.OVERFLOW == type) { - return +1; - } - + @Override + public int compareTo(HistogramBucket that) { + if (this.equals(that)) { return 0; + } else if (BucketType.UNDERFLOW == type) { + return -1; + } else if (BucketType.REGULAR == type) { + int lower_bound_compare = Float.compare(getLowerBound(), + that.getLowerBound()); + if (lower_bound_compare != 0) { + return lower_bound_compare; + } else { + return Float.compare(getUpperBound(), that.getUpperBound()); + } + } else if (BucketType.OVERFLOW == type) { + return +1; } + + return 0; } - - /** - * Get buckets from this histogram data point - * @return - */ - Map getHistogramBucketsIfHas(); - - /** - void aggregate(List histos, HistoAggregation func); - */ + } + + /** + * Get buckets from this histogram data point + * @return + */ + Map getHistogramBucketsIfHas(); } diff --git a/src/core/HistogramDataPointDecoder.java b/src/core/HistogramDataPointDecoder.java index 4965ac7f3c..eca7e9f8ec 100644 --- a/src/core/HistogramDataPointDecoder.java +++ b/src/core/HistogramDataPointDecoder.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2011-2012 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -17,14 +17,16 @@ * * NOTE: Implementation of this interface should be thread safe. * @see HistogramDataPointDecoderManager + * + * @since 2.4 */ public interface HistogramDataPointDecoder { - /** - * Creates {@code HistogramDataPoint} from raw data and timestamp. - * @param raw_data The encoded byte array of the histogram data - * @param timestamp The timestamp of this data point - * @return The decoded histogram data point instance - */ - HistogramDataPoint decode(final byte[] raw_data, final long timestamp); + /** + * Creates {@code HistogramDataPoint} from raw data and timestamp. + * @param raw_data The encoded byte array of the histogram data + * @param timestamp The timestamp of this data point + * @return The decoded histogram data point instance + */ + HistogramDataPoint decode(final byte[] raw_data, final long timestamp); } diff --git a/src/core/HistogramDataPointDecoderManager.java b/src/core/HistogramDataPointDecoderManager.java index 08f1d7c54b..7c17bb2df3 100644 --- a/src/core/HistogramDataPointDecoderManager.java +++ b/src/core/HistogramDataPointDecoderManager.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2011-2012 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -20,15 +20,17 @@ * Manages the histogram decoder singletons. *

    *

    - * This manage accepts the full class name of the decoder, use reflection to create the decoder, - * and it ensures each type of the decoder will be created only once, after that, the cached decoder - * instance will be returned. + * This manage accepts the full class name of the decoder, use reflection to + * create the decoder, and it ensures each type of the decoder will be created + * only once, after that, the cached decoder instance will be returned. *

    *

    * This behavior actually makes each decoder a singleton. *

    * *

    This class is thread safe

    + * + * @since 2.4 */ public class HistogramDataPointDecoderManager { @@ -59,7 +61,7 @@ public static HistogramDataPointDecoder getDecoder(final String decoder_name) { private static HistogramDataPointDecoder createInstance(final String decoder_name) { try { - Class c = Class.forName(decoder_name); + Class c = Class.forName(decoder_name); return (HistogramDataPointDecoder) c.newInstance(); } catch (Exception exp) { throw new RuntimeException("Failed to create the decoder instance of " diff --git a/src/core/HistogramDataPoints.java b/src/core/HistogramDataPoints.java index 9d8ba3b783..96d0dd5238 100644 --- a/src/core/HistogramDataPoints.java +++ b/src/core/HistogramDataPoints.java @@ -1,3 +1,15 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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; @@ -8,162 +20,159 @@ import java.util.Map; /** - * Created by haiyang on 6/15/16. + * A clone of the DataPoints interface except for handling histograms. + * + * @since 2.4 */ public interface HistogramDataPoints extends Iterable { - /** - * Returns the name of the series. - * @return The name of the metric as a string. - */ - String metricName(); - - /** - * Returns the name of the series. - * @return The name of the metric in a deferred (may contain an exception). - * @since 1.2 - */ - Deferred metricNameAsync(); - - /** - * @return the metric UID - * @return The metric UID as an array of bytes. - * @since 2.3 - */ - byte[] metricUID(); - - /** - * Returns the tags associated with these data points. - * @return A non-{@code null} map of tag names (keys), tag values (values). - */ - Map getTags(); - - /** - * Returns the tags associated with these data points. - * @return A non-{@code null} map of tag names (keys), tag values (values). - * @since 1.2 - */ - Deferred> getTagsAsync(); - - /** - * 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 - */ - Bytes.ByteMap getTagUids(); - - /** - * 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. - */ - List getAggregatedTags(); - - /** - * 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 - */ - Deferred> getAggregatedTagsAsync(); - - /** - * Returns the tagk UIDs associated with some but not all of the data points. - * @return a non-{@code null} list of tagk UIDs. - */ - List getAggregatedTagUids(); - - /** - * Returns a list of unique TSUIDs contained in the results - * @return an empty list if there were no results, otherwise a list of TSUIDs - */ - public List getTSUIDs(); - - /** - * 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 - */ - public List getAnnotations(); - - /** - * Returns a warning about the query, i.e if it terminated prematurely - * @return A null if no warning, otherwise a string to return to the user - */ - public String getWarning(); - - /** - * Returns the number of histogram data points. - *

    - * This method must be implemented in {@code O(1)} or {@code O(n)} - * where n = {@link #aggregatedSize} > 0. - * @return A positive integer. - */ - int 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. - */ - int 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. - * @return An iterator over the data points. - */ - HistogramSeekableView iterator(); - - /** - * 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 The index to fetch a timestamp for - * @return A strictly positive integer. - * @throws IndexOutOfBoundsException if {@code i} is not in the range - * [0, {@link #size} - 1] - */ - long timestamp(int i); - - /** - * Return the query index that maps this datapoints to the original subquery - * @return index of the query in the TSQuery class - */ - int getQueryIndex(); + /** + * Returns the name of the series. + * @return The name of the metric as a string. + */ + String metricName(); + + /** + * Returns the name of the series. + * @return The name of the metric in a deferred (may contain an exception). + */ + Deferred metricNameAsync(); + + /** + * @return the metric UID + * @return The metric UID as an array of bytes. + */ + byte[] metricUID(); + + /** + * Returns the tags associated with these data points. + * @return A non-{@code null} map of tag names (keys), tag values (values). + */ + Map getTags(); + + /** + * Returns the tags associated with these data points. + * @return A non-{@code null} map of tag names (keys), tag values (values). + */ + Deferred> getTagsAsync(); + + /** + * 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 + */ + Bytes.ByteMap getTagUids(); + + /** + * 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. + */ + List getAggregatedTags(); + + /** + * 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. + */ + Deferred> getAggregatedTagsAsync(); + + /** + * Returns the tagk UIDs associated with some but not all of the data points. + * @return a non-{@code null} list of tagk UIDs. + */ + List getAggregatedTagUids(); + + /** + * Returns a list of unique TSUIDs contained in the results + * @return an empty list if there were no results, otherwise a list of TSUIDs + */ + public List getTSUIDs(); + + /** + * 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 + */ + public List getAnnotations(); + + /** + * Returns a warning about the query, i.e if it terminated prematurely + * @return A null if no warning, otherwise a string to return to the user + */ + public String getWarning(); + + /** + * Returns the number of histogram data points. + *

    + * This method must be implemented in {@code O(1)} or {@code O(n)} + * where n = {@link #aggregatedSize} > 0. + * @return A positive integer. + */ + int 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. + */ + int 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. + * @return An iterator over the data points. + */ + HistogramSeekableView iterator(); + + /** + * 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 The index to fetch a timestamp for + * @return A strictly positive integer. + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * [0, {@link #size} - 1] + */ + long timestamp(int i); + + /** + * Return the query index that maps this datapoints to the original subquery + * @return index of the query in the TSQuery class + */ + int getQueryIndex(); } diff --git a/src/core/HistogramDataPointsToDataPointsAdaptor.java b/src/core/HistogramDataPointsToDataPointsAdaptor.java index b87162b544..3b1cc0658f 100644 --- a/src/core/HistogramDataPointsToDataPointsAdaptor.java +++ b/src/core/HistogramDataPointsToDataPointsAdaptor.java @@ -1,8 +1,19 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import org.hbase.async.Bytes.ByteMap; @@ -11,24 +22,32 @@ import net.opentsdb.meta.Annotation; +/** + * Converts histogram DataPoints to DataPoints for using the existing metric + * serialization code when querying percentiles. + * + * @since 2.4 + */ public class HistogramDataPointsToDataPointsAdaptor implements DataPoints { final private HistogramDataPoints hist_data_points; final private float percentile; - - public HistogramDataPointsToDataPointsAdaptor(final HistogramDataPoints hdps, final float percentile) { + public HistogramDataPointsToDataPointsAdaptor(final HistogramDataPoints hdps, + final float percentile) { this.hist_data_points = hdps; this.percentile = percentile; } @Override public String metricName() { - return this.hist_data_points.metricName() + "_pct_" + Float.toString(this.percentile); + return this.hist_data_points.metricName() + "_pct_" + + Float.toString(this.percentile); } @Override public Deferred metricNameAsync() { - return this.hist_data_points.metricNameAsync().addCallback(new Callback() { + return this.hist_data_points.metricNameAsync() + .addCallback(new Callback() { public String call(final String name) { return name + "_pct_" + Float.toString(percentile); } @@ -152,9 +171,9 @@ private Iterator internalIterator() { return new Iterator(); } - ////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// // internal iterator - ///////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// final class Iterator implements SeekableView, DataPoint { final private HistogramSeekableView source; private double value; diff --git a/src/core/HistogramDownsampler.java b/src/core/HistogramDownsampler.java index b0349ee0ea..57fca5518f 100644 --- a/src/core/HistogramDownsampler.java +++ b/src/core/HistogramDownsampler.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2015 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -12,19 +12,18 @@ // see . package net.opentsdb.core; -import net.opentsdb.core.HistogramDataPoint.HistogramBucket; import net.opentsdb.utils.DateTime; import java.util.Calendar; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.Set; - /** * Iterator that downsamples histogram data points using an * {@link HistogramAggregation}. + * + * @since 2.4 */ public class HistogramDownsampler implements HistogramSeekableView, HistogramDataPoint { @@ -137,7 +136,8 @@ public HistogramDataPoint next() { value = values_in_interval.nextHistogramValue(); while (values_in_interval.hasNextValue()) { // this call will change the data in @{code value} - value.aggregate(values_in_interval.nextHistogramValue(), specification.getHistogramAggregation()); + value.aggregate(values_in_interval.nextHistogramValue(), + specification.getHistogramAggregation()); } timestamp = values_in_interval.getIntervalTimestamp(); @@ -161,9 +161,17 @@ public void seek(long timestamp) { @Override public String toString() { final StringBuilder buf = new StringBuilder(); - buf.append("HistogramDownsampler: ").append(", downsampler=").append(specification).append(", query_start=") - .append(query_start).append(", current data=(timestamp=").append(timestamp).append(", value=").append(value) - .append("), values_in_interval=").append(values_in_interval); + buf.append("HistogramDownsampler: ") + .append(", downsampler=") + .append(specification) + .append(", query_start=") + .append(query_start) + .append(", current data=(timestamp=") + .append(timestamp) + .append(", value=") + .append(value) + .append("), values_in_interval=") + .append(values_in_interval); return buf.toString(); } @@ -213,9 +221,11 @@ protected void initializeIfNotDone() { moveToNextValue(); if (!run_all) { if (specification.useCalendar()) { - previous_calendar = DateTime.previousInterval(next_dp.timestamp(), interval, unit, + previous_calendar = + DateTime.previousInterval(next_dp.timestamp(), interval, unit, specification.getTimezone()); - next_calendar = DateTime.previousInterval(next_dp.timestamp(), interval, unit, + next_calendar = + DateTime.previousInterval(next_dp.timestamp(), interval, unit, specification.getTimezone()); if (unit == WEEK_UNIT) { next_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); @@ -224,7 +234,8 @@ protected void initializeIfNotDone() { } timestamp_end_interval = next_calendar.getTimeInMillis(); } else { - timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + specification.getInterval(); + timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + + specification.getInterval(); } } } @@ -277,7 +288,8 @@ private void resetEndOfInterval() { timestamp_end_interval = next_calendar.getTimeInMillis(); } } else { - timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + specification.getInterval(); + timestamp_end_interval = alignTimestamp(next_dp.timestamp()) + + specification.getInterval(); } } } @@ -297,8 +309,8 @@ void seekInterval(final long timestamp) { if (run_all) { source.seek(timestamp); } else if (specification.useCalendar()) { - final Calendar seek_calendar = DateTime.previousInterval(timestamp, interval, unit, - specification.getTimezone()); + final Calendar seek_calendar = DateTime.previousInterval(timestamp, + interval, unit, specification.getTimezone()); if (timestamp > seek_calendar.getTimeInMillis()) { if (unit == WEEK_UNIT) { seek_calendar.add(DAY_UNIT, interval * WEEK_LENGTH); @@ -338,7 +350,8 @@ public boolean hasNextValue() { if (run_all) { return has_next_value_from_source; } - return has_next_value_from_source && next_dp.timestamp() < timestamp_end_interval; + return has_next_value_from_source && next_dp.timestamp() < + timestamp_end_interval; } @Override @@ -346,22 +359,30 @@ public HistogramDataPoint nextHistogramValue() { if (hasNextValue()) { if (next_dp != null) { HistogramDataPoint value = null; - // we have to clone the object, else when moveToNextValue in the next step will - // also change the @{code next_dp} and @{code value} here + // we have to clone the object, else when moveToNextValue in the + // next step will also change the @{code next_dp} and @{code value} + // here value = next_dp.clone(); moveToNextValue(); return value; } } - throw new NoSuchElementException("no more values in interval of " + timestamp_end_interval); + throw new NoSuchElementException("no more values in interval of " + + timestamp_end_interval); } @Override public String toString() { final StringBuilder buf = new StringBuilder(); - buf.append("ValuesInInterval{").append(", timestamp_end_interval=").append(timestamp_end_interval) - .append(", unit=").append(unit).append(", interval=").append(interval).append(", has_next_value_from_source=") - .append(has_next_value_from_source); + buf.append("ValuesInInterval{") + .append(", timestamp_end_interval=") + .append(timestamp_end_interval) + .append(", unit=") + .append(unit) + .append(", interval=") + .append(interval) + .append(", has_next_value_from_source=") + .append(has_next_value_from_source); if (has_next_value_from_source) { buf.append(", nextValue=(").append(next_dp).append(')'); } diff --git a/src/core/HistogramRowSeq.java b/src/core/HistogramRowSeq.java index 11a8b1bbbe..f76d14bc05 100644 --- a/src/core/HistogramRowSeq.java +++ b/src/core/HistogramRowSeq.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -15,11 +15,18 @@ import com.stumbleupon.async.Deferred; -import net.opentsdb.core.HistogramDataPoint.HistogramBucket; import net.opentsdb.meta.Annotation; -import org.hbase.async.Bytes; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.hbase.async.Bytes; /** * Represents a read-only sequence of continuous HBase rows. @@ -28,6 +35,8 @@ * a given time series. To consolidate memory, the data points are stored in two * byte arrays: one for the time offsets/flags and another for the values. * Access is granted via pointers. + * + * @since 3.0 */ public class HistogramRowSeq implements iHistogramRowSeq { @@ -61,7 +70,8 @@ public void addRow(final List row) { int index_local = 0; int index_remote = 0; - List combinedRows = new ArrayList(this.rowSeq.size() + row.size()); + List combinedRows = + new ArrayList(this.rowSeq.size() + row.size()); while (index_local < this.rowSeq.size() && index_remote < row.size()) { HistogramDataPoint hdp_local = this.rowSeq.get(index_local); HistogramDataPoint hdp_remote = row.get(index_remote); @@ -132,7 +142,8 @@ public Deferred metricNameAsync() { @Override public byte[] metricUID() { - return Arrays.copyOfRange(key, Const.SALT_WIDTH(), Const.SALT_WIDTH() + TSDB.metrics_width()); + return Arrays.copyOfRange(key, Const.SALT_WIDTH(), Const.SALT_WIDTH() + + TSDB.metrics_width()); } @Override @@ -222,10 +233,12 @@ public int getQueryIndex() { */ private void checkIndex(final int i) { if (i >= size()) { - throw new IndexOutOfBoundsException("index " + i + " >= " + size() + " for this=" + this); + throw new IndexOutOfBoundsException("index " + i + " >= " + size() + + " for this=" + this); } if (i < 0) { - throw new IndexOutOfBoundsException("negative index " + i + " for this=" + this); + throw new IndexOutOfBoundsException("negative index " + i + + " for this=" + this); } } @@ -267,7 +280,8 @@ public String toString() { * * @since 2.0 */ - public static final class HistogramRowSeqComparator implements Comparator { + public static final class HistogramRowSeqComparator implements + Comparator { public int compare(final iHistogramRowSeq a, final iHistogramRowSeq b) { if (null == a || null == b) { if (a == b) { @@ -347,7 +361,8 @@ public void seek(long timestamp) { // TODO: this can be optimized to O(nlogn) next_index = 0; - while (next_index < rowSeq.size() && rowSeq.get(next_index).timestamp() < timestamp) { + while (next_index < rowSeq.size() && + rowSeq.get(next_index).timestamp() < timestamp) { ++next_index; } } diff --git a/src/core/HistogramSeekableView.java b/src/core/HistogramSeekableView.java index 5532486a6d..debc03b444 100644 --- a/src/core/HistogramSeekableView.java +++ b/src/core/HistogramSeekableView.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -10,47 +10,48 @@ // 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.Iterator; import java.util.NoSuchElementException; /** - * Created by haiyang on 6/15/16. + * A clone of the DataPoints seekable view but for histograms. + * + * @since 2.4 */ public interface HistogramSeekableView extends Iterator { - /** - * Returns {@code true} if this view has more elements. - */ - boolean 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}. - */ - HistogramDataPoint next(); - - /** - * Unsupported operation. - * @throws UnsupportedOperationException always. - */ - void remove(); - - /** - * 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!). - */ - void seek(long timestamp); + /** + * Returns {@code true} if this view has more elements. + */ + boolean 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}. + */ + HistogramDataPoint next(); + + /** + * Unsupported operation. + * @throws UnsupportedOperationException always. + */ + void remove(); + + /** + * 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!). + */ + void seek(long timestamp); } diff --git a/src/core/HistogramSpan.java b/src/core/HistogramSpan.java index f4c740d376..d5c00d5423 100644 --- a/src/core/HistogramSpan.java +++ b/src/core/HistogramSpan.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -25,6 +25,8 @@ * Represents a read-only sequence of continuous histogram data points. *

    * This class stores a continuous sequence of {@link HistogramRowSeq}s in memory. + * + * @since 2.4 */ public class HistogramSpan implements HistogramDataPoints { @@ -181,7 +183,8 @@ public List getTSUIDs() { if (rows.size() < 1) { return null; } - final byte[] tsuid = UniqueId.getTSUIDFromKey(rows.get(0).key(), TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + final byte[] tsuid = UniqueId.getTSUIDFromKey(rows.get(0).key(), + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); final List tsuids = new ArrayList(1); tsuids.add(UniqueId.uidToString(tsuid)); return tsuids; @@ -222,8 +225,9 @@ public List getAnnotations() { * |-------| |-------| * * - * This method expects the caller adds the rows from the same salt in the timestamp order. - * When the caller adds the rows from salt A, the result rows will be as below: + * This method expects the caller adds the rows from the same salt in the + * timestamp order. When the caller adds the rows from salt A, the result + * rows will be as below: *

        * |-------|       
        * |  T0   |      
    @@ -250,8 +254,8 @@ public List getAnnotations() {
        * |  T5   |
        * |-------|
        * 
    - * When the caller iterates the data points in the Span, the Span will firstly sort the rows. - * Then the final result rows will be as below: + * When the caller iterates the data points in the Span, the Span will firstly + * sort the rows. Then the final result rows will be as below: *
        * |-------|       
        * |  T0   |      
    @@ -270,11 +274,14 @@ public List getAnnotations() {
        * 

    * @param key The row key of the row that want to add in the span * @param data_points histogram data points in the row - * @throws IllegalArgumentException if the argument and this span are for two different time series. + * @throws IllegalArgumentException if the argument and this span are for + * two different time series. */ - protected void addRow(final byte[] key, final List data_points) { + protected void addRow(final byte[] key, + final List data_points) { if (null == key || null == data_points) { - throw new NullPointerException("row key and histogram data points can't be null"); + throw new NullPointerException("row key and histogram data points " + + "can't be null"); } long last_ts = 0; @@ -282,7 +289,8 @@ protected void addRow(final byte[] key, final List data_poin // Verify that we have the same metric id and tags. final iHistogramRowSeq last = rows.get(rows.size() - 1); final short metric_width = tsdb.metrics.width(); - final short tags_offset = (short) (Const.SALT_WIDTH() + metric_width + Const.TIMESTAMP_BYTES); + final short tags_offset = (short) (Const.SALT_WIDTH() + metric_width + + Const.TIMESTAMP_BYTES); final short tags_bytes = (short) (key.length - tags_offset); String error = null; if (key.length != last.key().length) { @@ -293,8 +301,10 @@ protected void addRow(final byte[] key, final List data_poin error = "tags mismatch"; } if (error != null) { - throw new IllegalArgumentException(error + ". " + "This Span's last row key is " + Arrays.toString(last.key()) - + " whereas the row key being added is " + Arrays.toString(key) + " and metric_width=" + metric_width); + throw new IllegalArgumentException(error + ". " + + "This Span's last row key is " + Arrays.toString(last.key()) + + " whereas the row key being added is " + Arrays.toString(key) + + " and metric_width=" + metric_width); } last_ts = last.timestamp(last.size() - 1); // O(n) } @@ -306,7 +316,8 @@ protected void addRow(final byte[] key, final List data_poin // scan to see if we need to merge into an existing row for (final iHistogramRowSeq rs : rows) { if ((rs.key().length == key.length) - && (Bytes.memcmp(rs.key(), key, Const.SALT_WIDTH(), (rs.key().length - Const.SALT_WIDTH())) == 0)) { + && (Bytes.memcmp(rs.key(), key, Const.SALT_WIDTH(), + (rs.key().length - Const.SALT_WIDTH())) == 0)) { rs.addRow(data_points); return; } @@ -522,7 +533,8 @@ public void seek(final long timestamp) { @Override public String toString() { - return "HistogramSpan.Iterator(row_index=" + row_index + ", current_row=" + current_row + ", span=" + return "HistogramSpan.Iterator(row_index=" + row_index + + ", current_row=" + current_row + ", span=" + HistogramSpan.this + ')'; } @@ -546,7 +558,8 @@ HistogramDownsampler downsampler(final long start_time, final long query_start, final long query_end) { // ignore the fill policy - return new HistogramDownsampler(spanIterator(), downsampler, query_start, query_end); + return new HistogramDownsampler(spanIterator(), downsampler, query_start, + query_end); } /** @@ -555,7 +568,8 @@ HistogramDownsampler downsampler(final long start_time, * @return index of the query in the TSQuery class */ public int getQueryIndex() { - throw new UnsupportedOperationException("Span.java: getQueryIndex not supported"); + throw new UnsupportedOperationException("Span.java: getQueryIndex not " + + "supported"); } /** diff --git a/src/core/HistogramSpanGroup.java b/src/core/HistogramSpanGroup.java index 9e95b0d77b..496a6f2485 100644 --- a/src/core/HistogramSpanGroup.java +++ b/src/core/HistogramSpanGroup.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -10,7 +10,6 @@ // 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; @@ -31,6 +30,11 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.utils.ByteSet; +/** + * Clone of the regular SpanGroup but handles histogram data points. + * + * @since 2.4 + */ final class HistogramSpanGroup implements HistogramDataPoints { /** Annotations */ @@ -90,7 +94,6 @@ final class HistogramSpanGroup implements HistogramDataPoints { /** whether we are handling rollup data points*/ private final boolean is_rollup; - /** * Ctor. * @param tsdb The TSDB we belong to. @@ -116,8 +119,10 @@ final class HistogramSpanGroup implements HistogramDataPoints { final boolean is_rollup, final ByteSet query_tags) { annotations = new ArrayList(); - this.start_time = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; - this.end_time = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; + this.start_time = (start_time & Const.SECOND_MASK) == 0 ? + start_time * 1000 : start_time; + this.end_time = (end_time & Const.SECOND_MASK) == 0 ? + end_time * 1000 : end_time; if (spans != null) { for (final HistogramSpan span : spans) { add(span); @@ -154,12 +159,15 @@ public String getWarning() { */ void add(final HistogramSpan span) { if (tags != null) { - throw new AssertionError("The set of tags has already been computed" + ", you can't add more Spans to " + this); + throw new AssertionError("The set of tags has already been computed" + + ", you can't add more Spans to " + this); } // normalize timestamps to milliseconds for proper comparison - final long start = (start_time & Const.SECOND_MASK) == 0 ? start_time * 1000 : start_time; - final long end = (end_time & Const.SECOND_MASK) == 0 ? end_time * 1000 : end_time; + final long start = (start_time & Const.SECOND_MASK) == 0 ? + start_time * 1000 : start_time; + final long end = (end_time & Const.SECOND_MASK) == 0 ? + end_time * 1000 : end_time; if (span.size() == 0) { // copy annotations that are in the time range @@ -337,8 +345,8 @@ public int aggregatedSize() { } public HistogramSeekableView iterator() { - return HistogramAggregationIterator.create(this.spans, this.start_time, this.end_time, this.aggregation, - this.downsampler, this.query_start, this.query_end, this.is_rollup); + return HistogramAggregationIterator.create(spans, start_time, + end_time, aggregation, downsampler, query_start, query_end, is_rollup); } /** diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index f1c7a97167..0d47e2dd11 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2015 The OpenTSDB Authors. +// 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 @@ -84,7 +84,8 @@ public class SaltScanner { new TreeMap>(new RowKey.SaltCmp())); private final Map>>> - histMap = new ConcurrentHashMap>>>(); + histMap = new ConcurrentHashMap>>>(); /** A deferred to call with the spans on completion */ private final Deferred> results = @@ -429,11 +430,13 @@ final class ScannerCB implements Callback keepers = Collections.newSetFromMap( new ConcurrentHashMap()); - // use list here because we want to keep the rows in the scan order - timestamp order. - // i don't want to define an additional class to store the information of the row key and - // the histogram data points in the row, then use {@link SimpleEntry} + // use list here because we want to keep the rows in the scan order - + // timestamp order. I don't want to define an additional class to store the + // information of the row key and the histogram data points in the row, + // then use {@link SimpleEntry} private List>> histograms = - Collections.synchronizedList(Lists.>>newArrayList()); + Collections.synchronizedList(Lists.>>newArrayList()); private long scanner_start = -1; /** nanosecond timestamps */ @@ -667,8 +670,9 @@ void processRow(final byte[] key, final ArrayList row) { final byte[] qual = kv.qualifier(); if (qual.length > 0) { - // Todo: Bug! Here we shouldn't use the first byte to check the type of this row - // Instead should parse the byte array to find the suffix and determine the actual type + // TODO: Bug! Here we shouldn't use the first byte to check the + // type of this row. Instead should parse the byte array to find + // the suffix and determine the actual type if (qual[0] == Annotation.PREFIX()) { // This could be a row with only an annotation in it final Annotation note = JSON.parseToObject(kv.value(), @@ -683,7 +687,8 @@ void processRow(final byte[] key, final ArrayList row) { } } else if (qual[0] == HistogramDataPoint.PREFIX) { try { - HistogramDataPoint histogram = Internal.decodeHistogramDataPoint(kv, tsdb.getConfig()); + HistogramDataPoint histogram = + Internal.decodeHistogramDataPoint(kv, tsdb.getConfig()); hists.add(histogram); } catch (Throwable t) { LOG.error("Failed to decode histogram data point", t); diff --git a/src/core/iHistogramRowSeq.java b/src/core/iHistogramRowSeq.java index ecd1680e0d..d61469189d 100644 --- a/src/core/iHistogramRowSeq.java +++ b/src/core/iHistogramRowSeq.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -14,19 +14,53 @@ import java.util.List; - +/** + * Clone of the {@link iRowSeq} interface but for histograms. + * + * @since 2.4 + */ public interface iHistogramRowSeq extends HistogramDataPoints { - void setRow(final byte[] key, final List row); + + /** + * Sets the initial column in the sequence. The key cannot be empty. + * @param key The row key. + * @param row A non-null list of histogram points. + * @throws IllegalStateException if {@link #setRow(byte[], List)} or + * {@link #addRow(List)} has already been called. + */ + public void setRow(final byte[] key, final List row); - void addRow(final List row); + /** + * Adds a column in the proper sequence in the row. Must be called after + * {@link #setRow(byte[], List)} has been called. + * @param row A non-null list of histogram points. + * @throws IllegalStateException if {@link #setRow(byte[], List)} has not been + * called first. + */ + public void addRow(final List row); - byte[] key(); + /** + * Returns the row key this sequence represents. May be null if + * {@link #setRow(byte[], List)} has not been called. + * @return The row key for this sequence. + */ + public byte[] key(); - long baseTime(); + /** + * Returns the base time for the row in Unix epoch seconds. + * @return The base time for the row. + * @throws NullPointerException if {@link #setRow(byte[], List)} has not been + * called. + */ + public long baseTime(); - Iterator internalIterator(); + /** @return an internal iterator for this row sequence. */ + public Iterator internalIterator(); - interface Iterator extends HistogramSeekableView, HistogramDataPoint { + /** + * An interface for an iterator that all row sequences must implement. + */ + public interface Iterator extends HistogramSeekableView, HistogramDataPoint { - } + } } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index 54c6ef8842..e94d28642e 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2013-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 diff --git a/test/core/HistogramSeekableViewForTest.java b/test/core/HistogramSeekableViewForTest.java index dd376c66e4..2591aad5de 100644 --- a/test/core/HistogramSeekableViewForTest.java +++ b/test/core/HistogramSeekableViewForTest.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -27,16 +27,19 @@ public class HistogramSeekableViewForTest { /** - * Creates a {@link HistogramSeekableView} object to iterate the given data points. + * Creates a {@link HistogramSeekableView} object to iterate the given data + * points. * @param data_points Test data. * @return A {@link HistogramSeekableView} object */ - public static HistogramSeekableView fromArray(final HistogramDataPoint[] data_points) { + public static HistogramSeekableView fromArray( + final HistogramDataPoint[] data_points) { return new MockHistogramSeekableView(data_points); } /** - * Creates a {@link HistogramSeekableView} that generates a sequence of data points. + * Creates a {@link HistogramSeekableView} that generates a sequence of data + * points. * @param start_time Starting timestamp * @param sample_period Average sample period of data points * @param num_data_points Total number of data points to generate @@ -96,7 +99,8 @@ private static class DataPointGenerator implements HistogramSeekableView { private final long start_time_ms; private final long sample_period_ms; private final int num_data_points; - private final LongHistogramDataPointForTest current_data = new LongHistogramDataPointForTest(100L, Bytes.fromLong(0L)); + private final LongHistogramDataPointForTest current_data = + new LongHistogramDataPointForTest(100L, Bytes.fromLong(0L)); private int current = 0; DataPointGenerator(final long start_time_ms, final long sample_period_ms, @@ -169,7 +173,8 @@ public void testDataPointGenerator() { assertTrue(hdpg.hasNext()); HistogramDataPoint dp = hdpg.next(); assertEquals(expected.timestamp(), dp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(dp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData()), + Bytes.getLong(dp.getRawData())); } assertFalse(hdpg.hasNext()); } @@ -187,7 +192,8 @@ public void testDataPointGenerator_seek() { assertTrue(hdpg.hasNext()); HistogramDataPoint hdp = hdpg.next(); assertEquals(expected.timestamp(), hdp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(hdp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData()), + Bytes.getLong(hdp.getRawData())); } assertFalse(hdpg.hasNext()); } @@ -206,7 +212,8 @@ public void testDataPointGenerator_seekToFirst() { assertTrue(hdpg.hasNext()); HistogramDataPoint hdp = hdpg.next(); assertEquals(expected.timestamp(), hdp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(hdp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData()), + Bytes.getLong(hdp.getRawData())); } assertFalse(hdpg.hasNext()); } @@ -225,7 +232,8 @@ public void testDataPointGenerator_seekToSecond() { assertTrue(hdpg.hasNext()); HistogramDataPoint hdp = hdpg.next(); assertEquals(expected.timestamp(), hdp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), Bytes.getLong(hdp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData()), + Bytes.getLong(hdp.getRawData())); } assertFalse(hdpg.hasNext()); } diff --git a/test/core/LongHistogramDataPointForTest.java b/test/core/LongHistogramDataPointForTest.java index 80dc2aa1a8..8e251c001c 100644 --- a/test/core/LongHistogramDataPointForTest.java +++ b/test/core/LongHistogramDataPointForTest.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -16,10 +16,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; import org.hbase.async.Bytes; -import net.opentsdb.core.HistogramDataPoint.HistogramBucket; public class LongHistogramDataPointForTest implements HistogramDataPoint { private long timestamp; @@ -35,7 +33,8 @@ protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs) this.data = rhs.data; } - protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs, final long timestamp) { + protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs, + final long timestamp) { this.data = rhs.data; this.timestamp = timestamp; } @@ -81,7 +80,8 @@ public List percentile(List p) { @Override public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { if (!(histo instanceof LongHistogramDataPointForTest)) { - throw new IllegalArgumentException("The object must be an instance of the " + "LongHistogramDataPointForTest"); + throw new IllegalArgumentException("The object must be an instance of the " + + "LongHistogramDataPointForTest"); } long agg = this.data + Bytes.getLong(histo.getRawData()); diff --git a/test/core/LongHistogramDataPointForTestDecoder.java b/test/core/LongHistogramDataPointForTestDecoder.java index 3740704f36..a8f481fa1d 100644 --- a/test/core/LongHistogramDataPointForTestDecoder.java +++ b/test/core/LongHistogramDataPointForTestDecoder.java @@ -1,3 +1,15 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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; public class LongHistogramDataPointForTestDecoder implements HistogramDataPointDecoder { diff --git a/test/core/TestHistogramAggregationIterator.java b/test/core/TestHistogramAggregationIterator.java index 8a20b8e24f..3d547c22e1 100644 --- a/test/core/TestHistogramAggregationIterator.java +++ b/test/core/TestHistogramAggregationIterator.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -13,11 +13,11 @@ package net.opentsdb.core; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.powermock.api.mockito.PowerMockito.mock; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.hbase.async.Bytes; @@ -34,8 +34,10 @@ @RunWith(PowerMockRunner.class) //"Classloader hell"... It's real. Tell PowerMock to ignore these classes //because they fiddle with the class loader. We don't test them anyway. -@PowerMockIgnore({ "javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*" }) -@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, Config.class, +@PowerMockIgnore({ "javax.management.*", "javax.xml.*", "ch.qos.*", + "org.slf4j.*", "com.sum.*", "org.xml.*" }) +@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, + UniqueId.class, KeyValue.class, Config.class, RowKey.class }) public class TestHistogramAggregationIterator { @@ -48,7 +50,8 @@ public class TestHistogramAggregationIterator { public void testOneHistogramSpanWithNoDownsampler() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -57,8 +60,10 @@ public void testOneHistogramSpanWithNoDownsampler() { List spans = new ArrayList(); spans.add(hspan); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps = new ArrayList(); @@ -79,7 +84,8 @@ public void testOneHistogramSpanWithNoDownsampler() { public void testOneHistogramSpanWithDownsampler_10secs() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -88,9 +94,12 @@ public void testOneHistogramSpanWithDownsampler_10secs() { List spans = new ArrayList(); spans.add(hspan); - DownsamplingSpecification specification = new DownsamplingSpecification("10s-sum"); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 10, HistogramAggregation.SUM, specification, 0, 0, false); + DownsamplingSpecification specification = + new DownsamplingSpecification("10s-sum"); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, specification, 0, 0, + false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -126,7 +135,8 @@ public void testOneHistogramSpanWithDownsampler_10secs() { public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -135,8 +145,10 @@ public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { List spans = new ArrayList(); spans.add(hspan); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME + 5000L, - BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME + 5000L, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -149,7 +161,8 @@ public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { assertEquals(9, values.size()); for (int i = 0; i < values.size(); ++i) { assertEquals(i + 1, values.get(i).longValue()); - assertEquals(BASE_TIME + 5000L * (i + 1), timestamps_in_millis.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * (i + 1), + timestamps_in_millis.get(i).longValue()); } } // end testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() @@ -157,7 +170,8 @@ public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { public void testOneHistogramSpanNoDownSamplerOutofRange() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -166,8 +180,10 @@ public void testOneHistogramSpanNoDownSamplerOutofRange() { List spans = new ArrayList(); spans.add(hspan); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME + 5000L * 10, - BASE_TIME + 5000L * 20, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME + 5000L * 10, + BASE_TIME + 5000L * 20, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); assertFalse(histAggIt.hasNext()); } // end testOneHistogramSpanNoDownSamplerOutofRange() @@ -175,7 +191,8 @@ public void testOneHistogramSpanNoDownSamplerOutofRange() { public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -184,8 +201,10 @@ public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { List spans = new ArrayList(); spans.add(hspan); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 5, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 5, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -206,7 +225,8 @@ public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { public void testOneHistogramSpanDownSamplerLaterDataPoints() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -215,8 +235,10 @@ public void testOneHistogramSpanDownSamplerLaterDataPoints() { List spans = new ArrayList(); spans.add(hspan); - DownsamplingSpecification specification = new DownsamplingSpecification("10s-sum"); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + DownsamplingSpecification specification = + new DownsamplingSpecification("10s-sum"); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, BASE_TIME + 5000L * 5, HistogramAggregation.SUM, specification, 0, 0, false); List values = new ArrayList(); @@ -245,7 +267,8 @@ public void testOneHistogramSpanDownSamplerLaterDataPoints() { public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -256,15 +279,18 @@ public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { List row2 = new ArrayList(); for (int i = 0; i < 10; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); hspan2.addRow(KEY, row2); spans.add(hspan2); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -285,7 +311,8 @@ public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { public void testTwoHistogramSpanDownSamplerSameTimestamp() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -296,15 +323,18 @@ public void testTwoHistogramSpanDownSamplerSameTimestamp() { List row2 = new ArrayList(); for (int i = 0; i < 10; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); hspan2.addRow(KEY, row2); spans.add(hspan2); - DownsamplingSpecification specification = new DownsamplingSpecification("10s-sum"); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, + DownsamplingSpecification specification = + new DownsamplingSpecification("10s-sum"); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, BASE_TIME + 5000L * 10, HistogramAggregation.SUM, specification, 0, 0, false); List values = new ArrayList(); @@ -327,7 +357,8 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { List row = new ArrayList(); // 0, 2, 4... for (int i = 0; i < 10; ) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); i += 2; } @@ -340,7 +371,8 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { List row2 = new ArrayList(); // 1, 3, 5... for (int i = 1; i < 10; ) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); i += 2; } @@ -348,8 +380,10 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { hspan2.addRow(KEY, row2); spans.add(hspan2); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -370,7 +404,8 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { public void testTwoHistogramSpanNoDownSamplerMergeSome() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -381,11 +416,13 @@ public void testTwoHistogramSpanNoDownSamplerMergeSome() { List row2 = new ArrayList(); for (int i = 1; i < 5; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } for (int i = 5; i < 10; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * (5 + i), Bytes.fromLong(5 + i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * (5 + i), + Bytes.fromLong(5 + i))); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); @@ -393,8 +430,10 @@ public void testTwoHistogramSpanNoDownSamplerMergeSome() { spans.add(hspan2); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 20, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 20, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -426,7 +465,8 @@ public void testTwoHistogramSpanNoDownSamplerOneHasMore() { // span 1 has 10 data points List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -438,15 +478,18 @@ public void testTwoHistogramSpanNoDownSamplerOneHasMore() { // span 2 has 5 data points List row2 = new ArrayList(); for (int i = 1; i < 5; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); hspan2.addRow(KEY, row2); spans.add(hspan2); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME, - BASE_TIME + 5000L * 20, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME, + BASE_TIME + 5000L * 20, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -473,7 +516,8 @@ public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { // span1 has 10 data points List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -485,15 +529,18 @@ public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { // span 2 has 5 data points List row2 = new ArrayList(); for (int i = 1; i < 5; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); hspan2.addRow(KEY, row2); spans.add(hspan2); - HistogramAggregationIterator histAggIt = HistogramAggregationIterator.create(spans, BASE_TIME + 5000L * 5, - BASE_TIME + 5000L * 10, HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); + HistogramAggregationIterator histAggIt = + HistogramAggregationIterator.create(spans, BASE_TIME + 5000L * 5, + BASE_TIME + 5000L * 10, HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, false); List values = new ArrayList(); List timestamps_in_millis = new ArrayList(); @@ -506,7 +553,8 @@ public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { assertEquals(5, values.size()); for (int i = 0; i < values.size(); ++i) { assertEquals(5 + i, values.get(i).longValue()); - assertEquals(BASE_TIME + 5000L * (5 + i), timestamps_in_millis.get(i).longValue()); + assertEquals(BASE_TIME + 5000L * (5 + i), + timestamps_in_millis.get(i).longValue()); } // end for } // end testTwoHistogramSpanNoDownSamplerOneOutofRange() } diff --git a/test/core/TestHistogramDataPointsToDataPointsAdaptor.java b/test/core/TestHistogramDataPointsToDataPointsAdaptor.java index 11c263c01b..c9fd3caad1 100644 --- a/test/core/TestHistogramDataPointsToDataPointsAdaptor.java +++ b/test/core/TestHistogramDataPointsToDataPointsAdaptor.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -42,8 +42,9 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({ HistogramSpanGroup.class, HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, -Config.class, RowKey.class }) +@PrepareForTest({ HistogramSpanGroup.class, HistogramSpan.class, + HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, + Config.class, RowKey.class }) public final class TestHistogramDataPointsToDataPointsAdaptor { private static final long BASE_TIME = 1356998400000L; public static final byte[] KEY = @@ -66,14 +67,17 @@ public void getTagUids() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); final ArrayList spans = Whitebox.getInternalState(group, "spans"); spans.add(span); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final ByteMap uids_read = dps_ada.getTagUids(); assertEquals(1, uids_read.size()); @@ -94,15 +98,18 @@ public void getTagUidsAggedOut() throws Exception { final HistogramSpan span2 = mock(HistogramSpan.class); when(span2.getTagUids()).thenReturn(uids2); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); final ArrayList spans = Whitebox.getInternalState(group, "spans"); spans.add(span); spans.add(span2); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final ByteMap uids_read = dps_ada.getTagUids(); assertEquals(0, uids_read.size()); @@ -110,11 +117,14 @@ public void getTagUidsAggedOut() throws Exception { @Test public void getTagUidsNoSpans() throws Exception { - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final ByteMap uids_read = dps_ada.getTagUids(); assertEquals(0, uids_read.size()); } @@ -126,14 +136,18 @@ public void getAggregatedTagUidsNotAgged() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final List uids_read = dps_ada.getAggregatedTagUids(); assertEquals(0, uids_read.size()); @@ -151,15 +165,18 @@ public void getAggregatedTagUids() throws Exception { final HistogramSpan span2 = mock(HistogramSpan.class); when(span2.getTagUids()).thenReturn(uids2); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); final ArrayList spans = Whitebox.getInternalState(group, "spans"); spans.add(span); spans.add(span2); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final List uids_read = dps_ada.getAggregatedTagUids(); assertEquals(1, uids_read.size()); @@ -168,11 +185,14 @@ public void getAggregatedTagUids() throws Exception { @Test public void getAggregatedTagUidsNoSpans() throws Exception { - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = new + DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final List uids_read = dps_ada.getAggregatedTagUids(); assertEquals(0, uids_read.size()); @@ -188,14 +208,19 @@ public void getTagUidsAggedNotInQuery() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, - end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + DownsamplingSpecification specification = new + DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, + query_tags)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final ByteMap uids_read = dps_ada.getTagUids(); assertEquals(0, uids_read.size()); @@ -214,14 +239,19 @@ public void getTagUidsInQueryTags() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, - end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, + query_tags)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final ByteMap uids_read = dps_ada.getTagUids(); assertEquals(1, uids_read.size()); assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); @@ -237,14 +267,18 @@ public void getTagUidsNullQueryTags() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(group, 0.98f); final ByteMap uids_read = dps_ada.getTagUids(); assertEquals(1, uids_read.size()); assertEquals(0, Bytes.memcmp(new byte[] {0, 0, 0, 1 }, uids_read.firstKey())); @@ -257,7 +291,8 @@ public void getTagUidsNullQueryTags() throws Exception { public void iteratorAllItems() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -268,10 +303,13 @@ public void iteratorAllItems() { final ByteSet query_tags = new ByteSet(); query_tags.add(new byte[] { 0, 0, 0, 1 }); - HistogramSpanGroup hist_span_group = new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, - HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); + HistogramSpanGroup hist_span_group = + new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, + HistogramAggregation.SUM, + DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); List values = new ArrayList(); List timestamp_in_ms = new ArrayList(); for (DataPoint dp : dps_ada) { @@ -288,7 +326,8 @@ public void iteratorAllItems() { assertEquals(values.size(), to_checks.size()); for (int i = 0; i < values.size(); ++i) { - assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), 0.0001); + assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), + 0.0001); assertEquals(timestamp_in_ms.get(i).longValue(), BASE_TIME + 5000L * i); } // end for } @@ -297,7 +336,8 @@ public void iteratorAllItems() { public void doubleIteratorAllItems() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -308,10 +348,13 @@ public void doubleIteratorAllItems() { final ByteSet query_tags = new ByteSet(); query_tags.add(new byte[] { 0, 0, 0, 1 }); - HistogramSpanGroup hist_span_group = new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, - HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); + HistogramSpanGroup hist_span_group = + new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, + HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, + 0, 0, 0, false, query_tags); - HistogramDataPointsToDataPointsAdaptor dps_ada = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada = + new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); List values = new ArrayList(); for (DataPoint dp : dps_ada) { assertFalse(dp.isInteger()); @@ -331,8 +374,10 @@ public void doubleIteratorAllItems() { assertEquals(values.size(), to_checks.size()); for (int i = 0; i < values.size(); ++i) { - assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), 0.0001); - assertEquals(values.get(i).doubleValue(), values2.get(i).doubleValue(), 0.0001); + assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), + 0.0001); + assertEquals(values.get(i).doubleValue(), values2.get(i).doubleValue(), + 0.0001); } // end for } @@ -340,7 +385,8 @@ public void doubleIteratorAllItems() { public void iteratorAllItemsWithDiffPercentile() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, Bytes.fromLong(i))); + row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, + Bytes.fromLong(i))); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -351,11 +397,14 @@ public void iteratorAllItemsWithDiffPercentile() { final ByteSet query_tags = new ByteSet(); query_tags.add(new byte[] { 0, 0, 0, 1 }); - HistogramSpanGroup hist_span_group = new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, - HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, 0, 0, 0, false, query_tags); + HistogramSpanGroup hist_span_group = + new HistogramSpanGroup(tsdb, BASE_TIME, BASE_TIME + 5000L * 10, spans, + HistogramAggregation.SUM, DownsamplingSpecification.NO_DOWNSAMPLER, + 0, 0, 0, false, query_tags); // 98 percentile - HistogramDataPointsToDataPointsAdaptor dps_ada_98 = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); + HistogramDataPointsToDataPointsAdaptor dps_ada_98 = + new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.98f); List values = new ArrayList(); for (DataPoint dp : dps_ada_98) { assertFalse(dp.isInteger()); @@ -370,11 +419,13 @@ public void iteratorAllItemsWithDiffPercentile() { assertEquals(values.size(), to_checks.size()); for (int i = 0; i < values.size(); ++i) { - assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), 0.0001); + assertEquals(values.get(i).doubleValue(), to_checks.get(i).doubleValue(), + 0.0001); } // end for // 95 percentile - HistogramDataPointsToDataPointsAdaptor dps_ada_95 = new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.95f); + HistogramDataPointsToDataPointsAdaptor dps_ada_95 = + new HistogramDataPointsToDataPointsAdaptor(hist_span_group, 0.95f); List values_95 = new ArrayList(); for (DataPoint dp : dps_ada_95) { assertFalse(dp.isInteger()); @@ -389,7 +440,8 @@ public void iteratorAllItemsWithDiffPercentile() { assertEquals(values_95.size(), to_checks_95.size()); for (int i = 0; i < values.size(); ++i) { - assertEquals(values_95.get(i).doubleValue(), to_checks_95.get(i).doubleValue(), 0.0001); + assertEquals(values_95.get(i).doubleValue(), + to_checks_95.get(i).doubleValue(), 0.0001); } // end for } } diff --git a/test/core/TestHistogramDownsampler.java b/test/core/TestHistogramDownsampler.java index 80f0fdea48..2f876c8a88 100644 --- a/test/core/TestHistogramDownsampler.java +++ b/test/core/TestHistogramDownsampler.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -21,15 +21,12 @@ import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.TimeZone; import com.google.common.collect.Lists; -import com.stumbleupon.async.Deferred; -import net.opentsdb.core.SeekableViewsForTest.MockSeekableView; import net.opentsdb.uid.UniqueId; import net.opentsdb.core.HistogramSeekableViewForTest.MockHistogramSeekableView; import net.opentsdb.utils.Config; @@ -48,8 +45,10 @@ @RunWith(PowerMockRunner.class) // "Classloader hell"... It's real. Tell PowerMock to ignore these classes // because they fiddle with the class loader. We don't test them anyway. -@PowerMockIgnore({ "javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*" }) -@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, Config.class, +@PowerMockIgnore({ "javax.management.*", "javax.xml.*", "ch.qos.*", + "org.slf4j.*", "com.sum.*", "org.xml.*" }) +@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, + UniqueId.class, KeyValue.class, Config.class, RowKey.class }) public class TestHistogramDownsampler { private TSDB tsdb = mock(TSDB.class); @@ -58,26 +57,25 @@ public class TestHistogramDownsampler { private static final long BASE_TIME = 1356998400000L; - private static final HistogramDataPoint[] HIST_DATA_POINTS = new HistogramDataPoint[] { - // timestamp = 1,356,998,400,000 ms - new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(40L)), - // timestamp = 1,357,000,400,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 2000000, Bytes.fromLong(50L)), - // timestamp = 1,357,002,000,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 3600000, Bytes.fromLong(40L)), - // timestamp = 1,357,002,005,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 3605000, Bytes.fromLong(50L)), - // timestamp = 1,357,005,600,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 7200000, Bytes.fromLong(40L)), - // timestamp = 1,357,007,600,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 9200000, Bytes.fromLong(50L)) }; + private static final HistogramDataPoint[] HIST_DATA_POINTS = + new HistogramDataPoint[] { + // timestamp = 1,356,998,400,000 ms + new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(40L)), + // timestamp = 1,357,000,400,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 2000000, Bytes.fromLong(50L)), + // timestamp = 1,357,002,000,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 3600000, Bytes.fromLong(40L)), + // timestamp = 1,357,002,005,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 3605000, Bytes.fromLong(50L)), + // timestamp = 1,357,005,600,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 7200000, Bytes.fromLong(40L)), + // timestamp = 1,357,007,600,000 ms + new LongHistogramDataPointForTest(BASE_TIME + 9200000, Bytes.fromLong(50L)) + }; public static final byte[] KEY = new byte[] { 0, 0, 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 2 }; - - private static final int THOUSAND_SEC_INTERVAL = (int) DateTime.parseDuration("1000s"); - private static final int TEN_SEC_INTERVAL = (int) DateTime.parseDuration("10s"); - + // 30 minute offset final static TimeZone AF = DateTime.timezones.get("Asia/Kabul"); @@ -135,17 +133,18 @@ public void testDownsampler() { public void testDownsampler_10seconds() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 0, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 1, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 2, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 3, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 4, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 5, Bytes.fromLong(32L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 6, Bytes.fromLong(64L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 7, Bytes.fromLong(128L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 8, Bytes.fromLong(256L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 9, Bytes.fromLong(512L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 10, Bytes.fromLong(1024L)) })); + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 0, Bytes.fromLong(1L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 1, Bytes.fromLong(2L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 2, Bytes.fromLong(4L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 3, Bytes.fromLong(8L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 4, Bytes.fromLong(16L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 5, Bytes.fromLong(32L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 6, Bytes.fromLong(64L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 7, Bytes.fromLong(128L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 8, Bytes.fromLong(256L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 9, Bytes.fromLong(512L)), + new LongHistogramDataPointForTest(BASE_TIME + 5000L * 10, Bytes.fromLong(1024L)) + })); specification = new DownsamplingSpecification("10s-sum"); downsampler = new HistogramDownsampler(source, specification, 0, 0); @@ -862,21 +861,26 @@ public void testSeek_abandoningIncompleteInterval() { assertEquals("seek(1356998400000)", 400, Bytes.getLong(first_dp.getRawData())); // No seeks but the last one is aligned by the downsampling window. - for (long seek_timestamp = BASE_TIME + 1000L; seek_timestamp < BASE_TIME + 10100L; seek_timestamp += 1000) { + for (long seek_timestamp = BASE_TIME + 1000L; seek_timestamp < BASE_TIME + + 10100L; seek_timestamp += 1000) { downsampler.seek(seek_timestamp); assertTrue("ts = " + seek_timestamp, downsampler.hasNext()); HistogramDataPoint dp = downsampler.next(); // Timestamp should be greater than or equal to the seek timestamp. - assertTrue(String.format("%d >= %d", dp.timestamp(), seek_timestamp), dp.timestamp() >= seek_timestamp); - assertEquals(String.format("seek(%d)", seek_timestamp), BASE_TIME + 10000L, dp.timestamp()); - assertEquals(String.format("seek(%d)", seek_timestamp), 40, Bytes.getLong(dp.getRawData())); + assertTrue(String.format("%d >= %d", dp.timestamp(), seek_timestamp), + dp.timestamp() >= seek_timestamp); + assertEquals(String.format("seek(%d)", seek_timestamp), + BASE_TIME + 10000L, dp.timestamp()); + assertEquals(String.format("seek(%d)", seek_timestamp), + 40, Bytes.getLong(dp.getRawData())); } } @Test public void testSeek_useCalendar() { source = spy(HistogramSeekableViewForTest - .fromArray(new HistogramDataPoint[] { new LongHistogramDataPointForTest(1356998400000L, Bytes.fromLong(1L)), + .fromArray(new HistogramDataPoint[] { new LongHistogramDataPointForTest( + 1356998400000L, Bytes.fromLong(1L)), new LongHistogramDataPointForTest(1388534400000L, Bytes.fromLong(2L)), new LongHistogramDataPointForTest(1420070400000L, Bytes.fromLong(4L)), new LongHistogramDataPointForTest(1451606400000L, Bytes.fromLong(8L)) })); diff --git a/test/core/TestHistogramRowSeq.java b/test/core/TestHistogramRowSeq.java index 1e702acab0..88bb7c0558 100644 --- a/test/core/TestHistogramRowSeq.java +++ b/test/core/TestHistogramRowSeq.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016-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 diff --git a/test/core/TestHistogramSpan.java b/test/core/TestHistogramSpan.java index dc674cc3e0..b23ff99b4c 100644 --- a/test/core/TestHistogramSpan.java +++ b/test/core/TestHistogramSpan.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -44,8 +44,8 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, -Config.class, RowKey.class }) +@PrepareForTest({ HistogramSpan.class, HistogramRowSeq.class, TSDB.class, + UniqueId.class, KeyValue.class, Config.class, RowKey.class }) public final class TestHistogramSpan { protected TSDB tsdb = mock(TSDB.class); protected Config config = mock(Config.class); @@ -117,7 +117,8 @@ public void addRowBadKeyLength() { row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); - final byte[] bad_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x43, 0x20, 0, 0, 0, 1 }; + final byte[] bad_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x43, + 0x20, 0, 0, 0, 1 }; histSpan.addRow(bad_key, row2); } @@ -134,7 +135,8 @@ public void addRowMissMatchedMetric() { row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); - final byte[] not_matched_mitric_key = new byte[] { 0, 0, 0, 2, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 2 }; + final byte[] not_matched_mitric_key = new byte[] { 0, 0, 0, 2, 0x50, + (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 2 }; histSpan.addRow(not_matched_mitric_key, row2); } @@ -151,7 +153,8 @@ public void addRowMissMatchedTagk() { row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); - final byte[] not_matched_tagk_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 2, 0, 0, 0, 2 }; + final byte[] not_matched_tagk_key = new byte[] { 0, 0, 0, 1, 0x50, + (byte)0xE2, 0x35, 0x10, 0, 0, 0, 2, 0, 0, 0, 2 }; histSpan.addRow(not_matched_tagk_key, row2); } @@ -168,7 +171,8 @@ public void addRowMissMatchedTagv() { row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); - final byte[] not_matched_tagv_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 3 }; + final byte[] not_matched_tagv_key = new byte[] { 0, 0, 0, 1, 0x50, + (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 3 }; histSpan.addRow(not_matched_tagv_key, row2); } diff --git a/test/core/TestHistogramSpanGroup.java b/test/core/TestHistogramSpanGroup.java index f47f792a51..14ccfb21e3 100644 --- a/test/core/TestHistogramSpanGroup.java +++ b/test/core/TestHistogramSpanGroup.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -56,11 +56,14 @@ public void getTagUids() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); final ByteMap uids_read = group.getTagUids(); @@ -82,11 +85,14 @@ public void getTagUidsAggedOut() throws Exception { final HistogramSpan span2 = mock(HistogramSpan.class); when(span2.getTagUids()).thenReturn(uids2); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); spans.add(span2); @@ -96,8 +102,10 @@ public void getTagUidsAggedOut() throws Exception { @Test public void getTagUidsNoSpans() throws Exception { - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); final ByteMap uids_read = group.getTagUids(); @@ -111,11 +119,14 @@ public void getAggregatedTagUidsNotAgged() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); final List uids_read = group.getAggregatedTagUids(); @@ -134,11 +145,14 @@ public void getAggregatedTagUids() throws Exception { final HistogramSpan span2 = mock(HistogramSpan.class); when(span2.getTagUids()).thenReturn(uids2); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); spans.add(span2); @@ -149,8 +163,10 @@ public void getAggregatedTagUids() throws Exception { @Test public void getAggregatedTagUidsNoSpans() throws Exception { - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); final List uids_read = group.getAggregatedTagUids(); @@ -167,11 +183,15 @@ public void getTagUidsAggedNotInQuery() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, - end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, + query_tags)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); final ByteMap uids_read = group.getTagUids(); @@ -191,11 +211,15 @@ public void getTagUidsInQueryTags() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, - end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, query_tags)); + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, + end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, + query_tags)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); final ByteMap uids_read = group.getTagUids(); @@ -213,11 +237,14 @@ public void getTagUidsNullQueryTags() throws Exception { final HistogramSpan span = mock(HistogramSpan.class); when(span.getTagUids()).thenReturn(uids); - DownsamplingSpecification specification = new DownsamplingSpecification("1dc-sum"); - final HistogramSpanGroup group = PowerMockito.spy(new HistogramSpanGroup(tsdb, start_ts, + DownsamplingSpecification specification = + new DownsamplingSpecification("1dc-sum"); + final HistogramSpanGroup group = PowerMockito.spy( + new HistogramSpanGroup(tsdb, start_ts, end_ts, null, HistogramAggregation.SUM, specification, 0, 0, 0, false, null)); - final ArrayList spans = Whitebox.getInternalState(group, "spans"); + final ArrayList spans = + Whitebox.getInternalState(group, "spans"); spans.add(span); final ByteMap uids_read = group.getTagUids(); diff --git a/test/core/TestTsdbQueryHistogramQueries.java b/test/core/TestTsdbQueryHistogramQueries.java index 90b1dc2ccf..c705151add 100644 --- a/test/core/TestTsdbQueryHistogramQueries.java +++ b/test/core/TestTsdbQueryHistogramQueries.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -43,7 +43,8 @@ public void beforeLocal() throws Exception { @Test public void runSingleTsMsSinglePercentile() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); storage.dumpToSystemOut(); @@ -79,7 +80,8 @@ public void runSingleTsMsSinglePercentile() throws Exception { @Test public void runSingleTsMsDoulePercentile() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(1); @@ -130,7 +132,8 @@ public void runSingleTsMsDoulePercentile() throws Exception { @Test public void runSingleTsMsTwoAggSum() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(); @@ -162,7 +165,8 @@ public void runSingleTsMsTwoAggSum() throws Exception { @Test public void runSingleTsMsAggNone() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(); @@ -213,7 +217,8 @@ public void runSingleTsMsAggNone() throws Exception { @Test public void runSingleTsMsAggSumTwoGroups() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(); @@ -265,7 +270,8 @@ public void runSingleTsMsAggSumTwoGroups() throws Exception { @Test public void runWithAnnotation() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); final Annotation note = new Annotation(); @@ -301,7 +307,8 @@ public void runWithAnnotation() throws Exception { @Test public void runWithOnlyAnnotation() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); byte[] key = getRowKey(HISTOGRAM_METRIC_STRING, 1357002000, TAGK_STRING, TAGV_STRING); @@ -343,7 +350,8 @@ public void runWithOnlyAnnotation() throws Exception { @Test public void runTSUIDQuery() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); query.setStartTime(1356998400); @@ -375,7 +383,8 @@ public void runTSUIDQuery() throws Exception { @Test public void runTSUIDsAggSum() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); + Whitebox.setInternalState(config, "hist_decoder_name", + "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); query.setStartTime(1356998400); From 78652121788ad1b139a8917a4a1ea7c0c30544cc Mon Sep 17 00:00:00 2001 From: rohannog Date: Sun, 28 May 2017 21:44:53 -0700 Subject: [PATCH 635/826] Add the Histogram interface for a means of definining histo implementations. Add the SimpleHistogram class and import Kryo for serdes. It's what we use at Yahoo and does a decent job of encoding the histograms in a smaller space. Signed-off-by: Chris Larsen --- pom.xml.in | 6 + src/core/Histogram.java | 35 ++ src/core/HistogramDataPoint.java | 38 ++- src/core/SimpleHistogram.java | 301 +++++++++++++++++ test/core/TestSimpleHistogram.java | 498 +++++++++++++++++++++++++++++ 5 files changed, 873 insertions(+), 5 deletions(-) create mode 100644 src/core/Histogram.java create mode 100644 src/core/SimpleHistogram.java create mode 100644 test/core/TestSimpleHistogram.java diff --git a/pom.xml.in b/pom.xml.in index c44b692ff9..235e8a1df1 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -400,6 +400,12 @@ jgrapht-core @JGRAPHT_VERSION@ + + + com.esotericsoftware.kryo + kryo + 2.21.1 + diff --git a/src/core/Histogram.java b/src/core/Histogram.java new file mode 100644 index 0000000000..5631ede2ad --- /dev/null +++ b/src/core/Histogram.java @@ -0,0 +1,35 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 java.util.List; +import java.util.Map; + +public interface Histogram { + + public byte[] histogram(); + + public void fromHistogram(final byte[] raw); + + public double percentile(final double p); + + public List percentiles(List p); + + public Map getHistogram(); + + public Histogram clone(); + + void aggregate(Histogram histo, HistogramAggregation func); + + void aggregate(List histos, HistogramAggregation func); +} diff --git a/src/core/HistogramDataPoint.java b/src/core/HistogramDataPoint.java index 59ceb2201d..fded3ec5cc 100644 --- a/src/core/HistogramDataPoint.java +++ b/src/core/HistogramDataPoint.java @@ -15,6 +15,11 @@ import java.util.List; import java.util.Map; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.KryoSerializable; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + /** * Represents a single histogram data point, e.g. all of the buckets for a * measurement at a point in time. @@ -68,22 +73,26 @@ public interface HistogramDataPoint extends Cloneable { */ HistogramDataPoint clone(); - HistogramDataPoint cloneAndSetTimestamp(final long timestamp); - /////////////////////////////////////////////////////////////////////////// // A nested class to present the bucket information /////////////////////////////////////////////////////////////////////////// - public class HistogramBucket implements Comparable { + public class HistogramBucket implements KryoSerializable, Comparable { public enum BucketType { UNDERFLOW, REGULAR, OVERFLOW } private final BucketType type; - private final float lower_bound; - private final float upper_bound; + private float lower_bound; + private float upper_bound; + public HistogramBucket() { + this.type = BucketType.REGULAR; + this.lower_bound = 0; + this.upper_bound = 0; + } + public HistogramBucket(final BucketType type, final float lower_bound, final float uper_bound) { this.type = type; @@ -152,6 +161,25 @@ public int compareTo(HistogramBucket that) { return 0; } + + public void write(Kryo kryo, Output output) { + output.writeFloat(lower_bound); + output.writeFloat(upper_bound); + } + + public void read(Kryo kryo, Input input) { + lower_bound = input.readFloat(); + upper_bound = input.readFloat(); + } + + @Override + public String toString() { + if (this.getUpperBound() != Float.NaN) { + return this.getLowerBound() + "-" + this.getUpperBound(); + } else { + return this.getLowerBound() + "-"; + } + } } /** diff --git a/src/core/SimpleHistogram.java b/src/core/SimpleHistogram.java new file mode 100644 index 0000000000..ec04871f2a --- /dev/null +++ b/src/core/SimpleHistogram.java @@ -0,0 +1,301 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javax.annotation.Generated; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import net.opentsdb.core.HistogramDataPoint.HistogramBucket; +import net.opentsdb.core.HistogramDataPoint.HistogramBucket.BucketType; + +@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) +@Generated("org.jsonschema2pojo") +public class SimpleHistogram implements Histogram { + private static Logger LOG = LoggerFactory.getLogger(SimpleHistogram.class); + + @JsonProperty("buckets") + TreeMap buckets = new TreeMap(); + + @JsonProperty("underflow") + Long underflow = 0L; + + @JsonProperty("overflow") + Long overflow = 0L; + + public void addBucket(Float min, Float max, Long count) { + if (min == null || max == null) { + return; + } + if (count == null) { + count = 0L; //Prevent Null Exception + } + + buckets.put(new HistogramBucket(BucketType.REGULAR, min, max), count); + } + + public byte[] histogram() { + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + + int bucketCount = buckets.size(); + try { + output.writeShort(bucketCount); + + for (Map.Entry bucket : buckets.entrySet()) { + output.writeFloat(bucket.getKey().getLowerBound()); + output.writeFloat(bucket.getKey().getUpperBound()); + output.writeLong(bucket.getValue(), true); + } + + output.writeLong(this.getUnderflow(), true); + output.writeLong(this.getOverflow(), true); + } finally { + output.close(); + } + return outBuffer.toByteArray(); + } + + public void fromHistogram(byte[] raw) { + if (raw.length < 6) { + LOG.debug("Byte array shorter than 6 bytes detected"); + return; + } + Input input = new Input(new ByteArrayInputStream(raw)); + int bucketCount = input.readShort(); + + for (int i = 0; i < bucketCount; i++) { + buckets.put(new HistogramBucket(BucketType.REGULAR, input.readFloat(), input.readFloat()), input.readLong(true)); + } + + this.setUnderflow(input.readLong(true)); + this.setOverflow(input.readLong(true)); + } + + private int calcBucketSum () { + int sum = 0; + for (Map.Entry entry : buckets.entrySet()) { + sum += entry.getValue().intValue(); + } + + return sum; + } + + public double percentile(double perc) { + if (perc < 1.0 || perc > 100.0) { + return -1.0; + } + + int bucketSum = calcBucketSum(); + + long runningCount = 0; + double prevBucketArea = 0.0; + double percValue = 0.0; + + for (Map.Entry entry : buckets.entrySet()) { + runningCount += entry.getValue().intValue(); + Double currBucketArea = runningCount * 100.0 / bucketSum; + + if (currBucketArea >= perc) { + //Find closest ranks + //Float currBucketStart = entry.getKey().getLowerBound(); + //Float nextBucketStart = entry.getKey().getUpperBound(); + + //percValue = currBucketStart + ((nextBucketStart - currBucketStart) * (perc - prevBucketArea) / (currBucketArea - prevBucketArea)); + percValue = (entry.getKey().getLowerBound() + entry.getKey().getUpperBound())/2; + break; + } else { + prevBucketArea = runningCount * 100.0 / bucketSum; + } + } + + return percValue; + } + + @Override + public List percentiles(List percs) { + List percValues = new ArrayList(); + + for (Double perc : percs) { + percValues.add(percentile(perc)); + } + + return percValues; + } + + @JsonIgnore + @Override + public Map getHistogram() { + return Collections.unmodifiableMap(buckets); + } + + @Override + public SimpleHistogram clone() { + SimpleHistogram cloneObj = new SimpleHistogram(); + + for (Map.Entry bucket : buckets.entrySet()) { + cloneObj.addBucket(bucket.getKey().getLowerBound(), bucket.getKey().getUpperBound(), bucket.getValue()); + } + cloneObj.setUnderflow(underflow); + cloneObj.setOverflow(overflow); + + return cloneObj; + } + + public Long getBucketCount(Float min, Float max) { + HistogramBucket qryBucket = new HistogramBucket(BucketType.REGULAR, min, max); + if (buckets.containsKey(qryBucket)) { + Long bucketCount = buckets.get(qryBucket); + if (bucketCount == null) { + return 0L; + } else { + return bucketCount; + } + } else { + return 0L; + } + } + + public void write(Kryo kryo, Output output) { + int bucketCount = buckets.size(); + output.writeShort(bucketCount); + + for (Map.Entry bucket : buckets.entrySet()) { + bucket.getKey().write(kryo, output); + output.writeLong(bucket.getValue(), true); + } + + output.writeLong(this.getUnderflow(), true); + output.writeLong(this.getOverflow(), true); + } + + public void read(Kryo kryo, Input input) { + int bucketCount = input.readShort(); + if (bucketCount < 1) { + LOG.debug("Byte array passed has less than 1 histogram entry"); + return; + } + + for (int i = 0; i < bucketCount; i++) { + HistogramBucket bucket = new HistogramBucket(); + bucket.read(kryo, input); + buckets.put(bucket, input.readLong(true)); + } + + this.setUnderflow(input.readLong(true)); + this.setOverflow(input.readLong(true)); + } + + + public void aggregate(Histogram histo, HistogramAggregation func) { + if (func == HistogramAggregation.SUM) { + SimpleHistogram y1Histo = (SimpleHistogram) histo; + for (Map.Entry bucket : (Set>) histo.getHistogram().entrySet()) { + Long newCount = this.getBucketCount(bucket.getKey().getLowerBound(), bucket.getKey().getUpperBound()) + bucket.getValue(); + this.addBucket(bucket.getKey().getLowerBound(), bucket.getKey().getUpperBound(), newCount); + } + this.setOverflow(y1Histo.getOverflow() + this.getOverflow()); + this.setUnderflow(y1Histo.getUnderflow() + this.getUnderflow()); + } else { + LOG.debug("Unsupported histogram aggregation used"); + } + } + + public void aggregate(List histos, HistogramAggregation func) { + if (func == HistogramAggregation.SUM) { + for (Histogram histo : histos) { + this.aggregate(histo, HistogramAggregation.SUM); + } + } else { + LOG.debug("Unsupported histogram aggregation used"); + } + } + + public Long getUnderflow() { + return underflow; + } + + public void setUnderflow(Long underflow) { + this.underflow = underflow; + } + + public Long getOverflow() { + return overflow; + } + + public void setOverflow(Long overflow) { + this.overflow = overflow; + } + + public static double[] initializeHistogram (float start, float end, float focusRangeStart, float focusRangeEnd, float errorPct) { + if (Float.compare(start, end) >= 0) { + throw new RuntimeException("Histogram start (" + start + ") must be less than Histogram end (" + end +")"); + } else if (Float.compare(focusRangeStart, focusRangeEnd) >= 0) { + throw new RuntimeException("Histogram focus range start (" + focusRangeStart + ") must be less than Histogram focus end (" + focusRangeEnd + ")"); + } else if (Float.compare(start, focusRangeStart) > 0 || Float.compare(focusRangeStart, end) >= 0 || Float.compare(start, focusRangeEnd) >= 0 || Float.compare(focusRangeEnd, end) > 0) { + throw new RuntimeException("Focus range start (" + focusRangeStart + ") and Focus range end (" + focusRangeEnd + ") must be greater than Histogram start (" + start + ") and less than Histogram end (" + end + ")"); + } else if (Float.compare(errorPct, 0.0f) <= 0) { + throw new RuntimeException("Error rate (" + errorPct + ") must be greater than zero"); + } + int MAX_BUCKETS = 100; + float stepSize = (1 + errorPct)/(1 - errorPct); + int bucketcount = Double.valueOf(Math.ceil(Math.log(focusRangeEnd/focusRangeStart)/Math.log(stepSize))).intValue() + 1; + + if (Float.compare(start, focusRangeStart) < 0) { + bucketcount++; + } + + if (Float.compare(focusRangeEnd, end) < 0) { + bucketcount++; + } + + if (bucketcount > MAX_BUCKETS) { + throw new RuntimeException("A max of " + MAX_BUCKETS +" buckets are supported. " + bucketcount + " were requested"); + } + + double[] retval = new double[bucketcount]; + int j = 0; + if (Float.compare(start, focusRangeStart) < 0) { + retval[j] = start; + j++; + } + + for (float i = focusRangeStart; i < focusRangeEnd; i*=stepSize, j++) { + retval[j] = i; + } + + if (Float.compare(focusRangeEnd, end) < 0) { + retval[j++] = focusRangeEnd; + } + retval[j] = end; + + return retval; +} +} diff --git a/test/core/TestSimpleHistogram.java b/test/core/TestSimpleHistogram.java new file mode 100644 index 0000000000..ce37a954ef --- /dev/null +++ b/test/core/TestSimpleHistogram.java @@ -0,0 +1,498 @@ +package net.opentsdb.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Map; + +import org.junit.Test; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.fasterxml.jackson.databind.ObjectMapper; + +import net.opentsdb.core.HistogramDataPoint.HistogramBucket; +import net.opentsdb.core.HistogramDataPoint.HistogramBucket.BucketType; + +public class TestSimpleHistogram { + + @Test + public void verifyE2EKryo() { + Kryo kryo = new Kryo(); + + //Encoding stage + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeByte(0 /* This is the type of histogram (or sketch) written to storage. HistoType.SimpleHistogramType.ordinal()*/); + output.writeShort(8); + output.writeFloat(1.0f); + output.writeFloat(2.0f); + output.writeLong(5, true); + output.writeFloat(2.0f); + output.writeFloat(3.0f); + output.writeLong(5, true); + output.writeFloat(3.0f); + output.writeFloat(4.0f); + output.writeLong(5, true); + output.writeFloat(4.0f); + output.writeFloat(5.0f); + output.writeLong(0, true); + output.writeFloat(5.0f); + output.writeFloat(6.0f); + output.writeLong(0, true); + output.writeFloat(6.0f); + output.writeFloat(7.0f); + output.writeLong(0, true); + output.writeFloat(7.0f); + output.writeFloat(8.0f); + output.writeLong(0, true); + output.writeFloat(8.0f); + output.writeFloat(9.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(2, true); + output.close(); + + System.out.println("Byte array ouput size: " + outBuffer.toByteArray().length); + + //Decoding stage + Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); + int metricType = input.readByte(); + + switch (metricType) { + case 0: + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.read(kryo, input); + Input verifyHist = new Input(new ByteArrayInputStream(y1Hist.histogram())); + + int bucketCount = verifyHist.readShort(); + assertEquals(bucketCount, 8); + + Float bucketLowerBound = verifyHist.readFloat(); + Float bucketUpperBound = verifyHist.readFloat(); + long bucketVal = verifyHist.readLong(true); + assertEquals(bucketLowerBound, 1.0f, 0.0001); + assertEquals(bucketVal, 5); + assertEquals(y1Hist.getOverflow(), Long.valueOf(2L)); + break; + default: + System.out.println("Failed to detect histogram type"); + assertTrue(false); + } + input.close(); + } + + @Test + public void testHistogramSerialization() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.addBucket(1.0f, 2.0f, 5L); + y1Hist.addBucket(2.0f, 3.0f, 5L); + y1Hist.addBucket(3.0f, 10.0f, 0L); + y1Hist.write(kryo, output); + output.close(); + + SimpleHistogram y1HistVerify = new SimpleHistogram(); + y1HistVerify.fromHistogram(outBuffer.toByteArray()); + + Input input = new Input(new ByteArrayInputStream(y1HistVerify.histogram())); + + int bucketCount = input.readShort(); + assertEquals(bucketCount, 3); + + Float bucketLB = input.readFloat(); + Float bucketUB = input.readFloat(); + long bucketVal = input.readLong(true); + assertEquals(bucketLB, 1.0f, 0.001); + assertEquals(bucketVal, 5); + } + + @Test + public void testIncompletByteArray() { + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.close(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + boolean exceptionCaught = false; + try { + y1Hist.fromHistogram(outBuffer.toByteArray()); + } + catch(Exception e) { + exceptionCaught = true; + } + + assertFalse(exceptionCaught); + } + + @Test + public void testInvalidHistogramLength() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(1); + output.writeInt(-1); + output.close(); + Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); + Integer metricType = input.readInt(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + boolean exceptionCaught = false; + try { + y1Hist.read(kryo, input); + } + catch(Exception e) { + exceptionCaught = true; + } + + assertFalse(exceptionCaught); + } + + @Test + public void testSinglePercentile() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(3); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + double perc50 = y1Hist.percentile(50.0f); + + assertEquals(perc50, 8.0f, 0.0001); + assertEquals(y1Hist.percentile(1000.0f), -1.0f, 0.0001); + } + + @Test + public void testPercentileList() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + ArrayList percs = new ArrayList(); + percs.add(50.0); + percs.add(99.0); + ArrayList percValues = (ArrayList) y1Hist.percentiles(percs); + double perc50 = percValues.get(0); + double perc99 = percValues.get(1); + + assertEquals(perc50, 8.0, 0.001); + assertEquals(perc99, 15.0, 0.001); + } + + @Test + public void testSingleHistogramMerge() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + + SimpleHistogram y1Hist1 = new SimpleHistogram(); + y1Hist1.fromHistogram(outBuffer.toByteArray()); + y1Hist1.setUnderflow(2L); + + y1Hist.aggregate(y1Hist1, HistogramAggregation.SUM); + assertEquals(y1Hist.getBucketCount(1.0f, 6.0f), Long.valueOf(10L)); + assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(20L)); + assertEquals(y1Hist.getBucketCount(10.0f, 20.0f), Long.valueOf(2L)); + assertEquals(y1Hist.getOverflow(), Long.valueOf(10L)); + assertEquals(y1Hist.getUnderflow(), Long.valueOf(2L)); + } + + @Test + public void testMultipleHistogramMerge() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + + ArrayList histos = new ArrayList(); + SimpleHistogram y1Hist1 = new SimpleHistogram(); + y1Hist1.fromHistogram(outBuffer.toByteArray()); + histos.add(y1Hist1); + SimpleHistogram y1Hist2 = new SimpleHistogram(); + y1Hist2.fromHistogram(outBuffer.toByteArray()); + histos.add(y1Hist2); + + y1Hist.aggregate(histos, HistogramAggregation.SUM); + assertEquals(y1Hist.getBucketCount(1.0f, 6.0f), Long.valueOf(15L)); + assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(30L)); + assertEquals(y1Hist.getBucketCount(10.0f, 20.0f), Long.valueOf(3L)); + assertEquals(y1Hist.getOverflow(), Long.valueOf(15L)); + } + + @Test + public void testArbitraryBuckets() { + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); + + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + + assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(10L)); + assertEquals(y1Hist.getBucketCount(5.0f, 6.0f), Long.valueOf(0L)); + assertEquals(y1Hist.getBucketCount(5.0f, 12.0f), Long.valueOf(0L)); + assertEquals(y1Hist.getBucketCount(1.0f, 10.0f), Long.valueOf(0L)); + } + + @Test + public void testAddBuckets() { + SimpleHistogram y1Hist = new SimpleHistogram(); + + y1Hist.addBucket(5.0f, 7.0f, 3L); + assertEquals(y1Hist.getBucketCount(5.0f, 7.0f), Long.valueOf(3L));; + + y1Hist.addBucket(5.0f, 7.0f, 5L); + assertEquals(y1Hist.getBucketCount(5.0f, 7.0f), Long.valueOf(5L)); + } + + @Test + public void testNullBucketVal() { + SimpleHistogram y1Hist = new SimpleHistogram(); + + y1Hist.addBucket(5.0f, 7.0f, null); + assertEquals(y1Hist.getBucketCount(5.0f, 7.0f), Long.valueOf(0L)); + + y1Hist.addBucket(5.0f, 7.0f, 5L); + } + + @Test + public void testJsonSerialization() { + SimpleHistogram y1Hist = new SimpleHistogram(); + + y1Hist.addBucket(5.0f, 7.0f, 3L); + y1Hist.addBucket(7.0f, 10.0f, 5L); + y1Hist.setOverflow(1L); + + String jsonOut = new String(); + ObjectMapper mapper = new ObjectMapper(); + try { + StringWriter output = new StringWriter(); + mapper.writeValue(output, y1Hist); + jsonOut = output.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + System.out.println(jsonOut); + assertTrue("{\"buckets\":{\"5.0-7.0\":3,\"7.0-10.0\":5},\"underflow\":0,\"overflow\":1}".equals(jsonOut)); + + SimpleHistogram y1Hist1 = new SimpleHistogram(); + + y1Hist1.addBucket(Float.NEGATIVE_INFINITY, 5.0f, 3L); + y1Hist1.addBucket(7.0f, 10.0f, 5L); + y1Hist1.addBucket(10.0f, null, 1L); + try { + StringWriter output = new StringWriter(); + mapper.writeValue(output, y1Hist1); + jsonOut = output.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + System.out.println(jsonOut); + assertTrue("{\"buckets\":{\"-Infinity-5.0\":3,\"7.0-10.0\":5},\"underflow\":0,\"overflow\":0}".equals(jsonOut)); + } + + @Test + public void testImmutableGetHistogram() { + SimpleHistogram y1Hist = new SimpleHistogram(); + + y1Hist.addBucket(5.0f, 7.0f, 3L); + y1Hist.addBucket(7.0f, 10.0f, 5L); + y1Hist.addBucket(10.0f, null, 1L); + + Map histMap = y1Hist.getHistogram(); + boolean histModBlocked = false; + try { + histMap.put(new HistogramBucket(BucketType.REGULAR, 1.0f, 5.0f), 3L); + } catch (UnsupportedOperationException e) { + histModBlocked = true; + } + + assertTrue(histModBlocked); + } + + @Test + public void testMissingBucketsHistogramAggregation() { + SimpleHistogram hist1 = new SimpleHistogram(); + hist1.addBucket(5.0f, 7.0f, 3L); + hist1.addBucket(7.0f, 10.0f, 5L); + hist1.addBucket(15.0f, 20.0f, 2L); + + SimpleHistogram hist2 = new SimpleHistogram(); + hist2.addBucket(5.0f, 7.0f, 3L); + hist2.addBucket(7.0f, 10.0f, 5L); + hist2.addBucket(10.0f, 15.0f, 2L); + hist2.addBucket(15.0f, 20.0f, 1L); + + hist1.aggregate(hist2, HistogramAggregation.SUM); + assertEquals(hist1.getBucketCount(10.0f, 15.0f).longValue(), 2L); + } + + @Test + public void testInit() { + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); + System.out.println("No. of buckets: " + dynaHist.length); + for (int i = 0; i < dynaHist.length; i++) { + System.out.print(dynaHist[i] + ", "); + } + System.out.println(); + assertEquals(33, dynaHist.length); + } + + @Test + public void testWholeRangeFocus() { + double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 2000.0f, 100.0f, 2000.0f, 0.05f); + System.out.println("No. of buckets: " + dynaHist.length); + for (int i = 0; i < dynaHist.length; i++) { + System.out.print(dynaHist[i] + ", "); + } + System.out.println(); + assertEquals(31, dynaHist.length); + } + + @Test + public void testStartEqualtoFocusStart() { + double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); + System.out.println("No. of buckets: " + dynaHist.length); + for (int i = 0; i < dynaHist.length; i++) { + System.out.print(dynaHist[i] + ", "); + } + System.out.println(); + assertEquals(32, dynaHist.length); + } + + @Test + public void testEndEqualtoFocusEnd() { + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 2000.0f, 100.0f, 2000.0f, 0.05f); + System.out.println("No. of buckets: " + dynaHist.length); + for (int i = 0; i < dynaHist.length; i++) { + System.out.print(dynaHist[i] + ", "); + } + + assertEquals(32, dynaHist.length); + } + + @Test (expected = RuntimeException.class) + public void testErrorStartGreaterThanEnd() { + double[] dynaHist = SimpleHistogram.initializeHistogram(10000.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); + } + + @Test (expected = RuntimeException.class) + public void testErrorFocusStartGreaterThanFocusEnd() { + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 3000.0f, 2000.0f, 0.05f); + } + + @Test (expected = RuntimeException.class) + public void testErrorRateLessThanZero() { + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 100.0f, 2000.0f, -0.05f); + } + + @Test (expected = RuntimeException.class) + public void testFocusEndGreaterThanEnd() { + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 1000.0f, 100.0f, 2000.0f, 0.05f); + } + + @Test (expected = RuntimeException.class) + public void testFocusStartLessThanStart() { + double[] dynaHist = SimpleHistogram.initializeHistogram(200.0f, 100.0f, 1500.0f, 2000.0f, 0.05f); + } + + @Test (expected = RuntimeException.class) + public void testExcessiveBuckets() { + double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 6000.0f, 100.0f, 6000.0f, 0.01f); + } +} \ No newline at end of file From 8d2fcca6c3475671827e5a6e008a8e2bc615b1fa Mon Sep 17 00:00:00 2001 From: HiramJ Date: Mon, 29 May 2017 07:14:41 -0700 Subject: [PATCH 636/826] Add the Simple histogram adapter and decoder along with unit tests for the salt scanner. Also set the SimpleHistogramDecoder as the default. Signed-off-by: Chris Larsen --- src/core/SimpleHistogramDataPointAdapter.java | 121 +++++++ src/core/SimpleHistogramDecoder.java | 48 +++ src/utils/Config.java | 1 + test/core/TestSaltScannerHistogram.java | 326 ++++++++++++++++++ 4 files changed, 496 insertions(+) create mode 100644 src/core/SimpleHistogramDataPointAdapter.java create mode 100644 src/core/SimpleHistogramDecoder.java create mode 100644 test/core/TestSaltScannerHistogram.java diff --git a/src/core/SimpleHistogramDataPointAdapter.java b/src/core/SimpleHistogramDataPointAdapter.java new file mode 100644 index 0000000000..22074edfb5 --- /dev/null +++ b/src/core/SimpleHistogramDataPointAdapter.java @@ -0,0 +1,121 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * An adapter of TSDB's {@code HistogramDataPoint} interface with Yamas's + * {@code Histogram} interface + */ +public class SimpleHistogramDataPointAdapter implements HistogramDataPoint { + + private final Histogram histogram; + + private final long timestamp; + + public SimpleHistogramDataPointAdapter(final Histogram histogram, final long timestamp) { + this.histogram = histogram; + this.timestamp = timestamp; + } + + protected SimpleHistogramDataPointAdapter(final SimpleHistogramDataPointAdapter rhs) { + this.histogram = rhs.histogram.clone();; + this.timestamp = rhs.timestamp; + } + + protected SimpleHistogramDataPointAdapter(final SimpleHistogramDataPointAdapter rhs, final long timestamp) { + this.histogram = rhs.histogram.clone();; + this.timestamp = timestamp; + } + + @Override + public long timestamp() { + return timestamp; + } + + @Override + public byte[] getRawData() { + return histogram.histogram(); + } + + @Override + public void resetFromRawData(final byte[] raw_data) { + histogram.fromHistogram(raw_data); + } + + @Override + public double percentile(final double p) { + return histogram.percentile(p); + } + + @Override + public List percentile(final List p) { + return histogram.percentiles(p); + } + + @Override + public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + if (!(histo instanceof SimpleHistogramDataPointAdapter)) { + throw new IllegalArgumentException("The object must be an instance of the " + "YamasHistogramDataPointAdapter"); + } + histogram.aggregate(((SimpleHistogramDataPointAdapter) histo).histogram, mapAggregation(func)); + } + + @Override + public HistogramDataPoint clone() { + return new SimpleHistogramDataPointAdapter(this); + } + + @Override + public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { + return new SimpleHistogramDataPointAdapter(this, timestamp); + } + + private HistogramAggregation mapAggregation(HistogramAggregation func) { + switch (func) { + case SUM: + return HistogramAggregation.SUM; + } + + throw new UnsupportedOperationException("Failed to map the aggregator."); + } + + @Override + public Map getHistogramBucketsIfHas() { + if (histogram instanceof SimpleHistogram) { + SimpleHistogram yms1_histogram = (SimpleHistogram) (histogram); + Map buckets = yms1_histogram.getHistogram(); + if (null != buckets) { + Map result = new TreeMap(); + for (Map.Entry entry : buckets.entrySet()) { + HistogramBucket bucket = new HistogramBucket(HistogramBucket.BucketType.REGULAR, + entry.getKey().getLowerBound(), entry.getKey().getUpperBound()); + result.put(bucket, entry.getValue()); + } + + HistogramBucket underflow_bucket = new HistogramBucket(HistogramBucket.BucketType.UNDERFLOW, 0.0f, 0.0f); + result.put(underflow_bucket, yms1_histogram.getUnderflow()); + + HistogramBucket overflow_bucket = new HistogramBucket(HistogramBucket.BucketType.OVERFLOW, 0.0f, 0.0f); + result.put(overflow_bucket, yms1_histogram.getOverflow()); + return result; + } + } else { + throw new UnsupportedOperationException("The founding histogram object is not one of class Yamas1Histogram"); + } + return null; + } +} diff --git a/src/core/SimpleHistogramDecoder.java b/src/core/SimpleHistogramDecoder.java new file mode 100644 index 0000000000..c67d0eec0e --- /dev/null +++ b/src/core/SimpleHistogramDecoder.java @@ -0,0 +1,48 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2011-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.Arrays; + +/** + *

    + * Histogram decoder for Yamas style histograms. + *

    + *

    + * It currently checks the first byte of the encoded data and decide the type of + * the histogram. Then it uses that histogram types builtin factory method to + * create the instance. + *

    + *

    + * This class is thread safe as it has no state. + *

    + */ +public class SimpleHistogramDecoder implements HistogramDataPointDecoder { + @Override + public HistogramDataPoint decode(final byte[] raw_data, final long timestamp) { + final Histogram histogram; + switch (raw_data[0]) { + case 0x0: // should be 0, refer to core-library/src/test/java/com/yahoo/yamas/metrics/Yamas1HistogramTest.java + { + histogram = new SimpleHistogram(); + byte[] hist_raw_data = Arrays.copyOfRange(raw_data, 1, raw_data.length); + histogram.fromHistogram(hist_raw_data); + } + break; + default: + throw new IllegalDataException("Unknown header of histogram data, the header is: " + raw_data[0]); + } + + return new SimpleHistogramDataPointAdapter(histogram, timestamp); + } +} diff --git a/src/utils/Config.java b/src/utils/Config.java index 66e4c6cece..1ff7cc0a95 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -535,6 +535,7 @@ protected void setDefaults() { default_map.put("tsd.core.connections.limit", "0"); default_map.put("tsd.core.enable_api", "true"); default_map.put("tsd.core.enable_ui", "true"); + default_map.put("tsd.core.hist_decoder", "net.opentsdb.core.SimpleHistogramDecoder"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java new file mode 100644 index 0000000000..fc3e40b341 --- /dev/null +++ b/test/core/TestSaltScannerHistogram.java @@ -0,0 +1,326 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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.esotericsoftware.kryo.io.Output; +import com.stumbleupon.async.Deferred; +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVRegexFilter; +import net.opentsdb.query.filter.TagVWildcardFilter; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.DateTime; +import org.hbase.async.KeyValue; +import org.hbase.async.Scanner; +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 java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, Scanner.class, SaltScanner.class, Span.class, + Const.class, UniqueId.class, Tags.class, QueryStats.class, DateTime.class, + HistogramDataPointDecoderManager.class, + SimpleHistogram.class, SimpleHistogramDecoder.class}) +public class TestSaltScannerHistogram extends BaseTsdbTest { + private final static byte[] FAMILY = "t".getBytes(); + private final static byte[] QUALIFIER_A = { 0x06, 0x00, 0x00}; + private final static byte[] QUALIFIER_B = { 0x06, 0x10, 0x00 }; + + private byte[] VALUE; + + private final static int NUM_BUCKETS = 2; + private List scanners; + private TreeMap spans; + + private List>> kvs_a; + private List>> kvs_b; + + private Scanner scanner_a; + private Scanner scanner_b; + private QueryStats query_stats; + + private byte[] key_a; + //different tagv + private byte[] key_b; + //same as A bug different time + private byte[] key_c; + + @Before + public void beforeLocal() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); + + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeByte(0 /*HistoType.SimpleHistogramType.ordinal()*/); + output.writeShort(8); + output.writeFloat(1.0f); + output.writeFloat(2.0f); + output.writeLong(5, true); + output.writeFloat(2.0f); + output.writeFloat(3.0f); + output.writeLong(5, true); + output.writeFloat(3.0f); + output.writeFloat(4.0f); + output.writeLong(5, true); + output.writeFloat(4.0f); + output.writeFloat(5.0f); + output.writeLong(0, true); + output.writeFloat(5.0f); + output.writeFloat(6.0f); + output.writeLong(0, true); + output.writeFloat(6.0f); + output.writeFloat(7.0f); + output.writeLong(0, true); + output.writeFloat(7.0f); + output.writeFloat(8.0f); + output.writeLong(0, true); + output.writeFloat(8.0f); + output.writeFloat(9.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(2, true); + output.close(); + VALUE = outBuffer.toByteArray(); + + query_stats = mock(QueryStats.class); + spans = new TreeMap(new RowKey.SaltCmp()); + setupMockScanners(true); + + key_a = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + TAGK_B_STRING, TAGV_STRING); + key_b = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_B_STRING, + TAGK_B_STRING, TAGV_STRING); + key_c = getRowKey(METRIC_STRING, 1359680400, TAGK_STRING, TAGV_STRING, + TAGK_B_STRING, TAGV_STRING); + } + + @Test + public void scan() throws Exception { + setupMockScanners(false); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, null, false, null, query_stats, 0, spans); + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(3, spans.size()); + + HistogramSpan span = spans.get(key_a); + assertEquals(2, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1357002496000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(key_b); + assertEquals(1, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(key_c); + assertEquals(2, span.size()); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(1359684496000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanWithFilter() throws Exception { + setupMockScanners(false); + List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter(TAGK_STRING, "web*")); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, null, false, null, query_stats, 0, spans); + + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(3, spans.size()); + + HistogramSpan span = spans.get(key_a); + assertEquals(2, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1357002496000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(key_b); + assertEquals(1, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(key_c); + assertEquals(2, span.size()); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(1359684496000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanWithFiltersOnSameTag() throws Exception { + setupMockScanners(false); + List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "web*")); + filters.add(new TagVWildcardFilter("host", "w*b*")); + filters.add(new TagVRegexFilter("host", "w.*")); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, null, false, null, query_stats, 0, spans); + + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(3, spans.size()); + + HistogramSpan span = spans.get(key_a); + assertEquals(2, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1357002496000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(key_b); + assertEquals(1, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(key_c); + assertEquals(2, span.size()); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(1359684496000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanWithFiltersOnSameTagOneFail() throws Exception { + setupMockScanners(false); + List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "web*")); + filters.add(new TagVWildcardFilter("host", "drood*")); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, filters, false, null, query_stats, 0, spans); + + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(0, spans.size()); + } + + /** + * Sets up a pair of scanners with either a list of values or no data + * @param no_data Whether or not to return 0 data. + */ + private void setupMockScanners(final boolean no_data) { + scanners = new ArrayList(NUM_BUCKETS); + scanner_a = mock(Scanner.class); + scanner_b = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + when(scanner_b.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); + } + scanners.add(scanner_a); + scanners.add(scanner_b); + } + + /** + * This method sets up some row keys and values to pass to the scanners. + * The values aren't exactly what would normally be passed to a salt scanner + * in that we have the same series salted across separate buckets. That would + * only happen if you add the timestamp to the salt calculation, which we + * may do in the future. We're testing now for future proofing. + */ + private void setupValues() { + kvs_a = new ArrayList>>(3); + kvs_b = new ArrayList>>(2); + + final String note = "{\"tsuid\":\"000000010000000100000001\"," + + "\"startTime\":1356998490,\"endTime\":0,\"description\":" + + "\"The Great A'Tuin!\",\"notes\":\"Millenium hand and shrimp\"," + + "\"custom\":null}"; + + for (int i = 0; i < 5; i++) { + final ArrayList> rows = + new ArrayList>(1); + final ArrayList row = new ArrayList(2); + rows.add(row); + byte[] key = null; + + switch (i) { + case 0: + row.add(new KeyValue(key_a, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 1: + row.add(new KeyValue(key_b, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 2: + row.add(new KeyValue(key_c, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 3: + key = Arrays.copyOf(key_a, key_a.length); + key[0] = 1; + row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); + row.add(new KeyValue(key, FAMILY, new byte[] { 1, 0, 0 }, 0, + note.getBytes(Charset.forName("UTF8")))); + kvs_b.add(rows); + break; + case 4: + key = Arrays.copyOf(key_c, key_c.length); + key[0] = 1; + row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); + kvs_b.add(rows); + break; + } + } + + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.>>fromResult(null)); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); + } +} From 213d82a0327c5277ea4a8278f717a4e2e175d701 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 29 May 2017 07:45:33 -0700 Subject: [PATCH 637/826] Header and code cleanup. Signed-off-by: Chris Larsen --- src/core/SimpleHistogram.java | 270 ++++---- src/core/SimpleHistogramDataPointAdapter.java | 45 +- src/core/SimpleHistogramDecoder.java | 10 +- test/core/TestSaltScannerHistogram.java | 546 ++++++++-------- test/core/TestSimpleHistogram.java | 581 +++++++++--------- 5 files changed, 767 insertions(+), 685 deletions(-) diff --git a/src/core/SimpleHistogram.java b/src/core/SimpleHistogram.java index ec04871f2a..f20a4b9b35 100644 --- a/src/core/SimpleHistogram.java +++ b/src/core/SimpleHistogram.java @@ -21,8 +21,7 @@ import java.util.Set; import java.util.TreeMap; -import javax.annotation.Generated; - +import org.hbase.async.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,13 +30,16 @@ import com.esotericsoftware.kryo.io.Output; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import net.opentsdb.core.HistogramDataPoint.HistogramBucket; import net.opentsdb.core.HistogramDataPoint.HistogramBucket.BucketType; -@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) -@Generated("org.jsonschema2pojo") +/** + * A simple bucketed histogram with a fixed number of buckets, an underflow + * counter and an overflow counter. + * + * @since 3.0 + */ public class SimpleHistogram implements Histogram { private static Logger LOG = LoggerFactory.getLogger(SimpleHistogram.class); @@ -85,18 +87,26 @@ public byte[] histogram() { public void fromHistogram(byte[] raw) { if (raw.length < 6) { - LOG.debug("Byte array shorter than 6 bytes detected"); + LOG.warn("Byte array shorter than 6 bytes detected: " + Bytes.pretty(raw)); return; } - Input input = new Input(new ByteArrayInputStream(raw)); - int bucketCount = input.readShort(); + Input input = null; + try { + input = new Input(new ByteArrayInputStream(raw)); + int bucketCount = input.readShort(); - for (int i = 0; i < bucketCount; i++) { - buckets.put(new HistogramBucket(BucketType.REGULAR, input.readFloat(), input.readFloat()), input.readLong(true)); - } + for (int i = 0; i < bucketCount; i++) { + buckets.put(new HistogramBucket(BucketType.REGULAR, input.readFloat(), + input.readFloat()), input.readLong(true)); + } - this.setUnderflow(input.readLong(true)); - this.setOverflow(input.readLong(true)); + this.setUnderflow(input.readLong(true)); + this.setOverflow(input.readLong(true)); + } finally { + if (input != null) { + input.close(); + } + } } private int calcBucketSum () { @@ -109,193 +119,231 @@ private int calcBucketSum () { } public double percentile(double perc) { - if (perc < 1.0 || perc > 100.0) { - return -1.0; - } - - int bucketSum = calcBucketSum(); - - long runningCount = 0; - double prevBucketArea = 0.0; - double percValue = 0.0; + if (perc < 1.0 || perc > 100.0) { + return -1.0; + } - for (Map.Entry entry : buckets.entrySet()) { - runningCount += entry.getValue().intValue(); - Double currBucketArea = runningCount * 100.0 / bucketSum; + int bucketSum = calcBucketSum(); - if (currBucketArea >= perc) { - //Find closest ranks - //Float currBucketStart = entry.getKey().getLowerBound(); - //Float nextBucketStart = entry.getKey().getUpperBound(); + long runningCount = 0; + double prevBucketArea = 0.0; + double percValue = 0.0; - //percValue = currBucketStart + ((nextBucketStart - currBucketStart) * (perc - prevBucketArea) / (currBucketArea - prevBucketArea)); - percValue = (entry.getKey().getLowerBound() + entry.getKey().getUpperBound())/2; - break; - } else { - prevBucketArea = runningCount * 100.0 / bucketSum; - } + for (Map.Entry entry : buckets.entrySet()) { + runningCount += entry.getValue().intValue(); + Double currBucketArea = runningCount * 100.0 / bucketSum; + + if (currBucketArea >= perc) { + //Find closest ranks + //Float currBucketStart = entry.getKey().getLowerBound(); + //Float nextBucketStart = entry.getKey().getUpperBound(); + + //percValue = currBucketStart + ((nextBucketStart - currBucketStart) * + // (perc - prevBucketArea) / (currBucketArea - prevBucketArea)); + percValue = (entry.getKey().getLowerBound() + + entry.getKey().getUpperBound()) / 2; + break; + } else { + prevBucketArea = runningCount * 100.0 / bucketSum; } + } - return percValue; + return percValue; } @Override public List percentiles(List percs) { - List percValues = new ArrayList(); + List percValues = new ArrayList(); - for (Double perc : percs) { - percValues.add(percentile(perc)); - } + for (Double perc : percs) { + percValues.add(percentile(perc)); + } - return percValues; + return percValues; } @JsonIgnore @Override public Map getHistogram() { - return Collections.unmodifiableMap(buckets); + return Collections.unmodifiableMap(buckets); } @Override public SimpleHistogram clone() { SimpleHistogram cloneObj = new SimpleHistogram(); - for (Map.Entry bucket : buckets.entrySet()) { - cloneObj.addBucket(bucket.getKey().getLowerBound(), bucket.getKey().getUpperBound(), bucket.getValue()); - } - cloneObj.setUnderflow(underflow); - cloneObj.setOverflow(overflow); + for (Map.Entry bucket : buckets.entrySet()) { + cloneObj.addBucket(bucket.getKey().getLowerBound(), + bucket.getKey().getUpperBound(), bucket.getValue()); + } + cloneObj.setUnderflow(underflow); + cloneObj.setOverflow(overflow); - return cloneObj; + return cloneObj; } public Long getBucketCount(Float min, Float max) { HistogramBucket qryBucket = new HistogramBucket(BucketType.REGULAR, min, max); if (buckets.containsKey(qryBucket)) { - Long bucketCount = buckets.get(qryBucket); - if (bucketCount == null) { - return 0L; - } else { - return bucketCount; - } + Long bucketCount = buckets.get(qryBucket); + if (bucketCount == null) { + return 0L; + } else { + return bucketCount; + } } else { - return 0L; + return 0L; } } public void write(Kryo kryo, Output output) { - int bucketCount = buckets.size(); - output.writeShort(bucketCount); + int bucketCount = buckets.size(); + output.writeShort(bucketCount); - for (Map.Entry bucket : buckets.entrySet()) { - bucket.getKey().write(kryo, output); - output.writeLong(bucket.getValue(), true); - } + for (Map.Entry bucket : buckets.entrySet()) { + bucket.getKey().write(kryo, output); + output.writeLong(bucket.getValue(), true); + } - output.writeLong(this.getUnderflow(), true); - output.writeLong(this.getOverflow(), true); + output.writeLong(this.getUnderflow(), true); + output.writeLong(this.getOverflow(), true); } public void read(Kryo kryo, Input input) { - int bucketCount = input.readShort(); - if (bucketCount < 1) { - LOG.debug("Byte array passed has less than 1 histogram entry"); - return; - } + int bucketCount = input.readShort(); + if (bucketCount < 1) { + LOG.debug("Byte array passed has less than 1 histogram entry"); + return; + } - for (int i = 0; i < bucketCount; i++) { - HistogramBucket bucket = new HistogramBucket(); - bucket.read(kryo, input); - buckets.put(bucket, input.readLong(true)); - } + for (int i = 0; i < bucketCount; i++) { + HistogramBucket bucket = new HistogramBucket(); + bucket.read(kryo, input); + buckets.put(bucket, input.readLong(true)); + } - this.setUnderflow(input.readLong(true)); - this.setOverflow(input.readLong(true)); + this.setUnderflow(input.readLong(true)); + this.setOverflow(input.readLong(true)); } - public void aggregate(Histogram histo, HistogramAggregation func) { - if (func == HistogramAggregation.SUM) { - SimpleHistogram y1Histo = (SimpleHistogram) histo; - for (Map.Entry bucket : (Set>) histo.getHistogram().entrySet()) { - Long newCount = this.getBucketCount(bucket.getKey().getLowerBound(), bucket.getKey().getUpperBound()) + bucket.getValue(); - this.addBucket(bucket.getKey().getLowerBound(), bucket.getKey().getUpperBound(), newCount); - } - this.setOverflow(y1Histo.getOverflow() + this.getOverflow()); - this.setUnderflow(y1Histo.getUnderflow() + this.getUnderflow()); - } else { - LOG.debug("Unsupported histogram aggregation used"); + if (func == HistogramAggregation.SUM) { + SimpleHistogram y1Histo = (SimpleHistogram) histo; + for (Map.Entry bucket : + (Set>) histo.getHistogram().entrySet()) { + Long newCount = this.getBucketCount(bucket.getKey().getLowerBound(), + bucket.getKey().getUpperBound()) + bucket.getValue(); + this.addBucket(bucket.getKey().getLowerBound(), + bucket.getKey().getUpperBound(), newCount); } + this.setOverflow(y1Histo.getOverflow() + this.getOverflow()); + this.setUnderflow(y1Histo.getUnderflow() + this.getUnderflow()); + } else { + LOG.debug("Unsupported histogram aggregation used"); + } } public void aggregate(List histos, HistogramAggregation func) { - if (func == HistogramAggregation.SUM) { - for (Histogram histo : histos) { - this.aggregate(histo, HistogramAggregation.SUM); - } - } else { - LOG.debug("Unsupported histogram aggregation used"); + if (func == HistogramAggregation.SUM) { + for (Histogram histo : histos) { + this.aggregate(histo, HistogramAggregation.SUM); } + } else { + LOG.debug("Unsupported histogram aggregation used"); + } } public Long getUnderflow() { - return underflow; + return underflow; } public void setUnderflow(Long underflow) { - this.underflow = underflow; + this.underflow = underflow; } public Long getOverflow() { - return overflow; + return overflow; } public void setOverflow(Long overflow) { - this.overflow = overflow; + this.overflow = overflow; } - public static double[] initializeHistogram (float start, float end, float focusRangeStart, float focusRangeEnd, float errorPct) { + /** + * Generates an array of bucket lower bounds from {@code start} to {@code end} + * with a fixed error interval (i.e. the span of the buckets). Up to 100 + * buckets can be created using this method. Additionally, a range of + * measurements can be provided to "focus" on with a smaller bucket span while + * the remaining buckets have a wider span. + * @param start The starting bucket measurement (lower bound). + * @param end The ending bucket measurement (upper bound). + * @param focusRangeStart The focus range start bound. Can be the same as + * {@code start}. + * @param focusRangeEnd The focus rang end bound. Can be the same as + * {@code end}. + * @param errorPct The acceptable error with respect to bucket width. E.g. + * 0.05 for 5%. + * @return An array of bucket lower bounds. + */ + public static double[] initializeHistogram (final float start, + final float end, + final float focusRangeStart, + final float focusRangeEnd, + final float errorPct) { if (Float.compare(start, end) >= 0) { - throw new RuntimeException("Histogram start (" + start + ") must be less than Histogram end (" + end +")"); + throw new IllegalArgumentException("Histogram start (" + start + ") must be " + + "less than Histogram end (" + end +")"); } else if (Float.compare(focusRangeStart, focusRangeEnd) >= 0) { - throw new RuntimeException("Histogram focus range start (" + focusRangeStart + ") must be less than Histogram focus end (" + focusRangeEnd + ")"); - } else if (Float.compare(start, focusRangeStart) > 0 || Float.compare(focusRangeStart, end) >= 0 || Float.compare(start, focusRangeEnd) >= 0 || Float.compare(focusRangeEnd, end) > 0) { - throw new RuntimeException("Focus range start (" + focusRangeStart + ") and Focus range end (" + focusRangeEnd + ") must be greater than Histogram start (" + start + ") and less than Histogram end (" + end + ")"); + throw new IllegalArgumentException("Histogram focus range start (" + + focusRangeStart + ") must be less than Histogram focus end (" + + focusRangeEnd + ")"); + } else if (Float.compare(start, focusRangeStart) > 0 || + Float.compare(focusRangeStart, end) >= 0 || + Float.compare(start, focusRangeEnd) >= 0 || + Float.compare(focusRangeEnd, end) > 0) { + throw new IllegalArgumentException("Focus range start (" + focusRangeStart + + ") and Focus range end (" + focusRangeEnd + ") must be greater " + + "than Histogram start (" + start + ") and less than " + + "Histogram end (" + end + ")"); } else if (Float.compare(errorPct, 0.0f) <= 0) { - throw new RuntimeException("Error rate (" + errorPct + ") must be greater than zero"); + throw new IllegalArgumentException("Error rate (" + errorPct + ") must be " + + "greater than zero"); } int MAX_BUCKETS = 100; float stepSize = (1 + errorPct)/(1 - errorPct); - int bucketcount = Double.valueOf(Math.ceil(Math.log(focusRangeEnd/focusRangeStart)/Math.log(stepSize))).intValue() + 1; + int bucketcount = Double.valueOf(Math.ceil( + Math.log(focusRangeEnd/focusRangeStart) / Math.log(stepSize))) + .intValue() + 1; if (Float.compare(start, focusRangeStart) < 0) { - bucketcount++; + bucketcount++; } if (Float.compare(focusRangeEnd, end) < 0) { - bucketcount++; + bucketcount++; } if (bucketcount > MAX_BUCKETS) { - throw new RuntimeException("A max of " + MAX_BUCKETS +" buckets are supported. " + bucketcount + " were requested"); + throw new IllegalArgumentException("A max of " + MAX_BUCKETS + + " buckets are supported. " + bucketcount + " were requested"); } double[] retval = new double[bucketcount]; int j = 0; if (Float.compare(start, focusRangeStart) < 0) { - retval[j] = start; - j++; + retval[j] = start; + j++; } for (float i = focusRangeStart; i < focusRangeEnd; i*=stepSize, j++) { - retval[j] = i; + retval[j] = i; } if (Float.compare(focusRangeEnd, end) < 0) { - retval[j++] = focusRangeEnd; + retval[j++] = focusRangeEnd; } retval[j] = end; return retval; -} + } } diff --git a/src/core/SimpleHistogramDataPointAdapter.java b/src/core/SimpleHistogramDataPointAdapter.java index 22074edfb5..e1798369be 100644 --- a/src/core/SimpleHistogramDataPointAdapter.java +++ b/src/core/SimpleHistogramDataPointAdapter.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2011-2012 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -18,7 +18,9 @@ /** * An adapter of TSDB's {@code HistogramDataPoint} interface with Yamas's - * {@code Histogram} interface + * {@code Histogram} interface. + * + * @since 2.4 */ public class SimpleHistogramDataPointAdapter implements HistogramDataPoint { @@ -26,18 +28,22 @@ public class SimpleHistogramDataPointAdapter implements HistogramDataPoint { private final long timestamp; - public SimpleHistogramDataPointAdapter(final Histogram histogram, final long timestamp) { + public SimpleHistogramDataPointAdapter(final Histogram histogram, + final long timestamp) { this.histogram = histogram; this.timestamp = timestamp; } - protected SimpleHistogramDataPointAdapter(final SimpleHistogramDataPointAdapter rhs) { - this.histogram = rhs.histogram.clone();; - this.timestamp = rhs.timestamp; + protected SimpleHistogramDataPointAdapter( + final SimpleHistogramDataPointAdapter rhs) { + histogram = rhs.histogram.clone(); + timestamp = rhs.timestamp; } - protected SimpleHistogramDataPointAdapter(final SimpleHistogramDataPointAdapter rhs, final long timestamp) { - this.histogram = rhs.histogram.clone();; + protected SimpleHistogramDataPointAdapter( + final SimpleHistogramDataPointAdapter rhs, + final long timestamp) { + histogram = rhs.histogram.clone(); this.timestamp = timestamp; } @@ -67,11 +73,14 @@ public List percentile(final List p) { } @Override - public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + public void aggregate(final HistogramDataPoint histo, + final HistogramAggregation func) { if (!(histo instanceof SimpleHistogramDataPointAdapter)) { - throw new IllegalArgumentException("The object must be an instance of the " + "YamasHistogramDataPointAdapter"); + throw new IllegalArgumentException("The object must be an instance of the " + + "YamasHistogramDataPointAdapter"); } - histogram.aggregate(((SimpleHistogramDataPointAdapter) histo).histogram, mapAggregation(func)); + histogram.aggregate(((SimpleHistogramDataPointAdapter) histo).histogram, + mapAggregation(func)); } @Override @@ -84,7 +93,7 @@ public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { return new SimpleHistogramDataPointAdapter(this, timestamp); } - private HistogramAggregation mapAggregation(HistogramAggregation func) { + private HistogramAggregation mapAggregation(final HistogramAggregation func) { switch (func) { case SUM: return HistogramAggregation.SUM; @@ -101,20 +110,24 @@ public Map getHistogramBucketsIfHas() { if (null != buckets) { Map result = new TreeMap(); for (Map.Entry entry : buckets.entrySet()) { - HistogramBucket bucket = new HistogramBucket(HistogramBucket.BucketType.REGULAR, + HistogramBucket bucket = new HistogramBucket( + HistogramBucket.BucketType.REGULAR, entry.getKey().getLowerBound(), entry.getKey().getUpperBound()); result.put(bucket, entry.getValue()); } - HistogramBucket underflow_bucket = new HistogramBucket(HistogramBucket.BucketType.UNDERFLOW, 0.0f, 0.0f); + HistogramBucket underflow_bucket = new HistogramBucket( + HistogramBucket.BucketType.UNDERFLOW, 0.0f, 0.0f); result.put(underflow_bucket, yms1_histogram.getUnderflow()); - HistogramBucket overflow_bucket = new HistogramBucket(HistogramBucket.BucketType.OVERFLOW, 0.0f, 0.0f); + HistogramBucket overflow_bucket = new HistogramBucket( + HistogramBucket.BucketType.OVERFLOW, 0.0f, 0.0f); result.put(overflow_bucket, yms1_histogram.getOverflow()); return result; } } else { - throw new UnsupportedOperationException("The founding histogram object is not one of class Yamas1Histogram"); + throw new UnsupportedOperationException("The founding histogram object " + + "is not one of class Yamas1Histogram"); } return null; } diff --git a/src/core/SimpleHistogramDecoder.java b/src/core/SimpleHistogramDecoder.java index c67d0eec0e..29eae3aa1d 100644 --- a/src/core/SimpleHistogramDecoder.java +++ b/src/core/SimpleHistogramDecoder.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2011-2012 The OpenTSDB Authors. +// Copyright (C) 2016-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 @@ -16,7 +16,7 @@ /** *

    - * Histogram decoder for Yamas style histograms. + * Histogram decoder for Simple style histograms. *

    *

    * It currently checks the first byte of the encoded data and decide the type of @@ -26,13 +26,14 @@ *

    * This class is thread safe as it has no state. *

    + * @since 2.4 */ public class SimpleHistogramDecoder implements HistogramDataPointDecoder { @Override public HistogramDataPoint decode(final byte[] raw_data, final long timestamp) { final Histogram histogram; switch (raw_data[0]) { - case 0x0: // should be 0, refer to core-library/src/test/java/com/yahoo/yamas/metrics/Yamas1HistogramTest.java + case 0x0: { histogram = new SimpleHistogram(); byte[] hist_raw_data = Arrays.copyOfRange(raw_data, 1, raw_data.length); @@ -40,7 +41,8 @@ public HistogramDataPoint decode(final byte[] raw_data, final long timestamp) { } break; default: - throw new IllegalDataException("Unknown header of histogram data, the header is: " + raw_data[0]); + throw new IllegalDataException("Unknown header of histogram data, " + + "the header is: " + raw_data[0]); } return new SimpleHistogramDataPointAdapter(histogram, timestamp); diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index fc3e40b341..11013820ba 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -44,283 +44,283 @@ @RunWith(PowerMockRunner.class) @PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) @PrepareForTest({ TSDB.class, Scanner.class, SaltScanner.class, Span.class, - Const.class, UniqueId.class, Tags.class, QueryStats.class, DateTime.class, - HistogramDataPointDecoderManager.class, - SimpleHistogram.class, SimpleHistogramDecoder.class}) + Const.class, UniqueId.class, Tags.class, QueryStats.class, DateTime.class, + HistogramDataPointDecoderManager.class, + SimpleHistogram.class, SimpleHistogramDecoder.class}) public class TestSaltScannerHistogram extends BaseTsdbTest { - private final static byte[] FAMILY = "t".getBytes(); - private final static byte[] QUALIFIER_A = { 0x06, 0x00, 0x00}; - private final static byte[] QUALIFIER_B = { 0x06, 0x10, 0x00 }; - - private byte[] VALUE; - - private final static int NUM_BUCKETS = 2; - private List scanners; - private TreeMap spans; - - private List>> kvs_a; - private List>> kvs_b; - - private Scanner scanner_a; - private Scanner scanner_b; - private QueryStats query_stats; - - private byte[] key_a; - //different tagv - private byte[] key_b; - //same as A bug different time - private byte[] key_c; + private final static byte[] FAMILY = "t".getBytes(); + private final static byte[] QUALIFIER_A = { 0x06, 0x00, 0x00}; + private final static byte[] QUALIFIER_B = { 0x06, 0x10, 0x00 }; + + private byte[] VALUE; + + private final static int NUM_BUCKETS = 2; + private List scanners; + private TreeMap spans; + + private List>> kvs_a; + private List>> kvs_b; + + private Scanner scanner_a; + private Scanner scanner_b; + private QueryStats query_stats; + + private byte[] key_a; + //different tagv + private byte[] key_b; + //same as A bug different time + private byte[] key_c; + + @Before + public void beforeLocal() { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); - @Before - public void beforeLocal() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); - - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeByte(0 /*HistoType.SimpleHistogramType.ordinal()*/); - output.writeShort(8); - output.writeFloat(1.0f); - output.writeFloat(2.0f); - output.writeLong(5, true); - output.writeFloat(2.0f); - output.writeFloat(3.0f); - output.writeLong(5, true); - output.writeFloat(3.0f); - output.writeFloat(4.0f); - output.writeLong(5, true); - output.writeFloat(4.0f); - output.writeFloat(5.0f); - output.writeLong(0, true); - output.writeFloat(5.0f); - output.writeFloat(6.0f); - output.writeLong(0, true); - output.writeFloat(6.0f); - output.writeFloat(7.0f); - output.writeLong(0, true); - output.writeFloat(7.0f); - output.writeFloat(8.0f); - output.writeLong(0, true); - output.writeFloat(8.0f); - output.writeFloat(9.0f); - output.writeLong(0, true); - output.writeLong(0, true); - output.writeLong(2, true); - output.close(); - VALUE = outBuffer.toByteArray(); - - query_stats = mock(QueryStats.class); - spans = new TreeMap(new RowKey.SaltCmp()); - setupMockScanners(true); - - key_a = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - TAGK_B_STRING, TAGV_STRING); - key_b = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_B_STRING, - TAGK_B_STRING, TAGV_STRING); - key_c = getRowKey(METRIC_STRING, 1359680400, TAGK_STRING, TAGV_STRING, - TAGK_B_STRING, TAGV_STRING); - } - - @Test - public void scan() throws Exception { - setupMockScanners(false); - - SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); - - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, null, false, null, query_stats, 0, spans); - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); - assertEquals(3, spans.size()); - - HistogramSpan span = spans.get(key_a); - assertEquals(2, span.size()); - assertEquals(1356998400000L, span.timestamp(0)); - assertEquals(1357002496000L, span.timestamp(1)); - assertEquals(1, span.getAnnotations().size()); - - span = spans.get(key_b); - assertEquals(1, span.size()); - assertEquals(1356998400000L, span.timestamp(0)); - assertEquals(0, span.getAnnotations().size()); - - span = spans.get(key_c); - assertEquals(2, span.size()); - assertEquals(1359680400000L, span.timestamp(0)); - assertEquals(1359684496000L, span.timestamp(1)); - assertEquals(0, span.getAnnotations().size()); - } - - @Test - public void scanWithFilter() throws Exception { - setupMockScanners(false); - List filters = new ArrayList(1); - filters.add(new TagVWildcardFilter(TAGK_STRING, "web*")); - - SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); - - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, null, false, null, query_stats, 0, spans); - - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); - assertEquals(3, spans.size()); - - HistogramSpan span = spans.get(key_a); - assertEquals(2, span.size()); - assertEquals(1356998400000L, span.timestamp(0)); - assertEquals(1357002496000L, span.timestamp(1)); - assertEquals(1, span.getAnnotations().size()); - - span = spans.get(key_b); - assertEquals(1, span.size()); - assertEquals(1356998400000L, span.timestamp(0)); - assertEquals(0, span.getAnnotations().size()); - - span = spans.get(key_c); - assertEquals(2, span.size()); - assertEquals(1359680400000L, span.timestamp(0)); - assertEquals(1359684496000L, span.timestamp(1)); - assertEquals(0, span.getAnnotations().size()); - } - - @Test - public void scanWithFiltersOnSameTag() throws Exception { - setupMockScanners(false); - List filters = new ArrayList(1); - filters.add(new TagVWildcardFilter("host", "web*")); - filters.add(new TagVWildcardFilter("host", "w*b*")); - filters.add(new TagVRegexFilter("host", "w.*")); - - SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); - - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, null, false, null, query_stats, 0, spans); - - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); - assertEquals(3, spans.size()); - - HistogramSpan span = spans.get(key_a); - assertEquals(2, span.size()); - assertEquals(1356998400000L, span.timestamp(0)); - assertEquals(1357002496000L, span.timestamp(1)); - assertEquals(1, span.getAnnotations().size()); - - span = spans.get(key_b); - assertEquals(1, span.size()); - assertEquals(1356998400000L, span.timestamp(0)); - assertEquals(0, span.getAnnotations().size()); - - span = spans.get(key_c); - assertEquals(2, span.size()); - assertEquals(1359680400000L, span.timestamp(0)); - assertEquals(1359684496000L, span.timestamp(1)); - assertEquals(0, span.getAnnotations().size()); - } - - @Test - public void scanWithFiltersOnSameTagOneFail() throws Exception { - setupMockScanners(false); - List filters = new ArrayList(1); - filters.add(new TagVWildcardFilter("host", "web*")); - filters.add(new TagVWildcardFilter("host", "drood*")); - - SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); - - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, filters, false, null, query_stats, 0, spans); - - assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); - assertEquals(0, spans.size()); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeByte(0 /*HistoType.SimpleHistogramType.ordinal()*/); + output.writeShort(8); + output.writeFloat(1.0f); + output.writeFloat(2.0f); + output.writeLong(5, true); + output.writeFloat(2.0f); + output.writeFloat(3.0f); + output.writeLong(5, true); + output.writeFloat(3.0f); + output.writeFloat(4.0f); + output.writeLong(5, true); + output.writeFloat(4.0f); + output.writeFloat(5.0f); + output.writeLong(0, true); + output.writeFloat(5.0f); + output.writeFloat(6.0f); + output.writeLong(0, true); + output.writeFloat(6.0f); + output.writeFloat(7.0f); + output.writeLong(0, true); + output.writeFloat(7.0f); + output.writeFloat(8.0f); + output.writeLong(0, true); + output.writeFloat(8.0f); + output.writeFloat(9.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(2, true); + output.close(); + VALUE = outBuffer.toByteArray(); + + query_stats = mock(QueryStats.class); + spans = new TreeMap(new RowKey.SaltCmp()); + setupMockScanners(true); + + key_a = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + TAGK_B_STRING, TAGV_STRING); + key_b = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_B_STRING, + TAGK_B_STRING, TAGV_STRING); + key_c = getRowKey(METRIC_STRING, 1359680400, TAGK_STRING, TAGV_STRING, + TAGK_B_STRING, TAGV_STRING); + } + + @Test + public void scan() throws Exception { + setupMockScanners(false); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, null, false, null, query_stats, 0, spans); + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(3, spans.size()); + + HistogramSpan span = spans.get(key_a); + assertEquals(2, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1357002496000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(key_b); + assertEquals(1, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(key_c); + assertEquals(2, span.size()); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(1359684496000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanWithFilter() throws Exception { + setupMockScanners(false); + List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter(TAGK_STRING, "web*")); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, null, false, null, query_stats, 0, spans); + + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(3, spans.size()); + + HistogramSpan span = spans.get(key_a); + assertEquals(2, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1357002496000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(key_b); + assertEquals(1, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(key_c); + assertEquals(2, span.size()); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(1359684496000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanWithFiltersOnSameTag() throws Exception { + setupMockScanners(false); + List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "web*")); + filters.add(new TagVWildcardFilter("host", "w*b*")); + filters.add(new TagVRegexFilter("host", "w.*")); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, null, false, null, query_stats, 0, spans); + + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(3, spans.size()); + + HistogramSpan span = spans.get(key_a); + assertEquals(2, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(1357002496000L, span.timestamp(1)); + assertEquals(1, span.getAnnotations().size()); + + span = spans.get(key_b); + assertEquals(1, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(0, span.getAnnotations().size()); + + span = spans.get(key_c); + assertEquals(2, span.size()); + assertEquals(1359680400000L, span.timestamp(0)); + assertEquals(1359684496000L, span.timestamp(1)); + assertEquals(0, span.getAnnotations().size()); + } + + @Test + public void scanWithFiltersOnSameTagOneFail() throws Exception { + setupMockScanners(false); + List filters = new ArrayList(1); + filters.add(new TagVWildcardFilter("host", "web*")); + filters.add(new TagVWildcardFilter("host", "drood*")); + + SimpleHistogram y1Hist = mock(SimpleHistogram.class); + PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + null, filters, false, null, query_stats, 0, spans); + + assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); + assertEquals(0, spans.size()); + } + + /** + * Sets up a pair of scanners with either a list of values or no data + * @param no_data Whether or not to return 0 data. + */ + private void setupMockScanners(final boolean no_data) { + scanners = new ArrayList(NUM_BUCKETS); + scanner_a = mock(Scanner.class); + scanner_b = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + when(scanner_b.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); } - - /** - * Sets up a pair of scanners with either a list of values or no data - * @param no_data Whether or not to return 0 data. - */ - private void setupMockScanners(final boolean no_data) { - scanners = new ArrayList(NUM_BUCKETS); - scanner_a = mock(Scanner.class); - scanner_b = mock(Scanner.class); - if (no_data) { - when(scanner_a.nextRows()).thenReturn( - Deferred.>>fromResult(null)); - when(scanner_b.nextRows()).thenReturn( - Deferred.>>fromResult(null)); - } else { - setupValues(); - } - scanners.add(scanner_a); - scanners.add(scanner_b); + scanners.add(scanner_a); + scanners.add(scanner_b); + } + + /** + * This method sets up some row keys and values to pass to the scanners. + * The values aren't exactly what would normally be passed to a salt scanner + * in that we have the same series salted across separate buckets. That would + * only happen if you add the timestamp to the salt calculation, which we + * may do in the future. We're testing now for future proofing. + */ + private void setupValues() { + kvs_a = new ArrayList>>(3); + kvs_b = new ArrayList>>(2); + + final String note = "{\"tsuid\":\"000000010000000100000001\"," + + "\"startTime\":1356998490,\"endTime\":0,\"description\":" + + "\"The Great A'Tuin!\",\"notes\":\"Millenium hand and shrimp\"," + + "\"custom\":null}"; + + for (int i = 0; i < 5; i++) { + final ArrayList> rows = + new ArrayList>(1); + final ArrayList row = new ArrayList(2); + rows.add(row); + byte[] key = null; + + switch (i) { + case 0: + row.add(new KeyValue(key_a, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 1: + row.add(new KeyValue(key_b, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 2: + row.add(new KeyValue(key_c, FAMILY, QUALIFIER_A, 0, VALUE)); + kvs_a.add(rows); + break; + case 3: + key = Arrays.copyOf(key_a, key_a.length); + key[0] = 1; + row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); + row.add(new KeyValue(key, FAMILY, new byte[] { 1, 0, 0 }, 0, + note.getBytes(Charset.forName("UTF8")))); + kvs_b.add(rows); + break; + case 4: + key = Arrays.copyOf(key_c, key_c.length); + key[0] = 1; + row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); + kvs_b.add(rows); + break; + } } - /** - * This method sets up some row keys and values to pass to the scanners. - * The values aren't exactly what would normally be passed to a salt scanner - * in that we have the same series salted across separate buckets. That would - * only happen if you add the timestamp to the salt calculation, which we - * may do in the future. We're testing now for future proofing. - */ - private void setupValues() { - kvs_a = new ArrayList>>(3); - kvs_b = new ArrayList>>(2); - - final String note = "{\"tsuid\":\"000000010000000100000001\"," - + "\"startTime\":1356998490,\"endTime\":0,\"description\":" - + "\"The Great A'Tuin!\",\"notes\":\"Millenium hand and shrimp\"," - + "\"custom\":null}"; - - for (int i = 0; i < 5; i++) { - final ArrayList> rows = - new ArrayList>(1); - final ArrayList row = new ArrayList(2); - rows.add(row); - byte[] key = null; - - switch (i) { - case 0: - row.add(new KeyValue(key_a, FAMILY, QUALIFIER_A, 0, VALUE)); - kvs_a.add(rows); - break; - case 1: - row.add(new KeyValue(key_b, FAMILY, QUALIFIER_A, 0, VALUE)); - kvs_a.add(rows); - break; - case 2: - row.add(new KeyValue(key_c, FAMILY, QUALIFIER_A, 0, VALUE)); - kvs_a.add(rows); - break; - case 3: - key = Arrays.copyOf(key_a, key_a.length); - key[0] = 1; - row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); - row.add(new KeyValue(key, FAMILY, new byte[] { 1, 0, 0 }, 0, - note.getBytes(Charset.forName("UTF8")))); - kvs_b.add(rows); - break; - case 4: - key = Arrays.copyOf(key_c, key_c.length); - key[0] = 1; - row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); - kvs_b.add(rows); - break; - } - } - - when(scanner_a.nextRows()) - .thenReturn(Deferred.fromResult(kvs_a.get(0))) - .thenReturn(Deferred.fromResult(kvs_a.get(1))) - .thenReturn(Deferred.fromResult(kvs_a.get(2))) - .thenReturn(Deferred.>>fromResult(null)); - when(scanner_b.nextRows()) - .thenReturn(Deferred.fromResult(kvs_b.get(0))) - .thenReturn(Deferred.fromResult(kvs_b.get(1))) - .thenReturn(Deferred.>>fromResult(null)); - } + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.>>fromResult(null)); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); + } } diff --git a/test/core/TestSimpleHistogram.java b/test/core/TestSimpleHistogram.java index ce37a954ef..f1b134dc97 100644 --- a/test/core/TestSimpleHistogram.java +++ b/test/core/TestSimpleHistogram.java @@ -1,3 +1,15 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 static org.junit.Assert.assertEquals; @@ -25,282 +37,276 @@ public class TestSimpleHistogram { @Test public void verifyE2EKryo() { - Kryo kryo = new Kryo(); - - //Encoding stage - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeByte(0 /* This is the type of histogram (or sketch) written to storage. HistoType.SimpleHistogramType.ordinal()*/); - output.writeShort(8); - output.writeFloat(1.0f); - output.writeFloat(2.0f); - output.writeLong(5, true); - output.writeFloat(2.0f); - output.writeFloat(3.0f); - output.writeLong(5, true); - output.writeFloat(3.0f); - output.writeFloat(4.0f); - output.writeLong(5, true); - output.writeFloat(4.0f); - output.writeFloat(5.0f); - output.writeLong(0, true); - output.writeFloat(5.0f); - output.writeFloat(6.0f); - output.writeLong(0, true); - output.writeFloat(6.0f); - output.writeFloat(7.0f); - output.writeLong(0, true); - output.writeFloat(7.0f); - output.writeFloat(8.0f); - output.writeLong(0, true); - output.writeFloat(8.0f); - output.writeFloat(9.0f); - output.writeLong(0, true); - output.writeLong(0, true); - output.writeLong(2, true); - output.close(); - - System.out.println("Byte array ouput size: " + outBuffer.toByteArray().length); - - //Decoding stage - Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); - int metricType = input.readByte(); - - switch (metricType) { - case 0: - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.read(kryo, input); - Input verifyHist = new Input(new ByteArrayInputStream(y1Hist.histogram())); - - int bucketCount = verifyHist.readShort(); - assertEquals(bucketCount, 8); - - Float bucketLowerBound = verifyHist.readFloat(); - Float bucketUpperBound = verifyHist.readFloat(); - long bucketVal = verifyHist.readLong(true); - assertEquals(bucketLowerBound, 1.0f, 0.0001); - assertEquals(bucketVal, 5); - assertEquals(y1Hist.getOverflow(), Long.valueOf(2L)); - break; - default: - System.out.println("Failed to detect histogram type"); - assertTrue(false); - } - input.close(); + Kryo kryo = new Kryo(); + + //Encoding stage + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeByte(0 /* This is the type of histogram (or sketch) written + // to storage. HistoType.SimpleHistogramType.ordinal()*/); + output.writeShort(8); + output.writeFloat(1.0f); + output.writeFloat(2.0f); + output.writeLong(5, true); + output.writeFloat(2.0f); + output.writeFloat(3.0f); + output.writeLong(5, true); + output.writeFloat(3.0f); + output.writeFloat(4.0f); + output.writeLong(5, true); + output.writeFloat(4.0f); + output.writeFloat(5.0f); + output.writeLong(0, true); + output.writeFloat(5.0f); + output.writeFloat(6.0f); + output.writeLong(0, true); + output.writeFloat(6.0f); + output.writeFloat(7.0f); + output.writeLong(0, true); + output.writeFloat(7.0f); + output.writeFloat(8.0f); + output.writeLong(0, true); + output.writeFloat(8.0f); + output.writeFloat(9.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(2, true); + output.close(); + + //Decoding stage + Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); + int metricType = input.readByte(); + + switch (metricType) { + case 0: + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.read(kryo, input); + Input verifyHist = new Input(new ByteArrayInputStream(y1Hist.histogram())); + + int bucketCount = verifyHist.readShort(); + assertEquals(bucketCount, 8); + + Float bucketLowerBound = verifyHist.readFloat(); + Float bucketUpperBound = verifyHist.readFloat(); + long bucketVal = verifyHist.readLong(true); + assertEquals(bucketLowerBound, 1.0f, 0.0001); + assertEquals(bucketVal, 5); + assertEquals(y1Hist.getOverflow(), Long.valueOf(2L)); + break; + default: + System.out.println("Failed to detect histogram type"); + assertTrue(false); + } + input.close(); } @Test public void testHistogramSerialization() { - Kryo kryo = new Kryo(); - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.addBucket(1.0f, 2.0f, 5L); - y1Hist.addBucket(2.0f, 3.0f, 5L); - y1Hist.addBucket(3.0f, 10.0f, 0L); - y1Hist.write(kryo, output); - output.close(); - - SimpleHistogram y1HistVerify = new SimpleHistogram(); - y1HistVerify.fromHistogram(outBuffer.toByteArray()); - - Input input = new Input(new ByteArrayInputStream(y1HistVerify.histogram())); + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); - int bucketCount = input.readShort(); - assertEquals(bucketCount, 3); - - Float bucketLB = input.readFloat(); - Float bucketUB = input.readFloat(); - long bucketVal = input.readLong(true); - assertEquals(bucketLB, 1.0f, 0.001); - assertEquals(bucketVal, 5); + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.addBucket(1.0f, 2.0f, 5L); + y1Hist.addBucket(2.0f, 3.0f, 5L); + y1Hist.addBucket(3.0f, 10.0f, 0L); + y1Hist.write(kryo, output); + output.close(); + + SimpleHistogram y1HistVerify = new SimpleHistogram(); + y1HistVerify.fromHistogram(outBuffer.toByteArray()); + + Input input = new Input(new ByteArrayInputStream(y1HistVerify.histogram())); + int bucketCount = input.readShort(); + assertEquals(bucketCount, 3); + + Float bucketLB = input.readFloat(); + Float bucketUB = input.readFloat(); + long bucketVal = input.readLong(true); + assertEquals(bucketLB, 1.0f, 0.001); + assertEquals(bucketVal, 5); + input.close(); } @Test public void testIncompletByteArray() { - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeShort(4); - output.close(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - boolean exceptionCaught = false; - try { - y1Hist.fromHistogram(outBuffer.toByteArray()); - } - catch(Exception e) { - exceptionCaught = true; - } - - assertFalse(exceptionCaught); + SimpleHistogram y1Hist = new SimpleHistogram(); + boolean exceptionCaught = false; + try { + y1Hist.fromHistogram(outBuffer.toByteArray()); + } + catch(Exception e) { + exceptionCaught = true; + } + + assertFalse(exceptionCaught); } @Test public void testInvalidHistogramLength() { - Kryo kryo = new Kryo(); - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeShort(1); - output.writeInt(-1); - output.close(); - Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); - Integer metricType = input.readInt(); + Kryo kryo = new Kryo(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(1); + output.writeInt(-1); + output.close(); + Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); + Integer metricType = input.readInt(); - SimpleHistogram y1Hist = new SimpleHistogram(); - boolean exceptionCaught = false; - try { - y1Hist.read(kryo, input); - } - catch(Exception e) { - exceptionCaught = true; - } - - assertFalse(exceptionCaught); + SimpleHistogram y1Hist = new SimpleHistogram(); + boolean exceptionCaught = false; + try { + y1Hist.read(kryo, input); + } + catch(Exception e) { + exceptionCaught = true; + } + + assertFalse(exceptionCaught); } @Test public void testSinglePercentile() { - Kryo kryo = new Kryo(); - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeShort(3); - output.writeFloat(1.0f); - output.writeFloat(6.0f); - output.writeLong(5, true); - output.writeFloat(6.0f); - output.writeFloat(10.0f); - output.writeLong(10, true); - output.writeFloat(10.0f); - output.writeFloat(20.0f); - output.writeLong(1, true); - output.writeLong(0, true); - output.writeLong(5, true); - output.close(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(3); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); - double perc50 = y1Hist.percentile(50.0f); + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + double perc50 = y1Hist.percentile(50.0f); - assertEquals(perc50, 8.0f, 0.0001); - assertEquals(y1Hist.percentile(1000.0f), -1.0f, 0.0001); + assertEquals(perc50, 8.0f, 0.0001); + assertEquals(y1Hist.percentile(1000.0f), -1.0f, 0.0001); } @Test public void testPercentileList() { - Kryo kryo = new Kryo(); - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeShort(4); - output.writeFloat(1.0f); - output.writeFloat(6.0f); - output.writeLong(5, true); - output.writeFloat(6.0f); - output.writeFloat(10.0f); - output.writeLong(10, true); - output.writeFloat(10.0f); - output.writeFloat(20.0f); - output.writeLong(1, true); - output.writeFloat(20.0f); - output.writeFloat(40.0f); - output.writeLong(0, true); - output.writeLong(0, true); - output.writeLong(5, true); - output.close(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); - ArrayList percs = new ArrayList(); - percs.add(50.0); - percs.add(99.0); - ArrayList percValues = (ArrayList) y1Hist.percentiles(percs); - double perc50 = percValues.get(0); - double perc99 = percValues.get(1); - - assertEquals(perc50, 8.0, 0.001); - assertEquals(perc99, 15.0, 0.001); + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); + ArrayList percs = new ArrayList(); + percs.add(50.0); + percs.add(99.0); + ArrayList percValues = (ArrayList) y1Hist.percentiles(percs); + double perc50 = percValues.get(0); + double perc99 = percValues.get(1); + + assertEquals(perc50, 8.0, 0.001); + assertEquals(perc99, 15.0, 0.001); } @Test public void testSingleHistogramMerge() { - Kryo kryo = new Kryo(); - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeShort(4); - output.writeFloat(1.0f); - output.writeFloat(6.0f); - output.writeLong(5, true); - output.writeFloat(6.0f); - output.writeFloat(10.0f); - output.writeLong(10, true); - output.writeFloat(10.0f); - output.writeFloat(20.0f); - output.writeLong(1, true); - output.writeFloat(20.0f); - output.writeFloat(40.0f); - output.writeLong(0, true); - output.writeLong(0, true); - output.writeLong(5, true); - output.close(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); - - SimpleHistogram y1Hist1 = new SimpleHistogram(); - y1Hist1.fromHistogram(outBuffer.toByteArray()); - y1Hist1.setUnderflow(2L); + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); - y1Hist.aggregate(y1Hist1, HistogramAggregation.SUM); - assertEquals(y1Hist.getBucketCount(1.0f, 6.0f), Long.valueOf(10L)); - assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(20L)); - assertEquals(y1Hist.getBucketCount(10.0f, 20.0f), Long.valueOf(2L)); - assertEquals(y1Hist.getOverflow(), Long.valueOf(10L)); - assertEquals(y1Hist.getUnderflow(), Long.valueOf(2L)); + SimpleHistogram y1Hist1 = new SimpleHistogram(); + y1Hist1.fromHistogram(outBuffer.toByteArray()); + y1Hist1.setUnderflow(2L); + + y1Hist.aggregate(y1Hist1, HistogramAggregation.SUM); + assertEquals(y1Hist.getBucketCount(1.0f, 6.0f), Long.valueOf(10L)); + assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(20L)); + assertEquals(y1Hist.getBucketCount(10.0f, 20.0f), Long.valueOf(2L)); + assertEquals(y1Hist.getOverflow(), Long.valueOf(10L)); + assertEquals(y1Hist.getUnderflow(), Long.valueOf(2L)); } @Test public void testMultipleHistogramMerge() { - Kryo kryo = new Kryo(); - ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); - Output output = new Output(outBuffer); - output.writeShort(4); - output.writeFloat(1.0f); - output.writeFloat(6.0f); - output.writeLong(5, true); - output.writeFloat(6.0f); - output.writeFloat(10.0f); - output.writeLong(10, true); - output.writeFloat(10.0f); - output.writeFloat(20.0f); - output.writeLong(1, true); - output.writeFloat(20.0f); - output.writeFloat(40.0f); - output.writeLong(0, true); - output.writeLong(0, true); - output.writeLong(5, true); - output.close(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + Output output = new Output(outBuffer); + output.writeShort(4); + output.writeFloat(1.0f); + output.writeFloat(6.0f); + output.writeLong(5, true); + output.writeFloat(6.0f); + output.writeFloat(10.0f); + output.writeLong(10, true); + output.writeFloat(10.0f); + output.writeFloat(20.0f); + output.writeLong(1, true); + output.writeFloat(20.0f); + output.writeFloat(40.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(5, true); + output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist = new SimpleHistogram(); + y1Hist.fromHistogram(outBuffer.toByteArray()); - ArrayList histos = new ArrayList(); - SimpleHistogram y1Hist1 = new SimpleHistogram(); - y1Hist1.fromHistogram(outBuffer.toByteArray()); - histos.add(y1Hist1); - SimpleHistogram y1Hist2 = new SimpleHistogram(); - y1Hist2.fromHistogram(outBuffer.toByteArray()); - histos.add(y1Hist2); - - y1Hist.aggregate(histos, HistogramAggregation.SUM); - assertEquals(y1Hist.getBucketCount(1.0f, 6.0f), Long.valueOf(15L)); - assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(30L)); - assertEquals(y1Hist.getBucketCount(10.0f, 20.0f), Long.valueOf(3L)); - assertEquals(y1Hist.getOverflow(), Long.valueOf(15L)); + ArrayList histos = new ArrayList(); + SimpleHistogram y1Hist1 = new SimpleHistogram(); + y1Hist1.fromHistogram(outBuffer.toByteArray()); + histos.add(y1Hist1); + SimpleHistogram y1Hist2 = new SimpleHistogram(); + y1Hist2.fromHistogram(outBuffer.toByteArray()); + histos.add(y1Hist2); + + y1Hist.aggregate(histos, HistogramAggregation.SUM); + assertEquals(y1Hist.getBucketCount(1.0f, 6.0f), Long.valueOf(15L)); + assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(30L)); + assertEquals(y1Hist.getBucketCount(10.0f, 20.0f), Long.valueOf(3L)); + assertEquals(y1Hist.getOverflow(), Long.valueOf(15L)); } @Test public void testArbitraryBuckets() { - Kryo kryo = new Kryo(); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); Output output = new Output(outBuffer); output.writeShort(4); @@ -367,9 +373,12 @@ public void testJsonSerialization() { } catch (IOException e) { e.printStackTrace(); } - System.out.println(jsonOut); - assertTrue("{\"buckets\":{\"5.0-7.0\":3,\"7.0-10.0\":5},\"underflow\":0,\"overflow\":1}".equals(jsonOut)); - + assertTrue(jsonOut.contains("\"buckets\":{")); + assertTrue(jsonOut.contains("\"5.0-7.0\":3")); + assertTrue(jsonOut.contains("\"7.0-10.0\":5")); + assertTrue(jsonOut.contains("\"underflow\":0")); + assertTrue(jsonOut.contains("\"overflow\":1")); + SimpleHistogram y1Hist1 = new SimpleHistogram(); y1Hist1.addBucket(Float.NEGATIVE_INFINITY, 5.0f, 3L); @@ -382,8 +391,11 @@ public void testJsonSerialization() { } catch (IOException e) { e.printStackTrace(); } - System.out.println(jsonOut); - assertTrue("{\"buckets\":{\"-Infinity-5.0\":3,\"7.0-10.0\":5},\"underflow\":0,\"overflow\":0}".equals(jsonOut)); + assertTrue(jsonOut.contains("\"buckets\":{")); + assertTrue(jsonOut.contains("\"-Infinity-5.0\":3")); + assertTrue(jsonOut.contains("\"7.0-10.0\":5")); + assertTrue(jsonOut.contains("\"underflow\":0")); + assertTrue(jsonOut.contains("\"overflow\":0")); } @Test @@ -397,9 +409,9 @@ public void testImmutableGetHistogram() { Map histMap = y1Hist.getHistogram(); boolean histModBlocked = false; try { - histMap.put(new HistogramBucket(BucketType.REGULAR, 1.0f, 5.0f), 3L); + histMap.put(new HistogramBucket(BucketType.REGULAR, 1.0f, 5.0f), 3L); } catch (UnsupportedOperationException e) { - histModBlocked = true; + histModBlocked = true; } assertTrue(histModBlocked); @@ -424,75 +436,82 @@ public void testMissingBucketsHistogramAggregation() { @Test public void testInit() { - double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); - System.out.println("No. of buckets: " + dynaHist.length); - for (int i = 0; i < dynaHist.length; i++) { - System.out.print(dynaHist[i] + ", "); - } - System.out.println(); - assertEquals(33, dynaHist.length); + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, + 100.0f, 2000.0f, 0.05f); + // TODO - proper bucket testing +// System.out.println("No. of buckets: " + dynaHist.length); +// for (int i = 0; i < dynaHist.length; i++) { +// System.out.print(dynaHist[i] + ", "); +// } +// System.out.println(); + assertEquals(33, dynaHist.length); } @Test public void testWholeRangeFocus() { - double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 2000.0f, 100.0f, 2000.0f, 0.05f); - System.out.println("No. of buckets: " + dynaHist.length); - for (int i = 0; i < dynaHist.length; i++) { - System.out.print(dynaHist[i] + ", "); - } - System.out.println(); - assertEquals(31, dynaHist.length); + double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 2000.0f, + 100.0f, 2000.0f, 0.05f); + // TODO - proper bucket testing +// System.out.println("No. of buckets: " + dynaHist.length); +// for (int i = 0; i < dynaHist.length; i++) { +// System.out.print(dynaHist[i] + ", "); +// } +// System.out.println(); + assertEquals(31, dynaHist.length); } @Test public void testStartEqualtoFocusStart() { - double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); - System.out.println("No. of buckets: " + dynaHist.length); - for (int i = 0; i < dynaHist.length; i++) { - System.out.print(dynaHist[i] + ", "); - } - System.out.println(); - assertEquals(32, dynaHist.length); + double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 6000.0f, + 100.0f, 2000.0f, 0.05f); + // TODO - proper bucket testing +// System.out.println("No. of buckets: " + dynaHist.length); +// for (int i = 0; i < dynaHist.length; i++) { +// System.out.print(dynaHist[i] + ", "); +// } +// System.out.println(); + assertEquals(32, dynaHist.length); } @Test public void testEndEqualtoFocusEnd() { - double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 2000.0f, 100.0f, 2000.0f, 0.05f); - System.out.println("No. of buckets: " + dynaHist.length); - for (int i = 0; i < dynaHist.length; i++) { - System.out.print(dynaHist[i] + ", "); - } - - assertEquals(32, dynaHist.length); + double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 2000.0f, + 100.0f, 2000.0f, 0.05f); + // TODO - proper bucket testing +// System.out.println("No. of buckets: " + dynaHist.length); +// for (int i = 0; i < dynaHist.length; i++) { +// System.out.print(dynaHist[i] + ", "); +// } + assertEquals(32, dynaHist.length); } - @Test (expected = RuntimeException.class) + @Test (expected = IllegalArgumentException.class) public void testErrorStartGreaterThanEnd() { - double[] dynaHist = SimpleHistogram.initializeHistogram(10000.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); + SimpleHistogram.initializeHistogram(10000.0f, 6000.0f, 100.0f, 2000.0f, 0.05f); } - @Test (expected = RuntimeException.class) + @Test (expected = IllegalArgumentException.class) public void testErrorFocusStartGreaterThanFocusEnd() { - double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 3000.0f, 2000.0f, 0.05f); + SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 3000.0f, 2000.0f, 0.05f); } - @Test (expected = RuntimeException.class) + @Test (expected = IllegalArgumentException.class) public void testErrorRateLessThanZero() { - double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 100.0f, 2000.0f, -0.05f); + SimpleHistogram.initializeHistogram(1.0f, 6000.0f, 100.0f, 2000.0f, -0.05f); } - @Test (expected = RuntimeException.class) + @Test (expected = IllegalArgumentException.class) public void testFocusEndGreaterThanEnd() { - double[] dynaHist = SimpleHistogram.initializeHistogram(1.0f, 1000.0f, 100.0f, 2000.0f, 0.05f); + SimpleHistogram.initializeHistogram(1.0f, 1000.0f, 100.0f, 2000.0f, 0.05f); } - @Test (expected = RuntimeException.class) + @Test (expected = IllegalArgumentException.class) public void testFocusStartLessThanStart() { - double[] dynaHist = SimpleHistogram.initializeHistogram(200.0f, 100.0f, 1500.0f, 2000.0f, 0.05f); + SimpleHistogram.initializeHistogram(200.0f, 100.0f, 1500.0f, 2000.0f, 0.05f); } - @Test (expected = RuntimeException.class) + @Test (expected = IllegalArgumentException.class) public void testExcessiveBuckets() { - double[] dynaHist = SimpleHistogram.initializeHistogram(100.0f, 6000.0f, 100.0f, 6000.0f, 0.01f); + SimpleHistogram.initializeHistogram(100.0f, 6000.0f, 100.0f, 6000.0f, 0.01f); } } \ No newline at end of file From 92853c871587eb00ef22e2c753e3f231bb013a37 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 29 May 2017 10:35:51 -0700 Subject: [PATCH 638/826] Modify the SaltScanner class to work without salting enabled. Remove the scanner code from the TsdbQuery class. Now it will be much more maintainable. Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 36 +-- src/core/TsdbQuery.java | 379 +-------------------------- test/core/TestSaltScanner.java | 180 +++++++------ test/core/TestSaltScannerSalted.java | 53 ++++ 4 files changed, 175 insertions(+), 473 deletions(-) create mode 100644 test/core/TestSaltScannerSalted.java diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 0d47e2dd11..36b60376c7 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.CountDownLatch; import net.opentsdb.meta.Annotation; import net.opentsdb.query.filter.TagVFilter; @@ -59,6 +59,8 @@ * * Concurrency is important in this class as the scanners are executing * asynchronously and can modify variables at any time. + * + * @since 2.2 */ public class SaltScanner { private static final Logger LOG = LoggerFactory.getLogger(SaltScanner.class); @@ -108,8 +110,8 @@ public class SaltScanner { /** Index of the sub query in the main query list */ private final int query_index; - /** A counter used to determine how many scanners are still running */ - private AtomicInteger completed_tasks = new AtomicInteger(); + /** A latch used to determine how many scanners are still running */ + private final CountDownLatch countdown; /** When the scanning started. We store the scan latency once all scanners * are done.*/ @@ -172,10 +174,6 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final QueryStats query_stats, final int query_index, final TreeMap histogramSpans) { - if (Const.SALT_WIDTH() < 1) { - throw new IllegalArgumentException( - "Salting is disabled. Use the regular scanner"); - } if (tsdb == null) { throw new IllegalArgumentException("The TSDB argument was null."); } @@ -196,6 +194,9 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, throw new IllegalArgumentException("Not enough or too many scanners " + scanners.size() + " when the salt bucket count is " + Const.SALT_BUCKETS()); + } else if (Const.SALT_WIDTH() <= 0 && scanners.size() > 1) { + throw new IllegalArgumentException("Not enough or too many scanners " + + scanners.size() + " when the salting is disabled."); } if (metric == null) { throw new IllegalArgumentException("The metric array was null."); @@ -215,6 +216,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()); } /** @@ -825,7 +827,7 @@ void close(final boolean ok) { if (ok && exception == null) { validateAndTriggerCallback(kvs, annotations, histograms); } else { - completed_tasks.incrementAndGet(); + countdown.countDown(); } } } @@ -835,13 +837,15 @@ void close(final boolean ok) { * @param kvs The compacted columns fetched by the scanner * @param annotations The annotations fetched by the scanners */ - private void validateAndTriggerCallback(final List kvs, - final Map> annotations, - final List>> histograms) { + private void validateAndTriggerCallback( + final List kvs, + final Map> annotations, + final List>> histograms) { - final int tasks = completed_tasks.incrementAndGet(); + countdown.countDown(); + final long count = countdown.getCount(); if (kvs.size() > 0) { - kv_map.put(tasks, kvs); + kv_map.put((int) count, kvs); } for (final byte[] key : annotations.keySet()) { @@ -853,10 +857,10 @@ private void validateAndTriggerCallback(final List kvs, } if (histograms.size() > 0) { - histMap.put(tasks, histograms); + histMap.put((int) count, histograms); } - if (tasks >= Const.SALT_BUCKETS()) { + if (countdown.getCount() <= 0) { try { mergeAndReturnResults(); } catch (final Exception ex) { @@ -874,7 +878,7 @@ private void validateAndTriggerCallback(final List kvs, */ private void handleException(final Exception e) { // make sure only one scanner can set the exception - completed_tasks.incrementAndGet(); + countdown.countDown(); if (exception == null) { synchronized (this) { if (exception == null) { diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index ae7935ff12..47a5f645c5 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -17,11 +17,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.TreeMap; import org.slf4j.Logger; @@ -29,10 +27,8 @@ import org.hbase.async.BinaryPrefixComparator; import org.hbase.async.Bytes; import org.hbase.async.CompareFilter; -import org.hbase.async.DeleteRequest; import org.hbase.async.FilterList; import org.hbase.async.HBaseException; -import org.hbase.async.KeyValue; import org.hbase.async.QualifierFilter; import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; @@ -45,14 +41,12 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; -import net.opentsdb.meta.Annotation; import net.opentsdb.query.QueryUtil; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.query.filter.TagVLiteralOrFilter; import net.opentsdb.rollup.NoSuchRollupForIntervalException; import net.opentsdb.rollup.RollupInterval; import net.opentsdb.rollup.RollupQuery; -import net.opentsdb.rollup.RollupSpan; import net.opentsdb.rollup.RollupUtils; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.QueryStats; @@ -62,7 +56,6 @@ import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.ByteSet; import net.opentsdb.utils.DateTime; -import net.opentsdb.utils.JSON; /** * Non-synchronized implementation of {@link Query}. @@ -733,373 +726,13 @@ private Deferred> findSpans() throws HBaseException { scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, delete, rollup_query, query_stats, query_index, null).scan(); + } else { + final List scanners = new ArrayList(1); + scanners.add(getScanner(0)); + scan_start_time = DateTime.nanoTime(); + return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, + delete, rollup_query, query_stats, query_index, null).scan(); } - - scan_start_time = DateTime.nanoTime(); - final Scanner scanner = getScanner(); - if (query_stats != null) { - query_stats.addScannerId(query_index, 0, scanner.toString()); - } - final Deferred> results = - new Deferred>(); - - /** - * Scanner callback executed recursively each time we get a set of data - * from storage. This is responsible for determining what columns are - * returned and issuing requests to load leaf objects. - * When the scanner returns a null set of rows, the method initiates the - * final callback. - */ - final class ScannerCB implements Callback>> { - - int nrows = 0; - boolean seenAnnotation = false; - long scanner_start = DateTime.nanoTime(); - long timeout = tsdb.getConfig().getLong("tsd.query.timeout"); - private final Set skips = new HashSet(); - private final Set keepers = new HashSet(); - private final int index = 0; // only used for salted scanners - /** nanosecond timestamps */ - private long fetch_start = 0; // reset each time we send an RPC to HBase - private long fetch_time = 0; // cumulation of time waiting on HBase - private long uid_resolve_time = 0; // cumulation of time resolving UIDs - private long uids_resolved = 0; - private long compaction_time = 0; // cumulation of time compacting - private long dps_pre_filter = 0; - private long rows_pre_filter = 0; - private long dps_post_filter = 0; - private long rows_post_filter = 0; - - /** Error callback that will capture an exception from AsyncHBase and store - * it so we can bubble it up to the caller. - */ - class ErrorCB implements Callback { - @Override - public Object call(final Exception e) throws Exception { - LOG.error("Scanner " + scanner + " threw an exception", e); - close(e); - return null; - } - } - - /** - * Starts the scanner and is called recursively to fetch the next set of - * rows from the scanner. - * @return The map of spans if loaded successfully, null if no data was - * found - */ - public Object scan() { - fetch_start = DateTime.nanoTime(); - return scanner.nextRows().addCallback(this).addErrback(new ErrorCB()); - } - - /** - * Loops through each row of the scanner results and parses out data - * points and optional meta data - * @return null if no rows were found, otherwise the TreeMap with spans - */ - @Override - public Object call(final ArrayList> rows) - throws Exception { - fetch_time += DateTime.nanoTime() - fetch_start; - try { - if (rows == null) { - scanlatency.add((int)DateTime.msFromNano(fetch_time)); - LOG.info(TsdbQuery.this + " matched " + nrows + " rows in " + - spans.size() + " spans in " + DateTime.msFromNano(fetch_time) + "ms"); - close(null); - return null; - } - - if (timeout > 0 && DateTime.msFromNanoDiff( - DateTime.nanoTime(), scanner_start) > timeout) { - throw new InterruptedException("Query timeout exceeded!"); - } - - rows_pre_filter += rows.size(); - - // used for UID resolution if a filter is involved - final List> lookups = - filters != null && !filters.isEmpty() ? - new ArrayList>(rows.size()) : null; - - for (final ArrayList row : rows) { - final byte[] key = row.get(0).key(); - if (Bytes.memcmp(metric, key, 0, metric_width) != 0) { - scanner.close(); - throw new IllegalDataException( - "HBase returned a row that doesn't match" - + " our scanner (" + scanner + ")! " + row + " does not start" - + " with " + Arrays.toString(metric)); - } - - // calculate estimated data point count. We don't want to deserialize - // the byte arrays so we'll just get a rough estimate of compacted - // columns. - for (final KeyValue kv : row) { - if (kv.qualifier().length % 2 == 0) { - if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { - ++dps_pre_filter; - } else { - // for now we'll assume that all compacted columns are of the - // same precision. This is likely incorrect. - if (Internal.inMilliseconds(kv.qualifier())) { - dps_pre_filter += (kv.qualifier().length / 4); - } else { - dps_pre_filter += (kv.qualifier().length / 2); - } - } - } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { - // with appends we don't have a good rough estimate as the length - // can vary widely with the value length variability. Therefore we - // have to iterate. - int idx = 0; - int qlength = 0; - while (idx < kv.value().length) { - qlength = Internal.getQualifierLength(kv.value(), idx); - idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); - ++dps_pre_filter; - } - } - } - - // If any filters have made it this far then we need to resolve - // the row key UIDs to their names for string comparison. We'll - // try to avoid the resolution with some sets but we may dupe - // resolve a few times. - // TODO - more efficient resolution - // TODO - byte set instead of a string for the uid may be faster - if (scanner_filters != null && !scanner_filters.isEmpty()) { - lookups.clear(); - final String tsuid = - UniqueId.uidToString(UniqueId.getTSUIDFromKey(key, - TSDB.metrics_width(), Const.TIMESTAMP_BYTES)); - if (skips.contains(tsuid)) { - continue; - } - if (!keepers.contains(tsuid)) { - final long uid_start = DateTime.nanoTime(); - - /** CB to called after all of the UIDs have been resolved */ - class MatchCB implements Callback> { - @Override - public Object call(final ArrayList matches) - throws Exception { - for (final boolean matched : matches) { - if (!matched) { - skips.add(tsuid); - return null; - } - } - // matched all, good data - keepers.add(tsuid); - processRow(key, row); - return null; - } - } - - /** Resolves all of the row key UIDs to their strings for filtering */ - class GetTagsCB implements - Callback>, Map> { - @Override - public Deferred> call( - final Map tags) throws Exception { - uid_resolve_time += (DateTime.nanoTime() - uid_start); - uids_resolved += tags.size(); - final List> matches = - new ArrayList>(scanner_filters.size()); - - for (final TagVFilter filter : scanner_filters) { - matches.add(filter.match(tags)); - } - - return Deferred.group(matches); - } - } - - lookups.add(Tags.getTagsAsync(tsdb, key) - .addCallbackDeferring(new GetTagsCB()) - .addBoth(new MatchCB())); - } else { - processRow(key, row); - } - } else { - processRow(key, row); - } - } - - // either we need to wait on the UID resolutions or we can go ahead - // if we don't have filters. - if (lookups != null && lookups.size() > 0) { - class GroupCB implements Callback> { - @Override - public Object call(final ArrayList group) throws Exception { - return scan(); - } - } - return Deferred.group(lookups).addCallback(new GroupCB()); - } else { - return scan(); - } - } catch (Exception e) { - close(e); - return null; - } - } - - /** - * Finds or creates the span for this row, compacts it and stores it. - * @param key The row key to use for fetching the span - * @param row The row to add - */ - void processRow(final byte[] key, final ArrayList row) { - ++rows_post_filter; - if (delete) { - final DeleteRequest del = new DeleteRequest(tsdb.dataTable(), key); - tsdb.getClient().delete(del); - } - - //Please move this logic to @CompactionQueue.compact API, if the - //qualifier prefix is set for rollup. Right now there is no way to - //identify whether a cell belong to rollup or default data table - //from the KeyValue/Hbase cell object - if (RollupQuery.isValidQuery(rollup_query)) { - //It is the rollup search result and rollup cells will not be - //compacted, so don't need to worry about complex or trivial - //compactions. It just need to consider the cells are different key - //values - - Span datapoints = spans.get(key); - if (datapoints == null) { - datapoints = new RollupSpan(tsdb, rollup_query); - spans.put(key, datapoints); - } - - for (KeyValue kv:row) { - final byte[] qual = kv.qualifier(); - - if (qual.length > 0) { - // Todo: Bug! Here we shouldn't use the first byte to check the type of this row - // Instead should parse the byte array to find the suffix and determine the actual type - if (qual[0] == Annotation.PREFIX()) { - // This could be a row with only an annotation in it - final Annotation note = JSON.parseToObject(kv.value(), - Annotation.class); - datapoints.getAnnotations().add(note); - } else { - if (rollup_query.getGroupBy() == Aggregators.AVG || - rollup_query.getGroupBy() == Aggregators.DEV) { - if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || - Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { - datapoints.addRow(kv); - } - } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), - qual, 0, rollup_query.getRollupAggPrefix().length) == 0) { - datapoints.addRow(kv); - } - } - } - } // end for - ++nrows; - seenAnnotation |= !datapoints.getAnnotations().isEmpty(); - } else { - // calculate estimated data point count. We don't want to deserialize - // the byte arrays so we'll just get a rough estimate of compacted - // columns. - for (final KeyValue kv : row) { - if (kv.qualifier().length % 2 == 0) { - if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { - ++dps_post_filter; - } else { - // for now we'll assume that all compacted columns are of the - // same precision. This is likely incorrect. - if (Internal.inMilliseconds(kv.qualifier())) { - dps_post_filter += (kv.qualifier().length / 4); - } else { - dps_post_filter += (kv.qualifier().length / 2); - } - } - } else if (kv.qualifier()[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { - // with appends we don't have a good rough estimate as the length - // can vary widely with the value length variability. Therefore we - // have to iterate. - int idx = 0; - int qlength = 0; - while (idx < kv.value().length) { - qlength = Internal.getQualifierLength(kv.value(), idx); - idx += qlength + Internal.getValueLengthFromQualifier(kv.value(), idx); - ++dps_post_filter; - } - } - } - - Span datapoints = spans.get(key); - if (datapoints == null) { - datapoints = new Span(tsdb); - spans.put(key, datapoints); - } - final long compaction_start = DateTime.nanoTime(); - final KeyValue compacted = - tsdb.compact(row, datapoints.getAnnotations(), null); - compaction_time += (DateTime.nanoTime() - compaction_start); - seenAnnotation |= !datapoints.getAnnotations().isEmpty(); - if (compacted != null) { // Can be null if we ignored all KVs. - datapoints.addRow(compacted); - ++nrows; - } - } - } - - void close(final Exception e) { - scanner.close(); - - if (query_stats != null) { - query_stats.addScannerStat(query_index, index, - QueryStat.SCANNER_TIME, DateTime.nanoTime() - scan_start_time); - - // Scanner Stats - /* Uncomment when AsyncHBase has this feature: - query_stats.addScannerStat(query_index, index, - QueryStat.ROWS_FROM_STORAGE, scanner.getRowsFetched()); - query_stats.addScannerStat(query_index, index, - QueryStat.COLUMNS_FROM_STORAGE, scanner.getColumnsFetched()); - query_stats.addScannerStat(query_index, index, - QueryStat.BYTES_FROM_STORAGE, scanner.getBytesFetched()); */ - query_stats.addScannerStat(query_index, index, - QueryStat.HBASE_TIME, fetch_time); - query_stats.addScannerStat(query_index, index, - QueryStat.SUCCESSFUL_SCAN, e == null ? 1 : 0); - - // Post Scan stats - query_stats.addScannerStat(query_index, index, - QueryStat.ROWS_PRE_FILTER, rows_pre_filter); - query_stats.addScannerStat(query_index, index, - QueryStat.DPS_PRE_FILTER, dps_pre_filter); - query_stats.addScannerStat(query_index, index, - QueryStat.ROWS_POST_FILTER, rows_post_filter); - query_stats.addScannerStat(query_index, index, - QueryStat.DPS_POST_FILTER, dps_post_filter); - query_stats.addScannerStat(query_index, index, - QueryStat.SCANNER_UID_TO_STRING_TIME, uid_resolve_time); - query_stats.addScannerStat(query_index, index, - QueryStat.UID_PAIRS_RESOLVED, uids_resolved); - query_stats.addScannerStat(query_index, index, - QueryStat.COMPACTION_TIME, compaction_time); - } - - if (e != null) { - results.callback(e); - } else if (nrows < 1 && !seenAnnotation) { - results.callback(null); - } else { - results.callback(spans); - } - } - } - - new ScannerCB().scan(); - return results; } private Deferred> findSpansWithMultiGetter() throws HBaseException { diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java index 189708f91f..8316ca295e 100644 --- a/test/core/TestSaltScanner.java +++ b/test/core/TestSaltScanner.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2015 The OpenTSDB Authors. +// 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 @@ -38,7 +38,6 @@ 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; @@ -52,36 +51,30 @@ @PrepareForTest({ TSDB.class, Scanner.class, SaltScanner.class, Span.class, Const.class, UniqueId.class }) public class TestSaltScanner extends BaseTsdbTest { - private final static byte[] KEY_A = { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01 }; - // different tagv - private final static byte[] KEY_B = { 0x00, 0x00, 0x00, 0x01, - 0x50, (byte) 0xE2, 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02 }; - // same as A bug different time - private final static byte[] KEY_C = { 0x00, 0x00, 0x00, 0x01, - 0x51, (byte) 0x0B, 0x13, (byte) 0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01 }; - private final static byte[] FAMILY = "t".getBytes(); - private final static byte[] QUALIFIER_A = { 0x00, 0x00 }; - private final static byte[] QUALIFIER_B = { 0x00, 0x10 }; - private final static byte[] VALUE = { 0x42 }; - private final static long VALUE_LONG = 66; + protected byte[] KEY_A; + protected byte[] KEY_B; + protected byte[] KEY_C; + protected final static byte[] FAMILY = "t".getBytes(); + protected final static byte[] QUALIFIER_A = { 0x00, 0x00 }; + protected final static byte[] QUALIFIER_B = { 0x00, 0x10 }; + protected final static byte[] VALUE = { 0x42 }; + protected final static long VALUE_LONG = 66; - private final static int NUM_BUCKETS = 2; - private List scanners; - private TreeMap spans; - private List filters; + protected List scanners; + protected TreeMap spans; + protected List filters; - private List>> kvs_a; - private List>> kvs_b; + protected List>> kvs_a; + protected List>> kvs_b; - private Scanner scanner_a; - private Scanner scanner_b; + protected Scanner scanner_a; + protected Scanner scanner_b; @Before - public void beforeLocal() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); + public void beforeLocal() throws Exception { + KEY_A = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + KEY_B = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_B_STRING); + KEY_C = getRowKey(METRIC_STRING, 1359680400, TAGK_STRING, TAGV_STRING); filters = new ArrayList(); @@ -94,12 +87,6 @@ public void ctor() { assertNotNull(new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters)); } - @Test (expected = IllegalArgumentException.class) - public void ctorSaltDisabled() { - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(0); - new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); - } - @Test (expected = IllegalArgumentException.class) public void ctorNullTSDB() { new SaltScanner(null, METRIC_BYTES, scanners, spans, filters); @@ -122,7 +109,7 @@ public void ctorNullScanners() { @Test (expected = IllegalArgumentException.class) public void ctorNotEnoughScanners() { - scanners.remove(1); + scanners.remove(0); new SaltScanner(tsdb, METRIC_BYTES, scanners, spans, filters); } @@ -312,22 +299,24 @@ public void scanHBaseScannerFromDeferredA() throws Exception { @Test public void scanHBaseScannerFromDeferredB() throws Exception { - setupMockScanners(false); - // we can't instantiate an HBaseException so just throw a RuntimeException - final RuntimeException e = new RuntimeException("From HBase"); - when(scanner_b.nextRows()) - .thenReturn(Deferred.fromResult(kvs_b.get(0))) - .thenReturn(Deferred.fromResult(kvs_b.get(1))) - .thenReturn(Deferred. - >>fromError(e)); - - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - spans, filters); - try { - scanner.scan().joinUninterruptibly(); - fail("Expected a runtime exception here"); - } catch (RuntimeException re) { - assertEquals(e, re); + if (Const.SALT_WIDTH() > 0) { + setupMockScanners(false); + // we can't instantiate an HBaseException so just throw a RuntimeException + final RuntimeException e = new RuntimeException("From HBase"); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred. + >>fromError(e)); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); + try { + scanner.scan().joinUninterruptibly(); + fail("Expected a runtime exception here"); + } catch (RuntimeException re) { + assertEquals(e, re); + } } } @@ -352,21 +341,23 @@ public void scanHBaseScannerThrownA() throws Exception { @Test public void scanHBaseScannerThrownB() throws Exception { - setupMockScanners(false); - // we can't instantiate an HBaseException so just throw a RuntimeException - final RuntimeException e = new RuntimeException("From HBase"); - when(scanner_b.nextRows()) - .thenReturn(Deferred.fromResult(kvs_b.get(0))) - .thenReturn(Deferred.fromResult(kvs_b.get(1))) - .thenThrow(e); - - final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - spans, filters); - try { - scanner.scan().joinUninterruptibly(); - fail("Expected a runtime exception here"); - } catch (RuntimeException re) { - assertEquals(e, re); + if (Const.SALT_WIDTH() > 0) { + setupMockScanners(false); + // we can't instantiate an HBaseException so just throw a RuntimeException + final RuntimeException e = new RuntimeException("From HBase"); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenThrow(e); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, filters); + try { + scanner.scan().joinUninterruptibly(); + fail("Expected a runtime exception here"); + } catch (RuntimeException re) { + assertEquals(e, re); + } } } @@ -424,20 +415,32 @@ public void scanCompactionRuntimeException() throws Exception { * Sets up a pair of scanners with either a list of values or no data * @param no_data Whether or not to return 0 data. */ - private void setupMockScanners(final boolean no_data) { - scanners = new ArrayList(NUM_BUCKETS); - scanner_a = mock(Scanner.class); - scanner_b = mock(Scanner.class); - if (no_data) { - when(scanner_a.nextRows()).thenReturn( - Deferred.>>fromResult(null)); - when(scanner_b.nextRows()).thenReturn( - Deferred.>>fromResult(null)); + protected void setupMockScanners(final boolean no_data) throws Exception { + if (Const.SALT_WIDTH() > 0) { + scanners = new ArrayList(Const.SALT_BUCKETS()); + scanner_a = mock(Scanner.class); + scanner_b = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + when(scanner_b.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); + } + scanners.add(scanner_a); + scanners.add(scanner_b); } else { - setupValues(); + scanners = new ArrayList(1); + scanner_a = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); + } + scanners.add(scanner_a); } - scanners.add(scanner_a); - scanners.add(scanner_b); } /** @@ -447,7 +450,8 @@ private void setupMockScanners(final boolean no_data) { * only happen if you add the timestamp to the salt calculation, which we * may do in the future. We're testing now for future proofing. */ - private void setupValues() { + protected void setupValues() throws Exception { + setDataPointStorage(); kvs_a = new ArrayList>>(3); kvs_b = new ArrayList>>(2); @@ -478,7 +482,6 @@ private void setupValues() { break; case 3: key = Arrays.copyOf(KEY_A, KEY_A.length); - key[0] = 1; row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); row.add(new KeyValue(key, FAMILY, new byte[] { 1, 0, 0 }, 0, note.getBytes(Charset.forName("UTF8")))); @@ -486,22 +489,31 @@ private void setupValues() { break; case 4: key = Arrays.copyOf(KEY_C, KEY_C.length); - key[0] = 1; row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); kvs_b.add(rows); break; } } - when(scanner_a.nextRows()) + if (Const.SALT_WIDTH() > 0) { + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.>>fromResult(null)); + + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); + } else { + when(scanner_a.nextRows()) .thenReturn(Deferred.fromResult(kvs_a.get(0))) .thenReturn(Deferred.fromResult(kvs_a.get(1))) .thenReturn(Deferred.fromResult(kvs_a.get(2))) - .thenReturn(Deferred.>>fromResult(null)); - - when(scanner_b.nextRows()) .thenReturn(Deferred.fromResult(kvs_b.get(0))) .thenReturn(Deferred.fromResult(kvs_b.get(1))) .thenReturn(Deferred.>>fromResult(null)); + } } } diff --git a/test/core/TestSaltScannerSalted.java b/test/core/TestSaltScannerSalted.java new file mode 100644 index 0000000000..bd859fb09f --- /dev/null +++ b/test/core/TestSaltScannerSalted.java @@ -0,0 +1,53 @@ +// 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 java.util.ArrayList; +import java.util.TreeMap; + +import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.uid.UniqueId; + +import org.hbase.async.Scanner; +import org.junit.Before; +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; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, Scanner.class, SaltScanner.class, Span.class, + Const.class, UniqueId.class }) +public class TestSaltScannerSalted extends TestSaltScanner { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + KEY_A = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + KEY_B = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_B_STRING); + KEY_C = getRowKey(METRIC_STRING, 1359680400, TAGK_STRING, TAGV_STRING); + + filters = new ArrayList(); + + spans = new TreeMap(new RowKey.SaltCmp()); + setupMockScanners(true); + } + +} From c0aba073d654ec6a213757aeeed5eabc36462d71 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Mon, 29 May 2017 12:27:04 -0700 Subject: [PATCH 639/826] Modify the HistogramDataPointDecoderManager to load plugins and create maps on instantiation that map 1 byte IDs to classes/decoders so that we can use that for writing/reading data in HBase. Signed-off-by: Chris Larsen --- src/core/CompactionQueue.java | 3 +- src/core/HistogramDataPointDecoder.java | 15 +- .../HistogramDataPointDecoderManager.java | 152 +++++++++++---- src/core/Internal.java | 17 +- src/core/SaltScanner.java | 2 +- src/core/SimpleHistogramDecoder.java | 2 +- src/core/TSDB.java | 34 +++- .../LongHistogramDataPointForTestDecoder.java | 2 +- .../TestHistogramDataPointDecoderManager.java | 154 ++++++++++++++++ test/core/TestSaltScanner.java | 12 +- test/core/TestSaltScannerHistogram.java | 173 +++++++++++++----- test/core/TestSaltScannerHistogramSalted.java | 34 ++++ test/core/TestTSDBAddHistogramPoint.java | 120 +++++++----- test/core/TestTsdbQueryHistogramQueries.java | 86 +++++++-- .../TestTsdbQueryHistogramQueriesSalted.java | 29 +++ 15 files changed, 665 insertions(+), 170 deletions(-) create mode 100644 test/core/TestHistogramDataPointDecoderManager.java create mode 100644 test/core/TestSaltScannerHistogramSalted.java create mode 100644 test/core/TestTsdbQueryHistogramQueriesSalted.java diff --git a/src/core/CompactionQueue.java b/src/core/CompactionQueue.java index 7c7fcdb541..857d60fbd2 100644 --- a/src/core/CompactionQueue.java +++ b/src/core/CompactionQueue.java @@ -444,7 +444,8 @@ private int buildHeapProcessAnnotations() { annotations.add(JSON.parseToObject(kv.value(), Annotation.class)); } else if (qual[0] == HistogramDataPoint.PREFIX) { try { - HistogramDataPoint histogram = Internal.decodeHistogramDataPoint(kv, tsdb.getConfig()); + HistogramDataPoint histogram = + Internal.decodeHistogramDataPoint(tsdb, kv); histograms.add(histogram); } catch (Throwable t) { LOG.error("Failed to decode histogram data point", t); diff --git a/src/core/HistogramDataPointDecoder.java b/src/core/HistogramDataPointDecoder.java index eca7e9f8ec..eed6004975 100644 --- a/src/core/HistogramDataPointDecoder.java +++ b/src/core/HistogramDataPointDecoder.java @@ -15,18 +15,27 @@ /** * Creates {@code HistogramDataPoint} from raw data and timestamp. * - * NOTE: Implementation of this interface should be thread safe. + * NOTE: Implementation of this plugin should be thread safe. * @see HistogramDataPointDecoderManager * * @since 2.4 */ -public interface HistogramDataPointDecoder { +public abstract class HistogramDataPointDecoder { + /** + * Default empty ctor, required for plugin and class instantiation. + * WARNING Any overrides with arguments will be ignored. + */ + public HistogramDataPointDecoder() { + + } + /** * Creates {@code HistogramDataPoint} from raw data and timestamp. * @param raw_data The encoded byte array of the histogram data * @param timestamp The timestamp of this data point * @return The decoded histogram data point instance */ - HistogramDataPoint decode(final byte[] raw_data, final long timestamp); + public abstract HistogramDataPoint decode(final byte[] raw_data, + final long timestamp); } diff --git a/src/core/HistogramDataPointDecoderManager.java b/src/core/HistogramDataPointDecoderManager.java index 7c17bb2df3..06778ebe46 100644 --- a/src/core/HistogramDataPointDecoderManager.java +++ b/src/core/HistogramDataPointDecoderManager.java @@ -12,8 +12,21 @@ // see . package net.opentsdb.core; -import java.util.HashMap; +import java.io.File; +import java.io.IOException; import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import com.google.common.io.Files; + +import net.opentsdb.utils.JSON; +import net.opentsdb.utils.PluginLoader; /** *

    @@ -33,42 +46,111 @@ * @since 2.4 */ public class HistogramDataPointDecoderManager { + private static final Logger LOG = LoggerFactory.getLogger( + HistogramDataPointDecoderManager.class); + + /** The map of IDs to decoders. */ + private final Map decoders; + + /** The map of classes to decoder IDs. */ + private final Map, Byte> decoder_ids; - private static final Map decoders = - new HashMap(); - - /** - * Return the singleton instance of the given decoder. - * @param decoder_name The full class name of the decoder - * @return The singleton instance of the given decoder - * - * @throws RuntimeException If failed to create the decoder - */ - public static HistogramDataPointDecoder getDecoder(final String decoder_name) { - HistogramDataPointDecoder decoder = decoders.get(decoder_name); - if (decoder == null) { - synchronized(decoders) { - decoder = decoders.get(decoder_name); - if (decoder == null) { - decoder = createInstance(decoder_name); - decoders.put(decoder_name, decoder); - } - } - } - - return decoder; + /** + * Default ctor that loads the decoder map. It will parse the + * 'tsd.core.histograms.config' parameter. If it ends with .json then we'll + * try to load a file of that name, otherwise we'll just parse it as raw JSON. + * For each map in the JSON we search the classpath then loaded plugins. + * + * @param tsdb A non-null TSDB to load the config from. + * @throws IllegalArgumentException if the config was null or empty or if the + * JSON was malformed. + * @throws RuntimeException if the file couldn't be opened. + * @throws IllegalStateException if no classes/plugins of the type could be + * found OR if one was found but couldn't be instantiated. + */ + public HistogramDataPointDecoderManager(final TSDB tsdb) { + final String config = tsdb.getConfig().getString("tsd.core.histograms.config"); + if (Strings.isNullOrEmpty(config)) { + throw new IllegalArgumentException("Missing configuration " + + "'tsd.core.histograms.config'"); } - - private static HistogramDataPointDecoder createInstance(final String decoder_name) { - try { - Class c = Class.forName(decoder_name); - return (HistogramDataPointDecoder) c.newInstance(); - } catch (Exception exp) { - throw new RuntimeException("Failed to create the decoder instance of " - + decoder_name, exp); - } + + final TypeReference> type_ref = + new TypeReference>() {}; + final Map mappings; + if (config.endsWith(".json")) { + final String json; + try { + json = Files.toString(new File(config), Const.UTF8_CHARSET); + } catch (IOException e) { + throw new RuntimeException("Unable to open plugin config file: " + + config, e); + } + mappings = JSON.parseToObject(json, type_ref); + } else { + mappings = JSON.parseToObject(config, type_ref); } - - private HistogramDataPointDecoderManager() { + + decoders = Maps.newHashMap(); + decoder_ids = Maps.newHashMap(); + + if (mappings.isEmpty()) { + LOG.warn("No histograms configured. Histogram writes and reads will " + + "throw exceptions."); + return; + } + + for (final Entry mapping : mappings.entrySet()) { + HistogramDataPointDecoder decoder = null; + try { + final Class clazz = Class.forName(mapping.getKey()); + decoder = (HistogramDataPointDecoder) clazz.newInstance(); + } catch (ClassNotFoundException e) { + decoder = PluginLoader + .loadSpecificPlugin(mapping.getKey(), HistogramDataPointDecoder.class); + } catch (InstantiationException e) { + throw new IllegalStateException("Found decoder '" + mapping.getKey() + + "' on the class path but failed to instantiate it.", e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Found decoder '" + mapping.getKey() + + "' on the class path but we did not have permission to access it.", e); + } + + if (decoder == null) { + throw new IllegalStateException("Unable to find a decoder named '" + + mapping.getKey() + "'"); + } else { + decoders.put(mapping.getValue(), decoder); + decoder_ids.put(decoder.getClass(), mapping.getValue()); + LOG.info("Successfully loaded decoder '" + mapping.getKey() + + "' with ID " + mapping.getValue()); + } + } + } + + /** + * Return the instance of the given decoder. + * @param id The numeric ID of the decoder (the first byte in storage). + * @return The instance of the given decoder + * @throws IllegalArgumentException if no decoder was found for the given ID. + */ + public HistogramDataPointDecoder getDecoder(final byte id) { + final HistogramDataPointDecoder decoder = decoders.get(id); + if (decoder == null) { + throw new IllegalArgumentException("No decoder found mapped to ID " + id); + } + return decoder; + } + + public byte getDecoder(final Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Clazz cannot be null."); + } + final Byte id = decoder_ids.get(clazz); + if (id == null) { + throw new IllegalArgumentException("No decoder ID assigned to class " + + clazz); } + return id; + } } diff --git a/src/core/Internal.java b/src/core/Internal.java index 6f64a6b4b2..4ef1182712 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -1056,25 +1056,26 @@ public static long getTimeStampFromNonDP(final long base_time, byte[] quantifier * to get the decoder * @return the decoded {@code HistogramDataPoint} */ - public static HistogramDataPoint decodeHistogramDataPoint(final KeyValue kv, final Config config) { + public static HistogramDataPoint decodeHistogramDataPoint(final TSDB tsdb, + final KeyValue kv) { long timestamp = Internal.baseTime(kv.key()); - return decodeHistogramDataPoint(timestamp, kv.qualifier(), kv.value(), config); + return decodeHistogramDataPoint(tsdb, timestamp, kv.qualifier(), kv.value()); } /** * Decode the histogram point from the given key and values + * @param tsdb The TSDB to use when fetching the decoder manager. * @param base_time the base time of the histogram * @param qualifier the qualifier used to store the histogram * @param value the encoded value of the histogram - * @param config config object of TSDB, will use {@code "tsd.core.hist_decoder"} - * to get the decoder * @return the decoded {@code HistogramDataPoint} */ - public static HistogramDataPoint decodeHistogramDataPoint(final long base_time, final byte[] qualifier, - final byte[] value, final Config config) { - final String decoder_name = config.hist_decoder_name(); + public static HistogramDataPoint decodeHistogramDataPoint(final TSDB tsdb, + final long base_time, + final byte[] qualifier, + final byte[] value) { final HistogramDataPointDecoder decoder = - HistogramDataPointDecoderManager.getDecoder(decoder_name); + tsdb.histogramManager().getDecoder(value[0]); long timestamp = getTimeStampFromNonDP(base_time, qualifier); return decoder.decode(value, timestamp); } diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 36b60376c7..8775ec3fd2 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -690,7 +690,7 @@ void processRow(final byte[] key, final ArrayList row) { } else if (qual[0] == HistogramDataPoint.PREFIX) { try { HistogramDataPoint histogram = - Internal.decodeHistogramDataPoint(kv, tsdb.getConfig()); + Internal.decodeHistogramDataPoint(tsdb, kv); hists.add(histogram); } catch (Throwable t) { LOG.error("Failed to decode histogram data point", t); diff --git a/src/core/SimpleHistogramDecoder.java b/src/core/SimpleHistogramDecoder.java index 29eae3aa1d..688439e7b7 100644 --- a/src/core/SimpleHistogramDecoder.java +++ b/src/core/SimpleHistogramDecoder.java @@ -28,7 +28,7 @@ *

    * @since 2.4 */ -public class SimpleHistogramDecoder implements HistogramDataPointDecoder { +public class SimpleHistogramDecoder extends HistogramDataPointDecoder { @Override public HistogramDataPoint decode(final byte[] raw_data, final long timestamp) { final Histogram histogram; diff --git a/src/core/TSDB.java b/src/core/TSDB.java index be00334796..9e2b153db2 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -168,6 +168,11 @@ public final class TSDB { /** Whether or not to block writing of derived rollups/pre-ags */ private final boolean rollups_block_derived; + /** An optional histogram manger used when the TSD will be dealing with + * histograms and sketches. Instantiated ONLY if + * {@link #initializePlugins(boolean)} was called.*/ + private HistogramDataPointDecoderManager histogram_manager; + /** Writes rejected by the filter */ private final AtomicLong rejected_dps = new AtomicLong(); private final AtomicLong rejected_aggregate_dps = new AtomicLong(); @@ -295,7 +300,7 @@ public TSDB(final HBaseClient client, final Config config) { // set any extra tags from the config for stats StatsCollector.setGlobalTags(config); - + LOG.debug(config.dumpConfiguration()); } @@ -501,6 +506,13 @@ 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 HistogramDataPointDecoderManager(this); + } else { + histogram_manager = null; + } } /** @@ -1067,17 +1079,19 @@ public Deferred addPoint(final String metric, * data. */ public Deferred addHistogramPoint(final String metric, - final long timestamp, - final byte[] raw_data, - final Map tags) { + final long timestamp, + final byte[] raw_data, + final Map tags) { if (raw_data == null || raw_data.length < MIN_HISTOGRAM_BYTES) { - throw new IllegalArgumentException("The histogram raw data is invalid: " + Bytes.pretty(raw_data)); + throw new IllegalArgumentException("The histogram raw data is invalid: " + + Bytes.pretty(raw_data)); } - + checkTimestampAndTags(metric, timestamp, raw_data, tags, (short) 0); final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); - final byte[] qualifier = Internal.getQualifier(timestamp, HistogramDataPoint.PREFIX); + final byte[] qualifier = Internal.getQualifier(timestamp, + HistogramDataPoint.PREFIX); return storeIntoDB(metric, timestamp, raw_data, tags, (short) 0, row, qualifier); } @@ -2058,6 +2072,12 @@ public String getRawTagValue() { return raw_agg_tag_value; } + /** @return The optional histogram manager registered to this TSD. + * @since 2.4 */ + public HistogramDataPointDecoderManager histogramManager() { + return histogram_manager; + } + private final boolean isHistogram(final byte[] qualifier) { return (qualifier.length & 0x1) == 1; } diff --git a/test/core/LongHistogramDataPointForTestDecoder.java b/test/core/LongHistogramDataPointForTestDecoder.java index a8f481fa1d..edfdad62c4 100644 --- a/test/core/LongHistogramDataPointForTestDecoder.java +++ b/test/core/LongHistogramDataPointForTestDecoder.java @@ -12,7 +12,7 @@ // see . package net.opentsdb.core; -public class LongHistogramDataPointForTestDecoder implements HistogramDataPointDecoder { +public class LongHistogramDataPointForTestDecoder extends HistogramDataPointDecoder { @Override public HistogramDataPoint decode(byte[] raw_data, long timestamp) { diff --git a/test/core/TestHistogramDataPointDecoderManager.java b/test/core/TestHistogramDataPointDecoderManager.java new file mode 100644 index 0000000000..671aa17d37 --- /dev/null +++ b/test/core/TestHistogramDataPointDecoderManager.java @@ -0,0 +1,154 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; + +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 com.google.common.io.Files; + +import net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, HistogramDataPointDecoderManager.class, + Files.class }) +public class TestHistogramDataPointDecoderManager { + + private TSDB tsdb; + private Config config; + + @Before + public void before() throws Exception { + tsdb = PowerMockito.mock(TSDB.class); + config = new Config(false); + + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\": 0," + + "\"net.opentsdb.core.TestHistogramDataPointDecoderManager$MockDecoder\":1}"); + when(tsdb.getConfig()).thenReturn(config); + PowerMockito.mockStatic(Files.class); + } + + @Test + public void ctor() throws Exception { + HistogramDataPointDecoderManager manager = + new HistogramDataPointDecoderManager(tsdb); + assertEquals(0, manager.getDecoder(SimpleHistogramDecoder.class)); + assertEquals(1, manager.getDecoder(MockDecoder.class)); + assertTrue(manager.getDecoder((byte) 0) instanceof SimpleHistogramDecoder); + assertTrue(manager.getDecoder((byte) 1) instanceof MockDecoder); + + // bad JSON + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\": "); + try { + new HistogramDataPointDecoderManager(tsdb); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + config.overrideConfig("tsd.core.histograms.config", "nosuchfile.json"); + when(Files.toString(any(File.class), eq(Const.UTF8_CHARSET))) + .thenReturn("{\"net.opentsdb.core.SimpleHistogramDecoder\": 0}"); + manager = new HistogramDataPointDecoderManager(tsdb); + assertEquals(0, manager.getDecoder(SimpleHistogramDecoder.class)); + assertTrue(manager.getDecoder((byte) 0) instanceof SimpleHistogramDecoder); + + when(Files.toString(any(File.class), eq(Const.UTF8_CHARSET))) + .thenThrow(new IOException("Boo!")); + try { + new HistogramDataPointDecoderManager(tsdb); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { } + + // no such plugin + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.NoSuchPlugin\":0}"); + try { + new HistogramDataPointDecoderManager(tsdb); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + + // bad plugin + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.TestHistogramDataPointDecoderManager$MockDecoderBadly\":0}"); + try { + new HistogramDataPointDecoderManager(tsdb); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + } + + @Test + public void getDecoder() throws Exception { + final HistogramDataPointDecoderManager manager = + new HistogramDataPointDecoderManager(tsdb); + assertTrue(manager.getDecoder((byte) 0) instanceof SimpleHistogramDecoder); + + try { + manager.getDecoder((byte) 43); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } + + @Test + public void getDecoderClass() throws Exception { + final HistogramDataPointDecoderManager manager = + new HistogramDataPointDecoderManager(tsdb); + assertEquals(0, manager.getDecoder(SimpleHistogramDecoder.class)); + assertEquals(1, manager.getDecoder(MockDecoder.class)); + + try { + manager.getDecoder(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + manager.getDecoder(MockDecoderBadly.class); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } + + public static class MockDecoder extends HistogramDataPointDecoder { + + @Override + public HistogramDataPoint decode(byte[] raw_data, long timestamp) { + return null; + } + + } + + static class MockDecoderBadly extends HistogramDataPointDecoder { + + // not allowed! + public MockDecoderBadly(final long unwanted_param) { } + + @Override + public HistogramDataPoint decode(byte[] raw_data, long timestamp) { + return null; + } + + } +} diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java index 8316ca295e..118fa6b294 100644 --- a/test/core/TestSaltScanner.java +++ b/test/core/TestSaltScanner.java @@ -508,12 +508,12 @@ protected void setupValues() throws Exception { .thenReturn(Deferred.>>fromResult(null)); } else { when(scanner_a.nextRows()) - .thenReturn(Deferred.fromResult(kvs_a.get(0))) - .thenReturn(Deferred.fromResult(kvs_a.get(1))) - .thenReturn(Deferred.fromResult(kvs_a.get(2))) - .thenReturn(Deferred.fromResult(kvs_b.get(0))) - .thenReturn(Deferred.fromResult(kvs_b.get(1))) - .thenReturn(Deferred.>>fromResult(null)); + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); } } } diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index 11013820ba..163edb31ac 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -13,15 +13,23 @@ package net.opentsdb.core; import com.esotericsoftware.kryo.io.Output; +import com.google.common.collect.Maps; import com.stumbleupon.async.Deferred; + import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.query.filter.TagVRegexFilter; import net.opentsdb.query.filter.TagVWildcardFilter; import net.opentsdb.stats.QueryStats; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.Threads; + +import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,16 +37,20 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.TreeMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -50,35 +62,86 @@ Const.class, UniqueId.class, Tags.class, QueryStats.class, DateTime.class, HistogramDataPointDecoderManager.class, SimpleHistogram.class, SimpleHistogramDecoder.class}) -public class TestSaltScannerHistogram extends BaseTsdbTest { - private final static byte[] FAMILY = "t".getBytes(); - private final static byte[] QUALIFIER_A = { 0x06, 0x00, 0x00}; - private final static byte[] QUALIFIER_B = { 0x06, 0x10, 0x00 }; - - private byte[] VALUE; +public class TestSaltScannerHistogram extends BaseTsdbTest { + protected final static byte[] FAMILY = "t".getBytes(); + protected final static byte[] QUALIFIER_A = { 0x06, 0x00, 0x00}; + protected final static byte[] QUALIFIER_B = { 0x06, 0x10, 0x00 }; - private final static int NUM_BUCKETS = 2; - private List scanners; - private TreeMap spans; + protected byte[] VALUE; + + protected List scanners; + protected TreeMap spans; - private List>> kvs_a; - private List>> kvs_b; + protected List>> kvs_a; + protected List>> kvs_b; - private Scanner scanner_a; - private Scanner scanner_b; - private QueryStats query_stats; + protected Scanner scanner_a; + protected Scanner scanner_b; + protected QueryStats query_stats; - private byte[] key_a; + protected byte[] key_a; //different tagv - private byte[] key_b; + protected byte[] key_b; //same as A bug different time - private byte[] key_c; + protected byte[] key_c; @Before - public void beforeLocal() { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(NUM_BUCKETS); + public void before() throws Exception { + // Copying the whole thing as the SPY in the base mucks up the references. + uid_map = Maps.newHashMap(); + PowerMockito.mockStatic(Threads.class); + timer = new FakeTaskTimer(); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + + config = new Config(false); + config.overrideConfig("tsd.storage.enable_compaction", "false"); + tsdb = new TSDB(config); + + config.setAutoMetric(true); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + setupMetricMaps(); + setupTagkMaps(); + setupTagvMaps(); + + mockUID(UniqueIdType.METRIC, HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + + // add metrics and tags to the UIDs list for other functions to share + uid_map.put(METRIC_STRING, METRIC_BYTES); + uid_map.put(METRIC_B_STRING, METRIC_B_BYTES); + uid_map.put(NSUN_METRIC, NSUI_METRIC); + uid_map.put(HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + + uid_map.put(TAGK_STRING, TAGK_BYTES); + uid_map.put(TAGK_B_STRING, TAGK_B_BYTES); + uid_map.put(NSUN_TAGK, NSUI_TAGK); + + uid_map.put(TAGV_STRING, TAGV_BYTES); + uid_map.put(TAGV_B_STRING, TAGV_B_BYTES); + uid_map.put(NSUN_TAGV, NSUI_TAGV); + + uid_map.putAll(UIDS); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + tags = new HashMap(1); + tags.put(TAGK_STRING, TAGV_STRING); + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.LongHistogramDataPointForTestDecoder\": 0}"); + HistogramDataPointDecoderManager manager = + new HistogramDataPointDecoderManager(tsdb); + Whitebox.setInternalState(tsdb, "histogram_manager", manager); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); Output output = new Output(outBuffer); @@ -244,20 +307,32 @@ public void scanWithFiltersOnSameTagOneFail() throws Exception { * Sets up a pair of scanners with either a list of values or no data * @param no_data Whether or not to return 0 data. */ - private void setupMockScanners(final boolean no_data) { - scanners = new ArrayList(NUM_BUCKETS); - scanner_a = mock(Scanner.class); - scanner_b = mock(Scanner.class); - if (no_data) { - when(scanner_a.nextRows()).thenReturn( - Deferred.>>fromResult(null)); - when(scanner_b.nextRows()).thenReturn( - Deferred.>>fromResult(null)); + protected void setupMockScanners(final boolean no_data) { + if (Const.SALT_WIDTH() > 0) { + scanners = new ArrayList(Const.SALT_BUCKETS()); + scanner_a = mock(Scanner.class); + scanner_b = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + when(scanner_b.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); + } + scanners.add(scanner_a); + scanners.add(scanner_b); } else { - setupValues(); + scanners = new ArrayList(1); + scanner_a = mock(Scanner.class); + if (no_data) { + when(scanner_a.nextRows()).thenReturn( + Deferred.>>fromResult(null)); + } else { + setupValues(); + } + scanners.add(scanner_a); } - scanners.add(scanner_a); - scanners.add(scanner_b); } /** @@ -267,7 +342,7 @@ private void setupMockScanners(final boolean no_data) { * only happen if you add the timestamp to the salt calculation, which we * may do in the future. We're testing now for future proofing. */ - private void setupValues() { + protected void setupValues() { kvs_a = new ArrayList>>(3); kvs_b = new ArrayList>>(2); @@ -298,7 +373,6 @@ private void setupValues() { break; case 3: key = Arrays.copyOf(key_a, key_a.length); - key[0] = 1; row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); row.add(new KeyValue(key, FAMILY, new byte[] { 1, 0, 0 }, 0, note.getBytes(Charset.forName("UTF8")))); @@ -306,21 +380,30 @@ private void setupValues() { break; case 4: key = Arrays.copyOf(key_c, key_c.length); - key[0] = 1; row.add(new KeyValue(key, FAMILY, QUALIFIER_B, 0, VALUE)); kvs_b.add(rows); break; } } - when(scanner_a.nextRows()) - .thenReturn(Deferred.fromResult(kvs_a.get(0))) - .thenReturn(Deferred.fromResult(kvs_a.get(1))) - .thenReturn(Deferred.fromResult(kvs_a.get(2))) - .thenReturn(Deferred.>>fromResult(null)); - when(scanner_b.nextRows()) - .thenReturn(Deferred.fromResult(kvs_b.get(0))) - .thenReturn(Deferred.fromResult(kvs_b.get(1))) - .thenReturn(Deferred.>>fromResult(null)); + if (Const.SALT_WIDTH() > 0) { + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.>>fromResult(null)); + when(scanner_b.nextRows()) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); + } else { + when(scanner_a.nextRows()) + .thenReturn(Deferred.fromResult(kvs_a.get(0))) + .thenReturn(Deferred.fromResult(kvs_a.get(1))) + .thenReturn(Deferred.fromResult(kvs_a.get(2))) + .thenReturn(Deferred.fromResult(kvs_b.get(0))) + .thenReturn(Deferred.fromResult(kvs_b.get(1))) + .thenReturn(Deferred.>>fromResult(null)); + } } } diff --git a/test/core/TestSaltScannerHistogramSalted.java b/test/core/TestSaltScannerHistogramSalted.java new file mode 100644 index 0000000000..9e4d56fac3 --- /dev/null +++ b/test/core/TestSaltScannerHistogramSalted.java @@ -0,0 +1,34 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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.powermock.api.mockito.PowerMockito; + +public class TestSaltScannerHistogramSalted extends TestSaltScannerHistogram { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + + key_a = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + TAGK_B_STRING, TAGV_STRING); + key_b = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_B_STRING, + TAGK_B_STRING, TAGV_STRING); + key_c = getRowKey(METRIC_STRING, 1359680400, TAGK_STRING, TAGV_STRING, + TAGK_B_STRING, TAGV_STRING); + } + +} diff --git a/test/core/TestTSDBAddHistogramPoint.java b/test/core/TestTSDBAddHistogramPoint.java index 31f4de3e74..fddd4f69fd 100644 --- a/test/core/TestTSDBAddHistogramPoint.java +++ b/test/core/TestTSDBAddHistogramPoint.java @@ -1,3 +1,15 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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; @@ -14,64 +26,72 @@ public class TestTSDBAddHistogramPoint extends BaseTsdbTest { - private static final byte HISTOGRAM_PREFIX = 0x6; + private static final byte HISTOGRAM_PREFIX = 0x6; - @Before - public void beforeLocal() throws Exception { - storage = new MockBase(tsdb, client, true, true, true, true); - } + @Before + public void beforeLocal() throws Exception { + storage = new MockBase(tsdb, client, true, true, true, true); + } - @Test - public void addHistogramPoint() throws Exception { - byte[] testRawValue = "Test Raw Value".getBytes(); - tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags).joinUninterruptibly(); - final byte[] row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); - byte[] qualifier = Internal.getQualifier(1356998400, HISTOGRAM_PREFIX); - final byte[] value = storage.getColumn(row, qualifier); - assertNotNull(value); - assertArrayEquals(testRawValue, value); - } + @Test + public void addHistogramPoint() throws Exception { + byte[] testRawValue = "Test Raw Value".getBytes(); + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags) + .joinUninterruptibly(); + final byte[] row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + byte[] qualifier = Internal.getQualifier(1356998400, HISTOGRAM_PREFIX); + final byte[] value = storage.getColumn(row, qualifier); + assertNotNull(value); + assertArrayEquals(testRawValue, value); + } - @Test (expected = IllegalArgumentException.class) - public void addHistogramPointShortRawData() throws Exception { - byte[] testRawValue = new byte[1]; - tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags).joinUninterruptibly(); - } + @Test (expected = IllegalArgumentException.class) + public void addHistogramPointShortRawData() throws Exception { + byte[] testRawValue = new byte[1]; + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags) + .joinUninterruptibly(); + } - @Test (expected = IllegalArgumentException.class) - public void addHistogramPointNullRawData() throws Exception { - tsdb.addHistogramPoint(METRIC_STRING, 1356998400, null, tags).joinUninterruptibly(); - } + @Test (expected = IllegalArgumentException.class) + public void addHistogramPointNullRawData() throws Exception { + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, null, tags) + .joinUninterruptibly(); + } - @Test - public void addHistogramPointCallRTPublisher() throws Exception { - byte[] raw_data = new byte[5]; - RTPublisher rt_publisher = mock(RTPublisher.class); - setField(tsdb, "rt_publisher", rt_publisher); - tsdb.addHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags).joinUninterruptibly(); + @Test + public void addHistogramPointCallRTPublisher() throws Exception { + byte[] raw_data = new byte[5]; + RTPublisher rt_publisher = mock(RTPublisher.class); + setField(tsdb, "rt_publisher", rt_publisher); + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags) + .joinUninterruptibly(); - byte[] tsuid = new byte[]{ 0, 0, 1, 0, 0, 1, 0, 0, 1}; - verify(rt_publisher, times(1)).publishHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags, tsuid); - } + byte[] tsuid = new byte[]{ 0, 0, 1, 0, 0, 1, 0, 0, 1}; + verify(rt_publisher, times(1)).publishHistogramPoint(METRIC_STRING, + 1356998400, raw_data, tags, tsuid); + } - @Test - public void addHistogramPointTSFilterPlugin() throws Exception { - byte[] raw_data = new byte[5]; - WriteableDataPointFilterPlugin ts_filter = mock(WriteableDataPointFilterPlugin.class); - when(ts_filter.filterDataPoints()).thenReturn(true); - when(ts_filter.allowHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags)). - thenReturn(Deferred.fromResult(true)); - setField(tsdb, "ts_filter", ts_filter); - tsdb.addHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags).joinUninterruptibly(); + @Test + public void addHistogramPointTSFilterPlugin() throws Exception { + byte[] raw_data = new byte[5]; + WriteableDataPointFilterPlugin ts_filter = + mock(WriteableDataPointFilterPlugin.class); + when(ts_filter.filterDataPoints()).thenReturn(true); + when(ts_filter.allowHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags)). + thenReturn(Deferred.fromResult(true)); + setField(tsdb, "ts_filter", ts_filter); + tsdb.addHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags) + .joinUninterruptibly(); - verify(ts_filter, times(1)).allowHistogramPoint(METRIC_STRING, 1356998400, raw_data, tags); - } + verify(ts_filter, times(1)).allowHistogramPoint(METRIC_STRING, + 1356998400, raw_data, tags); + } - private static void setField(TSDB tsdb, String field_name, Object field_value) - throws NoSuchFieldException, IllegalAccessException { - Field field = TSDB.class.getDeclaredField(field_name); - field.setAccessible(true); - field.set(tsdb, field_value); - } + private static void setField(TSDB tsdb, String field_name, Object field_value) + throws NoSuchFieldException, IllegalAccessException { + Field field = TSDB.class.getDeclaredField(field_name); + field.setAccessible(true); + field.set(tsdb, field_value); + } } diff --git a/test/core/TestTsdbQueryHistogramQueries.java b/test/core/TestTsdbQueryHistogramQueries.java index c705151add..322468ec2f 100644 --- a/test/core/TestTsdbQueryHistogramQueries.java +++ b/test/core/TestTsdbQueryHistogramQueries.java @@ -17,37 +17,100 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import org.hbase.async.HBaseClient; +import org.jboss.netty.util.HashedWheelTimer; 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 com.google.common.collect.Maps; + import net.opentsdb.meta.Annotation; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; +@RunWith(PowerMockRunner.class) +@PrepareForTest({TSDB.class, TsdbQuery.class }) public class TestTsdbQueryHistogramQueries extends BaseTsdbTest { - private TsdbQuery query = null; + protected TsdbQuery query = null; @Before - public void beforeLocal() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); - PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + public void before() throws Exception { + // Copying the whole thing as the SPY in the base mucks up the references. + uid_map = Maps.newHashMap(); + PowerMockito.mockStatic(Threads.class); + timer = new FakeTaskTimer(); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + + config = new Config(false); + config.overrideConfig("tsd.storage.enable_compaction", "false"); + tsdb = new TSDB(config); + + config.setAutoMetric(true); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + setupMetricMaps(); + setupTagkMaps(); + setupTagvMaps(); + + mockUID(UniqueIdType.METRIC, HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + + // add metrics and tags to the UIDs list for other functions to share + uid_map.put(METRIC_STRING, METRIC_BYTES); + uid_map.put(METRIC_B_STRING, METRIC_B_BYTES); + uid_map.put(NSUN_METRIC, NSUI_METRIC); + uid_map.put(HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + + uid_map.put(TAGK_STRING, TAGK_BYTES); + uid_map.put(TAGK_B_STRING, TAGK_B_BYTES); + uid_map.put(NSUN_TAGK, NSUI_TAGK); + + uid_map.put(TAGV_STRING, TAGV_BYTES); + uid_map.put(TAGV_B_STRING, TAGV_B_BYTES); + uid_map.put(NSUN_TAGV, NSUI_TAGV); + + uid_map.putAll(UIDS); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + tags = new HashMap(1); + tags.put(TAGK_STRING, TAGV_STRING); + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.LongHistogramDataPointForTestDecoder\": 0}"); + HistogramDataPointDecoderManager manager = + new HistogramDataPointDecoderManager(tsdb); + Whitebox.setInternalState(tsdb, "histogram_manager", manager); + query = new TsdbQuery(tsdb); } - + @Test public void runSingleTsMsSinglePercentile() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); - this.storeTestHistogramTimeSeriesMs(); - storage.dumpToSystemOut(); HashMap tags = new HashMap(1); tags.put("host", "web01"); query.setStartTime(1356998400); @@ -59,7 +122,6 @@ public void runSingleTsMsSinglePercentile() throws Exception { percentiles.add(per_98); query.setPercentiles(percentiles); - final DataPoints[] dps = query.runHistogram(); assertNotNull(dps); diff --git a/test/core/TestTsdbQueryHistogramQueriesSalted.java b/test/core/TestTsdbQueryHistogramQueriesSalted.java new file mode 100644 index 0000000000..89613cc7ba --- /dev/null +++ b/test/core/TestTsdbQueryHistogramQueriesSalted.java @@ -0,0 +1,29 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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.powermock.api.mockito.PowerMockito; + +public class TestTsdbQueryHistogramQueriesSalted extends TestTsdbQueryHistogramQueries { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + } + +} From 4afcd3dd532c898c87c8d5b9e142fd1a0bfebd1b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 31 May 2017 13:55:54 -0700 Subject: [PATCH 640/826] Add the HistogramDataPointRPC class so users can write histogram data to TSD over HTTP or telnet style socket with base64 encoding of the raw byte data. Still need to add maps for users coming from platforms other than Java. Rename the Histogram manager to HistogramCodecManager. Modify the histo methods to allow them to include or exclude the ID depending on if we're serializing for storage or consumption. Signed-off-by: Chris Larsen --- src/core/Histogram.java | 6 +- src/core/HistogramAggregationIterator.java | 13 +- src/core/HistogramCodecManager.java | 208 +++++++ src/core/HistogramDataPoint.java | 6 +- ...oder.java => HistogramDataPointCodec.java} | 32 +- .../HistogramDataPointDecoderManager.java | 156 ----- src/core/HistogramDownsampler.java | 11 +- src/core/HistogramPojo.java | 53 ++ src/core/HistogramRowSeq.java | 15 +- src/core/Internal.java | 8 +- src/core/SimpleHistogram.java | 27 +- src/core/SimpleHistogramDataPointAdapter.java | 13 +- src/core/SimpleHistogramDecoder.java | 39 +- src/core/TSDB.java | 14 +- src/tsd/HistogramDataPointRpc.java | 178 ++++++ src/tsd/PutDataPointRpc.java | 135 ++-- src/tsd/RollupDataPointRpc.java | 4 +- src/tsd/RpcManager.java | 3 + test/core/BaseTsdbTest.java | 42 +- test/core/HistogramSeekableViewForTest.java | 77 ++- test/core/LongHistogramDataPointForTest.java | 85 +-- .../LongHistogramDataPointForTestDecoder.java | 15 +- .../TestHistogramAggregationIterator.java | 98 +-- ...er.java => TestHistogramCodecManager.java} | 108 ++-- ...istogramDataPointsToDataPointsAdaptor.java | 12 +- test/core/TestHistogramDownsampler.java | 542 ++++++++++------ test/core/TestHistogramPojo.java | 55 ++ test/core/TestHistogramRowSeq.java | 167 +++-- test/core/TestHistogramSpan.java | 90 ++- test/core/TestSaltScannerHistogram.java | 14 +- test/core/TestSimpleHistogram.java | 176 ++++-- test/core/TestTSDBAddHistogramPoint.java | 4 +- test/core/TestTsdbQueryHistogramQueries.java | 4 +- test/tsd/TestHistogramDataPointRpc.java | 582 ++++++++++++++++++ test/tsd/TestRollupRpc.java | 2 - 35 files changed, 2227 insertions(+), 767 deletions(-) create mode 100644 src/core/HistogramCodecManager.java rename src/core/{HistogramDataPointDecoder.java => HistogramDataPointCodec.java} (62%) delete mode 100644 src/core/HistogramDataPointDecoderManager.java create mode 100644 src/core/HistogramPojo.java create mode 100644 src/tsd/HistogramDataPointRpc.java rename test/core/{TestHistogramDataPointDecoderManager.java => TestHistogramCodecManager.java} (55%) create mode 100644 test/core/TestHistogramPojo.java create mode 100644 test/tsd/TestHistogramDataPointRpc.java diff --git a/src/core/Histogram.java b/src/core/Histogram.java index 5631ede2ad..94bd2d7a5c 100644 --- a/src/core/Histogram.java +++ b/src/core/Histogram.java @@ -17,9 +17,9 @@ public interface Histogram { - public byte[] histogram(); + public byte[] histogram(final boolean include_id); - public void fromHistogram(final byte[] raw); + public void fromHistogram(final byte[] raw, final boolean includes_id); public double percentile(final double p); @@ -29,6 +29,8 @@ public interface Histogram { public Histogram clone(); + public int getId(); + void aggregate(Histogram histo, HistogramAggregation func); void aggregate(List histos, HistogramAggregation func); diff --git a/src/core/HistogramAggregationIterator.java b/src/core/HistogramAggregationIterator.java index 8c74465d9e..4e1d97acc2 100644 --- a/src/core/HistogramAggregationIterator.java +++ b/src/core/HistogramAggregationIterator.java @@ -186,13 +186,13 @@ public long timestamp() { } @Override - public byte[] getRawData() { - return value.getRawData(); + public byte[] getRawData(final boolean include_id) { + return value.getRawData(include_id); } @Override - public void resetFromRawData(byte[] raw_data) { - value.resetFromRawData(raw_data); + public void resetFromRawData(byte[] raw_data, final boolean includes_id) { + value.resetFromRawData(raw_data, includes_id); } @Override @@ -220,6 +220,11 @@ public HistogramDataPoint cloneAndSetTimestamp(long timestamp) { return value.cloneAndSetTimestamp(timestamp); } + @Override + public int getId() { + return value.getId(); + } + @Override public boolean hasNext() { for (int i = 0; i < iterators.length; ++i) { diff --git a/src/core/HistogramCodecManager.java b/src/core/HistogramCodecManager.java new file mode 100644 index 0000000000..faf4518dc0 --- /dev/null +++ b/src/core/HistogramCodecManager.java @@ -0,0 +1,208 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.io.Files; + +import net.opentsdb.utils.JSON; +import net.opentsdb.utils.PluginLoader; + +/** + *

    + * Manages the histogram codecs loaded by default or as plugins. Codecs are + * defined in the 'tsd.core.histograms.config' property as either direct, + * escaped JSON or a file with the ".json" extension. Each codec config entry + * must include the full class name and a unique ID for the type of histogram + * or digest stored. + * WARNING: After writing data with a given ID, you cannot change the + * ID and read that data any more. + *

    + * This class is thread safe + * + * @since 2.4 + */ +public class HistogramCodecManager { + private static final Logger LOG = LoggerFactory.getLogger( + HistogramCodecManager.class); + + /** The map of IDs to decoders. */ + private final Map codecs; + + /** The map of classes to decoder IDs. */ + private final Map, Integer> codecs_ids; + + /** + * Default ctor that loads the codec map. It will parse the + * 'tsd.core.histograms.config' parameter. If it ends with .json then we'll + * try to load a file of that name, otherwise we'll just parse it as raw JSON. + * For each map in the JSON we search the classpath then loaded plugins. + * + * @param tsdb A non-null TSDB to load the config from. + * @throws IllegalArgumentException if the config was null or empty or if the + * JSON was malformed. + * @throws RuntimeException if the file couldn't be opened. + * @throws IllegalStateException if no classes/plugins of the type could be + * found OR if one was found but couldn't be instantiated. + */ + public HistogramCodecManager(final TSDB tsdb) { + final String config = tsdb.getConfig().getString("tsd.core.histograms.config"); + if (Strings.isNullOrEmpty(config)) { + throw new IllegalArgumentException("Missing configuration " + + "'tsd.core.histograms.config'"); + } + + final TypeReference> type_ref = + new TypeReference>() {}; + final Map mappings; + if (config.endsWith(".json")) { + final String json; + try { + json = Files.toString(new File(config), Const.UTF8_CHARSET); + } catch (IOException e) { + throw new RuntimeException("Unable to open plugin config file: " + + config, e); + } + mappings = JSON.parseToObject(json, type_ref); + } else { + mappings = JSON.parseToObject(config, type_ref); + } + + codecs = Maps.newHashMap(); + codecs_ids = Maps.newHashMap(); + + if (mappings.isEmpty()) { + LOG.warn("No histograms configured. Histogram writes and reads will " + + "throw exceptions."); + return; + } + + final Set ids = Sets.newHashSet(); + for (final Entry mapping : mappings.entrySet()) { + if (mapping.getValue() < 0 || mapping.getValue() > 255) { + throw new IllegalArgumentException("ID for codec '" + mapping.getKey() + + "' must be from 0 to 255."); + } + if (ids.contains(mapping.getValue())) { + throw new IllegalArgumentException("Duplicate ID found for codec '" + + mapping.getKey() + "': " + mapping.getValue()); + } + ids.add(mapping.getValue()); + + HistogramDataPointCodec codec = null; + try { + final Class clazz = Class.forName(mapping.getKey()); + codec = (HistogramDataPointCodec) clazz.newInstance(); + } catch (ClassNotFoundException e) { + codec = PluginLoader + .loadSpecificPlugin(mapping.getKey(), HistogramDataPointCodec.class); + } catch (InstantiationException e) { + throw new IllegalStateException("Found decoder '" + mapping.getKey() + + "' on the class path but failed to instantiate it.", e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Found decoder '" + mapping.getKey() + + "' on the class path but we did not have permission to access it.", e); + } + + if (codec == null) { + throw new IllegalStateException("Unable to find a decoder named '" + + mapping.getKey() + "'"); + } else { + codec.setId(mapping.getValue()); + codecs.put(mapping.getValue(), codec); + codecs_ids.put(codec.getClass(), mapping.getValue()); + LOG.info("Successfully loaded decoder '" + mapping.getKey() + + "' with ID " + mapping.getValue()); + } + } + } + + /** + * Return the instance of the given codec. + * @param id The numeric ID of the codec (the first byte in storage). + * @return The instance of the given codec + * @throws IllegalArgumentException if no codec was found for the given ID. + */ + public HistogramDataPointCodec getCodec(final int id) { + final HistogramDataPointCodec codec = codecs.get(id); + if (codec == null) { + throw new IllegalArgumentException("No codec found mapped to ID " + id); + } + return codec; + } + + /** + * Return the ID of the given codec. + * @param clazz The non-null class to search for. + * @return The ID of the codec. + * @throws IllegalArgumentException if the class was null or no ID was assigned + * to the class. + */ + public int getCodec(final Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Clazz cannot be null."); + } + final Integer id = codecs_ids.get(clazz); + if (id == null) { + throw new IllegalArgumentException("No codec ID assigned to class " + + clazz); + } + return id; + } + + /** + * Finds the proper codec and calls it's encode method to create a byte array + * with the given ID as the first byte. + * @param id The ID of the histogram type to search for. + * @param data_point The non-null data point to encode. + * @param include_id Whether or not to include the ID prefix when encoding. + * @return A non-null and non-empty byte array if the codec was found. + * @throws IllegalArgumentException if no codec was found for the given ID or + * the histogram may have been of the wrong type or failed encoding. + */ + public byte[] encode(final int id, + final Histogram data_point, + final boolean include_id) { + final HistogramDataPointCodec codec = getCodec(id); + return codec.encode(data_point, include_id); + } + + /** + * Finds the proper codec and calls it's decode method to return the histogram + * data point for queries or validation. + * @param id The ID of the histogram type to search for. + * @param raw_data The non-null and non-empty byte array to parse. Should NOT + * include the first byte of the ID in the data. + * @param timestamp The timestamp associated with the data point. + * @param includes_id Whether or not the data includes the ID prefix. + * @return A non-null data point if decoding was successful. + */ + public Histogram decode(final int id, + final byte[] raw_data, + final boolean includes_id) { + final HistogramDataPointCodec codec = getCodec(id); + return codec.decode(raw_data, includes_id); + } +} diff --git a/src/core/HistogramDataPoint.java b/src/core/HistogramDataPoint.java index fded3ec5cc..01d32cd20f 100644 --- a/src/core/HistogramDataPoint.java +++ b/src/core/HistogramDataPoint.java @@ -41,15 +41,17 @@ public interface HistogramDataPoint extends Cloneable { * in the byte array so latter it can decide how to deserialize it back * @return The encoded value os this histogram data point */ - byte[] getRawData(); + byte[] getRawData(final boolean include_id); /** * Decode the raw data and reset the current histogram data point to the * decoded value * @param raw_data The encoded value of the histogram data point */ - void resetFromRawData(final byte[] raw_data); + void resetFromRawData(final byte[] raw_data, final boolean includes_id); + int getId(); + /** * Calculate percentile of this histogram data point * @param p the distribution threshold diff --git a/src/core/HistogramDataPointDecoder.java b/src/core/HistogramDataPointCodec.java similarity index 62% rename from src/core/HistogramDataPointDecoder.java rename to src/core/HistogramDataPointCodec.java index eed6004975..0956b989ba 100644 --- a/src/core/HistogramDataPointDecoder.java +++ b/src/core/HistogramDataPointCodec.java @@ -13,29 +13,47 @@ package net.opentsdb.core; /** - * Creates {@code HistogramDataPoint} from raw data and timestamp. + * Responsible for encoding or decoding {@code HistogramDataPoint}s to and from + * byte arrays. * * NOTE: Implementation of this plugin should be thread safe. - * @see HistogramDataPointDecoderManager + * @see HistogramCodecManager * * @since 2.4 */ -public abstract class HistogramDataPointDecoder { +public abstract class HistogramDataPointCodec { + /** The ID of this codec in the Histogram Manager. */ + protected int id; + /** * Default empty ctor, required for plugin and class instantiation. * WARNING Any overrides with arguments will be ignored. */ - public HistogramDataPointDecoder() { + public HistogramDataPointCodec() { } + public int getId() { + return id; + } + + public void setId(final int id) { + this.id = id; + } + /** - * Creates {@code HistogramDataPoint} from raw data and timestamp. + * Creates {@code HistogramDataPoint} from raw data and timestamp. Note that + * the data point identifier is separate. * @param raw_data The encoded byte array of the histogram data * @param timestamp The timestamp of this data point + * @param includes_id Whether or not to include the id prefix. * @return The decoded histogram data point instance */ - public abstract HistogramDataPoint decode(final byte[] raw_data, - final long timestamp); + public abstract Histogram decode(final byte[] raw_data, + final boolean includes_id); + + + public abstract byte[] encode(final Histogram data_point, + final boolean include_id); } diff --git a/src/core/HistogramDataPointDecoderManager.java b/src/core/HistogramDataPointDecoderManager.java deleted file mode 100644 index 06778ebe46..0000000000 --- a/src/core/HistogramDataPointDecoderManager.java +++ /dev/null @@ -1,156 +0,0 @@ -// This file is part of OpenTSDB. -// Copyright (C) 2016-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 java.io.File; -import java.io.IOException; -import java.util.Map; -import java.util.Map.Entry; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.google.common.base.Strings; -import com.google.common.collect.Maps; -import com.google.common.io.Files; - -import net.opentsdb.utils.JSON; -import net.opentsdb.utils.PluginLoader; - -/** - *

    - * Manages the histogram decoder singletons. - *

    - *

    - * This manage accepts the full class name of the decoder, use reflection to - * create the decoder, and it ensures each type of the decoder will be created - * only once, after that, the cached decoder instance will be returned. - *

    - *

    - * This behavior actually makes each decoder a singleton. - *

    - * - *

    This class is thread safe

    - * - * @since 2.4 - */ -public class HistogramDataPointDecoderManager { - private static final Logger LOG = LoggerFactory.getLogger( - HistogramDataPointDecoderManager.class); - - /** The map of IDs to decoders. */ - private final Map decoders; - - /** The map of classes to decoder IDs. */ - private final Map, Byte> decoder_ids; - - /** - * Default ctor that loads the decoder map. It will parse the - * 'tsd.core.histograms.config' parameter. If it ends with .json then we'll - * try to load a file of that name, otherwise we'll just parse it as raw JSON. - * For each map in the JSON we search the classpath then loaded plugins. - * - * @param tsdb A non-null TSDB to load the config from. - * @throws IllegalArgumentException if the config was null or empty or if the - * JSON was malformed. - * @throws RuntimeException if the file couldn't be opened. - * @throws IllegalStateException if no classes/plugins of the type could be - * found OR if one was found but couldn't be instantiated. - */ - public HistogramDataPointDecoderManager(final TSDB tsdb) { - final String config = tsdb.getConfig().getString("tsd.core.histograms.config"); - if (Strings.isNullOrEmpty(config)) { - throw new IllegalArgumentException("Missing configuration " - + "'tsd.core.histograms.config'"); - } - - final TypeReference> type_ref = - new TypeReference>() {}; - final Map mappings; - if (config.endsWith(".json")) { - final String json; - try { - json = Files.toString(new File(config), Const.UTF8_CHARSET); - } catch (IOException e) { - throw new RuntimeException("Unable to open plugin config file: " - + config, e); - } - mappings = JSON.parseToObject(json, type_ref); - } else { - mappings = JSON.parseToObject(config, type_ref); - } - - decoders = Maps.newHashMap(); - decoder_ids = Maps.newHashMap(); - - if (mappings.isEmpty()) { - LOG.warn("No histograms configured. Histogram writes and reads will " - + "throw exceptions."); - return; - } - - for (final Entry mapping : mappings.entrySet()) { - HistogramDataPointDecoder decoder = null; - try { - final Class clazz = Class.forName(mapping.getKey()); - decoder = (HistogramDataPointDecoder) clazz.newInstance(); - } catch (ClassNotFoundException e) { - decoder = PluginLoader - .loadSpecificPlugin(mapping.getKey(), HistogramDataPointDecoder.class); - } catch (InstantiationException e) { - throw new IllegalStateException("Found decoder '" + mapping.getKey() - + "' on the class path but failed to instantiate it.", e); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Found decoder '" + mapping.getKey() - + "' on the class path but we did not have permission to access it.", e); - } - - if (decoder == null) { - throw new IllegalStateException("Unable to find a decoder named '" - + mapping.getKey() + "'"); - } else { - decoders.put(mapping.getValue(), decoder); - decoder_ids.put(decoder.getClass(), mapping.getValue()); - LOG.info("Successfully loaded decoder '" + mapping.getKey() - + "' with ID " + mapping.getValue()); - } - } - } - - /** - * Return the instance of the given decoder. - * @param id The numeric ID of the decoder (the first byte in storage). - * @return The instance of the given decoder - * @throws IllegalArgumentException if no decoder was found for the given ID. - */ - public HistogramDataPointDecoder getDecoder(final byte id) { - final HistogramDataPointDecoder decoder = decoders.get(id); - if (decoder == null) { - throw new IllegalArgumentException("No decoder found mapped to ID " + id); - } - return decoder; - } - - public byte getDecoder(final Class clazz) { - if (clazz == null) { - throw new IllegalArgumentException("Clazz cannot be null."); - } - final Byte id = decoder_ids.get(clazz); - if (id == null) { - throw new IllegalArgumentException("No decoder ID assigned to class " - + clazz); - } - return id; - } -} diff --git a/src/core/HistogramDownsampler.java b/src/core/HistogramDownsampler.java index 57fca5518f..6545f6ec54 100644 --- a/src/core/HistogramDownsampler.java +++ b/src/core/HistogramDownsampler.java @@ -101,12 +101,12 @@ public long timestamp() { } @Override - public byte[] getRawData() { - return value.getRawData(); + public byte[] getRawData(final boolean include_id) { + return value.getRawData(include_id); } @Override - public void resetFromRawData(byte[] raw_data) { + public void resetFromRawData(byte[] raw_data, final boolean includes_id) { throw new UnsupportedOperationException(); } @@ -187,6 +187,11 @@ public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { return this.value.cloneAndSetTimestamp(timestamp); } + @Override + public int getId() { + return value.getId(); + } + class ValuesInInterval implements HistogramAggregator.Histograms { /** An optional calendar set to the current timestamp for the data point */ diff --git a/src/core/HistogramPojo.java b/src/core/HistogramPojo.java new file mode 100644 index 0000000000..8d88e7675e --- /dev/null +++ b/src/core/HistogramPojo.java @@ -0,0 +1,53 @@ +package net.opentsdb.core; + +import java.util.List; +import java.util.Map; + +import javax.xml.bind.DatatypeConverter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class HistogramPojo extends IncomingDataPoint { + private static final Logger LOG = LoggerFactory.getLogger(HistogramPojo.class); + + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public boolean validate(final List> details) { + if (!super.validate(details)) { + return false; + } + if (id < 0 || id > 255) { + if (details != null) { + details.add(getHttpDetails("Invalid type. Must be from 0 to 255.")); + } + LOG.warn("Invalid type. Must be from 0 to 255."); + return false; + } + return true; + } + + @JsonIgnore + public byte[] getBytes() { + return base64StringToBytes(value); + } + + public static String bytesToBase64String(final byte[] raw) { + return DatatypeConverter.printBase64Binary(raw); + } + + public static byte[] base64StringToBytes(final String encoded) { + return DatatypeConverter.parseBase64Binary(encoded); + } +} diff --git a/src/core/HistogramRowSeq.java b/src/core/HistogramRowSeq.java index f76d14bc05..8f808073eb 100644 --- a/src/core/HistogramRowSeq.java +++ b/src/core/HistogramRowSeq.java @@ -263,7 +263,7 @@ public String toString() { for (short i = 0; i < sz; ++i) { buf.append('+').append(rowSeq.get(i).timestamp()); - buf.append(":histogram(").append(Arrays.toString(rowSeq.get(i).getRawData())); + buf.append(":histogram(").append(Arrays.toString(rowSeq.get(i).getRawData(true))); buf.append(')'); if (i != sz -1) { buf.append(", "); @@ -309,13 +309,13 @@ public long timestamp() { } @Override - public byte[] getRawData() { - return getCurrent().getRawData(); + public byte[] getRawData(final boolean include_id) { + return getCurrent().getRawData(include_id); } @Override - public void resetFromRawData(byte[] raw_data) { - getCurrent().resetFromRawData(raw_data); + public void resetFromRawData(final byte[] raw_data, final boolean includes_id) { + getCurrent().resetFromRawData(raw_data, includes_id); } @Override @@ -372,6 +372,11 @@ public HistogramDataPoint clone() { return getCurrent().clone(); } + @Override + public int getId() { + return getCurrent().getId(); + } + @Override public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { return getCurrent().cloneAndSetTimestamp(timestamp); diff --git a/src/core/Internal.java b/src/core/Internal.java index 4ef1182712..fb0292d671 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -21,7 +21,6 @@ import java.util.Map; import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.Config; import org.hbase.async.Bytes; import org.hbase.async.KeyValue; @@ -1074,10 +1073,11 @@ public static HistogramDataPoint decodeHistogramDataPoint(final TSDB tsdb, final long base_time, final byte[] qualifier, final byte[] value) { - final HistogramDataPointDecoder decoder = - tsdb.histogramManager().getDecoder(value[0]); + final HistogramDataPointCodec decoder = + tsdb.histogramManager().getCodec((int) value[0]); long timestamp = getTimeStampFromNonDP(base_time, qualifier); - return decoder.decode(value, timestamp); + final Histogram histogram = decoder.decode(value, true); + return new SimpleHistogramDataPointAdapter(histogram, timestamp); } } diff --git a/src/core/SimpleHistogram.java b/src/core/SimpleHistogram.java index f20a4b9b35..e5e152cebb 100644 --- a/src/core/SimpleHistogram.java +++ b/src/core/SimpleHistogram.java @@ -43,6 +43,8 @@ public class SimpleHistogram implements Histogram { private static Logger LOG = LoggerFactory.getLogger(SimpleHistogram.class); + private final int id; + @JsonProperty("buckets") TreeMap buckets = new TreeMap(); @@ -52,6 +54,10 @@ public class SimpleHistogram implements Histogram { @JsonProperty("overflow") Long overflow = 0L; + public SimpleHistogram(final int id) { + this.id = id; + } + public void addBucket(Float min, Float max, Long count) { if (min == null || max == null) { return; @@ -63,12 +69,15 @@ public void addBucket(Float min, Float max, Long count) { buckets.put(new HistogramBucket(BucketType.REGULAR, min, max), count); } - public byte[] histogram() { + public byte[] histogram(final boolean include_id) { ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); Output output = new Output(outBuffer); int bucketCount = buckets.size(); try { + if (include_id) { + output.writeByte(id); + } output.writeShort(bucketCount); for (Map.Entry bucket : buckets.entrySet()) { @@ -85,14 +94,17 @@ public byte[] histogram() { return outBuffer.toByteArray(); } - public void fromHistogram(byte[] raw) { + public void fromHistogram(byte[] raw, final boolean include_id) { if (raw.length < 6) { - LOG.warn("Byte array shorter than 6 bytes detected: " + Bytes.pretty(raw)); - return; + throw new IllegalArgumentException("Byte array shorter than 6 bytes " + + "detected: " + Bytes.pretty(raw)); } Input input = null; try { input = new Input(new ByteArrayInputStream(raw)); + if (include_id) { + input.readByte(); // pull out the id. + } int bucketCount = input.readShort(); for (int i = 0; i < bucketCount; i++) { @@ -170,7 +182,7 @@ public Map getHistogram() { @Override public SimpleHistogram clone() { - SimpleHistogram cloneObj = new SimpleHistogram(); + SimpleHistogram cloneObj = new SimpleHistogram(id); for (Map.Entry bucket : buckets.entrySet()) { cloneObj.addBucket(bucket.getKey().getLowerBound(), @@ -182,6 +194,11 @@ public SimpleHistogram clone() { return cloneObj; } + @Override + public int getId() { + return id; + } + public Long getBucketCount(Float min, Float max) { HistogramBucket qryBucket = new HistogramBucket(BucketType.REGULAR, min, max); if (buckets.containsKey(qryBucket)) { diff --git a/src/core/SimpleHistogramDataPointAdapter.java b/src/core/SimpleHistogramDataPointAdapter.java index e1798369be..84c69cd9d7 100644 --- a/src/core/SimpleHistogramDataPointAdapter.java +++ b/src/core/SimpleHistogramDataPointAdapter.java @@ -53,13 +53,13 @@ public long timestamp() { } @Override - public byte[] getRawData() { - return histogram.histogram(); + public byte[] getRawData(final boolean include_id) { + return histogram.histogram(include_id); } @Override - public void resetFromRawData(final byte[] raw_data) { - histogram.fromHistogram(raw_data); + public void resetFromRawData(final byte[] raw_data, final boolean includes_id) { + histogram.fromHistogram(raw_data, includes_id); } @Override @@ -93,6 +93,11 @@ public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { return new SimpleHistogramDataPointAdapter(this, timestamp); } + @Override + public int getId() { + return histogram.getId(); + } + private HistogramAggregation mapAggregation(final HistogramAggregation func) { switch (func) { case SUM: diff --git a/src/core/SimpleHistogramDecoder.java b/src/core/SimpleHistogramDecoder.java index 688439e7b7..31d0fd3146 100644 --- a/src/core/SimpleHistogramDecoder.java +++ b/src/core/SimpleHistogramDecoder.java @@ -28,23 +28,32 @@ *

    * @since 2.4 */ -public class SimpleHistogramDecoder extends HistogramDataPointDecoder { +public class SimpleHistogramDecoder extends HistogramDataPointCodec { @Override - public HistogramDataPoint decode(final byte[] raw_data, final long timestamp) { - final Histogram histogram; - switch (raw_data[0]) { - case 0x0: - { - histogram = new SimpleHistogram(); - byte[] hist_raw_data = Arrays.copyOfRange(raw_data, 1, raw_data.length); - histogram.fromHistogram(hist_raw_data); + public Histogram decode(final byte[] raw_data, + final boolean includes_type) { + if (raw_data == null) { + throw new IllegalArgumentException("The data array cannot be null."); } - break; - default: - throw new IllegalDataException("Unknown header of histogram data, " - + "the header is: " + raw_data[0]); + if (includes_type && raw_data.length < 1) { + throw new IllegalArgumentException("The data array cannot be empty."); } - - return new SimpleHistogramDataPointAdapter(histogram, timestamp); + if (includes_type && (int) raw_data[0] != id) { + throw new IllegalArgumentException("Data ID " + (int) raw_data[0] + + " did not match the codec ID " + id); + } + final Histogram histogram = new SimpleHistogram(id); + histogram.fromHistogram(raw_data, includes_type); + return histogram; + } + + @Override + public byte[] encode(final Histogram data_point, + final boolean include_id) { + if (!(data_point instanceof SimpleHistogram)) { + throw new IllegalArgumentException("The given histogram is not a " + + "SimpleHistogram: " + data_point.getClass()); + } + return data_point.histogram(include_id); } } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 9e2b153db2..79844138b9 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -91,7 +91,7 @@ public final class TSDB { private static short TAG_NAME_WIDTH = 3; private static final String TAG_VALUE_QUAL = "tagv"; private static short TAG_VALUE_WIDTH = 3; - private static final int MIN_HISTOGRAM_BYTES = 2; + private static final int MIN_HISTOGRAM_BYTES = 1; /** Client for the HBase cluster to use. */ final HBaseClient client; @@ -171,7 +171,7 @@ public final class TSDB { /** An optional histogram manger used when the TSD will be dealing with * histograms and sketches. Instantiated ONLY if * {@link #initializePlugins(boolean)} was called.*/ - private HistogramDataPointDecoderManager histogram_manager; + private HistogramCodecManager histogram_manager; /** Writes rejected by the filter */ private final AtomicLong rejected_dps = new AtomicLong(); @@ -509,7 +509,7 @@ public void initializePlugins(final boolean init_rpcs) { // finally load the histo manager after plugins have been loaded. if (config.hasProperty("tsd.core.histograms.config")) { - histogram_manager = new HistogramDataPointDecoderManager(this); + histogram_manager = new HistogramCodecManager(this); } else { histogram_manager = null; } @@ -1083,8 +1083,8 @@ public Deferred addHistogramPoint(final String metric, final byte[] raw_data, final Map tags) { if (raw_data == null || raw_data.length < MIN_HISTOGRAM_BYTES) { - throw new IllegalArgumentException("The histogram raw data is invalid: " - + Bytes.pretty(raw_data)); + return Deferred.fromError(new IllegalArgumentException( + "The histogram raw data is invalid: " + Bytes.pretty(raw_data))); } checkTimestampAndTags(metric, timestamp, raw_data, tags, (short) 0); @@ -1092,7 +1092,7 @@ public Deferred addHistogramPoint(final String metric, final byte[] qualifier = Internal.getQualifier(timestamp, HistogramDataPoint.PREFIX); - + return storeIntoDB(metric, timestamp, raw_data, tags, (short) 0, row, qualifier); } @@ -2074,7 +2074,7 @@ public String getRawTagValue() { /** @return The optional histogram manager registered to this TSD. * @since 2.4 */ - public HistogramDataPointDecoderManager histogramManager() { + public HistogramCodecManager histogramManager() { return histogram_manager; } diff --git a/src/tsd/HistogramDataPointRpc.java b/src/tsd/HistogramDataPointRpc.java new file mode 100644 index 0000000000..712ff643f1 --- /dev/null +++ b/src/tsd/HistogramDataPointRpc.java @@ -0,0 +1,178 @@ +// 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.tsd; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.stumbleupon.async.Deferred; + +import net.opentsdb.core.Histogram; +import net.opentsdb.core.HistogramPojo; +import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.utils.Config; + +/** + * The class responsible for writing histograms from Telnet calls or HTTP + * requests. + * + * @since 2.4 + */ +public class HistogramDataPointRpc extends PutDataPointRpc + implements TelnetRpc, HttpRpc { + + /** Type ref for the histo pojo. */ + private static final TypeReference> TYPE_REF = + new TypeReference>() {}; + + /** Whether or not histograms are enabled. */ + private final boolean enabled; + + /** + * Default ctor. Checks the "tsd.core.histograms.config" value to see if + * histograms are enabled. If they are not, then exceptions are returned. + * @param config A non-null config to pull data from. + */ + public HistogramDataPointRpc(final Config config) { + super(config); + // drats, since we can't look at the manager we'll look at the settings. + final String histo_config = config.getString("tsd.core.histograms.config"); + if (Strings.isNullOrEmpty(histo_config)) { + enabled = false; + } else { + enabled = true; + } + } + + @Override + public void execute(final TSDB tsdb, final HttpQuery query) throws IOException { + http_requests.incrementAndGet(); + + if (!enabled) { + throw new BadRequestException(HttpResponseStatus.SERVICE_UNAVAILABLE, + "Histogram storage has not been enabled. Check the " + + "'tsd.core.histograms.config' configuration."); + } + + // 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(HistogramPojo.class, TYPE_REF); + processDataPoint(tsdb, query, dps); + } + + @Override + protected Deferred importDataPoint(final TSDB tsdb, + final String[] words) { + if (!enabled) { + throw new IllegalArgumentException( + "Histogram storage has not been enabled. Check the " + + "'tsd.core.histograms.config' configuration."); + } + + words[0] = null; // Ditch the "histogram". + if (words.length < 6) { // Need at least: metric timestamp value tag + // ^ 6 and not 5 because words[0] is "histogram". + throw new IllegalArgumentException("not enough arguments" + + " (need least 6, 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 int id = Integer.parseInt(words[3]); + + final String value = words[4]; + if (value.length() <= 0) { + throw new IllegalArgumentException("empty histogram value"); + } + final HashMap tags = new HashMap(); + for (int i = 5; i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + + // validation and prepend the ID. + try { + final Histogram dp = tsdb.histogramManager().decode(id, + HistogramPojo.base64StringToBytes(value), false); + return tsdb.addHistogramPoint(metric, timestamp, + tsdb.histogramManager().encode(id, dp, true), tags); + } catch (Exception e) { + return Deferred.fromError(e); + } + } + + @Override + protected IncomingDataPoint getDataPointFromString(final TSDB tsdb, + final String[] words) { + 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 int id = Integer.parseInt(words[3]); + + + final HistogramPojo dp = new HistogramPojo(); + dp.setMetric(words[1]); + dp.setTimestamp(timestamp); + dp.setId(id); + dp.setValue(words[4]); + final HashMap tags = new HashMap(); + for (int i = 5; i < words.length; i++) { + if (!words[i].isEmpty()) { + Tags.parse(tags, words[i]); + } + } + dp.setTags(tags); + return dp; + } + + @VisibleForTesting + boolean enabled() { + return enabled; + } +} diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 38d5657ed3..4a482dee0b 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -35,6 +35,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.opentsdb.core.Histogram; +import net.opentsdb.core.HistogramDataPoint; +import net.opentsdb.core.HistogramPojo; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; @@ -69,8 +72,10 @@ class PutDataPointRpc implements TelnetRpc, HttpRpc { protected static final AtomicLong telnet_requests = new AtomicLong(); protected static final AtomicLong http_requests = new AtomicLong(); protected static final AtomicLong raw_dps = new AtomicLong(); + protected static final AtomicLong raw_histograms = new AtomicLong(); protected static final AtomicLong rollup_dps = new AtomicLong(); protected static final AtomicLong raw_stored = new AtomicLong(); + protected static final AtomicLong raw_histograms_stored = new AtomicLong(); protected static final AtomicLong rollup_stored = new AtomicLong(); protected static final AtomicLong hbase_errors = new AtomicLong(); protected static final AtomicLong unknown_errors = new AtomicLong(); @@ -89,7 +94,8 @@ class PutDataPointRpc implements TelnetRpc, HttpRpc { * @since 2.4 */ public enum DataPointType { PUT("put"), - ROLLUP("rollup"); + ROLLUP("rollup"), + HISTOGRAM("histogram"); private final String name; DataPointType(final String name) { @@ -122,6 +128,9 @@ public Deferred execute(final TSDB tsdb, final Channel chan, } else if (command.equals("rollup")) { type = DataPointType.ROLLUP; rollup_dps.incrementAndGet(); + } else if (command.equals("histogram")) { + type = DataPointType.HISTOGRAM; + raw_histograms.incrementAndGet(); } else { throw new IllegalArgumentException("Unrecognized command: " + cmd[0]); } @@ -149,12 +158,16 @@ public Object call(final Exception arg) { } if (arg instanceof HBaseException) { hbase_errors.incrementAndGet(); + } else if (arg instanceof IllegalArgumentException) { + illegal_arguments.incrementAndGet(); + } else { + unknown_errors.incrementAndGet(); } } // we handle the storage exceptions here so as to avoid creating yet // another callback object on every data point. - handleStorageException(tsdb, getDataPointFromString(cmd), arg); + handleStorageException(tsdb, getDataPointFromString(tsdb, cmd), arg); if (send_telnet_errors) { if (chan.isConnected()) { @@ -181,15 +194,17 @@ final class SuccessCB implements Callback { public Object call(final Object obj) { if (type == DataPointType.PUT) { raw_stored.incrementAndGet(); - } else { + } else if (type == DataPointType.ROLLUP) { rollup_stored.incrementAndGet(); + } else if (type == DataPointType.HISTOGRAM) { + raw_histograms_stored.incrementAndGet(); } return true; } } - // Rollups override this method in their implementation so that it will - // route properly. + // Rollups and histos override this method in their implementation so + // that it will route properly. return importDataPoint(tsdb, cmd) .addCallback(new SuccessCB()) .addErrback(new PutErrback()); @@ -211,10 +226,10 @@ public Object call(final Object obj) { } catch (PleaseThrottleException x) { errmsg = type + ": Throttling exception: " + x.getMessage() + '\n'; inflight_exceeded.incrementAndGet(); - handleStorageException(tsdb, getDataPointFromString(cmd), x); + handleStorageException(tsdb, getDataPointFromString(tsdb, cmd), x); } catch (TimeoutException tex) { errmsg = type + ": Request timed out: " + tex.getMessage() + '\n'; - handleStorageException(tsdb, getDataPointFromString(cmd), tex); + handleStorageException(tsdb, getDataPointFromString(tsdb, cmd), tex); } catch (RuntimeException rex) { errmsg = type + ": Unexpected runtime exception: " + rex.getMessage() + '\n'; @@ -298,6 +313,9 @@ public void processDataPoint(final TSDB tsdb, if (dp instanceof RollUpDataPoint) { type = DataPointType.ROLLUP; rollup_dps.incrementAndGet(); + } else if (dp instanceof HistogramPojo) { + type = DataPointType.HISTOGRAM; + raw_histograms.incrementAndGet(); } else { type = DataPointType.PUT; raw_dps.incrementAndGet(); @@ -339,6 +357,9 @@ public Boolean call(final Object obj) { case ROLLUP: rollup_stored.incrementAndGet(); break; + case HISTOGRAM: + raw_histograms_stored.incrementAndGet(); + break; default: // don't care } @@ -354,48 +375,70 @@ public Boolean call(final Object obj) { // TODO - refactor the add calls someday or move some of this into the // actual data point class. final Deferred deferred; - if (Tags.looksLikeInteger(dp.getValue())) { - if (dp instanceof RollUpDataPoint) { - final RollUpDataPoint rdp = (RollUpDataPoint)dp; - deferred = tsdb.addAggregatePoint(rdp.getMetric(), - rdp.getTimestamp(), - Tags.parseLong(rdp.getValue()), - dp.getTags(), - rdp.getGroupByAggregator() != null, - rdp.getInterval(), - rdp.getAggregator(), - rdp.getGroupByAggregator()) - .addCallback(new SuccessCB()) - .addErrback(new PutErrback()); - } else { - deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - Tags.parseLong(dp.getValue()), dp.getTags()) + if (type == DataPointType.HISTOGRAM) { + final HistogramPojo pojo = (HistogramPojo) dp; + // validation before storage of histograms by decoding then re-encoding. + final Histogram hdp = tsdb.histogramManager().decode( + pojo.getId(), pojo.getBytes(), false); + deferred = tsdb.addHistogramPoint( + pojo.getMetric(), + pojo.getTimestamp(), + tsdb.histogramManager().encode(pojo.getId(), hdp, true), + pojo.getTags()) .addCallback(new SuccessCB()) .addErrback(new PutErrback()); - } } else { - if (dp instanceof RollUpDataPoint) { - final RollUpDataPoint rdp = (RollUpDataPoint)dp; - deferred = tsdb.addAggregatePoint(rdp.getMetric(), - rdp.getTimestamp(), - (Tags.fitsInFloat(dp.getValue()) ? - Float.parseFloat(dp.getValue()) : - Double.parseDouble(dp.getValue())), + if (Tags.looksLikeInteger(dp.getValue())) { + switch (type) { + case ROLLUP: + { + final RollUpDataPoint rdp = (RollUpDataPoint)dp; + deferred = tsdb.addAggregatePoint(rdp.getMetric(), + rdp.getTimestamp(), + Tags.parseLong(rdp.getValue()), dp.getTags(), - rdp.getGroupByAggregator() != null, - rdp.getInterval(), - rdp.getAggregator(), - rdp.getGroupByAggregator()) + rdp.getGroupByAggregator() != null, + rdp.getInterval(), + rdp.getAggregator(), + rdp.getGroupByAggregator()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + break; + } + default: + deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), + Tags.parseLong(dp.getValue()), dp.getTags()) .addCallback(new SuccessCB()) .addErrback(new PutErrback()); + } } else { - deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), - (Tags.fitsInFloat(dp.getValue()) ? - Float.parseFloat(dp.getValue()) : - Double.parseDouble(dp.getValue())), - dp.getTags()) - .addCallback(new SuccessCB()) - .addErrback(new PutErrback()); + switch (type) { + case ROLLUP: + { + final RollUpDataPoint rdp = (RollUpDataPoint)dp; + deferred = tsdb.addAggregatePoint(rdp.getMetric(), + rdp.getTimestamp(), + (Tags.fitsInFloat(dp.getValue()) ? + Float.parseFloat(dp.getValue()) : + Double.parseDouble(dp.getValue())), + dp.getTags(), + rdp.getGroupByAggregator() != null, + rdp.getInterval(), + rdp.getAggregator(), + rdp.getGroupByAggregator()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + break; + } + default: + deferred = tsdb.addPoint(dp.getMetric(), dp.getTimestamp(), + (Tags.fitsInFloat(dp.getValue()) ? + Float.parseFloat(dp.getValue()) : + Double.parseDouble(dp.getValue())), + dp.getTags()) + .addCallback(new SuccessCB()) + .addErrback(new PutErrback()); + } } } ++queued; @@ -545,6 +588,8 @@ public Object call(final ArrayList results) { } final int failures = dps.size() - queued; + System.out.println("GOOD: " + good_writes + " Failures: " + failures + " FW " + failed_writes + + " DPS: " + dps.size() + " Q " + queued); if (!show_summary && !show_details) { if (failures + failed_writes > 0) { query.sendReply(HttpResponseStatus.BAD_REQUEST, @@ -639,7 +684,7 @@ public static void collectStats(final StatsCollector collector) { * @throws NoSuchUniqueName if the metric isn't registered. */ protected Deferred importDataPoint(final TSDB tsdb, - final String[] words) { + 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". @@ -684,10 +729,12 @@ protected Deferred importDataPoint(final TSDB tsdb, * does not perform validation. It should only be used by the Telnet style * {@code execute} above within the error callback. At that point it means * the array parsed correctly as per {@code importDataPoint}. + * @param tsdb The TSDB for encoding/decoding. * @param words The array of strings representing a data point * @return An incoming data point object. */ - protected IncomingDataPoint getDataPointFromString(final String[] words) { + protected IncomingDataPoint getDataPointFromString(final TSDB tsdb, + final String[] words) { final IncomingDataPoint dp = new IncomingDataPoint(); dp.setMetric(words[1]); diff --git a/src/tsd/RollupDataPointRpc.java b/src/tsd/RollupDataPointRpc.java index 7545dd9f96..3798e529c9 100644 --- a/src/tsd/RollupDataPointRpc.java +++ b/src/tsd/RollupDataPointRpc.java @@ -173,11 +173,13 @@ protected Deferred importDataPoint(final TSDB tsdb, * does not perform validation. It should only be used by the Telnet style * {@code execute} above within the error callback. At that point it means * the array parsed correctly as per {@code importDataPoint}. + * @param tsdb The TSDB for encoding/decoding. * @param words The array of strings representing a data point * @return An incoming data point object. */ @Override - protected IncomingDataPoint getDataPointFromString(final String[] words) { + protected IncomingDataPoint getDataPointFromString(final TSDB tsdb, + final String[] words) { final RollUpDataPoint dp = new RollUpDataPoint(); final String interval_agg = words[TelnetIndex.INTERVAL_AGG.ordinal()]; diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 5dedfe761a..7a7dfb16b7 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -261,11 +261,14 @@ private void initializeBuiltinRpcs(final String mode, if (mode.equals("rw") || mode.equals("wo")) { final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); final RollupDataPointRpc rollups = new RollupDataPointRpc(tsdb.getConfig()); + final HistogramDataPointRpc histos = new HistogramDataPointRpc(tsdb.getConfig()); telnet.put("put", put); telnet.put("rollup", rollups); + telnet.put("histogram", histos); if (enableApi) { http.put("api/put", put); http.put("api/rollup", rollups); + http.put("api/histogram", histos); } } diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 71b94f3ec6..75ed4c3a36 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -171,7 +171,7 @@ public void before() throws Exception { } /** Adds the static UIDs to the metrics UID mock object */ - void setupMetricMaps() { + public void setupMetricMaps() { mockUID(UniqueIdType.METRIC, METRIC_STRING, METRIC_BYTES); mockUID(UniqueIdType.METRIC, METRIC_B_STRING, METRIC_B_BYTES); @@ -192,7 +192,7 @@ void setupMetricMaps() { } /** Adds the static UIDs to the tag keys UID mock object */ - void setupTagkMaps() { + public void setupTagkMaps() { mockUID(UniqueIdType.TAGK, TAGK_STRING, TAGK_BYTES); mockUID(UniqueIdType.TAGK, TAGK_B_STRING, TAGK_B_BYTES); @@ -213,7 +213,7 @@ void setupTagkMaps() { } /** Adds the static UIDs to the tag values UID mock object */ - void setupTagvMaps() { + public void setupTagvMaps() { mockUID(UniqueIdType.TAGV, TAGV_STRING, TAGV_BYTES); mockUID(UniqueIdType.TAGV, TAGV_B_STRING, TAGV_B_BYTES); @@ -772,8 +772,7 @@ protected void storeMixedTimeSeriesMsAndS() throws Exception { } } } - - + //store histogram data points of {@link LongHistogramDataPointForTest} with second timestamp protected void storeTestHistogramTimeSeriesSeconds(final boolean offset) throws Exception { setDataPointStorage(); @@ -783,10 +782,16 @@ protected void storeTestHistogramTimeSeriesSeconds(final boolean offset) throws HashMap tags_local = new HashMap(); tags_local.put("host", "web01"); + // note that the mock must have been configured properly + final int id = tsdb.histogramManager() + .getCodec(LongHistogramDataPointForTestDecoder.class); + long timestamp = 1356998400; for (int i = 1; i <= 300; i++) { - LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); - tsdb.addHistogramPoint(HISTOGRAM_METRIC_STRING, timestamp += 30, hdp.getRawData(), tags_local).joinUninterruptibly(); + final LongHistogramDataPointForTest hdp = + new LongHistogramDataPointForTest(id, i); + tsdb.addHistogramPoint(HISTOGRAM_METRIC_STRING, timestamp += 30, + hdp.histogram(true), tags_local).joinUninterruptibly(); } // dump a parallel set but invert the values @@ -794,14 +799,21 @@ protected void storeTestHistogramTimeSeriesSeconds(final boolean offset) throws tags_local.put("host", "web02"); timestamp = offset ? 1356998415 : 1356998400; for (int i = 300; i > 0; i--) { - LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); - tsdb.addHistogramPoint(HISTOGRAM_METRIC_STRING, timestamp += 30, hdp.getRawData(), tags_local).joinUninterruptibly(); + final LongHistogramDataPointForTest hdp = + new LongHistogramDataPointForTest(id, i); + tsdb.addHistogramPoint(HISTOGRAM_METRIC_STRING, timestamp += 30, + hdp.histogram(true), tags_local).joinUninterruptibly(); } } // store histogram data points of {@link LongHistogramDataPointForTest} with ms timestamp protected void storeTestHistogramTimeSeriesMs() throws Exception { setDataPointStorage(); + + // note that the mock must have been configured properly + final int id = tsdb.histogramManager() + .getCodec(LongHistogramDataPointForTestDecoder.class); + // dump a bunch of rows of two metrics so that we can test filtering out // on the metric HashMap tags = new HashMap(1); @@ -809,8 +821,10 @@ protected void storeTestHistogramTimeSeriesMs() throws Exception { long timestamp = 1356998400000L; for (int i = 1; i <= 300; i++) { timestamp += 500; - LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); - tsdb.addHistogramPoint("msg.end2end.latency", timestamp, hdp.getRawData(), tags).joinUninterruptibly(); + final LongHistogramDataPointForTest hdp = + new LongHistogramDataPointForTest(id, i); + tsdb.addHistogramPoint("msg.end2end.latency", timestamp, + hdp.histogram(true), tags).joinUninterruptibly(); } // end for // dump a parallel set but invert the values @@ -819,8 +833,10 @@ protected void storeTestHistogramTimeSeriesMs() throws Exception { timestamp = 1356998400000L; for (int i = 300; i > 0; i--) { timestamp += 500; - LongHistogramDataPointForTest hdp = new LongHistogramDataPointForTest(timestamp, Bytes.fromLong(i)); - tsdb.addHistogramPoint("msg.end2end.latency", timestamp, hdp.getRawData(), tags).joinUninterruptibly(); + final LongHistogramDataPointForTest hdp = + new LongHistogramDataPointForTest(id, i); + tsdb.addHistogramPoint("msg.end2end.latency", timestamp, + hdp.histogram(true), tags).joinUninterruptibly(); } // end for } diff --git a/test/core/HistogramSeekableViewForTest.java b/test/core/HistogramSeekableViewForTest.java index 2591aad5de..e68e5b3d67 100644 --- a/test/core/HistogramSeekableViewForTest.java +++ b/test/core/HistogramSeekableViewForTest.java @@ -19,11 +19,10 @@ import java.util.NoSuchElementException; import org.hbase.async.Bytes; -import org.junit.Ignore; import org.junit.Test; /** Helper class to mock HistogramSeekableView. */ -@Ignore + public class HistogramSeekableViewForTest { /** @@ -99,8 +98,7 @@ private static class DataPointGenerator implements HistogramSeekableView { private final long start_time_ms; private final long sample_period_ms; private final int num_data_points; - private final LongHistogramDataPointForTest current_data = - new LongHistogramDataPointForTest(100L, Bytes.fromLong(0L)); + private SimpleHistogramDataPointAdapter current_data; private int current = 0; DataPointGenerator(final long start_time_ms, final long sample_period_ms, @@ -108,6 +106,9 @@ private static class DataPointGenerator implements HistogramSeekableView { this.start_time_ms = start_time_ms; this.sample_period_ms = sample_period_ms; this.num_data_points = num_data_points; + current_data = new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 0), 100L); + rewind(); } @@ -149,8 +150,8 @@ private void rewind() { } private void generateData() { - current_data.setTimeStamp(generateTimestamp()); - current_data.setRawData(Bytes.fromLong(current)); + current_data = new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, current), generateTimestamp()); } private long generateTimestamp() { @@ -163,18 +164,23 @@ private long generateTimestamp() { public void testDataPointGenerator() { DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { - new LongHistogramDataPointForTest(99000, Bytes.fromLong(0L)), - new LongHistogramDataPointForTest(111000, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 0), 99000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 1), 111000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 2), 119000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 3), 131000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 4), 139000), }; for (HistogramDataPoint expected: expected_data_points) { assertTrue(hdpg.hasNext()); HistogramDataPoint dp = hdpg.next(); assertEquals(expected.timestamp(), dp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), - Bytes.getLong(dp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData(false)), + Bytes.getLong(dp.getRawData(false))); } assertFalse(hdpg.hasNext()); } @@ -184,16 +190,19 @@ public void testDataPointGenerator_seek() { DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); hdpg.seek(119000); HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { - new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 2), 119000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 3), 131000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 4), 139000), }; for (HistogramDataPoint expected: expected_data_points) { assertTrue(hdpg.hasNext()); HistogramDataPoint hdp = hdpg.next(); assertEquals(expected.timestamp(), hdp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), - Bytes.getLong(hdp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData(false)), + Bytes.getLong(hdp.getRawData(false))); } assertFalse(hdpg.hasNext()); } @@ -203,17 +212,21 @@ public void testDataPointGenerator_seekToFirst() { DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); hdpg.seek(100000); HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { - new LongHistogramDataPointForTest(111000, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 1), 111000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 2), 119000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 3), 131000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 4), 139000), }; for (HistogramDataPoint expected: expected_data_points) { assertTrue(hdpg.hasNext()); HistogramDataPoint hdp = hdpg.next(); assertEquals(expected.timestamp(), hdp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), - Bytes.getLong(hdp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData(false)), + Bytes.getLong(hdp.getRawData(false))); } assertFalse(hdpg.hasNext()); } @@ -223,17 +236,21 @@ public void testDataPointGenerator_seekToSecond() { DataPointGenerator hdpg = new DataPointGenerator(100000, 10000, 5); hdpg.seek(100001); HistogramDataPoint[] expected_data_points = new HistogramDataPoint[] { - new LongHistogramDataPointForTest(111000, Bytes.fromLong(1)), - new LongHistogramDataPointForTest(119000, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(131000, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(139000, Bytes.fromLong(4L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 1), 111000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 2), 119000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 3), 131000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(1, 4), 139000), }; for (HistogramDataPoint expected: expected_data_points) { assertTrue(hdpg.hasNext()); HistogramDataPoint hdp = hdpg.next(); assertEquals(expected.timestamp(), hdp.timestamp()); - assertEquals(Bytes.getLong(expected.getRawData()), - Bytes.getLong(hdp.getRawData())); + assertEquals(Bytes.getLong(expected.getRawData(false)), + Bytes.getLong(hdp.getRawData(false))); } assertFalse(hdpg.hasNext()); } diff --git a/test/core/LongHistogramDataPointForTest.java b/test/core/LongHistogramDataPointForTest.java index 8e251c001c..65b84ec691 100644 --- a/test/core/LongHistogramDataPointForTest.java +++ b/test/core/LongHistogramDataPointForTest.java @@ -19,57 +19,41 @@ import org.hbase.async.Bytes; -public class LongHistogramDataPointForTest implements HistogramDataPoint { - private long timestamp; +public class LongHistogramDataPointForTest implements Histogram { + private final int id; private long data; - - LongHistogramDataPointForTest(final long timestamp, final byte[] raw_data) { - this.timestamp = timestamp; - this.data = Bytes.getLong(raw_data); + + public LongHistogramDataPointForTest(final int id) { + this.id = id; + } + + LongHistogramDataPointForTest(final int id, final long value) { + this.id = id; + this.data = value; } protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs) { - this.timestamp = rhs.timestamp; + this.id = rhs.id; this.data = rhs.data; } protected LongHistogramDataPointForTest(final LongHistogramDataPointForTest rhs, final long timestamp) { + this.id = rhs.id; this.data = rhs.data; - this.timestamp = timestamp; - } - - @Override - public long timestamp() { - return this.timestamp; - } - - public void setTimeStamp(final long timestamp) { - this.timestamp = timestamp; } - @Override - public byte[] getRawData() { - return Bytes.fromLong(this.data); - } - public void setRawData(final byte[] data) { this.data = Bytes.getLong(data); } - @Override - public void resetFromRawData(byte[] raw_data) { - // TODO Auto-generated method stub - - } - @Override public double percentile(double p) { return data * p; } @Override - public List percentile(List p) { + public List percentiles(List p) { List rs = new ArrayList(); for (Double d : p) { rs.add(d.doubleValue() * data); @@ -78,29 +62,56 @@ public List percentile(List p) { } @Override - public void aggregate(HistogramDataPoint histo, HistogramAggregation func) { + public void aggregate(Histogram histo, HistogramAggregation func) { if (!(histo instanceof LongHistogramDataPointForTest)) { throw new IllegalArgumentException("The object must be an instance of the " + "LongHistogramDataPointForTest"); } - long agg = this.data + Bytes.getLong(histo.getRawData()); + long agg = this.data + Bytes.getLong(histo.histogram(false)); this.data = agg; } @Override - public HistogramDataPoint clone() { + public Histogram clone() { return new LongHistogramDataPointForTest(this); } @Override - public HistogramDataPoint cloneAndSetTimestamp(final long timestamp) { - return new LongHistogramDataPointForTest(this, timestamp); + public int getId() { + return id; } @Override - public Map getHistogramBucketsIfHas() { - throw new UnsupportedOperationException( - "LongHistogramDataPointForTest doesn't support getHistogramBuckets operation"); + public byte[] histogram(boolean include_id) { + if (include_id) { + final byte[] result = new byte[9]; + result[0] = (byte) id; + System.arraycopy(Bytes.fromLong(data), 0, result, 1, 8); + return result; + } + return Bytes.fromLong(data); + } + + @Override + public void fromHistogram(byte[] raw, boolean includes_id) { + if (includes_id) { + data = Bytes.getLong(raw, 1); + } else { + data = Bytes.getLong(raw); + } + } + + @Override + public Map getHistogram() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void aggregate(List histos, HistogramAggregation func) { + // TODO Auto-generated method stub + } + } diff --git a/test/core/LongHistogramDataPointForTestDecoder.java b/test/core/LongHistogramDataPointForTestDecoder.java index edfdad62c4..f7381643e9 100644 --- a/test/core/LongHistogramDataPointForTestDecoder.java +++ b/test/core/LongHistogramDataPointForTestDecoder.java @@ -12,11 +12,20 @@ // see . package net.opentsdb.core; -public class LongHistogramDataPointForTestDecoder extends HistogramDataPointDecoder { +public class LongHistogramDataPointForTestDecoder extends HistogramDataPointCodec { @Override - public HistogramDataPoint decode(byte[] raw_data, long timestamp) { - return new LongHistogramDataPointForTest(timestamp, raw_data); + public Histogram decode(byte[] raw_data, final boolean includes_id) { + final LongHistogramDataPointForTest dp = new LongHistogramDataPointForTest(id); + dp.fromHistogram(raw_data, includes_id); + return dp; + } + + + @Override + public byte[] encode(Histogram data_point, final boolean include_id) { + // TODO Auto-generated method stub + return null; } } diff --git a/test/core/TestHistogramAggregationIterator.java b/test/core/TestHistogramAggregationIterator.java index 3d547c22e1..7ae07c8649 100644 --- a/test/core/TestHistogramAggregationIterator.java +++ b/test/core/TestHistogramAggregationIterator.java @@ -50,8 +50,8 @@ public class TestHistogramAggregationIterator { public void testOneHistogramSpanWithNoDownsampler() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -69,7 +69,7 @@ public void testOneHistogramSpanWithNoDownsampler() { List timestamps = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps.add(hdp.timestamp()); } // end while @@ -84,8 +84,8 @@ public void testOneHistogramSpanWithNoDownsampler() { public void testOneHistogramSpanWithDownsampler_10secs() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -105,7 +105,7 @@ public void testOneHistogramSpanWithDownsampler_10secs() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -135,8 +135,8 @@ public void testOneHistogramSpanWithDownsampler_10secs() { public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -154,7 +154,7 @@ public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -170,8 +170,8 @@ public void testOneHistogramSpanNoDownSamplerSkipEarlyDataPoints() { public void testOneHistogramSpanNoDownSamplerOutofRange() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -191,8 +191,8 @@ public void testOneHistogramSpanNoDownSamplerOutofRange() { public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -210,7 +210,7 @@ public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -225,8 +225,8 @@ public void testOneHistogramSpanNoDownSamplerLaterDataPoints() { public void testOneHistogramSpanDownSamplerLaterDataPoints() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -245,7 +245,7 @@ public void testOneHistogramSpanDownSamplerLaterDataPoints() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -267,8 +267,8 @@ public void testOneHistogramSpanDownSamplerLaterDataPoints() { public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -279,8 +279,8 @@ public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { List row2 = new ArrayList(); for (int i = 0; i < 10; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); @@ -296,7 +296,7 @@ public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -311,8 +311,8 @@ public void testTwoHistogramSpanNoDownSamplerSameTimestamp() { public void testTwoHistogramSpanDownSamplerSameTimestamp() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -323,8 +323,8 @@ public void testTwoHistogramSpanDownSamplerSameTimestamp() { List row2 = new ArrayList(); for (int i = 0; i < 10; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); @@ -341,7 +341,7 @@ public void testTwoHistogramSpanDownSamplerSameTimestamp() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -357,8 +357,8 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { List row = new ArrayList(); // 0, 2, 4... for (int i = 0; i < 10; ) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); i += 2; } @@ -371,8 +371,8 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { List row2 = new ArrayList(); // 1, 3, 5... for (int i = 1; i < 10; ) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); i += 2; } @@ -389,7 +389,7 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -404,8 +404,8 @@ public void testTwoHistogramSpanNoDownSamplerDiffTimestamp() { public void testTwoHistogramSpanNoDownSamplerMergeSome() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -416,13 +416,13 @@ public void testTwoHistogramSpanNoDownSamplerMergeSome() { List row2 = new ArrayList(); for (int i = 1; i < 5; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } for (int i = 5; i < 10; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * (5 + i), - Bytes.fromLong(5 + i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5 + i), BASE_TIME + 5000L * (5 + i))); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); @@ -439,7 +439,7 @@ public void testTwoHistogramSpanNoDownSamplerMergeSome() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -465,8 +465,8 @@ public void testTwoHistogramSpanNoDownSamplerOneHasMore() { // span 1 has 10 data points List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -478,8 +478,8 @@ public void testTwoHistogramSpanNoDownSamplerOneHasMore() { // span 2 has 5 data points List row2 = new ArrayList(); for (int i = 1; i < 5; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); @@ -495,7 +495,7 @@ public void testTwoHistogramSpanNoDownSamplerOneHasMore() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while @@ -516,8 +516,8 @@ public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { // span1 has 10 data points List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -529,8 +529,8 @@ public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { // span 2 has 5 data points List row2 = new ArrayList(); for (int i = 1; i < 5; ++i) { - row2.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan2 = new HistogramSpan(tsdb); @@ -546,7 +546,7 @@ public void testTwoHistogramSpanNoDownSamplerOneOutofRange() { List timestamps_in_millis = new ArrayList(); while (histAggIt.hasNext()) { HistogramDataPoint hdp = histAggIt.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } // end while diff --git a/test/core/TestHistogramDataPointDecoderManager.java b/test/core/TestHistogramCodecManager.java similarity index 55% rename from test/core/TestHistogramDataPointDecoderManager.java rename to test/core/TestHistogramCodecManager.java index 671aa17d37..453d7a88cb 100644 --- a/test/core/TestHistogramDataPointDecoderManager.java +++ b/test/core/TestHistogramCodecManager.java @@ -34,9 +34,9 @@ import net.opentsdb.utils.Config; @RunWith(PowerMockRunner.class) -@PrepareForTest({ TSDB.class, HistogramDataPointDecoderManager.class, +@PrepareForTest({ TSDB.class, HistogramCodecManager.class, Files.class }) -public class TestHistogramDataPointDecoderManager { +public class TestHistogramCodecManager { private TSDB tsdb; private Config config; @@ -48,39 +48,68 @@ public void before() throws Exception { config.overrideConfig("tsd.core.histograms.config", "{\"net.opentsdb.core.SimpleHistogramDecoder\": 0," - + "\"net.opentsdb.core.TestHistogramDataPointDecoderManager$MockDecoder\":1}"); + + "\"net.opentsdb.core.TestHistogramCodecManager$MockDecoder\":1}"); when(tsdb.getConfig()).thenReturn(config); PowerMockito.mockStatic(Files.class); } @Test public void ctor() throws Exception { - HistogramDataPointDecoderManager manager = - new HistogramDataPointDecoderManager(tsdb); - assertEquals(0, manager.getDecoder(SimpleHistogramDecoder.class)); - assertEquals(1, manager.getDecoder(MockDecoder.class)); - assertTrue(manager.getDecoder((byte) 0) instanceof SimpleHistogramDecoder); - assertTrue(manager.getDecoder((byte) 1) instanceof MockDecoder); + HistogramCodecManager manager = + new HistogramCodecManager(tsdb); + assertEquals(0, manager.getCodec(SimpleHistogramDecoder.class)); + assertEquals(1, manager.getCodec(MockDecoder.class)); + HistogramDataPointCodec codec = manager.getCodec(0); + assertEquals(0, codec.getId()); + assertTrue(codec instanceof SimpleHistogramDecoder); + codec = manager.getCodec(1); + assertEquals(1, codec.getId()); + assertTrue(codec instanceof MockDecoder); // bad JSON config.overrideConfig("tsd.core.histograms.config", "{\"net.opentsdb.core.SimpleHistogramDecoder\": "); try { - new HistogramDataPointDecoderManager(tsdb); + new HistogramCodecManager(tsdb); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // id too small + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\":-1}s"); + try { + new HistogramCodecManager(tsdb); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // id too big + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\":256}s"); + try { + new HistogramCodecManager(tsdb); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // duplicate ID + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\": 42," + + "\"net.opentsdb.core.TestHistogramCodecManager$MockDecoder\":42}"); + try { + new HistogramCodecManager(tsdb); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { } config.overrideConfig("tsd.core.histograms.config", "nosuchfile.json"); when(Files.toString(any(File.class), eq(Const.UTF8_CHARSET))) .thenReturn("{\"net.opentsdb.core.SimpleHistogramDecoder\": 0}"); - manager = new HistogramDataPointDecoderManager(tsdb); - assertEquals(0, manager.getDecoder(SimpleHistogramDecoder.class)); - assertTrue(manager.getDecoder((byte) 0) instanceof SimpleHistogramDecoder); + manager = new HistogramCodecManager(tsdb); + assertEquals(0, manager.getCodec(SimpleHistogramDecoder.class)); + assertTrue(manager.getCodec(0) instanceof SimpleHistogramDecoder); when(Files.toString(any(File.class), eq(Const.UTF8_CHARSET))) .thenThrow(new IOException("Boo!")); try { - new HistogramDataPointDecoderManager(tsdb); + new HistogramCodecManager(tsdb); fail("Expected RuntimeException"); } catch (RuntimeException e) { } @@ -88,65 +117,74 @@ public void ctor() throws Exception { config.overrideConfig("tsd.core.histograms.config", "{\"net.opentsdb.core.NoSuchPlugin\":0}"); try { - new HistogramDataPointDecoderManager(tsdb); + new HistogramCodecManager(tsdb); fail("Expected IllegalStateException"); } catch (IllegalStateException e) { } // bad plugin config.overrideConfig("tsd.core.histograms.config", - "{\"net.opentsdb.core.TestHistogramDataPointDecoderManager$MockDecoderBadly\":0}"); + "{\"net.opentsdb.core.TestHistogramCodecManager$MockDecoderBadly\":0}"); try { - new HistogramDataPointDecoderManager(tsdb); + new HistogramCodecManager(tsdb); fail("Expected IllegalStateException"); } catch (IllegalStateException e) { } } @Test public void getDecoder() throws Exception { - final HistogramDataPointDecoderManager manager = - new HistogramDataPointDecoderManager(tsdb); - assertTrue(manager.getDecoder((byte) 0) instanceof SimpleHistogramDecoder); + final HistogramCodecManager manager = + new HistogramCodecManager(tsdb); + assertTrue(manager.getCodec(0) instanceof SimpleHistogramDecoder); try { - manager.getDecoder((byte) 43); + manager.getCodec(43); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { } } @Test public void getDecoderClass() throws Exception { - final HistogramDataPointDecoderManager manager = - new HistogramDataPointDecoderManager(tsdb); - assertEquals(0, manager.getDecoder(SimpleHistogramDecoder.class)); - assertEquals(1, manager.getDecoder(MockDecoder.class)); + final HistogramCodecManager manager = + new HistogramCodecManager(tsdb); + assertEquals(0, manager.getCodec(SimpleHistogramDecoder.class)); + assertEquals(1, manager.getCodec(MockDecoder.class)); try { - manager.getDecoder(null); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { } - - try { - manager.getDecoder(MockDecoderBadly.class); + manager.getCodec(MockDecoderBadly.class); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { } } - public static class MockDecoder extends HistogramDataPointDecoder { + public static class MockDecoder extends HistogramDataPointCodec { + + @Override + public Histogram decode(byte[] raw_data, boolean includes_id) { + return null; + } + @Override - public HistogramDataPoint decode(byte[] raw_data, long timestamp) { + public byte[] encode(Histogram data_point, boolean include_id) { + // TODO Auto-generated method stub return null; } } - static class MockDecoderBadly extends HistogramDataPointDecoder { + static class MockDecoderBadly extends HistogramDataPointCodec { // not allowed! public MockDecoderBadly(final long unwanted_param) { } @Override - public HistogramDataPoint decode(byte[] raw_data, long timestamp) { + public Histogram decode(byte[] raw_data, boolean includes_id) { + return null; + } + + + @Override + public byte[] encode(Histogram data_point, boolean include_id) { + // TODO Auto-generated method stub return null; } diff --git a/test/core/TestHistogramDataPointsToDataPointsAdaptor.java b/test/core/TestHistogramDataPointsToDataPointsAdaptor.java index c9fd3caad1..c3fad80dbe 100644 --- a/test/core/TestHistogramDataPointsToDataPointsAdaptor.java +++ b/test/core/TestHistogramDataPointsToDataPointsAdaptor.java @@ -291,8 +291,8 @@ public void getTagUidsNullQueryTags() throws Exception { public void iteratorAllItems() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -336,8 +336,8 @@ public void iteratorAllItems() { public void doubleIteratorAllItems() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); @@ -385,8 +385,8 @@ public void doubleIteratorAllItems() { public void iteratorAllItemsWithDiffPercentile() { List row = new ArrayList(); for (int i = 0; i < 10; ++i) { - row.add(new LongHistogramDataPointForTest(BASE_TIME + 5000L * i, - Bytes.fromLong(i))); + row.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), BASE_TIME + 5000L * i)); } final HistogramSpan hspan = new HistogramSpan(tsdb); diff --git a/test/core/TestHistogramDownsampler.java b/test/core/TestHistogramDownsampler.java index 2f876c8a88..c91a2ed39f 100644 --- a/test/core/TestHistogramDownsampler.java +++ b/test/core/TestHistogramDownsampler.java @@ -60,17 +60,23 @@ public class TestHistogramDownsampler { private static final HistogramDataPoint[] HIST_DATA_POINTS = new HistogramDataPoint[] { // timestamp = 1,356,998,400,000 ms - new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(40L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME), // timestamp = 1,357,000,400,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 2000000, Bytes.fromLong(50L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 50L), BASE_TIME + 2000000), // timestamp = 1,357,002,000,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 3600000, Bytes.fromLong(40L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 3600000), // timestamp = 1,357,002,005,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 3605000, Bytes.fromLong(50L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 50L), BASE_TIME + 3605000), // timestamp = 1,357,005,600,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 7200000, Bytes.fromLong(40L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 7200000), // timestamp = 1,357,007,600,000 ms - new LongHistogramDataPointForTest(BASE_TIME + 9200000, Bytes.fromLong(50L)) + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 50L), BASE_TIME + 9200000) }; public static final byte[] KEY = @@ -112,7 +118,7 @@ public void testDownsampler() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -133,17 +139,28 @@ public void testDownsampler() { public void testDownsampler_10seconds() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 0, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 1, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 2, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 3, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 4, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 5, Bytes.fromLong(32L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 6, Bytes.fromLong(64L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 7, Bytes.fromLong(128L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 8, Bytes.fromLong(256L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 9, Bytes.fromLong(512L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 10, Bytes.fromLong(1024L)) + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L * 0), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 5000L * 1), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 5000L * 2), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 5000L * 3), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 5000L * 4), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 5000L * 5), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 64L), BASE_TIME + 5000L * 6), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 128L), BASE_TIME + 5000L * 7), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 256L), BASE_TIME + 5000L * 8), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 512L), BASE_TIME + 5000L * 9), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1024L), BASE_TIME + 5000L * 10) })); specification = new DownsamplingSpecification("10s-sum"); @@ -153,7 +170,7 @@ public void testDownsampler_10seconds() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -176,12 +193,18 @@ public void testDownsampler_10seconds() { public void testDownsampler_15seconds() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) })); specification = new DownsamplingSpecification("15s-sum"); downsampler = new HistogramDownsampler(source, specification, 0, 0); verify(source, never()).next(); @@ -189,7 +212,7 @@ public void testDownsampler_15seconds() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -208,12 +231,18 @@ public void testDownsampler_15seconds() { public void testDownsampler_allFullRange() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) })); specification = new DownsamplingSpecification("0all-sum"); downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); verify(source, never()).next(); @@ -221,7 +250,7 @@ public void testDownsampler_allFullRange() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -234,20 +263,27 @@ public void testDownsampler_allFullRange() { public void testDownsampler_allFilterOnQuery() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) })); specification = new DownsamplingSpecification("0all-sum"); - downsampler = new HistogramDownsampler(source, specification, BASE_TIME + 15000L, BASE_TIME + 45000L); + downsampler = new HistogramDownsampler(source, specification, + BASE_TIME + 15000L, BASE_TIME + 45000L); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -260,20 +296,27 @@ public void testDownsampler_allFilterOnQuery() { public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) })); specification = new DownsamplingSpecification("0all-sum"); - downsampler = new HistogramDownsampler(source, specification, BASE_TIME + 65000L, BASE_TIME + 75000L); + downsampler = new HistogramDownsampler(source, specification, + BASE_TIME + 65000L, BASE_TIME + 75000L); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -284,20 +327,27 @@ public void testDownsampler_allFilterOnQueryOutOfRangeEarly() { public void testDownsampler_allFilterOnQueryOutOfRangeLate() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) })); specification = new DownsamplingSpecification("0all-sum"); - downsampler = new HistogramDownsampler(source, specification, BASE_TIME - 15000L, BASE_TIME - 5000L); + downsampler = new HistogramDownsampler(source, specification, + BASE_TIME - 15000L, BASE_TIME - 5000L); verify(source, never()).next(); List values = Lists.newArrayList(); List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -308,12 +358,18 @@ public void testDownsampler_allFilterOnQueryOutOfRangeLate() { public void testDownsampler_calendarHour() { source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 1800000, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 3599000L, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(BASE_TIME + 3600000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 5400000L, Bytes.fromLong(5L)), - new LongHistogramDataPointForTest(BASE_TIME + 7199000L, Bytes.fromLong(6L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 1800000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3L), BASE_TIME + 3599000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 3600000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5L), BASE_TIME + 5400000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 6L), BASE_TIME + 7199000L) })); specification = new DownsamplingSpecification("1hc-sum"); specification.setTimezone(TV); downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); @@ -323,7 +379,7 @@ public void testDownsampler_calendarHour() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); ts += 3600000; value = 15; } @@ -339,7 +395,7 @@ public void testDownsampler_calendarHour() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); ts += 3600000; if (value == 1) { value = 9; @@ -359,7 +415,7 @@ public void testDownsampler_calendarHour() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); } } @@ -368,14 +424,20 @@ public void testDownsampler_calendarDay() { // UTC source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(DST_TS, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(DST_TS + 86399000, Bytes.fromLong(2L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), DST_TS), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), DST_TS + 86399000), // falls to the next in FJ - new LongHistogramDataPointForTest(DST_TS + 126001000L, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(DST_TS + 172799000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(DST_TS + 172800000L, Bytes.fromLong(5L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3L), DST_TS + 126001000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), DST_TS + 172799000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5L), DST_TS + 172800000L), // falls within 30m offset - new LongHistogramDataPointForTest(DST_TS + 242999000L, Bytes.fromLong(6L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 6L), DST_TS + 242999000L) })); // control specification = new DownsamplingSpecification("1d-sum"); @@ -386,7 +448,7 @@ public void testDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 3) { value = 7; @@ -407,7 +469,7 @@ public void testDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 1) { value = 5; @@ -430,7 +492,7 @@ public void testDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 1) { value = 2; @@ -453,7 +515,7 @@ public void testDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 1) { value = 5; @@ -473,7 +535,7 @@ public void testDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); } } @@ -482,13 +544,18 @@ public void testDownsampler_calendarWeek() { source = HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { // a Tuesday in UTC land - new LongHistogramDataPointForTest(DST_TS, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(DST_TS + (86400000L * 7), Bytes.fromLong(2L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), DST_TS), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), DST_TS + (86400000L * 7)), // falls to the next in FJ - new LongHistogramDataPointForTest(1451129400000L, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(DST_TS + (86400000L * 21), Bytes.fromLong(4L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3L), 1451129400000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), DST_TS + (86400000L * 21)), // falls within 30m offset - new LongHistogramDataPointForTest(1452367799000L, Bytes.fromLong(5L)) + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5L), 1452367799000L) }); // control specification = new DownsamplingSpecification("1wc-sum"); @@ -499,7 +566,7 @@ public void testDownsampler_calendarWeek() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); if (ts == 1450569600000L) { ts = 1451779200000L; // skips a week } else { @@ -523,7 +590,7 @@ public void testDownsampler_calendarWeek() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); if (ts == 1450526400000L) { ts = 1451736000000L; // skip a week } else { @@ -549,7 +616,7 @@ public void testDownsampler_calendarWeek() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000L * 7; value++; } @@ -565,7 +632,7 @@ public void testDownsampler_calendarWeek() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); if (ts == 1449948600000L) { ts = 1450553400000L; } else { @@ -589,7 +656,7 @@ public void testDownsampler_calendarWeek() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts = 1451158200000L; value = 9; } @@ -600,17 +667,23 @@ public void testDownsampler_calendarMonth() { final long dec_1st = 1448928000000L; source = spy(HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(dec_1st, Bytes.fromLong(1L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), dec_1st), // falls to the next in FJ - new LongHistogramDataPointForTest(1451559600000L, Bytes.fromLong(2L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), 1451559600000L), // jan 1st - new LongHistogramDataPointForTest(1451606400000L, Bytes.fromLong(3L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3L), 1451606400000L), // feb 1st - new LongHistogramDataPointForTest(1454284800000L, Bytes.fromLong(4L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), 1454284800000L), // feb 29th (leap year) - new LongHistogramDataPointForTest(1456704000000L, Bytes.fromLong(5L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5L), 1456704000000L), // falls within 30m offset AT - new LongHistogramDataPointForTest(1456772400000L, Bytes.fromLong(6L)) + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 6L), 1456772400000L) })); // control @@ -622,7 +695,7 @@ public void testDownsampler_calendarMonth() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); if (ts == 1448928000000L) { ts = 1451606400000L; } else { @@ -642,7 +715,7 @@ public void testDownsampler_calendarMonth() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); if (ts == 1448884800000L) { ts = 1451563200000L; } else if (ts == 1451563200000L) { @@ -665,7 +738,7 @@ public void testDownsampler_calendarMonth() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); if (ts == 1448881200000L) { ts = 1451559600000L; value = 5; @@ -689,7 +762,7 @@ public void testDownsampler_calendarMonth() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); if (ts == 1448911800000L) { ts = 1451590200000L; } else { @@ -709,7 +782,7 @@ public void testDownsampler_calendarMonth() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); ts = 1451563200000L; value = 18; } @@ -718,10 +791,14 @@ public void testDownsampler_calendarMonth() { @Test public void testDownsampler_calendarSkipSomePoints() { source = spy(HistogramSeekableViewForTest - .fromArray(new HistogramDataPoint[] { new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 1800000, Bytes.fromLong(2L)), + .fromArray(new HistogramDataPoint[] { + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 1800000), // skip an hour - new LongHistogramDataPointForTest(BASE_TIME + 7200000, Bytes.fromLong(6L)) })); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 6L), BASE_TIME + 7200000) })); specification = new DownsamplingSpecification("1hc-sum"); specification.setTimezone(TV); downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); @@ -731,7 +808,7 @@ public void testDownsampler_calendarSkipSomePoints() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); ts = 1357005600000L; value = 6; } @@ -771,7 +848,7 @@ public void testSeek() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); - values.add(Bytes.getLong(dp.getRawData())); + values.add(Bytes.getLong(dp.getRawData(false))); timestamps_in_millis.add(dp.timestamp()); } @@ -794,7 +871,7 @@ public void testSeek_skipPartialInterval() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); - values.add(Bytes.getLong(dp.getRawData())); + values.add(Bytes.getLong(dp.getRawData(false))); timestamps_in_millis.add(dp.timestamp()); } @@ -823,7 +900,7 @@ public void testSeek_doubleIteration() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -840,17 +917,28 @@ public void testSeek_doubleIteration() { public void testSeek_abandoningIncompleteInterval() { source = HistogramSeekableViewForTest .fromArray(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 1100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 2100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 3100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 4100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 5100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 6100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 7100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 8100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 9100L, Bytes.fromLong(40L)), - new LongHistogramDataPointForTest(BASE_TIME + 10100L, Bytes.fromLong(40L)) }); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 1100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 2100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 3100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 4100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 5100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 6100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 7100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 8100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 9100L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 40L), BASE_TIME + 10100L) }); specification = new DownsamplingSpecification("10s-sum"); downsampler = new HistogramDownsampler(source, specification, 0, 0); // The seek is aligned by the downsampling window. @@ -858,7 +946,7 @@ public void testSeek_abandoningIncompleteInterval() { assertTrue("seek(BASE_TIME)", downsampler.hasNext()); HistogramDataPoint first_dp = downsampler.next(); assertEquals("seek(1356998400000)", BASE_TIME, first_dp.timestamp()); - assertEquals("seek(1356998400000)", 400, Bytes.getLong(first_dp.getRawData())); + assertEquals("seek(1356998400000)", 400, Bytes.getLong(first_dp.getRawData(false))); // No seeks but the last one is aligned by the downsampling window. for (long seek_timestamp = BASE_TIME + 1000L; seek_timestamp < BASE_TIME @@ -872,18 +960,22 @@ public void testSeek_abandoningIncompleteInterval() { assertEquals(String.format("seek(%d)", seek_timestamp), BASE_TIME + 10000L, dp.timestamp()); assertEquals(String.format("seek(%d)", seek_timestamp), - 40, Bytes.getLong(dp.getRawData())); + 40, Bytes.getLong(dp.getRawData(false))); } } @Test public void testSeek_useCalendar() { source = spy(HistogramSeekableViewForTest - .fromArray(new HistogramDataPoint[] { new LongHistogramDataPointForTest( - 1356998400000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(1388534400000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(1420070400000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(1451606400000L, Bytes.fromLong(8L)) })); + .fromArray(new HistogramDataPoint[] { + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), 1356998400000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), 1388534400000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), 1420070400000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), 1451606400000L) })); specification = new DownsamplingSpecification("1yc-sum"); downsampler = new HistogramDownsampler(source, specification, 0, Long.MAX_VALUE); @@ -895,9 +987,9 @@ public void testSeek_useCalendar() { long value = 4; while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); - System.out.println(dp.timestamp() + " " + Bytes.getLong(dp.getRawData())); + System.out.println(dp.timestamp() + " " + Bytes.getLong(dp.getRawData(false))); assertEquals(timestamp, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); timestamp = 1451606400000L; value = 8; } @@ -910,7 +1002,7 @@ public void testSeek_useCalendar() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(timestamp, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); } } @@ -925,7 +1017,7 @@ public void testHistogramSpanDownSampler() { HistogramSpan.Iterator it = hspan.spanIterator(); while (it.hasNext()) { HistogramDataPoint hdp = it.next(); - it_values.add(Bytes.getLong(hdp.getRawData())); + it_values.add(Bytes.getLong(hdp.getRawData(false))); } assertEquals(6, it_values.size()); assertEquals(40L, it_values.get(0).longValue()); @@ -942,7 +1034,7 @@ public void testHistogramSpanDownSampler() { downsampler = hspan.downsampler(0, 0, specification, false, 0, 0); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -961,18 +1053,29 @@ public void testHistogramSpanDownSampler() { @Test public void testHistogramSpanDownSampler_10seconds() { - List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 0, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 1, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 2, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 3, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 4, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 5, Bytes.fromLong(32L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 6, Bytes.fromLong(64L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 7, Bytes.fromLong(128L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 8, Bytes.fromLong(256L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 9, Bytes.fromLong(512L)), - new LongHistogramDataPointForTest(BASE_TIME + 5000L * 10, Bytes.fromLong(1024L)) }); + List row = Arrays.asList(new HistogramDataPoint[] { + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L * 0), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 5000L * 1), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 5000L * 2), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 5000L * 3), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 5000L * 4), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 5000L * 5), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 64L), BASE_TIME + 5000L * 6), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 128L), BASE_TIME + 5000L * 7), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 256L), BASE_TIME + 5000L * 8), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 512L), BASE_TIME + 5000L * 9), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1024L), BASE_TIME + 5000L * 10) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -984,7 +1087,7 @@ public void testHistogramSpanDownSampler_10seconds() { downsampler = hspan.downsampler(0, 0, specification, false, 0, 0); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -1006,12 +1109,18 @@ public void testHistogramSpanDownSampler_10seconds() { @Test public void testHistogramSpanDownSampler_15seconds() { List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1023,7 +1132,7 @@ public void testHistogramSpanDownSampler_15seconds() { downsampler = hspan.downsampler(0, 0, specification, false, 0, 0); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -1041,12 +1150,18 @@ public void testHistogramSpanDownSampler_15seconds() { @Test public void testHistogramSpanDownsampler_allFullRange() { List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1058,7 +1173,7 @@ public void testHistogramSpanDownsampler_allFullRange() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -1070,12 +1185,18 @@ public void testHistogramSpanDownsampler_allFullRange() { @Test public void testHistogramSpanDownsampler_allFilterOnQuery() { List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1086,7 +1207,7 @@ public void testHistogramSpanDownsampler_allFilterOnQuery() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -1098,12 +1219,18 @@ public void testHistogramSpanDownsampler_allFilterOnQuery() { @Test public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeEarly() { List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1115,7 +1242,7 @@ public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeEarly() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -1124,13 +1251,19 @@ public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeEarly() { @Test public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeLate() { - List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME + 5000L, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 15000L, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 25000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 35000L, Bytes.fromLong(8L)), - new LongHistogramDataPointForTest(BASE_TIME + 45000L, Bytes.fromLong(16L)), - new LongHistogramDataPointForTest(BASE_TIME + 55000L, Bytes.fromLong(32L)) }); + List row = Arrays.asList(new HistogramDataPoint[] { + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME + 5000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 15000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 25000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 8L), BASE_TIME + 35000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 16L), BASE_TIME + 45000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 32L), BASE_TIME + 55000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1142,7 +1275,7 @@ public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeLate() { List timestamps_in_millis = Lists.newArrayList(); while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); - values.add(Bytes.getLong(hdp.getRawData())); + values.add(Bytes.getLong(hdp.getRawData(false))); timestamps_in_millis.add(hdp.timestamp()); } @@ -1151,13 +1284,19 @@ public void testHistogramSpanDownsampler_allFilterOnQueryOutOfRangeLate() { @Test public void testHistogramSpanDownsampler_calendarHour() { - List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(BASE_TIME, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(BASE_TIME + 1800000, Bytes.fromLong(2L)), - new LongHistogramDataPointForTest(BASE_TIME + 3599000L, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(BASE_TIME + 3600000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(BASE_TIME + 5400000L, Bytes.fromLong(5L)), - new LongHistogramDataPointForTest(BASE_TIME + 7199000L, Bytes.fromLong(6L)) }); + List row = Arrays.asList(new HistogramDataPoint[] { + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), BASE_TIME), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), BASE_TIME + 1800000), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3L), BASE_TIME + 3599000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), BASE_TIME + 3600000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5L), BASE_TIME + 5400000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 6L), BASE_TIME + 7199000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1172,7 +1311,7 @@ public void testHistogramSpanDownsampler_calendarHour() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); ts += 3600000; value = 15; } @@ -1189,7 +1328,7 @@ public void testHistogramSpanDownsampler_calendarHour() { while (downsampler.hasNext()) { HistogramDataPoint dp = downsampler.next(); assertEquals(ts, dp.timestamp()); - assertEquals(value, Bytes.getLong(dp.getRawData())); + assertEquals(value, Bytes.getLong(dp.getRawData(false))); ts += 3600000; if (value == 1) { value = 9; @@ -1200,7 +1339,6 @@ public void testHistogramSpanDownsampler_calendarHour() { } - // multiple hours { specification = new DownsamplingSpecification("4hc-sum"); @@ -1212,7 +1350,7 @@ public void testHistogramSpanDownsampler_calendarHour() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); } } } @@ -1221,14 +1359,20 @@ public void testHistogramSpanDownsampler_calendarHour() { public void testHistogramSpanDownsampler_calendarDay() { // UTC List row = Arrays.asList(new HistogramDataPoint[] { - new LongHistogramDataPointForTest(DST_TS, Bytes.fromLong(1L)), - new LongHistogramDataPointForTest(DST_TS + 86399000, Bytes.fromLong(2L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1L), DST_TS), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2L), DST_TS + 86399000), // falls to the next in FJ - new LongHistogramDataPointForTest(DST_TS + 126001000L, Bytes.fromLong(3L)), - new LongHistogramDataPointForTest(DST_TS + 172799000L, Bytes.fromLong(4L)), - new LongHistogramDataPointForTest(DST_TS + 172800000L, Bytes.fromLong(5L)), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3L), DST_TS + 126001000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4L), DST_TS + 172799000L), + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5L), DST_TS + 172800000L), // falls within 30m offset - new LongHistogramDataPointForTest(DST_TS + 242999000L, Bytes.fromLong(6L)) }); + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 6L), DST_TS + 242999000L) }); final HistogramSpan hspan = new HistogramSpan(tsdb); hspan.addRow(KEY, row); @@ -1243,7 +1387,7 @@ public void testHistogramSpanDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 3) { value = 7; @@ -1264,7 +1408,7 @@ public void testHistogramSpanDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 1) { value = 5; @@ -1287,7 +1431,7 @@ public void testHistogramSpanDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 1) { value = 2; @@ -1310,7 +1454,7 @@ public void testHistogramSpanDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); ts += 86400000; if (value == 1) { value = 5; @@ -1331,7 +1475,7 @@ public void testHistogramSpanDownsampler_calendarDay() { while (downsampler.hasNext()) { HistogramDataPoint hdp = downsampler.next(); assertEquals(ts, hdp.timestamp()); - assertEquals(value, Bytes.getLong(hdp.getRawData())); + assertEquals(value, Bytes.getLong(hdp.getRawData(false))); } } } diff --git a/test/core/TestHistogramPojo.java b/test/core/TestHistogramPojo.java new file mode 100644 index 0000000000..57e0fb1799 --- /dev/null +++ b/test/core/TestHistogramPojo.java @@ -0,0 +1,55 @@ +package net.opentsdb.core; + +import static org.mockito.Mockito.when; + +import org.hbase.async.Bytes; +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 net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class }) +public class TestHistogramPojo { + + private TSDB tsdb; + private Config config; + private HistogramCodecManager manager; + + @Before + public void before() throws Exception { + tsdb = PowerMockito.mock(TSDB.class); + config = new Config(false); + + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\": 0}"); + when(tsdb.getConfig()).thenReturn(config); + + manager = new HistogramCodecManager(tsdb); + when(tsdb.histogramManager()).thenReturn(manager); + } + + @Test + public void foo() throws Exception { + int v = 255; + + System.out.println("CV: " + (byte) v); + +// SimpleHistogram y1Hist = new SimpleHistogram(); +// y1Hist.addBucket(1.0f, 2.0f, 5L); +// y1Hist.addBucket(2.0f, 3.0f, 5L); +// y1Hist.addBucket(3.0f, 10.0f, 0L); +// +// byte[] raw = y1Hist.histogram(manager); +// System.out.println(HistogramPojo.bytesToBase64String(raw)); +// System.out.println(HistogramPojo.bytesToHexString(raw)); +// + //HistogramDataPoint h = tsdb.histogramManager().getDecoder((byte) 0).decode(raw, 1356998400000L); + //System.out.println(h); + } + +} diff --git a/test/core/TestHistogramRowSeq.java b/test/core/TestHistogramRowSeq.java index 88bb7c0558..16394f2068 100644 --- a/test/core/TestHistogramRowSeq.java +++ b/test/core/TestHistogramRowSeq.java @@ -77,8 +77,10 @@ public void before() throws Exception { @Test public void setRow() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -91,8 +93,10 @@ public void setRow() throws Exception { @Test (expected = IllegalStateException.class) public void setRowAlreadySet() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -102,8 +106,10 @@ public void setRowAlreadySet() throws Exception { @Test public void addRowMergeLater() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -111,8 +117,10 @@ public void addRowMergeLater() throws Exception { List hdps2 = new ArrayList(); - hdps2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - hdps2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); hrs.addRow(hdps2); assertEquals(4, hrs.size()); @@ -125,8 +133,10 @@ public void addRowMergeLater() throws Exception { @Test public void addRowMergeEarlier() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - hdps.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -134,8 +144,10 @@ public void addRowMergeEarlier() throws Exception { List hdps2 = new ArrayList(); - hdps2.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps2.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); hrs.addRow(hdps2); assertEquals(4, hrs.size()); @@ -148,8 +160,10 @@ public void addRowMergeEarlier() throws Exception { @Test public void addRowMergeMiddle() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(120L, Bytes.fromLong(4))); - hdps.add(new LongHistogramDataPointForTest(125L, Bytes.fromLong(5))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4), 120L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 5), 125L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -157,14 +171,18 @@ public void addRowMergeMiddle() throws Exception { List hdps2 = new ArrayList(); - hdps2.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps2.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); hrs.addRow(hdps2); assertEquals(4, hrs.size()); List hdps3 = new ArrayList(); - hdps3.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - hdps3.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + hdps3.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + hdps3.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); hrs.addRow(hdps3); assertEquals(6, hrs.size()); @@ -179,8 +197,10 @@ public void addRowMergeMiddle() throws Exception { @Test public void addRowMergeDuplicateLater() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -188,8 +208,10 @@ public void addRowMergeDuplicateLater() throws Exception { List hdps2 = new ArrayList(); - hdps2.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(100))); - hdps2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 100), 100L)); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); hrs.addRow(hdps2); assertEquals(3, hrs.size()); @@ -201,8 +223,10 @@ public void addRowMergeDuplicateLater() throws Exception { @Test public void timestamp() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -215,8 +239,10 @@ public void timestamp() throws Exception { @Test (expected = IndexOutOfBoundsException.class) public void timestampOutofBounds() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -230,8 +256,10 @@ public void timestampOutofBounds() throws Exception { @Test public void iterateAllItems() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -242,11 +270,11 @@ public void iterateAllItems() throws Exception { HistogramDataPoint hdp = it.next(); assertEquals(100L, hdp.timestamp()); - assertEquals(0L, Bytes.getLong(hdp.getRawData())); + assertEquals(0L, Bytes.getLong(hdp.getRawData(false))); hdp = it.next(); assertEquals(105L, hdp.timestamp()); - assertEquals(1L, Bytes.getLong(hdp.getRawData())); + assertEquals(1L, Bytes.getLong(hdp.getRawData(false))); assertFalse(it.hasNext()); } @@ -254,8 +282,10 @@ public void iterateAllItems() throws Exception { @Test public void iterateAfterMergeDuplicate() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - hdps.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -263,18 +293,21 @@ public void iterateAfterMergeDuplicate() throws Exception { assertEquals(2, hrs.size()); List hdps2 = new ArrayList(); - hdps2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(20))); - hdps2.add(new LongHistogramDataPointForTest(120L, Bytes.fromLong(4))); + hdps2.add( + new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 20), 110L)); + hdps2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 4), 120L)); final HistogramSeekableView it = hrs.iterator(); HistogramDataPoint hdp = it.next(); assertEquals(110L, hdp.timestamp()); - assertEquals(2L, Bytes.getLong(hdp.getRawData())); + assertEquals(2L, Bytes.getLong(hdp.getRawData(false))); hdp = it.next(); assertEquals(115L, hdp.timestamp()); - assertEquals(3L, Bytes.getLong(hdp.getRawData())); + assertEquals(3L, Bytes.getLong(hdp.getRawData(false))); assertFalse(it.hasNext()); } @@ -285,7 +318,8 @@ public void iterateLarge() throws Exception { final int limit = 64 * 1000; List hdps = new ArrayList(); for (int i = 0; i < limit; ++i) { - hdps.add(new LongHistogramDataPointForTest(ts + 5 * i, Bytes.fromLong(i))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, i), ts + 5 * i)); } final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); @@ -302,9 +336,12 @@ public void iterateLarge() throws Exception { @Test public void seekStart() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -315,7 +352,7 @@ public void seekStart() throws Exception { it.seek(100L); HistogramDataPoint hdp = it.next(); assertEquals(100L, hdp.timestamp()); - assertEquals(0, Bytes.getLong(hdp.getRawData())); + assertEquals(0, Bytes.getLong(hdp.getRawData(false))); assertTrue(it.hasNext()); } @@ -323,9 +360,12 @@ public void seekStart() throws Exception { @Test public void seekMsBetween() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -336,7 +376,7 @@ public void seekMsBetween() throws Exception { it.seek(105L); HistogramDataPoint hdp = it.next(); assertEquals(105L, hdp.timestamp()); - assertEquals(1, Bytes.getLong(hdp.getRawData())); + assertEquals(1, Bytes.getLong(hdp.getRawData(false))); assertTrue(it.hasNext()); } @@ -344,9 +384,12 @@ public void seekMsBetween() throws Exception { @Test public void seekMsEnd() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -357,7 +400,7 @@ public void seekMsEnd() throws Exception { it.seek(110L); HistogramDataPoint hdp = it.next(); assertEquals(110L, hdp.timestamp()); - assertEquals(2, Bytes.getLong(hdp.getRawData())); + assertEquals(2, Bytes.getLong(hdp.getRawData(false))); assertFalse(it.hasNext()); } @@ -365,8 +408,10 @@ public void seekMsEnd() throws Exception { @Test public void seekMsTooEarly() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -377,7 +422,7 @@ public void seekMsTooEarly() throws Exception { it.seek(100L); HistogramDataPoint hdp = it.next(); assertEquals(105L, hdp.timestamp()); - assertEquals(1, Bytes.getLong(hdp.getRawData())); + assertEquals(1, Bytes.getLong(hdp.getRawData(false))); assertTrue(it.hasNext()); } @@ -385,9 +430,12 @@ public void seekMsTooEarly() throws Exception { @Test (expected = NoSuchElementException.class) public void seekMsPastLastDp() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); @@ -403,9 +451,12 @@ public void seekMsPastLastDp() throws Exception { @Test public void getTagUids() throws Exception { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); - hdps.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); final HistogramRowSeq hrs = new HistogramRowSeq(tsdb); hrs.setRow(key, hdps); diff --git a/test/core/TestHistogramSpan.java b/test/core/TestHistogramSpan.java index b23ff99b4c..c613374401 100644 --- a/test/core/TestHistogramSpan.java +++ b/test/core/TestHistogramSpan.java @@ -89,8 +89,10 @@ public void before() throws Exception { @Test public void addRow() { List hdps = new ArrayList(); - hdps.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - hdps.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + hdps.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, hdps); @@ -107,15 +109,19 @@ public void addRowNull() { @Test (expected = IllegalArgumentException.class) public void addRowBadKeyLength() { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); List row2 = new ArrayList(); - row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final byte[] bad_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x43, 0x20, 0, 0, 0, 1 }; @@ -125,15 +131,19 @@ public void addRowBadKeyLength() { @Test (expected = IllegalArgumentException.class) public void addRowMissMatchedMetric() { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); List row2 = new ArrayList(); - row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final byte[] not_matched_mitric_key = new byte[] { 0, 0, 0, 2, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 2 }; @@ -143,15 +153,19 @@ public void addRowMissMatchedMetric() { @Test (expected = IllegalArgumentException.class) public void addRowMissMatchedTagk() { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); List row2 = new ArrayList(); - row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final byte[] not_matched_tagk_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 2, 0, 0, 0, 2 }; @@ -161,15 +175,19 @@ public void addRowMissMatchedTagk() { @Test (expected = IllegalArgumentException.class) public void addRowMissMatchedTagv() { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); List row2 = new ArrayList(); - row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final byte[] not_matched_tagv_key = new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 3 }; @@ -179,16 +197,20 @@ public void addRowMissMatchedTagv() { @Test public void addRowOutOfOrder() { List row2 = new ArrayList(); - row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour2, row2); List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); histSpan.addRow(hour1, row1); assertEquals(4, histSpan.size()); @@ -202,8 +224,10 @@ public void addRowOutOfOrder() { @Test (expected = IllegalArgumentException.class) public void addDifferentKey() throws Exception { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); @@ -212,8 +236,10 @@ public void addDifferentKey() throws Exception { hour1_with_diff_key[hour1_with_diff_key.length - 1] = 3; List row2 = new ArrayList(); - row2.add(new LongHistogramDataPointForTest(110L, Bytes.fromLong(2))); - row2.add(new LongHistogramDataPointForTest(115L, Bytes.fromLong(3))); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 2), 110L)); + row2.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 3), 115L)); histSpan.addRow(hour1_with_diff_key, row2); } @@ -221,8 +247,10 @@ public void addDifferentKey() throws Exception { @Test public void getTagUids() { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); @@ -245,8 +273,10 @@ public void getTagUidsNotSet() { @Test public void getAggregatedTagUids() { List row1 = new ArrayList(); - row1.add(new LongHistogramDataPointForTest(100L, Bytes.fromLong(0))); - row1.add(new LongHistogramDataPointForTest(105L, Bytes.fromLong(1))); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 0), 100L)); + row1.add(new SimpleHistogramDataPointAdapter( + new LongHistogramDataPointForTest(0, 1), 105L)); final HistogramSpan histSpan = new HistogramSpan(tsdb); histSpan.addRow(hour1, row1); diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index 163edb31ac..51d023bf8d 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -60,7 +60,7 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({ TSDB.class, Scanner.class, SaltScanner.class, Span.class, Const.class, UniqueId.class, Tags.class, QueryStats.class, DateTime.class, - HistogramDataPointDecoderManager.class, + HistogramCodecManager.class, SimpleHistogram.class, SimpleHistogramDecoder.class}) public class TestSaltScannerHistogram extends BaseTsdbTest { protected final static byte[] FAMILY = "t".getBytes(); @@ -139,8 +139,8 @@ public void before() throws Exception { tags.put(TAGK_STRING, TAGV_STRING); config.overrideConfig("tsd.core.histograms.config", "{\"net.opentsdb.core.LongHistogramDataPointForTestDecoder\": 0}"); - HistogramDataPointDecoderManager manager = - new HistogramDataPointDecoderManager(tsdb); + HistogramCodecManager manager = + new HistogramCodecManager(tsdb); Whitebox.setInternalState(tsdb, "histogram_manager", manager); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); @@ -193,7 +193,7 @@ public void scan() throws Exception { setupMockScanners(false); SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, null, false, null, query_stats, 0, spans); @@ -225,7 +225,7 @@ public void scanWithFilter() throws Exception { filters.add(new TagVWildcardFilter(TAGK_STRING, "web*")); SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, null, false, null, query_stats, 0, spans); @@ -260,7 +260,7 @@ public void scanWithFiltersOnSameTag() throws Exception { filters.add(new TagVRegexFilter("host", "w.*")); SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, null, false, null, query_stats, 0, spans); @@ -294,7 +294,7 @@ public void scanWithFiltersOnSameTagOneFail() throws Exception { filters.add(new TagVWildcardFilter("host", "drood*")); SimpleHistogram y1Hist = mock(SimpleHistogram.class); - PowerMockito.whenNew(SimpleHistogram.class).withNoArguments().thenReturn(y1Hist); + PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, null, filters, false, null, query_stats, 0, spans); diff --git a/test/core/TestSimpleHistogram.java b/test/core/TestSimpleHistogram.java index f1b134dc97..046232678a 100644 --- a/test/core/TestSimpleHistogram.java +++ b/test/core/TestSimpleHistogram.java @@ -12,9 +12,11 @@ // see . package net.opentsdb.core; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -23,7 +25,12 @@ import java.util.ArrayList; import java.util.Map; +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 com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; @@ -32,8 +39,28 @@ import net.opentsdb.core.HistogramDataPoint.HistogramBucket; import net.opentsdb.core.HistogramDataPoint.HistogramBucket.BucketType; +import net.opentsdb.utils.Config; +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class }) public class TestSimpleHistogram { + + private TSDB tsdb; + private Config config; + private HistogramCodecManager manager; + + @Before + public void before() throws Exception { + tsdb = PowerMockito.mock(TSDB.class); + config = new Config(false); + + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\": 0}"); + when(tsdb.getConfig()).thenReturn(config); + + manager = new HistogramCodecManager(tsdb); + when(tsdb.histogramManager()).thenReturn(manager); + } @Test public void verifyE2EKryo() { @@ -42,8 +69,8 @@ public void verifyE2EKryo() { //Encoding stage ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); Output output = new Output(outBuffer); - output.writeByte(0 /* This is the type of histogram (or sketch) written - // to storage. HistoType.SimpleHistogramType.ordinal()*/); + // This is the type of histogram (or sketch) written to storage + output.writeByte(0); output.writeShort(8); output.writeFloat(1.0f); output.writeFloat(2.0f); @@ -79,9 +106,9 @@ public void verifyE2EKryo() { switch (metricType) { case 0: - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); y1Hist.read(kryo, input); - Input verifyHist = new Input(new ByteArrayInputStream(y1Hist.histogram())); + Input verifyHist = new Input(new ByteArrayInputStream(y1Hist.histogram(false))); int bucketCount = verifyHist.readShort(); assertEquals(bucketCount, 8); @@ -100,23 +127,102 @@ public void verifyE2EKryo() { input.close(); } + @Test + public void testToFromBytes() { + //Encoding stage + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Output output = new Output(out); + // This is the type of histogram (or sketch) written to storage + output.writeByte(42); + output.writeShort(8); + output.writeFloat(1.0f); + output.writeFloat(2.0f); + output.writeLong(5, true); + output.writeFloat(2.0f); + output.writeFloat(3.0f); + output.writeLong(5, true); + output.writeFloat(3.0f); + output.writeFloat(4.0f); + output.writeLong(5, true); + output.writeFloat(4.0f); + output.writeFloat(5.0f); + output.writeLong(0, true); + output.writeFloat(5.0f); + output.writeFloat(6.0f); + output.writeLong(0, true); + output.writeFloat(6.0f); + output.writeFloat(7.0f); + output.writeLong(0, true); + output.writeFloat(7.0f); + output.writeFloat(8.0f); + output.writeLong(0, true); + output.writeFloat(8.0f); + output.writeFloat(9.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(2, true); + output.close(); + + SimpleHistogram hist = new SimpleHistogram(42); + hist.fromHistogram(out.toByteArray(), true); + assertArrayEquals(out.toByteArray(), hist.histogram(true)); + + // skip the id + out = new ByteArrayOutputStream(); + output = new Output(out); + // This is the type of histogram (or sketch) written to storage + //output.writeByte(42); // <-- Skipped! + output.writeShort(8); + output.writeFloat(1.0f); + output.writeFloat(2.0f); + output.writeLong(5, true); + output.writeFloat(2.0f); + output.writeFloat(3.0f); + output.writeLong(5, true); + output.writeFloat(3.0f); + output.writeFloat(4.0f); + output.writeLong(5, true); + output.writeFloat(4.0f); + output.writeFloat(5.0f); + output.writeLong(0, true); + output.writeFloat(5.0f); + output.writeFloat(6.0f); + output.writeLong(0, true); + output.writeFloat(6.0f); + output.writeFloat(7.0f); + output.writeLong(0, true); + output.writeFloat(7.0f); + output.writeFloat(8.0f); + output.writeLong(0, true); + output.writeFloat(8.0f); + output.writeFloat(9.0f); + output.writeLong(0, true); + output.writeLong(0, true); + output.writeLong(2, true); + output.close(); + + hist = new SimpleHistogram(42); + hist.fromHistogram(out.toByteArray(), false); + assertArrayEquals(out.toByteArray(), hist.histogram(false)); + } + @Test public void testHistogramSerialization() { Kryo kryo = new Kryo(); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); Output output = new Output(outBuffer); - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); y1Hist.addBucket(1.0f, 2.0f, 5L); y1Hist.addBucket(2.0f, 3.0f, 5L); y1Hist.addBucket(3.0f, 10.0f, 0L); y1Hist.write(kryo, output); output.close(); - SimpleHistogram y1HistVerify = new SimpleHistogram(); - y1HistVerify.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1HistVerify = new SimpleHistogram(0); + y1HistVerify.fromHistogram(outBuffer.toByteArray(), false); - Input input = new Input(new ByteArrayInputStream(y1HistVerify.histogram())); + Input input = new Input(new ByteArrayInputStream(y1HistVerify.histogram(false))); int bucketCount = input.readShort(); assertEquals(bucketCount, 3); @@ -135,16 +241,16 @@ public void testIncompletByteArray() { output.writeShort(4); output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); boolean exceptionCaught = false; try { - y1Hist.fromHistogram(outBuffer.toByteArray()); + y1Hist.fromHistogram(outBuffer.toByteArray(), false); } catch(Exception e) { exceptionCaught = true; } - assertFalse(exceptionCaught); + assertTrue(exceptionCaught); } @Test @@ -158,7 +264,7 @@ public void testInvalidHistogramLength() { Input input = new Input(new ByteArrayInputStream(outBuffer.toByteArray())); Integer metricType = input.readInt(); - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); boolean exceptionCaught = false; try { y1Hist.read(kryo, input); @@ -188,8 +294,8 @@ public void testSinglePercentile() { output.writeLong(5, true); output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist = new SimpleHistogram(0); + y1Hist.fromHistogram(outBuffer.toByteArray(), false); double perc50 = y1Hist.percentile(50.0f); assertEquals(perc50, 8.0f, 0.0001); @@ -217,8 +323,8 @@ public void testPercentileList() { output.writeLong(5, true); output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist = new SimpleHistogram(0); + y1Hist.fromHistogram(outBuffer.toByteArray(), false); ArrayList percs = new ArrayList(); percs.add(50.0); percs.add(99.0); @@ -251,11 +357,11 @@ public void testSingleHistogramMerge() { output.writeLong(5, true); output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist = new SimpleHistogram(0); + y1Hist.fromHistogram(outBuffer.toByteArray(), false); - SimpleHistogram y1Hist1 = new SimpleHistogram(); - y1Hist1.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist1 = new SimpleHistogram(0); + y1Hist1.fromHistogram(outBuffer.toByteArray(), false); y1Hist1.setUnderflow(2L); y1Hist.aggregate(y1Hist1, HistogramAggregation.SUM); @@ -287,15 +393,15 @@ public void testMultipleHistogramMerge() { output.writeLong(5, true); output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist = new SimpleHistogram(0); + y1Hist.fromHistogram(outBuffer.toByteArray(), false); ArrayList histos = new ArrayList(); - SimpleHistogram y1Hist1 = new SimpleHistogram(); - y1Hist1.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist1 = new SimpleHistogram(0); + y1Hist1.fromHistogram(outBuffer.toByteArray(), false); histos.add(y1Hist1); - SimpleHistogram y1Hist2 = new SimpleHistogram(); - y1Hist2.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist2 = new SimpleHistogram(0); + y1Hist2.fromHistogram(outBuffer.toByteArray(), false); histos.add(y1Hist2); y1Hist.aggregate(histos, HistogramAggregation.SUM); @@ -326,8 +432,8 @@ public void testArbitraryBuckets() { output.writeLong(5, true); output.close(); - SimpleHistogram y1Hist = new SimpleHistogram(); - y1Hist.fromHistogram(outBuffer.toByteArray()); + SimpleHistogram y1Hist = new SimpleHistogram(0); + y1Hist.fromHistogram(outBuffer.toByteArray(), false); assertEquals(y1Hist.getBucketCount(6.0f, 10.0f), Long.valueOf(10L)); assertEquals(y1Hist.getBucketCount(5.0f, 6.0f), Long.valueOf(0L)); @@ -337,7 +443,7 @@ public void testArbitraryBuckets() { @Test public void testAddBuckets() { - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); y1Hist.addBucket(5.0f, 7.0f, 3L); assertEquals(y1Hist.getBucketCount(5.0f, 7.0f), Long.valueOf(3L));; @@ -348,7 +454,7 @@ public void testAddBuckets() { @Test public void testNullBucketVal() { - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); y1Hist.addBucket(5.0f, 7.0f, null); assertEquals(y1Hist.getBucketCount(5.0f, 7.0f), Long.valueOf(0L)); @@ -358,7 +464,7 @@ public void testNullBucketVal() { @Test public void testJsonSerialization() { - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); y1Hist.addBucket(5.0f, 7.0f, 3L); y1Hist.addBucket(7.0f, 10.0f, 5L); @@ -379,7 +485,7 @@ public void testJsonSerialization() { assertTrue(jsonOut.contains("\"underflow\":0")); assertTrue(jsonOut.contains("\"overflow\":1")); - SimpleHistogram y1Hist1 = new SimpleHistogram(); + SimpleHistogram y1Hist1 = new SimpleHistogram(0); y1Hist1.addBucket(Float.NEGATIVE_INFINITY, 5.0f, 3L); y1Hist1.addBucket(7.0f, 10.0f, 5L); @@ -400,7 +506,7 @@ public void testJsonSerialization() { @Test public void testImmutableGetHistogram() { - SimpleHistogram y1Hist = new SimpleHistogram(); + SimpleHistogram y1Hist = new SimpleHistogram(0); y1Hist.addBucket(5.0f, 7.0f, 3L); y1Hist.addBucket(7.0f, 10.0f, 5L); @@ -419,12 +525,12 @@ public void testImmutableGetHistogram() { @Test public void testMissingBucketsHistogramAggregation() { - SimpleHistogram hist1 = new SimpleHistogram(); + SimpleHistogram hist1 = new SimpleHistogram(0); hist1.addBucket(5.0f, 7.0f, 3L); hist1.addBucket(7.0f, 10.0f, 5L); hist1.addBucket(15.0f, 20.0f, 2L); - SimpleHistogram hist2 = new SimpleHistogram(); + SimpleHistogram hist2 = new SimpleHistogram(0); hist2.addBucket(5.0f, 7.0f, 3L); hist2.addBucket(7.0f, 10.0f, 5L); hist2.addBucket(10.0f, 15.0f, 2L); diff --git a/test/core/TestTSDBAddHistogramPoint.java b/test/core/TestTSDBAddHistogramPoint.java index fddd4f69fd..db3b3f5f3c 100644 --- a/test/core/TestTSDBAddHistogramPoint.java +++ b/test/core/TestTSDBAddHistogramPoint.java @@ -47,9 +47,9 @@ public void addHistogramPoint() throws Exception { @Test (expected = IllegalArgumentException.class) public void addHistogramPointShortRawData() throws Exception { - byte[] testRawValue = new byte[1]; + byte[] testRawValue = new byte[0]; tsdb.addHistogramPoint(METRIC_STRING, 1356998400, testRawValue, tags) - .joinUninterruptibly(); + .joinUninterruptibly(); } @Test (expected = IllegalArgumentException.class) diff --git a/test/core/TestTsdbQueryHistogramQueries.java b/test/core/TestTsdbQueryHistogramQueries.java index 322468ec2f..7a1866adbb 100644 --- a/test/core/TestTsdbQueryHistogramQueries.java +++ b/test/core/TestTsdbQueryHistogramQueries.java @@ -101,8 +101,8 @@ public void before() throws Exception { tags.put(TAGK_STRING, TAGV_STRING); config.overrideConfig("tsd.core.histograms.config", "{\"net.opentsdb.core.LongHistogramDataPointForTestDecoder\": 0}"); - HistogramDataPointDecoderManager manager = - new HistogramDataPointDecoderManager(tsdb); + HistogramCodecManager manager = + new HistogramCodecManager(tsdb); Whitebox.setInternalState(tsdb, "histogram_manager", manager); query = new TsdbQuery(tsdb); diff --git a/test/tsd/TestHistogramDataPointRpc.java b/test/tsd/TestHistogramDataPointRpc.java new file mode 100644 index 0000000000..37c417c226 --- /dev/null +++ b/test/tsd/TestHistogramDataPointRpc.java @@ -0,0 +1,582 @@ +// 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.tsd; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicLong; + +import org.hbase.async.HBaseClient; +import org.hbase.async.HBaseException; +import org.hbase.async.PleaseThrottleException; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.util.HashedWheelTimer; +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.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.google.common.collect.Maps; + +import net.opentsdb.core.HistogramCodecManager; +import net.opentsdb.core.HistogramPojo; +import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.core.SimpleHistogram; +import net.opentsdb.core.TSDB; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId.UniqueIdType; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; + +@RunWith(PowerMockRunner.class) +//"Classloader hell"... It's real. Tell PowerMock to ignore these classes +//because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +public class TestHistogramDataPointRpc extends BaseTestPutRpc { + + protected AtomicLong raw_histograms = new AtomicLong(); + protected AtomicLong raw_histograms_stored = new AtomicLong(); + + private HistogramCodecManager manager; + private SimpleHistogram test_histo; + + @Before + public void before() throws Exception { + uid_map = Maps.newHashMap(); + PowerMockito.mockStatic(Threads.class); + timer = new FakeTaskTimer(); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + + config = new Config(false); + config.overrideConfig("tsd.storage.enable_compaction", "false"); + config.overrideConfig("tsd.core.histograms.config", + "{\"net.opentsdb.core.SimpleHistogramDecoder\": 42}"); + tsdb = new TSDB(config); + + config.setAutoMetric(true); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + setupMetricMaps(); + setupTagkMaps(); + setupTagvMaps(); + + mockUID(UniqueIdType.METRIC, HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + + // add metrics and tags to the UIDs list for other functions to share + uid_map.put(METRIC_STRING, METRIC_BYTES); + uid_map.put(METRIC_B_STRING, METRIC_B_BYTES); + uid_map.put(NSUN_METRIC, NSUI_METRIC); + uid_map.put(HISTOGRAM_METRIC_STRING, HISTOGRAM_METRIC_BYTES); + + uid_map.put(TAGK_STRING, TAGK_BYTES); + uid_map.put(TAGK_B_STRING, TAGK_B_BYTES); + uid_map.put(NSUN_TAGK, NSUI_TAGK); + + uid_map.put(TAGV_STRING, TAGV_BYTES); + uid_map.put(TAGV_B_STRING, TAGV_B_BYTES); + uid_map.put(NSUN_TAGV, NSUI_TAGV); + + uid_map.putAll(UIDS); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + tags = new HashMap(1); + tags.put(TAGK_STRING, TAGV_STRING); + + + manager = new HistogramCodecManager(tsdb); + Whitebox.setInternalState(tsdb, "histogram_manager", manager); + + storage = new MockBase(tsdb, client, true, true, true, true); + + test_histo = new SimpleHistogram(42); + test_histo.addBucket(0F, 1F, 42L); + test_histo.addBucket(1F, 5F, 24L); + test_histo.setOverflow(1L); + + // counters + raw_histograms = Whitebox.getInternalState(PutDataPointRpc.class, "raw_histograms"); + raw_histograms.set(0); + raw_histograms_stored = Whitebox.getInternalState(PutDataPointRpc.class, "raw_histograms_stored"); + raw_histograms_stored.set(0); + } + + @Test + public void constructor() { + HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + assertTrue(rpc.enabled()); + + config.overrideConfig("tsd.core.histograms.config", null); + rpc = new HistogramDataPointRpc(tsdb.getConfig()); + assertFalse(rpc.enabled()); + } + + @Test + public void executeTelnet() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1); + verify(chan, never()).write(any()); + verify(chan, never()).isConnected(); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + byte[] value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + } + + @Test + public void executeTelnetHistosDisabled() throws Exception { + config.overrideConfig("tsd.core.histograms.config", null); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + assertNull(storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier)); + } + + @Test + public void executeTelnetValueTooShort() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + .substring(0, 4), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + assertNull(storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier)); + } + + @Test + public void executeTelnetCorruptValue() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + .substring(0, 8), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + assertNull(storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier)); + } + + @Test + public void executeTelnetHBaseError() throws Exception { + storage.throwException(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), mock(HBaseException.class)); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + assertNull(storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier)); + } + + @Test + public void executeTelnetePleaseThrottle() throws Exception { + storage.throwException(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), mock(PleaseThrottleException.class)); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(true); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + assertNull(storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier)); + } + + @Test (expected = NullPointerException.class) + public void executeTelnetNullTSDB() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + + rpc.execute(null, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + } + + @Test + public void executeTelnetNullChannelOK() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, null, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1); + validateSEH(false); + } + + @Test (expected = NullPointerException.class) + public void executeNullChannelError() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, null, new String[] { "histogram" }) + .joinUninterruptibly(); + } + + @Test (expected = NullPointerException.class) + public void executeNullArray() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, null).joinUninterruptibly(); + } + + @Test (expected = ArrayIndexOutOfBoundsException.class) + public void executeEmptyArray() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[0]).joinUninterruptibly(); + } + + @Test + public void executeShortArray() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42" }).joinUninterruptibly(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeMissingTags() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + "" }).joinUninterruptibly(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeNSUNTagk() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + NSUN_TAGK + "=" + TAGV_STRING }).joinUninterruptibly(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeNSUNTagV() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "42", + HistogramPojo.bytesToBase64String(test_histo.histogram(false)), + TAGK_STRING + "=" + NSUN_TAGV }).joinUninterruptibly(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0); + verify(chan, times(1)).write(any()); + verify(chan, times(1)).isConnected(); + validateSEH(false); + } + + @Test + public void executeHttpSingle() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + byte[] value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + } + + @Test + public void executeHttpTwo() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}," + + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998460," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + byte[] value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + + qualifier = new byte[] { 0x06, 0, 0x3C }; + value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + } + + @Test + public void executeHttpTwoOneGoodOneBad() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}," + + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998460," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + .substring(0, 4) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}]"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 1); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + byte[] value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + + qualifier = new byte[] { 0x06, 0, 0x3C }; + assertNull(storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier)); + } + + @Test + public void httpNSUNMetric() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + NSUN_METRIC + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0); + validateSEH(false); + } + + @Test + public void httpNSUNTagk() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + NSUN_TAGK + "\":\"" + TAGV_STRING + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0); + validateSEH(false); + } + + @Test + public void httpNSUNTagv() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + NSUN_TAGV + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0); + validateSEH(false); + } + + @Test + public void httpHBaseError() throws Exception { + storage.throwException(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), mock(HBaseException.class)); + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0); + validateSEH(true); + } + + @Test + public void httpPleaseThrottleError() throws Exception { + storage.throwException(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), mock(PleaseThrottleException.class)); + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"id\":42,\"value\":\"" + + HistogramPojo.bytesToBase64String(test_histo.histogram(false)) + + "\",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0); + validateSEH(true); + } + + @Override + protected void validateSEH(final boolean called) { + if (called) { + if (handler != null) { + verify(handler, times(1)).handleError((IncomingDataPoint)any(), + (Exception)any()); + } + } else { + if (handler != null) { + verify(handler, never()).handleError((IncomingDataPoint)any(), + (Exception)any()); + } + } + } + + protected void validateCounters( + final long telnet_requests, + final long http_requests, + final long raw_dps, + final long rollup_dps, + final long raw_stored, + final long rollup_stored, + final long hbase_errors, + final long unknown_errors, + final long invalid_values, + final long illegal_arguments, + final long unknown_metrics, + final long inflight_exceeded, + final long writes_blocked, + final long writes_timedout, + final long requests_timedout, + final long raw_histograms, + final long raw_histograms_stored) { + assertEquals(telnet_requests, this.telnet_requests.get()); + assertEquals(http_requests, this.http_requests.get()); + assertEquals(raw_dps, this.raw_dps.get()); + assertEquals(rollup_dps, this.rollup_dps.get()); + assertEquals(raw_stored, this.raw_stored.get()); + assertEquals(rollup_stored, this.rollup_stored.get()); + assertEquals(hbase_errors, this.hbase_errors.get()); + assertEquals(unknown_errors, this.unknown_errors.get()); + assertEquals(invalid_values, this.invalid_values.get()); + assertEquals(illegal_arguments, this.illegal_arguments.get()); + assertEquals(unknown_metrics, this.unknown_metrics.get()); + assertEquals(inflight_exceeded, this.inflight_exceeded.get()); + assertEquals(writes_blocked, this.writes_blocked.get()); + assertEquals(writes_timedout, this.writes_timedout.get()); + assertEquals(requests_timedout, this.requests_timedout.get()); + assertEquals(raw_histograms, this.raw_histograms.get()); + assertEquals(raw_histograms_stored, this.raw_histograms_stored.get()); + } +} diff --git a/test/tsd/TestRollupRpc.java b/test/tsd/TestRollupRpc.java index da09369cdf..d5a264ca2c 100644 --- a/test/tsd/TestRollupRpc.java +++ b/test/tsd/TestRollupRpc.java @@ -89,8 +89,6 @@ public void beforeLocal() throws Exception { Whitebox.setInternalState(tsdb, "raw_agg_tag_value", config.getString("tsd.rollups.raw_agg_tag_value")); setupGroupByTagValues(); - - setupGroupByTagValues(); row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); } From 43886873c49650c65549c2dc9fb1959489396883 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 2 Jun 2017 13:48:50 -0700 Subject: [PATCH 641/826] Add support for writing histograms without encoding. E.g. the user can pass in overflow, underflow and bucket definitions from non-Java code. Signed-off-by: Chris Larsen --- src/core/HistogramPojo.java | 108 +++++++++++++++++++++++- src/tsd/HistogramDataPointRpc.java | 94 ++++++++++++++++++--- src/tsd/PutDataPointRpc.java | 23 +++-- test/tsd/TestHistogramDataPointRpc.java | 46 +++++++++- 4 files changed, 242 insertions(+), 29 deletions(-) diff --git a/src/core/HistogramPojo.java b/src/core/HistogramPojo.java index 8d88e7675e..cc1ee78910 100644 --- a/src/core/HistogramPojo.java +++ b/src/core/HistogramPojo.java @@ -1,7 +1,20 @@ +// 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 java.util.List; import java.util.Map; +import java.util.Map.Entry; import javax.xml.bind.DatatypeConverter; @@ -9,12 +22,19 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Strings; public class HistogramPojo extends IncomingDataPoint { private static final Logger LOG = LoggerFactory.getLogger(HistogramPojo.class); private int id; + private Map buckets; + + private long underflow; + + private long overflow; + public int getId() { return id; } @@ -25,24 +45,104 @@ public void setId(int id) { @Override public boolean validate(final List> details) { - if (!super.validate(details)) { + if (this.getMetric() == null || this.getMetric().isEmpty()) { + if (details != null) { + details.add(getHttpDetails("Metric name was empty")); + } + LOG.warn("Metric name was empty: " + this); return false; } - if (id < 0 || id > 255) { + + if (this.getTimestamp() <= 0) { if (details != null) { - details.add(getHttpDetails("Invalid type. Must be from 0 to 255.")); + details.add(getHttpDetails("Invalid timestamp")); } - LOG.warn("Invalid type. Must be from 0 to 255."); + LOG.warn("Invalid timestamp: " + this); return false; } + + if (this.getTags() == null || this.getTags().size() < 1) { + if (details != null) { + details.add(getHttpDetails("Missing tags")); + } + LOG.warn("Missing tags: " + this); + return false; + } + + if (Strings.isNullOrEmpty(value)) { + if (buckets == null || buckets.isEmpty()) { + if (details != null) { + details.add(getHttpDetails("Histogram buckets cannot be null or empty " + + "if 'value' is empty.")); + } + LOG.warn("Histogram buckets cannot be null or empty if 'value' is empty."); + return false; + } + } else { + // ID is required for binary histos. + if (id < 0 || id > 255) { + if (details != null) { + details.add(getHttpDetails("Invalid type. Must be from 0 to 255.")); + } + LOG.warn("Invalid type. Must be from 0 to 255."); + return false; + } + } return true; } + public SimpleHistogram toSimpleHistogram(final TSDB tsdb) { + if (buckets == null || buckets.isEmpty()) { + throw new IllegalArgumentException("Buckets cannot be empty when " + + "creating a simple histogram."); + } + + final SimpleHistogram shdp = new SimpleHistogram( + tsdb.histogramManager().getCodec(SimpleHistogramDecoder.class)); + shdp.setOverflow(overflow); + shdp.setUnderflow(underflow); + + for (final Entry bucket : buckets.entrySet()) { + final String[] bounds = Tags.splitString(bucket.getKey(), ','); + if (bounds.length != 2) { + throw new IllegalArgumentException("Unable to parse bucket bounds: " + + bucket.getKey()); + } + shdp.addBucket(Float.parseFloat(bounds[0]), Float.parseFloat(bounds[1]), + bucket.getValue()); + } + return shdp; + } + + public Map getBuckets() { + return buckets; + } + + public long getUnderflow() { + return underflow; + } + + public long getOverflow() { + return overflow; + } + @JsonIgnore public byte[] getBytes() { return base64StringToBytes(value); } + public void setBuckets(final Map buckets) { + this.buckets = buckets; + } + + public void setUnderflow(final long underflow) { + this.underflow = underflow; + } + + public void setOverflow(final long overflow) { + this.overflow = overflow; + } + public static String bytesToBase64String(final byte[] raw) { return DatatypeConverter.printBase64Binary(raw); } diff --git a/src/tsd/HistogramDataPointRpc.java b/src/tsd/HistogramDataPointRpc.java index 712ff643f1..20cf29d386 100644 --- a/src/tsd/HistogramDataPointRpc.java +++ b/src/tsd/HistogramDataPointRpc.java @@ -28,6 +28,8 @@ import net.opentsdb.core.Histogram; import net.opentsdb.core.HistogramPojo; import net.opentsdb.core.IncomingDataPoint; +import net.opentsdb.core.SimpleHistogram; +import net.opentsdb.core.SimpleHistogramDecoder; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; import net.opentsdb.utils.Config; @@ -96,10 +98,10 @@ protected Deferred importDataPoint(final TSDB tsdb, } words[0] = null; // Ditch the "histogram". - if (words.length < 6) { // Need at least: metric timestamp value tag - // ^ 6 and not 5 because words[0] is "histogram". + if (words.length < 5) { // Need at least: metric timestamp value tag + // ^ 5 and not 4 because words[0] is "histogram". throw new IllegalArgumentException("not enough arguments" - + " (need least 6, got " + + " (need least 5, got " + (words.length - 1) + ')'); } final String metric = words[1]; @@ -116,14 +118,25 @@ protected Deferred importDataPoint(final TSDB tsdb, throw new IllegalArgumentException("invalid timestamp: " + timestamp); } - final int id = Integer.parseInt(words[3]); - - final String value = words[4]; + boolean has_id = false; + int id = 0; + try { + id = Integer.parseInt(words[3]); + has_id = true; + } catch (NumberFormatException e) { } + final String value; + if (has_id) { + value = words[4]; + } else { + // it's a simple Id + id = tsdb.histogramManager().getCodec(SimpleHistogramDecoder.class); + value = words[3]; + } if (value.length() <= 0) { throw new IllegalArgumentException("empty histogram value"); } final HashMap tags = new HashMap(); - for (int i = 5; i < words.length; i++) { + for (int i = has_id ? 5 : 4; i < words.length; i++) { if (!words[i].isEmpty()) { Tags.parse(tags, words[i]); } @@ -131,8 +144,13 @@ protected Deferred importDataPoint(final TSDB tsdb, // validation and prepend the ID. try { - final Histogram dp = tsdb.histogramManager().decode(id, + final Histogram dp; + if (has_id) { + dp = tsdb.histogramManager().decode(id, HistogramPojo.base64StringToBytes(value), false); + } else { + dp = parseTelnet(tsdb, value); + } return tsdb.addHistogramPoint(metric, timestamp, tsdb.histogramManager().encode(id, dp, true), tags); } catch (Exception e) { @@ -153,16 +171,33 @@ protected IncomingDataPoint getDataPointFromString(final TSDB tsdb, throw new IllegalArgumentException("invalid timestamp: " + timestamp); } - final int id = Integer.parseInt(words[3]); + boolean has_id = false; + int id = 0; + try { + id = Integer.parseInt(words[3]); + has_id = true; + } catch (NumberFormatException e) { } + final String value; + if (has_id) { + value = words[4]; + } else { + // it's a simple Id + id = tsdb.histogramManager().getCodec(SimpleHistogramDecoder.class); + value = words[3]; + } - final HistogramPojo dp = new HistogramPojo(); dp.setMetric(words[1]); dp.setTimestamp(timestamp); dp.setId(id); - dp.setValue(words[4]); + if (has_id) { + dp.setValue(value); + } else { + dp.setValue(HistogramPojo.bytesToBase64String( + parseTelnet(tsdb, value).histogram(false))); + } final HashMap tags = new HashMap(); - for (int i = 5; i < words.length; i++) { + for (int i = has_id ? 5 : 4; i < words.length; i++) { if (!words[i].isEmpty()) { Tags.parse(tags, words[i]); } @@ -171,6 +206,41 @@ protected IncomingDataPoint getDataPointFromString(final TSDB tsdb, return dp; } + SimpleHistogram parseTelnet(final TSDB tsdb, final String encoded) { + final SimpleHistogram shdp = new SimpleHistogram(tsdb.histogramManager() + .getCodec(SimpleHistogramDecoder.class)); + + final String[] buckets = Tags.splitString(encoded, ':'); + if (buckets.length < 1) { + throw new IllegalArgumentException("Must have at least one bucket in the " + + "histogram."); + } + + for (final String bucket : buckets) { + final String[] kv = Tags.splitString(bucket, '='); + if (kv.length != 2) { + throw new IllegalArgumentException("Improperly formatted bucket: " + + bucket); + } + + kv[0] = kv[0].toLowerCase(); + if (kv[0].equals("u")) { + shdp.setUnderflow(Long.parseLong(kv[1])); + } else if (kv[0].equals("o")) { + shdp.setOverflow(Long.parseLong(kv[1])); + } else { + final String[] bounds = Tags.splitString(kv[0], ','); + if (bounds.length != 2) { + throw new IllegalArgumentException("Improperly formatted bounds: " + + bucket); + } + shdp.addBucket(Float.parseFloat(bounds[0]), Float.parseFloat(bounds[1]), + Long.parseLong(kv[1])); + } + } + return shdp; + } + @VisibleForTesting boolean enabled() { return enabled; diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 4a482dee0b..cc79cbfc28 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import com.google.common.base.Strings; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.TimeoutException; @@ -36,7 +37,6 @@ import org.slf4j.LoggerFactory; import net.opentsdb.core.Histogram; -import net.opentsdb.core.HistogramDataPoint; import net.opentsdb.core.HistogramPojo; import net.opentsdb.core.IncomingDataPoint; import net.opentsdb.core.TSDB; @@ -209,6 +209,7 @@ public Object call(final Object obj) { .addCallback(new SuccessCB()) .addErrback(new PutErrback()); } catch (NumberFormatException x) { + x.printStackTrace(); errmsg = type + ": invalid value: " + x.getMessage() + '\n'; invalid_values.incrementAndGet(); } catch (NoSuchRollupForIntervalException x) { @@ -216,6 +217,7 @@ public Object call(final Object obj) { illegal_arguments.incrementAndGet(); } catch (IllegalArgumentException x) { errmsg = type + ": illegal argument: " + x.getMessage() + '\n'; + x.printStackTrace(); illegal_arguments.incrementAndGet(); } catch (NoSuchUniqueName x) { errmsg = type + ": unknown metric: " + x.getMessage() + '\n'; @@ -230,8 +232,7 @@ public Object call(final Object obj) { } catch (TimeoutException tex) { errmsg = type + ": Request timed out: " + tex.getMessage() + '\n'; handleStorageException(tsdb, getDataPointFromString(tsdb, cmd), tex); - } - catch (RuntimeException rex) { + } catch (RuntimeException rex) { errmsg = type + ": Unexpected runtime exception: " + rex.getMessage() + '\n'; throw rex; } @@ -377,13 +378,19 @@ public Boolean call(final Object obj) { final Deferred deferred; if (type == DataPointType.HISTOGRAM) { final HistogramPojo pojo = (HistogramPojo) dp; - // validation before storage of histograms by decoding then re-encoding. - final Histogram hdp = tsdb.histogramManager().decode( - pojo.getId(), pojo.getBytes(), false); + // validation and/or conversion before storage of histograms by + // decoding then re-encoding. + final Histogram hdp; + if (Strings.isNullOrEmpty(dp.getValue())) { + hdp = pojo.toSimpleHistogram(tsdb); + } else { + hdp = tsdb.histogramManager().decode( + pojo.getId(), pojo.getBytes(), false); + } deferred = tsdb.addHistogramPoint( pojo.getMetric(), pojo.getTimestamp(), - tsdb.histogramManager().encode(pojo.getId(), hdp, true), + tsdb.histogramManager().encode(hdp.getId(), hdp, true), pojo.getTags()) .addCallback(new SuccessCB()) .addErrback(new PutErrback()); @@ -588,8 +595,6 @@ public Object call(final ArrayList results) { } final int failures = dps.size() - queued; - System.out.println("GOOD: " + good_writes + " Failures: " + failures + " FW " + failed_writes - + " DPS: " + dps.size() + " Q " + queued); if (!show_summary && !show_details) { if (failures + failed_writes > 0) { query.sendReply(HttpResponseStatus.BAD_REQUEST, diff --git a/test/tsd/TestHistogramDataPointRpc.java b/test/tsd/TestHistogramDataPointRpc.java index 37c417c226..8254126a65 100644 --- a/test/tsd/TestHistogramDataPointRpc.java +++ b/test/tsd/TestHistogramDataPointRpc.java @@ -157,6 +157,26 @@ public void executeTelnet() throws Exception { final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, + "1356998400", "u=0:o=1:0,1=42:1,5=24", + TAGK_STRING + "=" + TAGV_STRING }) + .join(); + validateCounters(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1); + verify(chan, never()).write(any()); + verify(chan, never()).isConnected(); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + byte[] value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + } + + @Test + public void executeTelnetBinary() throws Exception { + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + final Channel chan = NettyMocks.fakeChannel(); + rpc.execute(tsdb, chan, new String[] { "histogram", METRIC_STRING, "1356998400", "42", HistogramPojo.bytesToBase64String(test_histo.histogram(false)), @@ -195,7 +215,7 @@ public void executeTelnetHistosDisabled() throws Exception { } @Test - public void executeTelnetValueTooShort() throws Exception { + public void executeTelnetBinaryValueTooShort() throws Exception { final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); @@ -216,7 +236,7 @@ public void executeTelnetValueTooShort() throws Exception { } @Test - public void executeTelnetCorruptValue() throws Exception { + public void executeTelnetBinaryCorruptValue() throws Exception { final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); final Channel chan = NettyMocks.fakeChannel(); @@ -381,6 +401,24 @@ public void executeNSUNTagV() throws Exception { @Test public void executeHttpSingle() throws Exception { + final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", + "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + + "\"overflow\":1,\"buckets\":{\"0,1\":42,\"1,5\":24}" + + ",\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); + final HistogramDataPointRpc rpc = new HistogramDataPointRpc(tsdb.getConfig()); + rpc.execute(tsdb, query); + validateCounters(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1); + assertEquals(HttpResponseStatus.NO_CONTENT, query.response().getStatus()); + validateSEH(false); + + byte[] qualifier = new byte[] {0x06, 0, 0}; + byte[] value = storage.getColumn(getRowKey(METRIC_STRING, 1356998400, + TAGK_STRING, TAGV_STRING), qualifier); + assertArrayEquals(test_histo.histogram(true), value); + } + + @Test + public void executeHttpSingleBinary() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", "{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + "\"id\":42,\"value\":\"" @@ -399,7 +437,7 @@ public void executeHttpSingle() throws Exception { } @Test - public void executeHttpTwo() throws Exception { + public void executeHttpTwoBinary() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + "\"id\":42,\"value\":\"" @@ -427,7 +465,7 @@ public void executeHttpTwo() throws Exception { } @Test - public void executeHttpTwoOneGoodOneBad() throws Exception { + public void executeHttpTwoOneGoodOneBadBinary() throws Exception { final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/histogram", "[{\"metric\":\"" + METRIC_STRING + "\",\"timestamp\":1356998400," + "\"id\":42,\"value\":\"" From 24bdbfcf48826e7bfe691936960d6650158b9942 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Tue, 6 Jun 2017 12:40:56 -0700 Subject: [PATCH 642/826] Modify the RollupConfig and RollupInterval classes so that they can be parsed from JSON. Also add aggregation IDs to the various aggregations the RollupConfig is expected to store. This will let us cut down on storage space in HBase. Modify TSDB so that it will load the rollup config from the opentsdb.conf file (or a JSON file) And add a Const.HASHCODE_FUNCTION method that we can use for creating hashes throughout the code. Signed-off-by: Chris Larsen --- src/core/Const.java | 12 + src/core/TSDB.java | 23 +- src/core/TsdbQuery.java | 8 +- src/rollup/RollupConfig.java | 244 ++++++--- src/rollup/RollupInterval.java | 161 ++++-- src/rollup/RollupQuery.java | 6 +- src/rollup/RollupSpan.java | 2 +- src/rollup/RollupUtils.java | 8 +- test/core/TestDownsampler.java | 32 +- test/core/TestFillingDownsampler.java | 56 +- test/core/TestRollupSpan.java | 337 +++++++++--- test/core/TestTSDB.java | 116 ++-- test/core/TestTSDBAddAggregatePoint.java | 37 +- .../core/TestTSDBAddAggregatePointSalted.java | 37 +- test/core/TestTsdbQueryQueries.java | 11 +- test/core/TestTsdbQueryRollup.java | 253 ++++----- test/rollup/TestRollupConfig.java | 337 ++++++++---- test/rollup/TestRollupInterval.java | 518 +++++++++++++----- test/rollup/TestRollupSeq.java | 403 +++++++------- test/rollup/TestRollupUtils.java | 515 +++++++++++------ test/tsd/TestRollupRpc.java | 34 +- 21 files changed, 2060 insertions(+), 1090 deletions(-) diff --git a/src/core/Const.java b/src/core/Const.java index 6369589354..472ff5f61e 100644 --- a/src/core/Const.java +++ b/src/core/Const.java @@ -15,6 +15,9 @@ import java.nio.charset.Charset; import java.util.TimeZone; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + /** Constants used in various places. */ public final class Const { @@ -172,4 +175,13 @@ static void setSaltWidth(final int width) { } SALT_WIDTH = width; } + + /** + * A global function to use for NON-SECURE hashing of things like queries and + * cache objects. Used for deterministic hashing. + */ + private static HashFunction HASH_FUNCTION = Hashing.murmur3_128(); + public static HashFunction HASH_FUNCTION() { + return HASH_FUNCTION; + } } diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 79844138b9..e30d5af4af 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.core; +import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; @@ -23,6 +24,8 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import com.google.common.base.Strings; +import com.google.common.io.Files; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; @@ -54,6 +57,7 @@ import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; import net.opentsdb.utils.PluginLoader; import net.opentsdb.utils.Threads; import net.opentsdb.meta.Annotation; @@ -252,16 +256,27 @@ public TSDB(final HBaseClient client, final Config config) { timer = Threads.newTimer("TSDB Timer"); if (config.getBoolean("tsd.rollups.enable")) { - rollup_config = new RollupConfig(); + String conf = config.getString("tsd.rollups.config"); + if (Strings.isNullOrEmpty(conf)) { + throw new IllegalArgumentException("Rollups were enabled but " + + "'tsd.rollups.config' is null or empty."); + } + if (conf.endsWith(".json")) { + try { + conf = Files.toString(new File(conf), Const.UTF8_CHARSET); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to open conf file: " + + conf, e); + } + } + rollup_config = JSON.parseToObject(conf, RollupConfig.class); RollupInterval config_default = null; for (final RollupInterval interval: rollup_config.getRollups().values()) { - if (interval.isDefaultRollupInterval()) { + if (interval.isDefaultInterval()) { config_default = interval; - System.out.println("Found default: " + interval); break; } } - if (config_default == null) { throw new IllegalArgumentException("None of the rollup intervals were " + "marked as the \"default\"."); diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 47a5f645c5..aa777672b9 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -218,7 +218,7 @@ public TsdbQuery(final TSDB tsdb) { * @since 2.4 */ public String getRollupTable() { if (RollupQuery.isValidQuery(rollup_query)) { - return rollup_query.getRollupInterval().getStringInterval(); + return rollup_query.getRollupInterval().getInterval(); } else { return "raw"; @@ -1220,7 +1220,7 @@ public Deferred call(final DataPoints[] datapoints) throws Excepti else if (best_match_rollups != null && best_match_rollups.size() > 0) { RollupInterval interval = best_match_rollups.remove(0); - if (interval.isDefaultRollupInterval()) { + if (interval.isDefaultInterval()) { transformRollupQueryToDownSampler(); } else { @@ -1556,7 +1556,7 @@ public void transformDownSamplerToRollupQuery(final Aggregator group_by, return; } - if (rollup_query.getRollupInterval().isDefaultRollupInterval()) { + if (rollup_query.getRollupInterval().isDefaultInterval()) { //Anyways it is a scan on raw data rollup_query = null; } @@ -1574,7 +1574,7 @@ private void transformRollupQueryToDownSampler() { if (rollup_query != null) { // TODO - clean up and handle fill downsampler = new DownsamplingSpecification( - rollup_query.getRollupInterval().getInterval() * 1000, + rollup_query.getRollupInterval().getIntervalSeconds() * 1000, rollup_query.getRollupAgg(), (downsampler != null ? downsampler.getFillPolicy() : FillPolicy.ZERO)); diff --git a/src/rollup/RollupConfig.java b/src/rollup/RollupConfig.java index 2f7d60acc8..55f0a84d50 100644 --- a/src/rollup/RollupConfig.java +++ b/src/rollup/RollupConfig.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2015 The OpenTSDB Authors. +// 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 @@ -14,22 +14,32 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import net.opentsdb.core.Aggregators; import net.opentsdb.core.TSDB; +import net.opentsdb.utils.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.stumbleupon.async.Deferred; import com.stumbleupon.async.DeferredGroupException; import java.util.TreeMap; /** - * A hard-coded rollup configuration class that stores the lookup map, config - * and other bits surrounding rollups and pre-aggregates. + * A class that contains the runtime configuration for a TSD's raw and rollup + * tables. * * Each rollup requires two table names for writing, an interval and a span. * temporal_table - The table name for raw, temporal only rollup data @@ -41,51 +51,85 @@ * 'y' holds a full year. Possible values are: * 'h' = hour * 'd' = day - * 'm' = month + * 'n' = month * 'y' = year + * * @since 2.4 */ +@JsonDeserialize(builder = RollupConfig.Builder.class) public class RollupConfig { private static final Logger LOG = LoggerFactory.getLogger(RollupConfig.class); - + /** The interval to interval map where keys are things like "10m" or "1d"*/ - final Map forward_intervals = - new HashMap(); + protected final Map forward_intervals; /** The table name to interval map for queries */ - final Map reverse_intervals = - new HashMap(); + protected final Map reverse_intervals; - /** - * Ctor that contains the hard coded intervals. - * TODO - now that we're not writing to a single table, we can load - * this from a config file - */ - public RollupConfig() { - final List config = new ArrayList(); - - /** ---------------- CONFIG --------------------- - * WARNING: Do NOT change these maps after you start pushing data or you - * will invalidate anything you've written to the database. You can always - * add intervals and delete them to stop accepting data, but never remove - */ - config.add(new RollupInterval("tsdb", - "tsdb-agg", "1m", "1h", true)); - config.add(new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d")); - - // don't remove this - validateAndCompileIntervals(config); - } + /** The map of IDs to aggregators for use at query time. */ + protected final Map ids_to_aggregations; + + /** The map of aggregators to IDs for use at write time. */ + protected final Map aggregations_to_ids; /** - * Ctor for unit testing or loading intervals from an alternate source - * @param config The list of rollup intervals to store in the config + * Default ctor for the builder. + * @param builder A non-null builder to load from. */ - public RollupConfig(final List config) { - validateAndCompileIntervals(config); + protected RollupConfig(final Builder builder) { + forward_intervals = Maps.newHashMapWithExpectedSize(2); + reverse_intervals = Maps.newHashMapWithExpectedSize(2); + ids_to_aggregations = Maps.newHashMapWithExpectedSize(4); + aggregations_to_ids = Maps.newHashMapWithExpectedSize(4); + + if (builder.intervals == null || builder.intervals.isEmpty()) { + throw new IllegalArgumentException("Rollup config given but no intervals " + + "were found."); + } + if (builder.aggregationIds == null || builder.aggregationIds.isEmpty()) { + throw new IllegalArgumentException("Rollup config given but no aggegation " + + "ID mappings found."); + } + int defaults = 0; + for (final RollupInterval config_interval : builder.intervals) { + if (forward_intervals.containsKey(config_interval.getInterval())) { + throw new IllegalArgumentException( + "Only one interval of each type can be configured: " + + config_interval); + } + if (config_interval.isDefaultInterval() && defaults++ >= 1) { + throw new IllegalArgumentException("Multiple default intervals " + + "configured. Only one is allowed: " + config_interval); + } + + forward_intervals.put(config_interval.getInterval(), config_interval); + reverse_intervals.put(config_interval.getTable(), config_interval); + reverse_intervals.put(config_interval.getPreAggregationTable(), + config_interval); + LOG.info("Loaded rollup interval: " + config_interval); + } + + for (final Entry entry : builder.aggregationIds.entrySet()) { + if (entry.getValue() < 0 || entry.getValue() > 127) { + throw new IllegalArgumentException("ID for aggregator must be between " + + "0 and 127: " + entry); + } + final String agg = entry.getKey().toLowerCase(); + if (ids_to_aggregations.containsKey(entry.getValue())) { + throw new IllegalArgumentException("Multiple mappings for the " + + "ID '" + entry.getValue() + "' are not allowed."); + } + if (Aggregators.get(agg) == null) { + throw new IllegalArgumentException("No such aggregator found for " + agg); + } + aggregations_to_ids.put(agg, entry.getValue()); + ids_to_aggregations.put(entry.getValue(), agg); + LOG.info("Mapping aggregator '" + agg + "' to ID " + entry.getValue()); + } + + LOG.info("Configured [" + forward_intervals.size() + "] rollup intervals"); } - + /** * Fetches the RollupInterval corresponding to the forward interval string map * @param interval The interval to lookup @@ -119,22 +163,23 @@ public RollupInterval getRollupInterval(final String interval) { * @throws NoSuchRollupForIntervalException if the interval was not configured */ public List getRollupInterval(final long interval, - final String str_interval) { + final String str_interval) { if (interval <= 0) { throw new IllegalArgumentException("Interval cannot be null or empty"); } - Map rollups = new TreeMap(Collections.reverseOrder()); + final Map rollups = + new TreeMap(Collections.reverseOrder()); boolean right_match = false; for (RollupInterval rollup: forward_intervals.values()) { - if (rollup.getInterval() == interval) { - rollups.put(new Long(rollup.getInterval()), rollup); + if (rollup.getIntervalSeconds() == interval) { + rollups.put((long) rollup.getIntervalSeconds(), rollup); right_match = true; } - else if (interval % rollup.getInterval() == 0) { - rollups.put(new Long(rollup.getInterval()), rollup); + else if (interval % rollup.getIntervalSeconds() == 0) { + rollups.put((long) rollup.getIntervalSeconds(), rollup); } } @@ -148,7 +193,7 @@ else if (interval % rollup.getInterval() == 0) { if (!right_match) { LOG.warn("No such rollup interval found, " + str_interval + ". So falling " + "back to the next best match " + best_matches.get(0). - getStringInterval()); + getInterval()); } return best_matches; @@ -179,7 +224,6 @@ public RollupInterval getRollupIntervalForTable(final String table) { * @param tsdb The TSDB to use for fetching the HBase client */ public void ensureTablesExist(final TSDB tsdb) { - final List> deferreds = new ArrayList>(forward_intervals.size() * 2); @@ -203,41 +247,103 @@ public void ensureTablesExist(final TSDB tsdb) { } /** @return an unmodifiable map of the rollups for printing and debugging */ + @JsonIgnore public Map getRollups() { return Collections.unmodifiableMap(forward_intervals); } + /** @return The immutable list of rollup intervals for serialization. */ + public List getIntervals() { + return Lists.newArrayList(forward_intervals.values()); + } + + /** @return The immutable map of aggregations to IDs for serialization. */ + public Map getAggregationIds() { + return Collections.unmodifiableMap(aggregations_to_ids); + } + + /** + * @param id The ID of an aggregator to search for. + * @return The aggregator if found, null if it was not mapped. + */ + public String getAggregatorForId(final int id) { + return ids_to_aggregations.get(id); + } + /** - * Determines if the config supplied in the ctor is valid. This will throw - * exceptions if: - * 1) One of the strings is bad when passed to {@link getIntervals} above - * 2) A table name is missing - * 3) If more than one interval (e.g. "1m") is configured. These must - * be unique. - * @param config The list of RollupIntervals to process - * @throws IllegalArgumentException if something is invalid + * @param aggregator The non-null and non-empty aggregator to search for. + * @return The ID of the aggregator if found. + * @throws IllegalArgumentException if the aggregator was not found or if the + * aggregator was null or empty. */ - void validateAndCompileIntervals(final List config) { - if (config.isEmpty()) { - LOG.info("No intervals configured for this TSD"); - return; + public int getIdForAggregator(final String aggregator) { + if (Strings.isNullOrEmpty(aggregator)) { + throw new IllegalArgumentException("Aggregator cannot be null or empty."); + } + Integer id = aggregations_to_ids.get(aggregator.toLowerCase()); + if (id == null) { + throw new IllegalArgumentException("No ID found mapping to aggregator " + + aggregator); } + return id; + } + + @Override + public String toString() { + return JSON.serializeToString(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class Builder { + @JsonProperty + private Map aggregationIds; + @JsonProperty + private List intervals; - for (final RollupInterval config_interval : config) { - - if (forward_intervals.containsKey(config_interval.getStringInterval())) { - throw new IllegalArgumentException( - "Only one interval of each type can be configured: " + - config_interval); + public Builder setAggregationIds(final Map aggregationIds) { + this.aggregationIds = aggregationIds; + return this; + } + + @JsonIgnore + public Builder addAggregationId(final String aggregation, final int id) { + if (aggregationIds == null) { + aggregationIds = Maps.newHashMapWithExpectedSize(1); } - - forward_intervals.put(config_interval.getStringInterval(), config_interval); - reverse_intervals.put(config_interval.getTemporalTableName(), config_interval); - reverse_intervals.put(config_interval.getGroupbyTableName(), config_interval); - LOG.debug("Configured rollup: " + config_interval); + aggregationIds.put(aggregation, id); + return this; } - LOG.info("Configured [" + forward_intervals.size() + "] rollup intervals"); + public Builder setIntervals(final List intervals) { + this.intervals = intervals; + return this; + } + + @JsonIgnore + public Builder addInterval(final RollupInterval interval) { + if (intervals == null) { + intervals = Lists.newArrayList(); + } + intervals.add(interval); + return this; + } + + @JsonIgnore + public Builder addInterval(final RollupInterval.Builder interval) { + if (intervals == null) { + intervals = Lists.newArrayList(); + } + intervals.add(interval.build()); + return this; + } + + public RollupConfig build() { + return new RollupConfig(this); + } } - } diff --git a/src/rollup/RollupInterval.java b/src/rollup/RollupInterval.java index 7a9a8a2b4c..3dea39542a 100644 --- a/src/rollup/RollupInterval.java +++ b/src/rollup/RollupInterval.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2015 The OpenTSDB Authors. +// 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 @@ -13,7 +13,12 @@ package net.opentsdb.rollup; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.common.base.Objects; +import com.google.common.hash.HashCode; import net.opentsdb.core.Const; import net.opentsdb.utils.DateTime; @@ -23,6 +28,7 @@ * are validated. * @since 2.4 */ +@JsonDeserialize(builder = RollupInterval.Builder.class) public class RollupInterval { /** Static intervals */ private static final int MAX_SECONDS_IN_HOUR = 60 * 60; @@ -50,6 +56,9 @@ public class RollupInterval { */ private final String string_interval; + /** How wide the row will be in values. */ + private final String row_span; + /** Width of the row as a time unit, e.g. 'h' for hour, 'd' for day, 'm' for * month and 'y' for year */ @@ -71,59 +80,26 @@ public class RollupInterval { * Default interval is of 1m interval, and will be stored in normal * tsdb table/s. So if true, which means the raw cell column qualifier format * also it might be compacted. - * TODO. This will be changed when the spatial aggregation logic is in place. - * Here it is added to handle the pre-aggregated data on raw data */ - private final boolean default_interval; + private final boolean is_default_interval; /** - * Default Ctor used when configuring rollups - * @param temporal_table_name The rollup only table name - * @param groupby_table_name The pre-agg rollup table name - * @param interval The rollup interval, e.g. 10m or 15m or 1h - * @param span The row span, e.g. 1h, 6h, 1d, 1m, 1y. Values greater than 1 - * are only allowed with the 'h' unit. - * @throws IllegalArgumentException if milliseconds were passed in the interval - * or the interval couldn't be parsed, the tables are missing, or if the - * duration is too large, too large for the span or the interval is too - * large or small for the span or if the span is invalid. - * @throws NullPointerException if the interval is empty or null - */ - public RollupInterval(final String temporal_table_name, - final String groupby_table_name, final String interval, - final String span) { - this(temporal_table_name, groupby_table_name, interval, span, false); - } - - /** - * Default Ctor used when configuring rollups - * @param temporal_table_name The rollup only table name - * @param groupby_table_name The pre-agg rollup table name - * @param interval The rollup interval, e.g. 10m or 15m or 1h - * @param span The row span, e.g. 1h, 6h, 1d, 1m, 1y. Values greater than 1 - * are only allowed with the 'h' unit. - * @param default_interval Tells whether it is the default rollup interval - * that needs to be written into default tsdb table - * @throws IllegalArgumentException if milliseconds were passed in the interval - * or the interval couldn't be parsed, the tables are missing, or if the - * duration is too large, too large for the span or the interval is too - * large or small for the span or if the span is invalid. - * @throws NullPointerException if the interval is empty or null + * Protected ctor used by the builder. + * @param builder The non-null builder to load from. */ - public RollupInterval(final String temporal_table_name, - final String groupby_table_name, final String interval, - final String span, boolean default_interval) { - this.temporal_table_name = temporal_table_name; - this.groupby_table_name = groupby_table_name; - this.string_interval = interval; - this.default_interval = default_interval; + protected RollupInterval(final Builder builder) { + temporal_table_name = builder.table; + groupby_table_name = builder.preAggregationTable; + string_interval = builder.interval; + row_span = builder.rowSpan; + is_default_interval = builder.defaultInterval; - final String parsed_units = DateTime.getDurationUnits(span); + final String parsed_units = DateTime.getDurationUnits(row_span); if (parsed_units.length() > 1) { throw new IllegalArgumentException("Milliseconds are not supported"); } units = parsed_units.charAt(0); - this.unit_multiplier = DateTime.getDurationInterval(span); + this.unit_multiplier = DateTime.getDurationInterval(row_span); validateAndCompile(); } @@ -132,7 +108,9 @@ public RollupInterval(final String temporal_table_name, public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("table=").append(temporal_table_name) - .append(", agg_table=").append(groupby_table_name) + .append(", preAggTable=").append(groupby_table_name) + .append(", rowSpan=").append(row_span) + .append(", isDefaultInterval=").append(is_default_interval) .append(", interval=").append(string_interval) .append(", units=").append(units) .append(", unit_multipier=").append(unit_multiplier) @@ -144,8 +122,18 @@ public String toString() { @Override public int hashCode() { - return Objects.hashCode(temporal_table_name, groupby_table_name, units, - unit_multiplier, string_interval, default_interval); + return buildHashCode().asInt(); + } + + /** @return A HashCode object for deterministic, non-secure hashing */ + public HashCode buildHashCode() { + return Const.HASH_FUNCTION().newHasher() + .putString(temporal_table_name, Const.UTF8_CHARSET) + .putString(groupby_table_name, Const.UTF8_CHARSET) + .putString(string_interval, Const.UTF8_CHARSET) + .putString(row_span, Const.UTF8_CHARSET) + .putBoolean(is_default_interval) + .hash(); } @Override @@ -162,10 +150,9 @@ public boolean equals(final Object obj) { final RollupInterval interval = (RollupInterval)obj; return Objects.equal(temporal_table_name, interval.temporal_table_name) && Objects.equal(groupby_table_name, interval.groupby_table_name) - && Objects.equal(units, interval.units) - && Objects.equal(unit_multiplier, interval.unit_multiplier) + && Objects.equal(row_span, interval.row_span) && Objects.equal(string_interval, interval.string_interval) - && Objects.equal(default_interval, interval.default_interval); + && Objects.equal(is_default_interval, interval.is_default_interval); } /** @@ -215,7 +202,7 @@ void validateAndCompile() { case 'd': num_span = MAX_SECONDS_IN_DAY; break; - case 'm': + case 'n': num_span = MAX_SECONDS_IN_MONTH; break; case 'y': @@ -245,7 +232,7 @@ void validateAndCompile() { } /** @return the string name of the temporal rollup table */ - public String getTemporalTableName() { + public String getTable() { return temporal_table_name; } @@ -256,7 +243,7 @@ public byte[] getTemporalTable() { } /** @return the string name of the group by rollup table */ - public String getGroupbyTableName() { + public String getPreAggregationTable() { return groupby_table_name; } @@ -267,31 +254,36 @@ public byte[] getGroupbyTable() { } /** @return the configured interval as a string */ - public String getStringInterval() { + public String getInterval() { return string_interval; } /** @return the character describing the span of this interval */ + @JsonIgnore public char getUnits() { return units; } /** @return the unit multiplier */ + @JsonIgnore public int getUnitMultiplier() { return unit_multiplier; } /** @return the interval units character */ + @JsonIgnore public char getIntervalUnits() { return interval_units; } /** @return the interval for this span in seconds */ - public int getInterval() { + @JsonIgnore + public int getIntervalSeconds() { return interval; } /** @return the count of intervals in this span */ + @JsonIgnore public int getIntervals() { return intervals; } @@ -303,7 +295,60 @@ public int getIntervals() { * compacted * @return true if it is default rollup interval */ - public boolean isDefaultRollupInterval() { - return default_interval; + public boolean isDefaultInterval() { + return is_default_interval; + } + + /** @return The width of each row as an interval string. */ + public String getRowSpan() { + return row_span; + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class Builder { + @JsonProperty + private String table; + @JsonProperty + private String preAggregationTable; + @JsonProperty + private String interval; + @JsonProperty + private String rowSpan; + @JsonProperty + private boolean defaultInterval; + + public Builder setTable(final String table) { + this.table = table; + return this; + } + + public Builder setPreAggregationTable(final String preAggregationTable) { + this.preAggregationTable = preAggregationTable; + return this; + } + + public Builder setInterval(final String interval) { + this.interval = interval; + return this; + } + + public Builder setRowSpan(final String rowSpan) { + this.rowSpan = rowSpan; + return this; + } + + public Builder setDefaultInterval(final boolean defaultInterval) { + this.defaultInterval = defaultInterval; + return this; + } + + public RollupInterval build() { + return new RollupInterval(this); + } } } diff --git a/src/rollup/RollupQuery.java b/src/rollup/RollupQuery.java index 449cb32dd0..648d5b33ff 100644 --- a/src/rollup/RollupQuery.java +++ b/src/rollup/RollupQuery.java @@ -122,7 +122,7 @@ public boolean equals(final Object obj) { public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("rollup interval=") - .append(rollup_interval.getStringInterval()) + .append(rollup_interval.getInterval()) .append(", rollup aggregator=") .append(rollup_agg.toString()) .append(", group_by=") @@ -160,7 +160,7 @@ public Aggregator getGroupBy() { */ public static boolean isValidQuery(final RollupQuery rollup_query) { return (rollup_query != null && rollup_query.rollup_interval != null && - !rollup_query.rollup_interval.isDefaultRollupInterval()); + !rollup_query.rollup_interval.isDefaultInterval()); } /** @@ -183,6 +183,6 @@ public long getSampleIntervalInMS() { * @return true if it is of lower sampling rate else false */ public boolean isLowerSamplingRate() { - return this.rollup_interval.getInterval() * 1000 < sample_interval_ms; + return this.rollup_interval.getIntervalSeconds() * 1000 < sample_interval_ms; } } diff --git a/src/rollup/RollupSpan.java b/src/rollup/RollupSpan.java index 86d581392e..2f9314c6fc 100644 --- a/src/rollup/RollupSpan.java +++ b/src/rollup/RollupSpan.java @@ -42,7 +42,7 @@ public final class RollupSpan extends Span { public RollupSpan(final TSDB tsdb, RollupQuery rollup_query) { super(tsdb); - if (rollup_query.getRollupInterval().isDefaultRollupInterval()) { + if (rollup_query.getRollupInterval().isDefaultInterval()) { throw new IllegalStateException("Rolup Span is not applicable to default " + "rollup interval. Default rollup interval is encoded in the same way" + " as the raw data."); diff --git a/src/rollup/RollupUtils.java b/src/rollup/RollupUtils.java index 1828c0954a..0598ab42d2 100644 --- a/src/rollup/RollupUtils.java +++ b/src/rollup/RollupUtils.java @@ -84,7 +84,7 @@ public static int getRollupBasetime(final long timestamp, case 'd': // all set via the zeros above break; - case 'm': + case 'n': calendar.set(Calendar.DAY_OF_MONTH, 1); break; case 'y': @@ -153,7 +153,7 @@ public static byte[] buildRollupQualifier(final long timestamp, // we shouldn't have a divide by 0 here as the rollup config validator makes // sure the interval is positive - int offset = (time_seconds - basetime) / interval.getInterval(); + int offset = (time_seconds - basetime) / interval.getIntervalSeconds(); if (offset >= interval.getIntervals()) { throw new IllegalArgumentException("Offset of " + offset + " was greater " + "than the configured intervals " + interval.getIntervals()); @@ -221,7 +221,7 @@ public static long getOffsetFromRollupQualifier(final byte[] qualifier, >>> Const.FLAG_BITS; } - return offset * interval.getInterval() * 1000; + return offset * interval.getIntervalSeconds() * 1000; } /** @@ -242,7 +242,7 @@ public static long getOffsetFromRollupQualifier(final int qualifier, } else { offset = (qualifier & 0xFFFF) >>> Const.FLAG_BITS; } - return offset * interval.getInterval() * 1000; + return offset * interval.getIntervalSeconds() * 1000; } /** diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 62e9ef0755..0a338f3b97 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -1178,8 +1178,12 @@ public void testDownsampler_1year_timezone() { @Test public void testDownsampler_rollupSum() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { @@ -1224,8 +1228,12 @@ public void testDownsampler_rollupSum() { @Test public void testDownsampler_rollupAvg() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.AVG); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { @@ -1255,8 +1263,12 @@ public void testDownsampler_rollupAvg() { @Test public void testDownsampler_rollupCount() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.COUNT); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { @@ -1286,8 +1298,12 @@ public void testDownsampler_rollupCount() { @Test (expected = UnsupportedOperationException.class) public void testDownsampler_rollupDev() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.DEV); specification = new DownsamplingSpecification("10s-dev"); diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index 95375d37d4..934fc9d178 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -818,8 +818,12 @@ public void testDownsampler_noDataCalendar() { @Test public void testDownsampler_rollup() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); final long baseTime = 1000L; @@ -852,8 +856,12 @@ public void testDownsampler_rollup() { @Test public void testDownsampler_rollupMissing() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); final long baseTime = 500L; @@ -889,8 +897,12 @@ public void testDownsampler_rollupMissing() { @Test public void testDownsampler_rollupAvg() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); final long baseTime = 1000L; @@ -925,8 +937,12 @@ public void testDownsampler_rollupAvg() { @Test public void testDownsampler_rollupAvgMissing() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); final long baseTime = 500L; @@ -962,8 +978,12 @@ public void testDownsampler_rollupAvgMissing() { @Test public void testDownsampler_rollupCount() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); final long baseTime = 1000L; @@ -998,8 +1018,12 @@ public void testDownsampler_rollupCount() { @Test public void testDownsampler_rollupCountMissing() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.SUM); final long baseTime = 500L; @@ -1035,8 +1059,12 @@ public void testDownsampler_rollupCountMissing() { @Test (expected = UnsupportedOperationException.class) public void testDownsampler_rollupDev() { - final RollupInterval interval = new RollupInterval("tsdb-rollup-1h", - "tsdb-agg-rollup-1h", "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-agg-rollup-1h") + .setInterval("1h") + .setRowSpan("1d") + .build(); final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, 3600000, Aggregators.DEV); final long baseTime = 1000L; diff --git a/test/core/TestRollupSpan.java b/test/core/TestRollupSpan.java index 96c0dd452a..e4cf442c9d 100644 --- a/test/core/TestRollupSpan.java +++ b/test/core/TestRollupSpan.java @@ -14,65 +14,39 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; -import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.Config; import org.hbase.async.Bytes; import org.hbase.async.KeyValue; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; - -import com.stumbleupon.async.Deferred; import net.opentsdb.rollup.RollupInterval; import net.opentsdb.rollup.RollupQuery; import net.opentsdb.rollup.RollupSpan; import static net.opentsdb.rollup.RollupUtils.ROLLUP_QUAL_DELIM; -@RunWith(PowerMockRunner.class) -//"Classloader hell"... It's real. Tell PowerMock to ignore these classes -//because they fiddle with the class loader. We don't test them anyway. -@PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) -@PrepareForTest({ RowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, -Config.class, RowKey.class }) -public final class TestRollupSpan { - private TSDB tsdb = mock(TSDB.class); - private Config config = mock(Config.class); - private UniqueId metrics = mock(UniqueId.class); - private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; - private static final byte[] HOUR1 = new byte[] - { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; - private static final byte[] HOUR2 = new byte[] - { 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 1, 0, 0, 2 }; - private static final byte[] HOUR3 = new byte[] - { 0, 0, 1, 0x50, (byte)0xE2, 0x43, 0x20, 0, 0, 1, 0, 0, 2 }; - private static final byte[] FAMILY = { 't' }; - private static final byte[] ZERO = { 0 }; - private static final Aggregator aggr_sum = Aggregators.SUM; - - private static final RollupQuery rollup_query = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - aggr_sum, 1000, aggr_sum); +public class TestRollupSpan extends BaseTsdbTest { + protected byte[] hour1 = null; + protected byte[] hour2 = null; + protected byte[] hour3 = null; + protected static final Aggregator aggr_sum = Aggregators.SUM; + + protected static final RollupQuery rollup_query = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1s") + .setRowSpan("1h") + .build(), + aggr_sum, + 1000, + aggr_sum); @Before - public void before() throws Exception { - // Inject the attributes we need into the "tsdb" object. - Whitebox.setInternalState(tsdb, "metrics", metrics); - Whitebox.setInternalState(tsdb, "table", TABLE); - Whitebox.setInternalState(tsdb, "config", config); - when(tsdb.getConfig()).thenReturn(config); - when(tsdb.metrics.width()).thenReturn((short)4); - when(RowKey.metricNameAsync(tsdb, HOUR1)) - .thenReturn(Deferred.fromResult("sys.cpu.user")); + public void beforeLocal() throws Exception { + hour1 = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + hour2 = getRowKey(METRIC_STRING, 1357002000, TAGK_STRING, TAGV_STRING); + hour3 = getRowKey(METRIC_STRING, 1357005600, TAGK_STRING, TAGV_STRING); } @Test @@ -81,7 +55,7 @@ public void addRow() { final byte[] val1 = Bytes.fromLong(4L); final Span span = new RollupSpan(tsdb, rollup_query); - span.addRow(new KeyValue(HOUR1, FAMILY, qual1, val1)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual1, val1)); assertEquals(1, span.size()); } @@ -91,7 +65,224 @@ public void addRowNull() { final Span span = new RollupSpan(tsdb, rollup_query); span.addRow(null); } + /* + * TODO - fix up these tests + @Test (expected = IllegalArgumentException.class) + public void addRowBadKeyLength() { + final byte[] qual1 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val2 = Bytes.fromLong(8L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qual1, val1)); + + final byte[] bad_key = + new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x43, 0x20, 0, 0, 0, 1 }; + span.addRow(new KeyValue(bad_key, TSDB.FAMILY(), qual2, + MockBase.concatByteArrays(val1, val2, ZERO))); + } + @Test (expected = IllegalArgumentException.class) + public void addRowMissMatchedMetric() { + final byte[] qual1 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val2 = Bytes.fromLong(8L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qual1,val1)); + + final byte[] bad_key = + new byte[] { 0, 0, 0, 2, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 2 }; + span.addRow(new KeyValue(bad_key, TSDB.FAMILY(), qual2,val2)); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMissMatchedTagk() { + final byte[] qual1 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val2 = Bytes.fromLong(8L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qual1, val1)); + + final byte[] bad_key = + new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 2, 0, 0, 0, 2 }; + span.addRow(new KeyValue(bad_key, TSDB.FAMILY(), qual2, val2)); + } + + @Test (expected = IllegalArgumentException.class) + public void addRowMissMatchedTagv() { + final byte[] qual1 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + final byte[] qual2 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val2 = Bytes.fromLong(8L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qual1, val1)); + + final byte[] bad_key = + new byte[] { 0, 0, 0, 1, 0x50, (byte)0xE2, 0x35, 0x10, 0, 0, 0, 1, 0, 0, 0, 3 }; + span.addRow(new KeyValue(bad_key, TSDB.FAMILY(), qual2, val2)); + } + + @Test + public void addRowOutOfOrder() { + //2nd hour + final byte[] qual1 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; + final byte[] val1 = Bytes.fromLong(4L); + //1st hour + final byte[] qual2 = {0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; + final byte[] val2 = Bytes.fromLong(5L); + + final Span span = new RollupSpan(tsdb, rollup_query); + span.addRow(new KeyValue(HOUR2, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qual2, val2)); + assertEquals(2, span.size()); + + assertEquals(1356998402000L, span.timestamp(0)); + assertEquals(5, span.longValue(0)); + assertEquals(1357002000000L, span.timestamp(1)); + assertEquals(4, span.longValue(1)); + } + + @Test + public void addDifferentSalt() throws Exception { + List qualifiers = new ArrayList(); + List values = new ArrayList(); + + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }); + values.add(Bytes.fromLong(4L)); + values.add(Bytes.fromLong(5L)); + + final Span span = new RollupSpan(tsdb, rollup_query); + final byte[] hour1 = Arrays.copyOf(HOUR1, HOUR1.length); + hour1[0] = 2; + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers.get(2), values.get(0))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers.get(3), values.get(1))); + assertEquals(4, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(4, span.longValue(0)); + assertEquals(1356998402000L, span.timestamp(1)); + assertEquals(5, span.longValue(1)); + assertEquals(1356998403000L, span.timestamp(2)); + assertEquals(4, span.longValue(2)); + assertEquals(1356998404000L, span.timestamp(3)); + assertEquals(5, span.longValue(3)); + } + + @Test + public void addDifferentSaltDiffHour() throws Exception { + List qualifiers = new ArrayList(); + List values = new ArrayList(); + + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }); + values.add(Bytes.fromLong(4L)); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }); + values.add(Bytes.fromLong(5L)); + + final Span span = new RollupSpan(tsdb, rollup_query); + final byte[] hour2 = Arrays.copyOf(HOUR2, HOUR2.length); + hour2[0] = 2; + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + assertEquals(4, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(4, span.longValue(0)); + assertEquals(1356998402000L, span.timestamp(1)); + assertEquals(5, span.longValue(1)); + assertEquals(1357002000000L, span.timestamp(2)); + assertEquals(4, span.longValue(2)); + assertEquals(1357002002000L, span.timestamp(3)); + assertEquals(5, span.longValue(3)); + } + + @Test + public void addDifferentSaltDiffHourOO() throws Exception { + when(tsdb.followAppendRowLogic()).thenReturn(true); + List qualifiers = new ArrayList(); + List values = new ArrayList(); + + + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }); + values.add(Bytes.fromLong(4L)); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }); + values.add(Bytes.fromLong(5L)); + + final Span span = new RollupSpan(tsdb, rollup_query); + final byte[] hour2 = Arrays.copyOf(HOUR2, HOUR2.length); + hour2[0] = 2; + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + + assertEquals(4, span.size()); + assertEquals(1356998400000L, span.timestamp(0)); + assertEquals(4, span.longValue(0)); + assertEquals(1356998402000L, span.timestamp(1)); + assertEquals(5, span.longValue(1)); + assertEquals(1357002000000L, span.timestamp(2)); + assertEquals(4, span.longValue(2)); + assertEquals(1357002002000L, span.timestamp(3)); + assertEquals(5, span.longValue(3)); + } + + @Test (expected = IllegalArgumentException.class) + public void addDifferentKey() throws Exception { + when(tsdb.followAppendRowLogic()).thenReturn(true); + List qualifiers = new ArrayList(); + List values = new ArrayList(); + + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }); + values.add(Bytes.fromLong(4L)); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }); + values.add(Bytes.fromLong(5L)); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }); + + final Span span = new RollupSpan(tsdb, rollup_query); + final byte[] hour1 = Arrays.copyOf(HOUR1, HOUR1.length); + hour1[hour1.length - 1] = 3; + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers.get(2), values.get(0))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers.get(3), values.get(1))); + } + + @Test (expected = IllegalArgumentException.class) + public void addDifferentSaltAndKey() throws Exception { + when(tsdb.followAppendRowLogic()).thenReturn(true); + List qualifiers = new ArrayList(); + List values = new ArrayList(); + + + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }); + values.add(Bytes.fromLong(4L)); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }); + values.add(Bytes.fromLong(5L)); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }); + qualifiers.add(new byte[]{ 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }); + + final Span span = new RollupSpan(tsdb, rollup_query); + final byte[] hour1 = Arrays.copyOf(HOUR1, HOUR1.length); + hour1[0] = 2; + hour1[hour1.length - 1] = 3; + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(0), values.get(0))); + span.addRow(new KeyValue(HOUR1, TSDB.FAMILY(), qualifiers.get(1), values.get(1))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers.get(2), values.get(0))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers.get(3), values.get(1))); + } + */ @Test public void timestampNormalized() throws Exception { final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; @@ -100,12 +291,12 @@ public void timestampNormalized() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final Span span = new RollupSpan(tsdb, rollup_query); - span.addRow(new KeyValue(HOUR1, FAMILY, qual1, val1)); - span.addRow(new KeyValue(HOUR1, FAMILY, qual2, val2)); - span.addRow(new KeyValue(HOUR2, FAMILY, qual1, val1)); - span.addRow(new KeyValue(HOUR2, FAMILY, qual2, val2)); - span.addRow(new KeyValue(HOUR3, FAMILY, qual1, val1)); - span.addRow(new KeyValue(HOUR3, FAMILY, qual2, val2)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual2, val2)); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qual2, val2)); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qual2, val2)); assertEquals(6, span.size()); assertEquals(1356998400000L, span.timestamp(0)); @@ -128,9 +319,9 @@ public void timestampFullSeconds() throws Exception { for (int i = 0; i < 100; i++) { final short qualifier = (short) (i << Const.FLAG_BITS | 0x07); System.arraycopy(Bytes.fromShort(qualifier), 0, qualifiers, agg.length, 2); - span.addRow(new KeyValue(HOUR1, FAMILY, qualifiers, Bytes.fromLong(i))); - span.addRow(new KeyValue(HOUR2, FAMILY, qualifiers, Bytes.fromLong(i))); - span.addRow(new KeyValue(HOUR3, FAMILY, qualifiers, Bytes.fromLong(i))); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qualifiers, Bytes.fromLong(i))); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qualifiers, Bytes.fromLong(i))); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qualifiers, Bytes.fromLong(i))); } @@ -145,12 +336,12 @@ public void timestampMS() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final Span span = new RollupSpan(tsdb, rollup_query); - span.addRow(new KeyValue(HOUR1, FAMILY, qual1, val1)); - span.addRow(new KeyValue(HOUR1, FAMILY, qual2, val2)); - span.addRow(new KeyValue(HOUR2, FAMILY, qual1, val1)); - span.addRow(new KeyValue(HOUR2, FAMILY, qual2, val2)); - span.addRow(new KeyValue(HOUR3, FAMILY, qual1, val1)); - span.addRow(new KeyValue(HOUR3, FAMILY, qual2, val2)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual2, val2)); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qual2, val2)); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qual1, val1)); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qual2, val2)); assertEquals(6, span.size()); assertEquals(1356998400000L, span.timestamp(0)); @@ -169,12 +360,12 @@ public void iterateNormalizedMS() throws Exception { final byte[] val2 = Bytes.fromLong(5L); final Span span = new RollupSpan(tsdb, rollup_query); - span.addRow(new KeyValue(HOUR1, FAMILY, qual1,val1)); - span.addRow(new KeyValue(HOUR1, FAMILY, qual2,val2)); - span.addRow(new KeyValue(HOUR2, FAMILY, qual1,val1)); - span.addRow(new KeyValue(HOUR2, FAMILY, qual2,val2)); - span.addRow(new KeyValue(HOUR3, FAMILY, qual1,val1)); - span.addRow(new KeyValue(HOUR3, FAMILY, qual2,val2)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual1,val1)); + span.addRow(new KeyValue(hour1, TSDB.FAMILY(), qual2,val2)); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qual1,val1)); + span.addRow(new KeyValue(hour2, TSDB.FAMILY(), qual2,val2)); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qual1,val1)); + span.addRow(new KeyValue(hour3, TSDB.FAMILY(), qual2,val2)); assertEquals(6, span.size()); final SeekableView it = span.iterator(); @@ -204,30 +395,24 @@ public void iterateNormalizedMS() throws Exception { assertEquals(5, dp.longValue()); assertFalse(it.hasNext()); - - } @Test public void lastTimestampInRow() throws Exception { - final byte[] qual1 = { 0x00, 0x07 }; - final byte[] val1 = Bytes.fromLong(4L); final byte[] qual2 = { 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); - final KeyValue kv = new KeyValue(HOUR1, FAMILY, qual2, val2); + final KeyValue kv = new KeyValue(hour1, TSDB.FAMILY(), qual2, val2); assertEquals(1356998402L, Span.lastTimestampInRow((short) 3, kv)); } @Test public void lastTimestampInRowMs() throws Exception { - final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; - final byte[] val1 = Bytes.fromLong(4L); final byte[] qual2 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); - final KeyValue kv = new KeyValue(HOUR1, FAMILY, qual2, val2); + final KeyValue kv = new KeyValue(hour1, TSDB.FAMILY(), qual2, val2); assertEquals(1356998400008L, Span.lastTimestampInRow((short) 3, kv)); } diff --git a/test/core/TestTSDB.java b/test/core/TestTSDB.java index 8911fd4b44..3bf1a01705 100644 --- a/test/core/TestTSDB.java +++ b/test/core/TestTSDB.java @@ -15,16 +15,14 @@ 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.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; +import java.io.File; import java.lang.reflect.Field; import java.util.HashMap; -import java.util.List; -import net.opentsdb.rollup.RollupConfig; -import net.opentsdb.rollup.RollupInterval; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; @@ -45,18 +43,17 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import org.powermock.reflect.Whitebox; -import com.google.common.collect.Lists; +import com.google.common.io.Files; import com.stumbleupon.async.Deferred; @RunWith(PowerMockRunner.class) @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) -@PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, +@PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, CompactionQueue.class, GetRequest.class, PutRequest.class, KeyValue.class, - Scanner.class, AtomicIncrementRequest.class, Const.class}) + Scanner.class, AtomicIncrementRequest.class, Const.class, Files.class }) public final class TestTSDB extends BaseTsdbTest { @Before @@ -74,50 +71,6 @@ public void ctorNullConfig() throws Exception { new TSDB(client, null); } - @Test - public void ctorRollups() throws Exception { - TSDB tsdb = new TSDB(client, config); - assertNull(Whitebox.getInternalState(tsdb, "rollup_config")); - assertNull(Whitebox.getInternalState(tsdb, "default_interval")); - - List intervals = Lists.newArrayList( - new RollupInterval("tsdb", "tsdb-agg", "1m", "1h", true)); - RollupConfig rollups = new RollupConfig(intervals); - PowerMockito.whenNew(RollupConfig.class).withAnyArguments() - .thenReturn(rollups); - - config.overrideConfig("tsd.rollups.enable", "true"); - tsdb = new TSDB(client, config); - assertSame(rollups, Whitebox.getInternalState(tsdb, "rollup_config")); - assertSame(intervals.get(0), Whitebox.getInternalState(tsdb, - "default_interval")); - } - - @Test (expected = IllegalArgumentException.class) - public void ctorRollupsNoDefault() throws Exception { - // no default - List intervals = Lists.newArrayList( - new RollupInterval("tsdb", "tsdb-agg", "1m", "1h")); - RollupConfig rollups = new RollupConfig(intervals); - PowerMockito.whenNew(RollupConfig.class).withAnyArguments() - .thenReturn(rollups); - - config.overrideConfig("tsd.rollups.enable", "true"); - new TSDB(client, config); - } - - @Test (expected = IllegalArgumentException.class) - public void ctorRollupsEmpty() throws Exception { - // no default - List intervals = Lists.newArrayList(); - RollupConfig rollups = new RollupConfig(intervals); - PowerMockito.whenNew(RollupConfig.class).withAnyArguments() - .thenReturn(rollups); - - config.overrideConfig("tsd.rollups.enable", "true"); - new TSDB(client, config); - } - @Test public void ctorOverrideUIDWidths() throws Exception { // assert defaults @@ -272,6 +225,63 @@ public void initializePluginsSEHNotFound() throws Exception { tsdb.initializePlugins(true); } + @Test + public void loadRollupConfig() throws Exception { + config.overrideConfig("tsd.rollups.enable", "true"); + config.overrideConfig("tsd.rollups.config", + "{\"intervals\":[{\"interval\":\"1m\",\"table\":\"tsdb\"," + + "\"preAggregationTable\":\"tsdb\",\"defaultInterval\":true," + + "\"rowSpan\":\"1h\"},{\"interval\":\"10m\",\"table\":" + + "\"tsdb-rollup-10m\",\"preAggregationTable\":\"tsdb-rollup-agg-10m\"," + + "\"defaultInterval\":false,\"rowSpan\":\"1d\"}],\"aggregationIds\":" + + "{\"sum\":0,\"max\":1}}"); + tsdb = new TSDB(config); + assertEquals(2, tsdb.getRollupConfig().getRollups().size()); + assertEquals("sum", tsdb.getRollupConfig().getAggregatorForId(0)); + assertEquals("max", tsdb.getRollupConfig().getAggregatorForId(1)); + } + + @Test + public void loadRollupConfigFile() throws Exception { + config.overrideConfig("tsd.rollups.enable", "true"); + config.overrideConfig("tsd.rollups.config", "nosuchfile.json"); + PowerMockito.mockStatic(Files.class); + when(Files.toString(any(File.class), eq(Const.UTF8_CHARSET))).thenReturn( + "{\"intervals\":[{\"interval\":\"1m\",\"table\":\"tsdb\"," + + "\"preAggregationTable\":\"tsdb\",\"defaultInterval\":true," + + "\"rowSpan\":\"1h\"},{\"interval\":\"10m\",\"table\":" + + "\"tsdb-rollup-10m\",\"preAggregationTable\":\"tsdb-rollup-agg-10m\"," + + "\"defaultInterval\":false,\"rowSpan\":\"1d\"}],\"aggregationIds\":" + + "{\"sum\":0,\"max\":1}}"); + tsdb = new TSDB(config); + assertEquals(2, tsdb.getRollupConfig().getRollups().size()); + assertEquals("sum", tsdb.getRollupConfig().getAggregatorForId(0)); + assertEquals("max", tsdb.getRollupConfig().getAggregatorForId(1)); + } + + @Test (expected = IllegalArgumentException.class) + public void loadRollupConfigFileCorruptJson() throws Exception { + config.overrideConfig("tsd.rollups.enable", "true"); + config.overrideConfig("tsd.rollups.config", "nosuchfile.json"); + PowerMockito.mockStatic(Files.class); + when(Files.toString(any(File.class), eq(Const.UTF8_CHARSET))).thenReturn( + "{\"intervals\":[{\"interval\":\"1m\",\"ta"); + new TSDB(config); + } + + @Test (expected = IllegalArgumentException.class) + public void loadRollupConfigNoDefault() throws Exception { + config.overrideConfig("tsd.rollups.enable", "true"); + config.overrideConfig("tsd.rollups.config", + "{\"intervals\":[{\"interval\":\"1m\",\"table\":\"tsdb\"," + + "\"preAggregationTable\":\"tsdb\",\"defaultInterval\":false," + + "\"rowSpan\":\"1h\"},{\"interval\":\"10m\",\"table\":" + + "\"tsdb-rollup-10m\",\"preAggregationTable\":\"tsdb-rollup-agg-10m\"," + + "\"defaultInterval\":false,\"rowSpan\":\"1d\"}],\"aggregationIds\":" + + "{\"sum\":0,\"max\":1}}"); + tsdb = new TSDB(config); + } + @Test public void getClient() { assertNotNull(tsdb.getClient()); diff --git a/test/core/TestTSDBAddAggregatePoint.java b/test/core/TestTSDBAddAggregatePoint.java index 2b0280abd9..f72fa32782 100644 --- a/test/core/TestTSDBAddAggregatePoint.java +++ b/test/core/TestTSDBAddAggregatePoint.java @@ -65,19 +65,30 @@ public void beforeLocal() throws Exception { storage.addTable("tsdb-rollup-agg-1d".getBytes(), families); storage.addTable(AGG_TABLE, families); - final List rollups = new ArrayList(); - rollups.add(new RollupInterval( - "tsdb", "tsdb-agg", "1m", "1h", true)); - rollups.add(new RollupInterval( - "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "1d")); - rollups.add(new RollupInterval( - "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1m")); - rollups.add(new RollupInterval( - "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1y")); - - rollup_config = new RollupConfig(rollups); + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1n")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1y")) + .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); - Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + Whitebox.setInternalState(tsdb, "default_interval", + rollup_config.getRollupInterval("10m")); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); @@ -719,7 +730,7 @@ public void addAggregatePointGroupByOnlyRouting() throws Exception { agg_tag_key, "SUM"); assertNull(tags.get(agg_tag_key)); - RollupInterval interval = rollup_config.getRollupInterval("1m"); + RollupInterval interval = rollup_config.getRollupInterval("10m"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, null, "sum").joinUninterruptibly(); diff --git a/test/core/TestTSDBAddAggregatePointSalted.java b/test/core/TestTSDBAddAggregatePointSalted.java index d2c9f405ee..824c5763b9 100644 --- a/test/core/TestTSDBAddAggregatePointSalted.java +++ b/test/core/TestTSDBAddAggregatePointSalted.java @@ -54,19 +54,30 @@ public void beforeLocal() throws Exception { storage.addTable("tsdb-rollup-agg-1d".getBytes(), families); storage.addTable(AGG_TABLE, families); - final List rollups = new ArrayList(); - rollups.add(new RollupInterval( - "tsdb", "tsdb-agg", "1m", "1h", true)); - rollups.add(new RollupInterval( - "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "1d")); - rollups.add(new RollupInterval( - "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1m")); - rollups.add(new RollupInterval( - "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1y")); - - rollup_config = new RollupConfig(rollups); + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1n")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1y")) + .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); - Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + Whitebox.setInternalState(tsdb, "default_interval", + rollup_config.getRollupInterval("10m")); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); @@ -74,8 +85,6 @@ public void beforeLocal() throws Exception { config.getString("tsd.rollups.raw_agg_tag_value")); setupGroupByTagValues(); - setupGroupByTagValues(); - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); } } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 17da164615..8acbc61bc4 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -13,7 +13,6 @@ package net.opentsdb.core; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -31,7 +30,6 @@ import java.util.List; import java.util.Map; -import net.opentsdb.rollup.RollupConfig; import net.opentsdb.rollup.RollupInterval; import org.hbase.async.Bytes; import org.hbase.async.FilterList; @@ -1567,8 +1565,13 @@ public void runPreAggregate() throws Exception { config.getString("tsd.rollups.agg_tag_key")); Whitebox.setInternalState(tsdb, "raw_agg_tag_value", config.getString("tsd.rollups.raw_agg_tag_value")); - Whitebox.setInternalState(tsdb, "default_interval", new RollupInterval("tsdb", - "tsdb-agg", "1m", "1h", true)); + Whitebox.setInternalState(tsdb, "default_interval", + RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1m") + .setRowSpan("1h") + .build()); tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, tags, true, null, null, "SUM"); diff --git a/test/core/TestTsdbQueryRollup.java b/test/core/TestTsdbQueryRollup.java index 9325d480e4..fd28b65b30 100644 --- a/test/core/TestTsdbQueryRollup.java +++ b/test/core/TestTsdbQueryRollup.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Arrays; import java.util.HashMap; @@ -71,18 +72,38 @@ public void beforeLocal() throws Exception { tags2 = new HashMap(1); tags2.put(TAGK_STRING, TAGV_B_STRING); - final List rollups = new ArrayList(); - rollups.add(new RollupInterval( - "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "6h")); - rollups.add(new RollupInterval( - "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1d")); - rollups.add(new RollupInterval( - "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1m")); - - rollup_config = new RollupConfig(rollups); +// final List rollups = new ArrayList(); +// rollups.add(new RollupInterval( +// "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "6h")); +// rollups.add(new RollupInterval( +// "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1d")); +// rollups.add(new RollupInterval( +// "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1m")); +// +// rollup_config = new RollupConfig(rollups); + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("6h")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); - Whitebox.setInternalState(tsdb, "default_interval", new RollupInterval( - "tsdb", "tsdb-agg", "1m", "1h")); + } // This test shows us falling back to raw data if the requested downsample @@ -135,6 +156,7 @@ public void run30mSumLongSingleTS() throws Exception { setQuery("30m", aggr, tags, aggr); query.configureFromQuery(ts_query, 0); + DataPoints[] dps = query.run(); assertEquals(1, dps.length); assertEquals(METRIC_STRING, dps[0].metricName()); @@ -149,7 +171,7 @@ public void run30mSumLongSingleTS() throws Exception { assertEquals(value, dp.doubleValue(), 0); assertEquals(ts, dp.timestamp()); value += 5400; - ts += (interval.getInterval() * 3) * 1000; + ts += (interval.getIntervalSeconds() * 3) * 1000; } assertEquals(24, dps[0].size()); } @@ -165,7 +187,7 @@ public void run10mZimSumLongSingleTS() throws Exception { interval, aggr); aggr = Aggregators.ZIMSUM; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -182,8 +204,8 @@ public void run10mZimSumLongSingleTS() throws Exception { assertFalse(dp.isInteger()); assertEquals(i, dp.doubleValue(), 0.0001); assertEquals(ts, dp.timestamp()); - ts += interval.getInterval() * 1000; - i += interval.getInterval(); + ts += interval.getIntervalSeconds() * 1000; + i += interval.getIntervalSeconds(); } assertEquals(72, dps[0].size()); } @@ -198,7 +220,7 @@ public void run10mMaxLongSingleTSNotFound() throws Exception { interval, aggr); aggr = Aggregators.MAX; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -213,8 +235,8 @@ public void run10mSumLongSingleTS() throws Exception { long end_timestamp = 1357041600L; storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); - final int time_interval = interval.getInterval(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + final int time_interval = interval.getIntervalSeconds(); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -245,7 +267,7 @@ public void run10mSumLongSingleTSInMS() throws Exception { //rollup doesn't accept timestamps in milliseconds tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, - 0, tags, false, ten_min_interval.getStringInterval(), + 0, tags, false, ten_min_interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); } @@ -259,7 +281,7 @@ public void run10mSumLongSingleTSRate() throws Exception { storeLongRollup(start_timestamp, end_timestamp, false, false, interval, aggr); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); ts_query.getQueries().get(0).setRate(true); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -270,11 +292,11 @@ public void run10mSumLongSingleTSRate() throws Exception { assertNull(dps[0].getAnnotations()); assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); - long expected_timestamp = (start_timestamp + interval.getInterval()) * 1000; + long expected_timestamp = (start_timestamp + interval.getIntervalSeconds()) * 1000; for (DataPoint dp : dps[0]) { assertEquals(1.0F, dp.doubleValue(), 0.00001); assertEquals(expected_timestamp, dp.timestamp()); - expected_timestamp += interval.getInterval() * 1000; + expected_timestamp += interval.getIntervalSeconds() * 1000; } assertEquals(72, dps[0].size()); @@ -288,7 +310,7 @@ public void run10mSumFloatSingleTS() throws Exception { final long end_timestamp = 1357041600; storeFloatRollup(start_timestamp, end_timestamp, true, false, interval, aggr); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -304,8 +326,8 @@ public void run10mSumFloatSingleTS() throws Exception { for (DataPoint dp : dps[0]) { assertEquals(value, dp.doubleValue(), 0.00001); assertEquals(expected_timestamp, dp.timestamp()); - value += interval.getInterval(); - expected_timestamp += interval.getInterval() * 1000; + value += interval.getIntervalSeconds(); + expected_timestamp += interval.getIntervalSeconds() * 1000; } assertEquals(73, dps[0].size()); @@ -321,7 +343,7 @@ public void run10mSumFloatSingleTSRate() throws Exception { storeFloatRollup(start_timestamp, end_timestamp, false, false, interval, aggr); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); ts_query.getQueries().get(0).setRate(true); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -332,11 +354,12 @@ public void run10mSumFloatSingleTSRate() throws Exception { assertNull(dps[0].getAnnotations()); assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); - long expected_timestamp = (start_timestamp + interval.getInterval()) * 1000; + long expected_timestamp = (start_timestamp + + interval.getIntervalSeconds()) * 1000; for (DataPoint dp : dps[0]) { assertEquals(1.0F, dp.doubleValue(), 0.00001); assertEquals(expected_timestamp, dp.timestamp()); - expected_timestamp += interval.getInterval() * 1000; + expected_timestamp += interval.getIntervalSeconds() * 1000; } assertEquals(72, dps[0].size()); } @@ -350,8 +373,8 @@ public void run10mSumLongDoubleTSFilter() throws Exception { long end_timestamp = 1357041600L; storeLongRollup(1356998400L, end_timestamp, true, false, interval, aggr); - final int time_interval = interval.getInterval(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + final int time_interval = interval.getIntervalSeconds(); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -382,9 +405,9 @@ public void run10mSumLongDoubleTS() throws Exception { long end_timestamp = 1357041600L; storeLongRollup(1356998400L, end_timestamp, true, false, interval, aggr); - final int time_interval = interval.getInterval(); + final int time_interval = interval.getIntervalSeconds(); tags.clear(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); ts_query.getQueries().get(0).getFilters().clear(); ts_query.validateAndSetQuery(); query.configureFromQuery(ts_query, 0); @@ -419,8 +442,8 @@ public void run10mSumLongDoubleTSFilterOtherAggs() throws Exception { storeLongRollup(1356998400L, end_timestamp, true, false, interval, Aggregators.MIN); - final int time_interval = interval.getInterval(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + final int time_interval = interval.getIntervalSeconds(); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -451,8 +474,8 @@ public void run10mMaxLongSingleTS() throws Exception { long end_timestamp = 1357041600L; storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); - final int time_interval = interval.getInterval(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + final int time_interval = interval.getIntervalSeconds(); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -483,8 +506,8 @@ public void run10mMinLongSingleTS() throws Exception { long end_timestamp = 1357041600L; storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); - final int time_interval = interval.getInterval(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + final int time_interval = interval.getIntervalSeconds(); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -515,9 +538,9 @@ public void run10mAvgLongSingleTS() throws Exception { long end_timestamp = 1357041600L; storeLongRollup(start_timestamp, end_timestamp, false, false, interval, aggr); storeCount(start_timestamp, end_timestamp, false, false, interval, 2); - + aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -534,8 +557,8 @@ public void run10mAvgLongSingleTS() throws Exception { assertFalse(dp.isInteger()); assertEquals(i, dp.doubleValue(), 0.0001); assertEquals(ts, dp.timestamp()); - ts += interval.getInterval() * 1000; - i += interval.getInterval() / 2; + ts += interval.getIntervalSeconds() * 1000; + i += interval.getIntervalSeconds() / 2; } assertEquals(73, dps[0].size()); } @@ -549,7 +572,7 @@ public void run10mAvgLongSingleTSMissingCount() throws Exception { storeLongRollup(start_timestamp, end_timestamp, false, false, interval, aggr); aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -570,7 +593,7 @@ public void run10mAvgLongSingleTSMissingSum() throws Exception { long end_timestamp = 1357041600L; storeCount(start_timestamp, end_timestamp, false, false, interval, 1); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -597,7 +620,7 @@ public void run10mAvgLongSingleTSMissingACount() throws Exception { storePoint(1357000200, 4, Aggregators.COUNT, interval); Aggregator aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -635,7 +658,7 @@ public void run10mAvgLongSingleTSMissingASum() throws Exception { storePoint(1357000200, 4, Aggregators.COUNT, interval); Aggregator aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -673,7 +696,7 @@ public void run10mAvgLongSingleTSMissingToZero() throws Exception { storePoint(1357000200, 4, Aggregators.COUNT, interval); Aggregator aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); final DataPoints[] dps = query.run(); @@ -709,7 +732,7 @@ public void run10mAvgLongSingleTSMissingToZeroOneSpan() throws Exception { storePoint(1357171800, 5, Aggregators.COUNT, interval); Aggregator aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); ts_query.setEnd("1359590400"); ts_query.validateAndSetQuery(); query.configureFromQuery(ts_query, 0); @@ -759,7 +782,7 @@ public void run10mAvgLongSingleTSMissingToZeroBookends() throws Exception { //storePoint(1357171800, 5, Aggregators.COUNT, interval); Aggregator aggr = Aggregators.AVG; - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); ts_query.setEnd("1359590400"); ts_query.validateAndSetQuery(); query.configureFromQuery(ts_query, 0); @@ -790,60 +813,25 @@ public void runDupes() throws Exception { final Aggregator aggr = Aggregators.SUM; tsdb.addAggregatePoint(METRIC_STRING, 1357026600L, Integer.MAX_VALUE, tags, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); tsdb.addAggregatePoint(METRIC_STRING, 1357026600L, 42.5F, tags, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); - setQuery(interval.getStringInterval(), aggr, tags, aggr); + setQuery(interval.getInterval(), aggr, tags, aggr); query.configureFromQuery(ts_query, 0); - DataPoints[] dps = null; try { - dps = query.run(); + query.run(); + fail("Expected IllegalDataException"); } catch (IllegalDataException e) { } config.setFixDuplicates(true); - dps = query.run(); + DataPoints[] dps = query.run(); + DataPoint dp = dps[0].iterator().next(); assertEquals(1357026600000L, dp.timestamp()); assertEquals(42.5F, dp.toDouble(), 0.0001); } - @Test - public void runRollupPreAgg() throws Exception { - setupGroupByTagValues(); - final RollupInterval interval = rollup_config.getRollupInterval("10m"); - final Aggregator aggr = Aggregators.SUM; - long start_timestamp = 1356998400L; - long end_timestamp = 1357041600L; - storeLongRollup(1356998400L, end_timestamp, false, false, interval, aggr); - 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")); - - tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42L, tags, true, "10m", - "SUM", "SUM"); - - tags.put(config.getString("tsd.rollups.agg_tag_key"), "SUM"); - - setQuery(interval.getStringInterval(), aggr, tags, aggr); - query.configureFromQuery(ts_query, 0); - - final DataPoints[] dps = query.run(); - assertEquals(1, dps.length); - 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(); - assertFalse(dp.isInteger()); - assertEquals(42, dp.doubleValue(), 0.0001); - assertEquals(ts, dp.timestamp()); - assertEquals(1, dps[0].size()); - } - // ----------------- // // Helper functions. // // ----------------- // @@ -857,7 +845,7 @@ private void storeLongRollup(final long start_timestamp, // dump a bunch of rows of two metrics so that we can test filtering out // on the metric - int time_interval = interval.getInterval(); + int time_interval = interval.getIntervalSeconds(); long start_a = start_timestamp; long start_b = start_timestamp + (offset ? time_interval : 0); int i = 0; @@ -865,10 +853,10 @@ private void storeLongRollup(final long start_timestamp, while (start_a <= end_timestamp) { i += time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); start_b += (offset ? time_interval * 2 : time_interval); @@ -881,10 +869,10 @@ private void storeLongRollup(final long start_timestamp, while (start_a <= end_timestamp) { i -= time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags2, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags2, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); @@ -898,43 +886,43 @@ private void storeCount(final long start_timestamp, final boolean offset, final RollupInterval interval, final int value) throws Exception { + + // dump a bunch of rows of two metrics so that we can test filtering out + // on the metric + int time_interval = interval.getIntervalSeconds(); + long start_a = start_timestamp; + long start_b = start_timestamp + (offset ? time_interval : 0); - // dump a bunch of rows of two metrics so that we can test filtering out - // on the metric - int time_interval = interval.getInterval(); - long start_a = start_timestamp; - long start_b = start_timestamp + (offset ? time_interval : 0); - - while (start_a <= end_timestamp) { - tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags, false, - interval.getStringInterval(), Aggregators.COUNT.toString(), null) - .joinUninterruptibly(); - if (two_metrics) { - tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags, false, - interval.getStringInterval(), Aggregators.COUNT.toString(), null) + while (start_a <= end_timestamp) { + tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags, false, + interval.getInterval(), Aggregators.COUNT.toString(), null) .joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags, false, + interval.getInterval(), Aggregators.COUNT.toString(), null) + .joinUninterruptibly(); + } + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); } - start_a += (offset ? time_interval * 2 : time_interval); - start_b += (offset ? time_interval * 2 : time_interval); - } - - start_a = start_timestamp; - start_b = start_timestamp + (offset ? time_interval : 0); - while (start_a <= end_timestamp) { - tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags2, false, - interval.getStringInterval(), Aggregators.COUNT.toString(), null) - .joinUninterruptibly(); - if (two_metrics) { - tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags2, false, - interval.getStringInterval(), Aggregators.COUNT.toString(), null) + start_a = start_timestamp; + start_b = start_timestamp + (offset ? time_interval : 0); + + while (start_a <= end_timestamp) { + tsdb.addAggregatePoint(METRIC_STRING, start_a, value, tags2, false, + interval.getInterval(), Aggregators.COUNT.toString(), null) .joinUninterruptibly(); + if (two_metrics) { + tsdb.addAggregatePoint(METRIC_B_STRING, start_b, value, tags2, false, + interval.getInterval(), Aggregators.COUNT.toString(), null) + .joinUninterruptibly(); + } + + start_a += (offset ? time_interval * 2 : time_interval); + start_b += (offset ? time_interval * 2 : time_interval); } - - start_a += (offset ? time_interval * 2 : time_interval); - start_b += (offset ? time_interval * 2 : time_interval); } -} private void storeFloatRollup(final long start_timestamp, final long end_timestamp, @@ -945,7 +933,7 @@ private void storeFloatRollup(final long start_timestamp, // dump a bunch of rows of two metrics so that we can test filtering out // on the metric - int time_interval = interval.getInterval(); + int time_interval = interval.getIntervalSeconds(); long start_a = start_timestamp; long start_b = start_timestamp + (offset ? time_interval : 0); float i = 0.5F; @@ -953,11 +941,11 @@ private void storeFloatRollup(final long start_timestamp, while (start_a <= end_timestamp) { i += time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b,i, tags, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); start_b += (offset ? time_interval * 2 : time_interval); @@ -970,10 +958,10 @@ private void storeFloatRollup(final long start_timestamp, while (start_a <= end_timestamp) { i -= time_interval; tsdb.addAggregatePoint(METRIC_STRING, start_a, i, tags2, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); if (two_metrics) { tsdb.addAggregatePoint(METRIC_B_STRING, start_b, i, tags2, false, - interval.getStringInterval(), aggr.toString(), null).joinUninterruptibly(); + interval.getInterval(), aggr.toString(), null).joinUninterruptibly(); } start_a += (offset ? time_interval * 2 : time_interval); @@ -984,10 +972,9 @@ private void storeFloatRollup(final long start_timestamp, private void storePoint(final long ts, final long value, final Aggregator agg, final RollupInterval interval) throws Exception { tsdb.addAggregatePoint(METRIC_STRING, ts, value, tags, false, - interval.getStringInterval(), agg.toString(), null).joinUninterruptibly(); + interval.getInterval(), agg.toString(), null).joinUninterruptibly(); } - @SuppressWarnings("deprecation") private void setQuery(final String ds_interval, final Aggregator ds_agg, final Map tags, final Aggregator group_by) { ts_query = new TSQuery(); diff --git a/test/rollup/TestRollupConfig.java b/test/rollup/TestRollupConfig.java index e61b6b59a7..0aef49e861 100644 --- a/test/rollup/TestRollupConfig.java +++ b/test/rollup/TestRollupConfig.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2015 The OpenTSDB Authors. +// 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 @@ -14,122 +14,261 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import org.hbase.async.HBaseClient; +import org.junit.Before; import org.junit.Test; -import org.powermock.reflect.Whitebox; +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 com.stumbleupon.async.Deferred; +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.JSON; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ TSDB.class, HBaseClient.class }) 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"; - - @Test - public void ctor() throws Exception { - final RollupConfig config = new RollupConfig(); - assertNotNull(config); - assertTrue(config.forward_intervals.size() >= 1); - assertEquals(config.forward_intervals.size() * 2, - config.reverse_intervals.size()); - } - - @Test - public void getRollupIntervalString() { - final RollupConfig config = new RollupConfig(); - final Map forward_intervals = - new HashMap(); - final RollupInterval rollup = new RollupInterval( - rollup_table, preagg_table, "10m", "1d"); - forward_intervals.put(rollup.getStringInterval(), rollup); - Whitebox.setInternalState(config, "forward_intervals", forward_intervals); - - final RollupInterval fetched = config.getRollupInterval("10m"); - assertTrue(rollup == fetched); - } - @Test (expected = NoSuchRollupForIntervalException.class) - public void getRollupIntervalStringNoSuchRollup() { - final RollupConfig config = new RollupConfig(); - final Map forward_intervals = - new HashMap(); - Whitebox.setInternalState(config, "forward_intervals", forward_intervals); - - config.getRollupInterval("10m"); - } + private TSDB tsdb; + private HBaseClient client; + private RollupConfig.Builder builder; + private RollupInterval raw; + private RollupInterval tenmin; - @Test (expected = IllegalArgumentException.class) - public void getRollupIntervalStringNullString() { - new RollupConfig().getRollupInterval((String)null); - } - - @Test (expected = IllegalArgumentException.class) - public void getRollupIntervalStringEmptyString() { - new RollupConfig().getRollupInterval(""); + @Before + public void before() throws Exception { + tsdb = PowerMockito.mock(TSDB.class); + client = PowerMockito.mock(HBaseClient.class); + when(tsdb.getClient()).thenReturn(client); + + raw = RollupInterval.builder() + .setTable(tsdb_table) + .setPreAggregationTable(tsdb_table) + .setInterval("1m") + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + + tenmin = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("10m") + .setRowSpan("1d") + .build(); + + builder = RollupConfig.builder() + .addAggregationId("Sum", 0) + .addAggregationId("Max", 1) + .addInterval(raw) + .addInterval(tenmin); } @Test - public void getRollupIntervalForTable() { - final RollupConfig config = new RollupConfig(); - final Map reverse_intervals = - new HashMap(); - final RollupInterval rollup = new RollupInterval( - rollup_table, preagg_table, "10m", "1d"); - reverse_intervals.put(rollup.getTemporalTableName(), rollup); - reverse_intervals.put(rollup.getGroupbyTableName(), rollup); - Whitebox.setInternalState(config, "reverse_intervals", reverse_intervals); - - RollupInterval fetched = config.getRollupIntervalForTable(rollup_table); - assertTrue(rollup == fetched); - fetched = config.getRollupIntervalForTable(preagg_table); - assertTrue(rollup == fetched); - } - - @Test (expected = NoSuchRollupForTableException.class) - public void getRollupIntervalForTableNoSuchRollup() { - final RollupConfig config = new RollupConfig(); - final Map reverse_intervals = - new HashMap(); - Whitebox.setInternalState(config, "reverse_intervals", reverse_intervals); - - config.getRollupIntervalForTable(rollup_table); - } - - @Test (expected = IllegalArgumentException.class) - public void getRollupIntervalForTableNull() { - new RollupConfig().getRollupIntervalForTable(null); - } - - @Test (expected = IllegalArgumentException.class) - public void getRollupIntervalForTableEmpty() { - new RollupConfig().getRollupIntervalForTable(""); + public void ctor() throws Exception { + RollupConfig config = builder.build(); + assertEquals(2, 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(raw, config.reverse_intervals.get(tsdb_table)); + assertSame(tenmin, config.reverse_intervals.get(rollup_table)); + assertSame(tenmin, config.reverse_intervals.get(preagg_table)); + + assertEquals(2, config.aggregations_to_ids.size()); + assertEquals(2, config.ids_to_aggregations.size()); + + assertEquals(0, (int) config.aggregations_to_ids.get("sum")); + assertEquals(1, (int) config.aggregations_to_ids.get("max")); + + assertEquals("sum", config.ids_to_aggregations.get(0)); + assertEquals("max", config.ids_to_aggregations.get(1)); + + // missing aggregations + builder = RollupConfig.builder() + .addInterval(raw) + .addInterval(tenmin); + try { + builder.build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // duplicate aggregation id + builder = RollupConfig.builder() + .addAggregationId("Sum", 1) + .addAggregationId("Max", 1) + .addInterval(raw) + .addInterval(tenmin); + try { + builder.build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // invalid ID + builder = RollupConfig.builder() + .addAggregationId("Sum", 0) + .addAggregationId("Max", 128) + .addInterval(raw) + .addInterval(tenmin); + try { + builder.build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // empty intervals + builder = RollupConfig.builder() + .addAggregationId("Sum", 0) + .addAggregationId("Max", 1); + try { + builder.build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // dupe intervals + builder = RollupConfig.builder() + .addAggregationId("Sum", 0) + .addAggregationId("Max", 1) + .addInterval(raw) + .addInterval(raw); + try { + builder.build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + // two defaults + tenmin = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("10m") + .setRowSpan("1d") + .setDefaultInterval(true) + .build(); + builder = RollupConfig.builder() + .addAggregationId("Sum", 0) + .addAggregationId("Max", 1) + .addInterval(raw) + .addInterval(raw); + try { + builder.build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } } - - // Does nothing effectively + @Test - public void validateAndCompileIntervalsEmptyList() throws Exception { - final RollupConfig config = new RollupConfig(); - config.validateAndCompileIntervals(Collections.emptyList()); - assertTrue(config.forward_intervals.size() >= 1); - assertEquals(config.forward_intervals.size() * 2, - config.reverse_intervals.size()); + public void getRollupIntervalString() throws Exception { + final RollupConfig config = builder.build(); + + assertSame(raw, config.getRollupInterval("1m")); + assertSame(tenmin, config.getRollupInterval("10m")); + + try { + config.getRollupInterval("5m"); + fail("Expected NoSuchRollupForIntervalException"); + } catch (NoSuchRollupForIntervalException e) { } + + try { + config.getRollupInterval(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + config.getRollupInterval(""); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } } - @Test (expected = NullPointerException.class) - public void validateAndCompileIntervalsNullList() throws Exception { - new RollupConfig().validateAndCompileIntervals(null); + @Test + public void getRollupIntervalForTable() throws Exception { + final RollupConfig config = builder.build(); + + assertSame(raw, config.getRollupIntervalForTable(tsdb_table)); + assertSame(tenmin, config.getRollupIntervalForTable(rollup_table)); + assertSame(tenmin, config.getRollupIntervalForTable(preagg_table)); + + try { + config.getRollupIntervalForTable("nosuchtable"); + fail("Expected NoSuchRollupForTableException"); + } catch (NoSuchRollupForTableException e) { } + + try { + config.getRollupIntervalForTable(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + config.getRollupIntervalForTable(""); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } } - @Test (expected = IllegalArgumentException.class) - public void validateAndCompileIntervalsDuplicate() throws Exception { - final List list = new ArrayList(); - list.add(new RollupInterval(rollup_table, preagg_table, "1h", "1d")); - final RollupConfig config = new RollupConfig(); - config.validateAndCompileIntervals(list); + @Test + public void serdes() throws Exception { + RollupConfig config = builder.build(); + String json = JSON.serializeToString(config); + + assertTrue(json.contains("\"intervals\":[")); + assertTrue(json.contains("\"interval\":\"1m\"")); + assertTrue(json.contains("interval\":\"10m\"")); + assertTrue(json.contains("\"aggregationIds\":{")); + assertTrue(json.contains("\"sum\":0")); + assertTrue(json.contains("\"max\":1")); + + json = "{\"intervals\":[{\"interval\":\"1m\",\"table\":\"tsdb\"," + + "\"preAggregationTable\":\"tsdb\",\"defaultInterval\":true," + + "\"rowSpan\":\"1h\"},{\"interval\":\"10m\",\"table\":" + + "\"tsdb-rollup-10m\",\"preAggregationTable\":\"tsdb-rollup-agg-10m\"," + + "\"defaultInterval\":false,\"rowSpan\":\"1d\"}],\"aggregationIds\":" + + "{\"sum\":0,\"max\":1}}"; + config = JSON.parseToObject(json, RollupConfig.class); + assertEquals(2, config.forward_intervals.size()); + assertNotNull(config.forward_intervals.get("1m")); + assertNotNull(config.forward_intervals.get("10m")); + + assertEquals(3, config.reverse_intervals.size()); + assertNotNull(config.reverse_intervals.get(tsdb_table)); + assertNotNull(config.reverse_intervals.get(rollup_table)); + assertNotNull(config.reverse_intervals.get(preagg_table)); + + assertEquals(2, config.aggregations_to_ids.size()); + assertEquals(2, config.ids_to_aggregations.size()); + + assertEquals(0, (int) config.aggregations_to_ids.get("sum")); + assertEquals(1, (int) config.aggregations_to_ids.get("max")); + + assertEquals("sum", config.ids_to_aggregations.get(0)); + assertEquals("max", config.ids_to_aggregations.get(1)); } + @Test + public void ensureTablesExist() throws Exception { + when(client.ensureTableExists(any(byte[].class))) + .thenAnswer(new Answer>() { + @Override + public Deferred answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(null); + } + }); + + final RollupConfig config = builder.build(); + config.ensureTablesExist(tsdb); + verify(client, times(2)).ensureTableExists(tsdb_table.getBytes()); + verify(client, times(1)).ensureTableExists(rollup_table.getBytes()); + verify(client, times(1)).ensureTableExists(preagg_table.getBytes()); + } } diff --git a/test/rollup/TestRollupInterval.java b/test/rollup/TestRollupInterval.java index 7692999ed9..61dd252a8d 100644 --- a/test/rollup/TestRollupInterval.java +++ b/test/rollup/TestRollupInterval.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2015 The OpenTSDB Authors. +// 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 @@ -13,7 +13,7 @@ package net.opentsdb.rollup; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import java.nio.charset.Charset; @@ -21,6 +21,8 @@ import org.hbase.async.Bytes; import org.junit.Test; +import net.opentsdb.utils.JSON; + public class TestRollupInterval { private final static Charset CHARSET = Charset.forName("ISO-8859-1"); private final static String rollup_table = "tsdb-rollup-10m"; @@ -30,15 +32,19 @@ public class TestRollupInterval { @Test public void ctor1SecondHour() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "1s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("1h") + .build(); assertEquals('h', interval.getUnits()); - assertEquals("1s", interval.getStringInterval()); + assertEquals("1s", interval.getInterval()); assertEquals('s', interval.getIntervalUnits()); assertEquals(3600, interval.getIntervals()); - assertEquals(1, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(1, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @@ -46,359 +52,567 @@ public void ctor1SecondHour() throws Exception { // test odd boundaries @Test public void ctor7SecondHour() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "7s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("7s") + .setRowSpan("1h") + .build(); assertEquals('h', interval.getUnits()); - assertEquals("7s", interval.getStringInterval()); + assertEquals("7s", interval.getInterval()); assertEquals('s', interval.getIntervalUnits()); assertEquals(514, interval.getIntervals()); - assertEquals(7, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(7, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor15SecondsHour() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "15s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("15s") + .setRowSpan("1h") + .build(); assertEquals('h', interval.getUnits()); - assertEquals("15s", interval.getStringInterval()); + assertEquals("15s", interval.getInterval()); assertEquals('s', interval.getIntervalUnits()); assertEquals(240, interval.getIntervals()); - assertEquals(15, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(15, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor30SecondsHour() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "30s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("30s") + .setRowSpan("1h") + .build(); assertEquals('h', interval.getUnits()); - assertEquals("30s", interval.getStringInterval()); + assertEquals("30s", interval.getInterval()); assertEquals('s', interval.getIntervalUnits()); assertEquals(120, interval.getIntervals()); - assertEquals(30, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(30, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor1MinuteDay() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "1m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1m") + .setRowSpan("1d") + .build(); assertEquals('d', interval.getUnits()); - assertEquals("1m", interval.getStringInterval()); + assertEquals("1m", interval.getInterval()); assertEquals('m', interval.getIntervalUnits()); assertEquals(1440, interval.getIntervals()); - assertEquals(60, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(60, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor10MinuteDay() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "10m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("10m") + .setRowSpan("1d") + .build(); assertEquals('d', interval.getUnits()); - assertEquals("10m", interval.getStringInterval()); + assertEquals("10m", interval.getInterval()); assertEquals('m', interval.getIntervalUnits()); assertEquals(144, interval.getIntervals()); - assertEquals(600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor10Minute6Hours() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "10m", "6h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("10m") + .setRowSpan("6h") + .build(); assertEquals('h', interval.getUnits()); - assertEquals("10m", interval.getStringInterval()); + assertEquals("10m", interval.getInterval()); assertEquals('m', interval.getIntervalUnits()); assertEquals(36, interval.getIntervals()); - assertEquals(600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor10Minute12Hours() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "10m", "12h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("10m") + .setRowSpan("12h") + .build(); assertEquals('h', interval.getUnits()); - assertEquals("10m", interval.getStringInterval()); + assertEquals("10m", interval.getInterval()); assertEquals('m', interval.getIntervalUnits()); assertEquals(72, interval.getIntervals()); - assertEquals(600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor15MinuteDay() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "15m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("15m") + .setRowSpan("1d") + .build(); assertEquals('d', interval.getUnits()); - assertEquals("15m", interval.getStringInterval()); + assertEquals("15m", interval.getInterval()); assertEquals('m', interval.getIntervalUnits()); assertEquals(96, interval.getIntervals()); - assertEquals(900, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(900, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor30MinuteDay() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "30m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("30m") + .setRowSpan("1d") + .build(); assertEquals('d', interval.getUnits()); - assertEquals("30m", interval.getStringInterval()); + assertEquals("30m", interval.getInterval()); assertEquals('m', interval.getIntervalUnits()); assertEquals(48, interval.getIntervals()); - assertEquals(1800, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(1800, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor1HourDay() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "1h", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1h") + .setRowSpan("1d") + .build(); assertEquals('d', interval.getUnits()); - assertEquals("1h", interval.getStringInterval()); + assertEquals("1h", interval.getInterval()); assertEquals('h', interval.getIntervalUnits()); assertEquals(24, interval.getIntervals()); - assertEquals(3600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(3600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor1HourMonth() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "1h", "1m"); - assertEquals('m', interval.getUnits()); - assertEquals("1h", interval.getStringInterval()); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1h") + .setRowSpan("1n") + .build(); + assertEquals('n', interval.getUnits()); + assertEquals("1h", interval.getInterval()); assertEquals('h', interval.getIntervalUnits()); assertEquals(768, interval.getIntervals()); - assertEquals(3600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(3600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor3HourMonth() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "3h", "1m"); - assertEquals('m', interval.getUnits()); - assertEquals("3h", interval.getStringInterval()); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("3h") + .setRowSpan("1n") + .build(); + assertEquals('n', interval.getUnits()); + assertEquals("3h", interval.getInterval()); assertEquals('h', interval.getIntervalUnits()); assertEquals(256, interval.getIntervals()); - assertEquals(10800, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(10800, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor6HourMonth() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "6h", "1m"); - assertEquals('m', interval.getUnits()); - assertEquals("6h", interval.getStringInterval()); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("6h") + .setRowSpan("1n") + .build(); + assertEquals('n', interval.getUnits()); + assertEquals("6h", interval.getInterval()); assertEquals('h', interval.getIntervalUnits()); assertEquals(128, interval.getIntervals()); - assertEquals(21600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(21600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor6HourYear() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "6h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("6h") + .setRowSpan("1y") + .build(); assertEquals('y', interval.getUnits()); - assertEquals("6h", interval.getStringInterval()); + assertEquals("6h", interval.getInterval()); assertEquals('h', interval.getIntervalUnits()); assertEquals(1464, interval.getIntervals()); - assertEquals(21600, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(21600, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor12HourYear() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "12h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("12h") + .setRowSpan("1y") + .build(); assertEquals('y', interval.getUnits()); - assertEquals("12h", interval.getStringInterval()); + assertEquals("12h", interval.getInterval()); assertEquals('h', interval.getIntervalUnits()); assertEquals(732, interval.getIntervals()); - assertEquals(43200, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(43200, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test public void ctor1DayYear() throws Exception { - final RollupInterval interval = new RollupInterval( - rollup_table, preagg_table, "1d", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1d") + .setRowSpan("1y") + .build(); assertEquals('y', interval.getUnits()); - assertEquals("1d", interval.getStringInterval()); + assertEquals("1d", interval.getInterval()); assertEquals('d', interval.getIntervalUnits()); assertEquals(366, interval.getIntervals()); - assertEquals(86400, interval.getInterval()); - assertEquals(rollup_table, interval.getTemporalTableName()); - assertEquals(preagg_table, interval.getGroupbyTableName()); + assertEquals(86400, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); } @Test (expected = IllegalArgumentException.class) public void ctorUnknownNullRollupTable() throws Exception { - new RollupInterval(null, preagg_table, "1d", "1h"); + RollupInterval.builder() + .setTable(null) + .setPreAggregationTable(preagg_table) + .setInterval("1h") + .setRowSpan("1d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorUnknownEmptyRollupTable() throws Exception { - new RollupInterval("", preagg_table, "1d", "1h"); + RollupInterval.builder() + .setTable("") + .setPreAggregationTable(preagg_table) + .setInterval("1h") + .setRowSpan("1d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorUnknownNullPreAggTable() throws Exception { - new RollupInterval(rollup_table, null, "1d", "1h"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(null) + .setInterval("1h") + .setRowSpan("1d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorUnknownEmptyPreAggTable() throws Exception { - new RollupInterval(rollup_table, "", "1d", "1h"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable("") + .setInterval("1h") + .setRowSpan("1d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorUnknownSpan() throws Exception { - new RollupInterval(rollup_table, preagg_table, "1d", "1s"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1h") + .setRowSpan("1s") + .build(); } @Test (expected = NullPointerException.class) public void ctorNullInterval() throws Exception { - new RollupInterval(rollup_table, preagg_table, null, "1d"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval(null) + .setRowSpan("1d") + .build(); } @Test (expected = StringIndexOutOfBoundsException.class) public void ctorEmptyInterval() throws Exception { - new RollupInterval(rollup_table, preagg_table, "", "1d"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("") + .setRowSpan("1d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorBigDuration() throws Exception { - new RollupInterval(rollup_table, preagg_table, "365y", "1d"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("365y") + .setRowSpan("1d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorTooManyIntervals() throws Exception { - new RollupInterval(rollup_table, preagg_table, "1s", "17"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("17") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorDurationTooBigForSpan() throws Exception { - new RollupInterval(rollup_table, preagg_table, "36500s", "1h"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("36500s") + .setRowSpan("1h") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorDurationEqualToSpan() throws Exception { - new RollupInterval(rollup_table, preagg_table, "3600s", "1h"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("3600s") + .setRowSpan("1h") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorTooFewIntervals() throws Exception { - new RollupInterval(rollup_table, preagg_table, "3000s", "1h"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("3000s") + .setRowSpan("1h") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorNoUnitsInSpan() throws Exception { - new RollupInterval(rollup_table, preagg_table, "365y", "1"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("365y") + .setRowSpan("1") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorNoIntervalInSpan() throws Exception { - new RollupInterval(rollup_table, preagg_table, "365y", "d"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("365y") + .setRowSpan("d") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctorNoMs() throws Exception { - new RollupInterval(rollup_table, preagg_table, "365y", "1000ms"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("365y") + .setRowSpan("1000ms") + .build(); } @Test (expected = IllegalArgumentException.class) public void ctor15Minute7Days() throws Exception { - new RollupInterval(rollup_table, preagg_table, "15m", "7d"); + RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("15m") + .setRowSpan("7d") + .build(); } @Test - public void testHashCodeAndEquals() throws Exception { - RollupInterval interval_a = new RollupInterval( - rollup_table, preagg_table, "7s", "1h"); - int hash_a = interval_a.hashCode(); - RollupInterval interval_b = new RollupInterval( - rollup_table, preagg_table, "7s", "1h"); - int hash_b = interval_b.hashCode(); + public void serdes() throws Exception { + RollupInterval interval = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + String json = JSON.serializeToString(interval); + assertTrue(json.contains("\"interval\":\"1s\"")); + assertTrue(json.contains("\"table\":\"tsdb-rollup-10m\"")); + assertTrue(json.contains("\"defaultInterval\":true")); + assertTrue(json.contains("\"rowSpan\":\"1h\"")); + assertTrue(json.contains("\"preAggregationTable\":\"tsdb-rollup-agg-10m\"")); + + json = "{\"interval\":\"1s\",\"table\":\"tsdb-rollup-10m\"," + + "\"defaultRollupInterval\":true,\"rowSpan\":\"1h\"," + + "\"preAggregationTable\":\"tsdb-rollup-agg-10m\"}"; + interval = JSON.parseToObject(json, RollupInterval.class); + assertEquals('h', interval.getUnits()); + assertEquals("1s", interval.getInterval()); + assertEquals('s', interval.getIntervalUnits()); + assertEquals(3600, interval.getIntervals()); + assertEquals(1, interval.getIntervalSeconds()); + assertEquals(rollup_table, interval.getTable()); + assertEquals(preagg_table, interval.getPreAggregationTable()); + assertEquals(0, Bytes.memcmp(table, interval.getTemporalTable())); + assertEquals(0, Bytes.memcmp(agg_table, interval.getGroupbyTable())); + } + + @Test + public void testHashCodeAndEquals() throws Exception { + final RollupInterval interval_a = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + RollupInterval interval_b = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + assertEquals(interval_a.hashCode(), interval_b.hashCode()); assertEquals(interval_a, interval_b); - assertTrue(interval_a != interval_b); - assertEquals(hash_a, hash_b); - interval_b = new RollupInterval( - rollup_table, preagg_table, "18s", "1h"); - hash_b = interval_b.hashCode(); - assertFalse(interval_a.equals(interval_b)); - assertFalse(hash_a == hash_b); + interval_b = RollupInterval.builder() + .setTable("nothertable") // <-- DIFF + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + assertNotEquals(interval_a.hashCode(), interval_b.hashCode()); + assertNotEquals(interval_a, interval_b); + + interval_b = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable("nothertable") // <-- DIFF + .setInterval("1s") + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + assertNotEquals(interval_a.hashCode(), interval_b.hashCode()); + assertNotEquals(interval_a, interval_b); - interval_b = new RollupInterval( - rollup_table, preagg_table, "7s", "2h"); - hash_b = interval_b.hashCode(); - assertFalse(interval_a.equals(interval_b)); - assertFalse(hash_a == hash_b); + interval_b = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("30s") // <-- DIFF + .setRowSpan("1h") + .setDefaultInterval(true) + .build(); + assertNotEquals(interval_a.hashCode(), interval_b.hashCode()); + assertNotEquals(interval_a, interval_b); - interval_b = new RollupInterval( - "tsdb-quirm", preagg_table, "7s", "1h"); - hash_b = interval_b.hashCode(); - assertFalse(interval_a.equals(interval_b)); - assertFalse(hash_a == hash_b); + interval_b = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("2h") // <-- DIFF + .setDefaultInterval(true) + .build(); + assertNotEquals(interval_a.hashCode(), interval_b.hashCode()); + assertNotEquals(interval_a, interval_b); - interval_b = new RollupInterval( - rollup_table, "tsdb-klatch", "7s", "1h"); - hash_b = interval_b.hashCode(); - assertFalse(interval_a.equals(interval_b)); - assertFalse(hash_a == hash_b); + interval_b = RollupInterval.builder() + .setTable(rollup_table) + .setPreAggregationTable(preagg_table) + .setInterval("1s") + .setRowSpan("1h") + //.setIsDefault(true) // <-- DIFF + .build(); + assertNotEquals(interval_a.hashCode(), interval_b.hashCode()); + assertNotEquals(interval_a, interval_b); } } diff --git a/test/rollup/TestRollupSeq.java b/test/rollup/TestRollupSeq.java index 6dd74b4b07..bfd7f4b0b3 100644 --- a/test/rollup/TestRollupSeq.java +++ b/test/rollup/TestRollupSeq.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2015 The OpenTSDB Authors. +// 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 @@ -12,23 +12,15 @@ // see . package net.opentsdb.rollup; +import net.opentsdb.core.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; - -import net.opentsdb.core.Aggregators; -import net.opentsdb.core.Const; -import net.opentsdb.core.DataPoint; -import net.opentsdb.core.IllegalDataException; -import net.opentsdb.core.Internal; -import net.opentsdb.core.RowKey; -import net.opentsdb.core.SeekableView; -import net.opentsdb.core.TSDB; -import net.opentsdb.core.TestRowSeq; import net.opentsdb.uid.UniqueId; +import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.Config; import org.hbase.async.Bytes; @@ -36,7 +28,6 @@ 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; @@ -54,41 +45,103 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({ RollupSeq.class, TSDB.class, UniqueId.class, KeyValue.class, Config.class, RowKey.class, Const.class }) -public final class TestRollupSeq { - private TSDB tsdb = mock(TSDB.class); - private Config config = mock(Config.class); - private UniqueId metrics = mock(UniqueId.class); - private static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; - private static final byte[] KEY = - new byte[] { 0, 0, 1, 0x50, (byte)0xE2, 0x27, 0, 0, 0, 1, 0, 0, 2 }; +public class TestRollupSeq { + protected TSDB tsdb = mock(TSDB.class); + protected Config config = mock(Config.class); + protected UniqueId metrics = mock(UniqueId.class); + protected static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; public static final byte[] FAMILY = { 't' }; - private static final RollupQuery rollup_query_sum = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - Aggregators.SUM, 1000, Aggregators.SUM); - private static final RollupQuery rollup_query_avg = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - Aggregators.AVG, 1000, Aggregators.AVG); - private static final RollupQuery rollup_query_sum_mimmax = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1s", "1h"), - Aggregators.MIMMAX, 1000, Aggregators.MIMMAX); - private static final RollupQuery rollup_query_10m_sum = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), - Aggregators.SUM, 600000, Aggregators.SUM); - private static final RollupQuery rollup_query_10m_avg = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), - Aggregators.AVG, 600000, Aggregators.AVG); - private static final RollupQuery rollup_query_10m_count = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "10m", "6h"), - Aggregators.COUNT, 600000, Aggregators.COUNT); - private static final RollupQuery rollup_query_1h_sum = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), - Aggregators.SUM, 3600000, Aggregators.SUM); - private static final RollupQuery rollup_query_1h_avg = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), - Aggregators.AVG, 3600000, Aggregators.AVG); - private static final RollupQuery rollup_query_1h_count = - new RollupQuery(new RollupInterval("tsdb", "tsdb-agg", "1h", "1d"), - Aggregators.COUNT, 3600000, Aggregators.COUNT); + protected byte[] key = null; + protected static final RollupQuery rollup_query_sum = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1s") + .setRowSpan("1h") + .build(), + Aggregators.SUM, + 1000, + Aggregators.SUM); + protected static final RollupQuery rollup_query_avg = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1s") + .setRowSpan("1h") + .build(), + Aggregators.AVG, + 1000, + Aggregators.AVG); + protected static final RollupQuery rollup_query_sum_mimmax = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1s") + .setRowSpan("1h") + .build(), + Aggregators.MIMMAX, + 1000, + Aggregators.MIMMAX); + protected static final RollupQuery rollup_query_10m_sum = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("10m") + .setRowSpan("6h") + .build(), + Aggregators.SUM, + 600000, + Aggregators.SUM); + protected static final RollupQuery rollup_query_10m_avg = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("10m") + .setRowSpan("6h") + .build(), + Aggregators.AVG, + 600000, + Aggregators.AVG); + protected static final RollupQuery rollup_query_10m_count = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("10m") + .setRowSpan("6h") + .build(), + Aggregators.COUNT, + 600000, + Aggregators.COUNT); + protected static final RollupQuery rollup_query_1h_sum = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1h") + .setRowSpan("1d") + .build(), + Aggregators.SUM, + 3600000, + Aggregators.SUM); + protected static final RollupQuery rollup_query_1h_avg = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1h") + .setRowSpan("1d") + .build(), + Aggregators.AVG, + 3600000, + Aggregators.AVG); + protected static final RollupQuery rollup_query_1h_count = + new RollupQuery(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1h") + .setRowSpan("1d") + .build(), + Aggregators.COUNT, + 3600000, + Aggregators.COUNT); @Before public void before() throws Exception { @@ -97,13 +150,18 @@ public void before() throws Exception { Whitebox.setInternalState(tsdb, "table", TABLE); Whitebox.setInternalState(tsdb, "config", config); when(tsdb.getConfig()).thenReturn(config); - when(RowKey.metricNameAsync(tsdb, TestRowSeq.KEY)) + key = BaseTsdbTest.getRowKey( + BaseTsdbTest.generateUID(UniqueIdType.METRIC, (byte) 1), + 1356998400, + BaseTsdbTest.generateUID(UniqueIdType.TAGK, (byte) 1), + BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1)); + when(RowKey.metricNameAsync(tsdb, key)) .thenReturn(Deferred.fromResult("sys.cpu.user")); } @Test public void setRow() throws Exception { - final KeyValue kv = getRollupKeyValue(1356998400000L, 4L, rollup_query_sum); + final KeyValue kv = getRollupKeyValue(key, 1356998400000L, 4L, rollup_query_sum); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); rs.setRow(kv); assertEquals(1, rs.size()); @@ -119,20 +177,20 @@ public void setRow() throws Exception { @Test (expected = IllegalStateException.class) public void setRowAlreadySet() throws Exception { - final KeyValue kv = getRollupKeyValue(1356998400000L, 4L, rollup_query_sum); + final KeyValue kv = getRollupKeyValue(key, 1356998400000L, 4L, rollup_query_sum); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); rs.setRow(kv); assertEquals(1, rs.size()); //Expects an IllegalStateException - final KeyValue kv1 = getRollupKeyValue(1356998500000L, 5L, rollup_query_sum); + final KeyValue kv1 = getRollupKeyValue(key, 1356998500000L, 5L, rollup_query_sum); rs.setRow(kv1); } @Test public void addRow() throws Exception { - final KeyValue kv1 = getRollupKeyValue(1356998400000L, 4L, rollup_query_sum); - final KeyValue kv2 = getRollupKeyValue(1356998500000L, 5L, rollup_query_sum); + final KeyValue kv1 = getRollupKeyValue(key, 1356998400000L, 4L, rollup_query_sum); + final KeyValue kv2 = getRollupKeyValue(key, 1356998500000L, 5L, rollup_query_sum); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); rs.setRow(kv1); @@ -158,50 +216,6 @@ public void addRow() throws Exception { assertFalse(it.hasNext()); } - // This should never happen - @Test - public void addRowMergeDifferentSalt() throws Exception { - PowerMockito.mockStatic(Const.class); - PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); - PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(20); - - final byte[] key = new byte[TestRowSeq.KEY.length + 1]; - key[0] = 1; - System.arraycopy(TestRowSeq.KEY, 0, key, 1, TestRowSeq.KEY.length); - final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; - final byte[] val1 = Bytes.fromLong(4L); - final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; - final byte[] val2 = Bytes.fromLong(5L); - final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); - assertEquals(2, rs.size()); - - final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; - final byte[] val3 = Bytes.fromLong(6L); - final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; - final byte[] val4 = Bytes.fromLong(7L); - final byte[] key2 = Arrays.copyOf(key, key.length); - key2[0] = 2; - rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); - rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); - - assertEquals(4, rs.size()); - - final SeekableView it = rs.iterator(); - long value = 4; - long ts = 1356998400000L; - while (it.hasNext()) { - final DataPoint dp = it.next(); - assertEquals(ts, dp.timestamp()); - assertTrue(dp.isInteger()); - assertEquals(value, dp.longValue()); - assertEquals(1, dp.valueCount()); - ++value; - ts += 1000; - } - } - @Test public void addRowMergeLater() throws Exception { // this happens if the same row key is used for the addRow call @@ -210,16 +224,16 @@ public void addRowMergeLater() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; final byte[] val4 = Bytes.fromLong(7L); - rs.addRow(TestRowSeq.makekv(qual3, val3)); - rs.addRow(TestRowSeq.makekv(qual4, val4)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual4, val4)); assertEquals(4, rs.size()); final SeekableView it = rs.iterator(); @@ -244,13 +258,13 @@ public void addRowMergeEarlier() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; final byte[] val2 = Bytes.fromLong(7L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; final byte[] val3 = Bytes.fromLong(4L); - rs.addRow(TestRowSeq.makekv( qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); } @Test (expected = IllegalDataException.class) @@ -261,21 +275,21 @@ public void addRowMergeMiddle() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x17 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; final byte[] val3 = Bytes.fromLong(8L); final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x57 }; final byte[] val4 = Bytes.fromLong(9L); - rs.addRow(TestRowSeq.makekv( qual3, val3)); - rs.addRow(TestRowSeq.makekv(qual4, val4)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual4, val4)); assertEquals(4, rs.size()); final byte[] qual5 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val5 = Bytes.fromLong(6L); - rs.addRow(TestRowSeq.makekv( qual5, val5)); + rs.addRow(TestRowSeq.makekv(key, qual5, val5)); } @Test (expected = IllegalDataException.class) @@ -288,17 +302,16 @@ public void addRowMergeDuplicateLater() throws Exception { final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); - rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); assertEquals(3, rs.size()); - rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); } @Test public void addRowMergeDuplicateLaterRepair() throws Exception { when(config.fix_duplicates()).thenReturn(true); - // this happens if the same row key is used for the addRow call final byte[] qual1 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; final byte[] val1 = Bytes.fromLong(4L); @@ -307,9 +320,9 @@ public void addRowMergeDuplicateLaterRepair() throws Exception { final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); - rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + rs.setRow(new KeyValue(key, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(key, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(key, FAMILY, qual3, 3, val3)); assertEquals(3, rs.size()); SeekableView it = rs.iterator(); long ts = 1356998400000L; @@ -321,7 +334,7 @@ public void addRowMergeDuplicateLaterRepair() throws Exception { ts += 1000; ++value; } - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 8, Bytes.fromLong(7L))); + rs.addRow(new KeyValue(key, FAMILY, qual3, 8, Bytes.fromLong(7L))); assertEquals(3, rs.size()); it = rs.iterator(); @@ -350,14 +363,14 @@ public void addRowMergeDuplicateEarlier() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; final byte[] val2 = Bytes.fromLong(7L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual4, val4)); - rs.addRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual4, val4)); + rs.addRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(3, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x07 }; final byte[] val3 = Bytes.fromLong(4L); - rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); } @Test @@ -371,9 +384,9 @@ public void addRowMergeDuplicateEarlierRepair() throws Exception { final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); - rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + rs.setRow(new KeyValue(key, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(key, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(key, FAMILY, qual3, 3, val3)); assertEquals(3, rs.size()); SeekableView it = rs.iterator(); long ts = 1356998400000L; @@ -386,7 +399,7 @@ public void addRowMergeDuplicateEarlierRepair() throws Exception { ++value; } - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 1, Bytes.fromLong(7L))); + rs.addRow(new KeyValue(key, FAMILY, qual3, 1, Bytes.fromLong(7L))); assertEquals(3, rs.size()); it = rs.iterator(); @@ -411,11 +424,11 @@ public void addRowMergeDuplicateCountLater() throws Exception { final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); - rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); assertEquals(0, rs.size()); - rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); } @Test @@ -429,13 +442,13 @@ public void addRowMergeDuplicateCountLaterRepair() throws Exception { final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); - rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); - rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + rs.setRow(new KeyValue(key, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(key, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(key, FAMILY, qual3, 3, val3)); assertEquals(6, rs.count_values[23]); assertEquals(0, rs.size()); - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 8, Bytes.fromLong(7L))); + rs.addRow(new KeyValue(key, FAMILY, qual3, 8, Bytes.fromLong(7L))); assertEquals(7, rs.count_values[23]); } @@ -449,14 +462,14 @@ public void addRowMergeDuplicateCountEarlier() throws Exception { final byte[] qual2 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x37 }; final byte[] val2 = Bytes.fromLong(7L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); - rs.setRow(TestRowSeq.makekv(qual4, val4)); - rs.addRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual4, val4)); + rs.addRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(0, rs.size()); final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x07 }; final byte[] val3 = Bytes.fromLong(4L); - rs.addRow(TestRowSeq.makekv(qual3, val3)); + rs.addRow(TestRowSeq.makekv(key, qual3, val3)); } @Test @@ -470,13 +483,13 @@ public void addRowMergeDuplicateCountEarlierRepair() throws Exception { final byte[] qual3 = { 0x63, 0x6F, 0x75, 0x6E, 0x74, 0x3A, 0x00, 0x27 }; final byte[] val3 = Bytes.fromLong(6L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_avg); - rs.setRow(new KeyValue(KEY, FAMILY, qual1, 1, val1)); - rs.addRow(new KeyValue(KEY, FAMILY, qual2, 2, val2)); - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 3, val3)); + rs.setRow(new KeyValue(key, FAMILY, qual1, 1, val1)); + rs.addRow(new KeyValue(key, FAMILY, qual2, 2, val2)); + rs.addRow(new KeyValue(key, FAMILY, qual3, 3, val3)); assertEquals(0, rs.size()); assertEquals(6, rs.count_values[23]); - rs.addRow(new KeyValue(KEY, FAMILY, qual3, 1, Bytes.fromLong(7L))); + rs.addRow(new KeyValue(key, FAMILY, qual3, 1, Bytes.fromLong(7L))); assertEquals(6, rs.count_values[23]); } @@ -487,8 +500,8 @@ public void addRowDiffBaseTime() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; @@ -507,8 +520,8 @@ public void addRowNotSet() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.addRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.addRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); } @@ -519,15 +532,15 @@ public void addRowMergeDifferentKey() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; final byte[] val3 = Bytes.fromLong(6L); final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; final byte[] val4 = Bytes.fromLong(7L); - final byte[] key2 = Arrays.copyOf(TestRowSeq.KEY, TestRowSeq.KEY.length); + final byte[] key2 = Arrays.copyOf(key, key.length); key2[key2.length - 1] = 3; rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); @@ -540,15 +553,15 @@ public void addRowMergeDifferentKeyAndSalt() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; final byte[] val3 = Bytes.fromLong(6L); final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; final byte[] val4 = Bytes.fromLong(7L); - final byte[] key2 = Arrays.copyOf(TestRowSeq.KEY, TestRowSeq.KEY.length); + final byte[] key2 = Arrays.copyOf(key, key.length); key2[0] = 2; key2[key2.length - 1] = 3; rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); @@ -562,15 +575,15 @@ public void addRowMergeDifferentTime() throws Exception { final byte[] qual2 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x27 }; final byte[] val2 = Bytes.fromLong(5L); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); - rs.setRow(TestRowSeq.makekv(qual1, val1)); - rs.addRow(TestRowSeq.makekv(qual2, val2)); + rs.setRow(TestRowSeq.makekv(key, qual1, val1)); + rs.addRow(TestRowSeq.makekv(key, qual2, val2)); assertEquals(2, rs.size()); final byte[] qual3 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x37 }; final byte[] val3 = Bytes.fromLong(6L); final byte[] qual4 = { 0x73, 0x75, 0x6D, 0x3A, 0x00, 0x47 }; final byte[] val4 = Bytes.fromLong(7L); - final byte[] key2 = Arrays.copyOf(TestRowSeq.KEY, TestRowSeq.KEY.length); + final byte[] key2 = Arrays.copyOf(key, key.length); key2[7] = 3; rs.addRow(TestRowSeq.makekv(key2, qual3, val3)); rs.addRow(TestRowSeq.makekv(key2, qual4, val4)); @@ -578,7 +591,7 @@ public void addRowMergeDifferentTime() throws Exception { @Test public void timestamp() throws Exception { - final KeyValue kv = getRollupKeyValue(1356998400000L, 7L, rollup_query_sum_mimmax); + final KeyValue kv = getRollupKeyValue(key,1356998400000L, 7L, rollup_query_sum_mimmax); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum_mimmax); rs.setRow(kv); @@ -597,7 +610,7 @@ public void timestamp() throws Exception { // NOTE: many of the tests below also test RollupSeq.size() @Test public void rollup10m() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -623,7 +636,7 @@ public void rollup10m() throws Exception { @Test public void rollup10mDouble() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, rollup_query_10m_sum)); @@ -649,7 +662,7 @@ public void rollup10mDouble() throws Exception { @Test public void rollup10mFloat() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 10.25F, rollup_query_10m_sum)); @@ -675,7 +688,7 @@ public void rollup10mFloat() throws Exception { @Test public void rollup10mMixFloatAndLong() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -712,7 +725,7 @@ public void rollup10mMixFloatAndLong() throws Exception { @Test public void rollupAvg10mWithCount() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -740,7 +753,7 @@ public void rollupAvg10mWithCount() throws Exception { @Test public void rollupAvg10mWithCountFirst() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); @@ -768,7 +781,7 @@ public void rollupAvg10mWithCountFirst() throws Exception { @Test public void rollupAvg10mMissingCount() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -783,7 +796,7 @@ public void rollupAvg10mMissingCount() throws Exception { @Test public void rollupAvg10mSkipFirstCount() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -811,7 +824,7 @@ public void rollupAvg10mSkipFirstCount() throws Exception { @Test public void rollupAvg10mSkipLastCount() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -839,7 +852,7 @@ public void rollupAvg10mSkipLastCount() throws Exception { @Test public void rollupAvg10mSkipMiddleCount() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -872,7 +885,7 @@ public void rollupAvg10mSkipMiddleCount() throws Exception { @Test public void rollupAvg10mMissingSum() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); @@ -886,7 +899,7 @@ public void rollupAvg10mMissingSum() throws Exception { } public void rollupAvg10mSkipFirstSum() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); //rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -914,7 +927,7 @@ public void rollupAvg10mSkipFirstSum() throws Exception { @Test public void rollupAvg10mSkipLastSum() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -942,7 +955,7 @@ public void rollupAvg10mSkipLastSum() throws Exception { @Test public void rollupAvg10mSkipMiddleSum() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -975,7 +988,7 @@ public void rollupAvg10mSkipMiddleSum() throws Exception { @Test public void rollupAvg10mUnaligned() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -995,7 +1008,7 @@ public void rollupAvg10mUnaligned() throws Exception { @Test public void endOfArrayDivergence() throws Exception { final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); int[]indices = new int[4]; @@ -1022,7 +1035,7 @@ public void endOfArrayDivergence() throws Exception { @Test public void arrayOverflowAvoidance() throws Exception { final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final int[]indices = new int[4]; @@ -1047,7 +1060,7 @@ public void arrayOverflowAvoidance() throws Exception { @Test public void rollup1hLong() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); @@ -1073,7 +1086,7 @@ public void rollup1hLong() throws Exception { @Test public void rollup1hFloat() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, rollup_query_1h_sum)); @@ -1099,7 +1112,7 @@ public void rollup1hFloat() throws Exception { @Test public void rollup1hLongWithCount() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); @@ -1130,7 +1143,7 @@ public void rollup1hLongWithCount() throws Exception { @Test public void rollup1hLongWithCountWithDoubles() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); @@ -1161,7 +1174,7 @@ public void rollup1hLongWithCountWithDoubles() throws Exception { @Test public void rollup10mSeekTop() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -1191,7 +1204,7 @@ public void rollup10mSeekTop() throws Exception { @Test public void rollup10mSeek() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -1221,7 +1234,7 @@ public void rollup10mSeek() throws Exception { @Test public void rollup10mSeekOOB() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -1241,7 +1254,7 @@ public void rollup10mSeekOOB() throws Exception { @Test public void rollup10mSeekSeconds() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -1271,7 +1284,7 @@ public void rollup10mSeekSeconds() throws Exception { @Test public void rollup10mSeekUnaligned() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -1301,7 +1314,7 @@ public void rollup10mSeekUnaligned() throws Exception { @Test public void rollup10mAvgSeekTopAligned() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1332,7 +1345,7 @@ public void rollup10mAvgSeekTopAligned() throws Exception { @Test public void rollup10mAvgSeekTopUnaligned() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1362,7 +1375,7 @@ public void rollup10mAvgSeekTopUnaligned() throws Exception { @Test public void rollup10mAvgSeekTopUnalignedMissingTop() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1393,7 +1406,7 @@ public void rollup10mAvgSeekTopUnalignedMissingTop() throws Exception { @Test public void rollup10mAvgSeekAligned() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1424,7 +1437,7 @@ public void rollup10mAvgSeekAligned() throws Exception { @Test public void rollup10mAvgSeekUnaligned() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1455,7 +1468,7 @@ public void rollup10mAvgSeekUnaligned() throws Exception { @Test public void rollup10mAvgSeekUnalignedEmpty() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1475,7 +1488,7 @@ public void rollup10mAvgSeekUnalignedEmpty() throws Exception { @Test public void rollup10mAvgSeekTopAlignedOOB() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1495,7 +1508,7 @@ public void rollup10mAvgSeekTopAlignedOOB() throws Exception { @Test public void rollup10mAvgSeekTopUnalignedOOB() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1515,7 +1528,7 @@ public void rollup10mAvgSeekTopUnalignedOOB() throws Exception { @Test public void rollup10mAvgSeekTopUnalignedEmptyOOB() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); @@ -1535,7 +1548,7 @@ public void rollup10mAvgSeekTopUnalignedEmptyOOB() throws Exception { @Test public void rollup10mTimestamp() throws Exception { - byte[] key = Arrays.copyOf(KEY, KEY.length); + Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); @@ -1557,12 +1570,6 @@ public void rollup10mTimestamp() throws Exception { } catch (IndexOutOfBoundsException e) { } } - private static KeyValue getRollupKeyValue(final long timestamp, - final long value, - final RollupQuery rollup_query) { - return getRollupKeyValue(TestRowSeq.KEY, timestamp, value, rollup_query); - } - private static KeyValue getRollupKeyValue(final byte[] key, final long timestamp, final long value, @@ -1602,7 +1609,7 @@ private static byte[] getQualifier(final long timestamp, final int base_time = RollupUtils.getRollupBasetime(timestamp, rollup_query.getRollupInterval()); return RollupUtils.buildRollupQualifier(timestamp, base_time, flags, - rollup_query.getGroupBy().toString(), + rollup_query.getRollupAgg().toString(), rollup_query.getRollupInterval()); } } diff --git a/test/rollup/TestRollupUtils.java b/test/rollup/TestRollupUtils.java index 5bfa239b9f..7f33c7c593 100644 --- a/test/rollup/TestRollupUtils.java +++ b/test/rollup/TestRollupUtils.java @@ -21,253 +21,274 @@ import net.opentsdb.core.Const; public class TestRollupUtils { - private RollupInterval hour_interval; private static final byte[] SUM_COL = "sum:".getBytes(Const.ASCII_CHARSET); - private final static String temporal_table = "tsdb-rollup-10m"; - private final static String groupby_table = "tsdb-rollup-agg-10m"; + private static final String temporal_table = "tsdb-rollup-10m"; + private static final String groupby_table = "tsdb-rollup-agg-10m"; + + private RollupInterval hour_interval; + private RollupInterval tenmin_oneday; + private RollupInterval month_interval; @Before public void before() { - hour_interval = new RollupInterval(temporal_table, groupby_table, "1s", "1h"); + hour_interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1s") + .setRowSpan("1h") + .build(); + tenmin_oneday = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("1d") + .build(); + month_interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1h") + .setRowSpan("1n") + .build(); } @Test public void getRollupBasetimeHourSecondsTop() throws Exception { // Thu, 06 Jun 2013 15:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(1370530800, RollupUtils.getRollupBasetime(1370530800L, interval)); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370530800L, + hour_interval)); } @Test public void getRollupBasetimeHourMilliSecondsTop() throws Exception { // Thu, 06 Jun 2013 15:00:00.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(1370530800, RollupUtils.getRollupBasetime(1370530800154L, interval)); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370530800154L, + hour_interval)); } @Test public void getRollupBasetimeHourSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(1370530800, RollupUtils.getRollupBasetime(1370532925L, interval)); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370532925L, + hour_interval)); } @Test public void getRollupBasetimeHourMilliSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(1370530800, RollupUtils.getRollupBasetime(1370532925154L, interval)); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370532925154L, + hour_interval)); } @Test public void getRollupBasetimeHourSecondsEnd() throws Exception { // Thu, 06 Jun 2013 15:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(1370530800, RollupUtils.getRollupBasetime(1370534399L, interval)); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370534399L, + hour_interval)); } @Test public void getRollupBasetimeHourMilliSecondsEnd() throws Exception { // Thu, 06 Jun 2013 15:59:59.999 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(1370530800, RollupUtils.getRollupBasetime(1370534399999L, interval)); + assertEquals(1370530800, RollupUtils.getRollupBasetime(1370534399999L, + hour_interval)); } @Test public void getRollupBasetime6HourSecondsTop() throws Exception { // Thu, 06 Jun 2013 12:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "6h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("6h") + .build(); assertEquals(1370520000, RollupUtils.getRollupBasetime(1370520000L, interval)); } @Test public void getRollupBasetime6HourSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "6h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("6h") + .build(); assertEquals(1370520000, RollupUtils.getRollupBasetime(1370530800L, interval)); } @Test public void getRollupBasetime6HourSecondsEnd() throws Exception { // Thu, 06 Jun 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "6h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("6h") + .build(); assertEquals(1370520000, RollupUtils.getRollupBasetime(1370541599L, interval)); } @Test public void getRollupBasetime2HourSecondsTop() throws Exception { // Thu, 06 Jun 2013 12:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "2h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("2h") + .build(); assertEquals(1370520000, RollupUtils.getRollupBasetime(1370520000L, interval)); } @Test public void getRollupBasetime2HourSecondsMid() throws Exception { // Thu, 06 Jun 2013 13:01:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "2h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("2h") + .build(); assertEquals(1370520000, RollupUtils.getRollupBasetime(1370523660L, interval)); } @Test public void getRollupBasetime2HourSecondsEnd() throws Exception { // Thu, 06 Jun 2013 13:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "2h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("2h") + .build(); assertEquals(1370520000, RollupUtils.getRollupBasetime(1370527199L, interval)); } @Test public void getRollupBasetimeDaySecondsTop() throws Exception { // Thu, 06 Jun 2013 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); - assertEquals(1370476800, RollupUtils.getRollupBasetime(1370476800L, interval)); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370476800L, + tenmin_oneday)); } @Test public void getRollupBasetimeDayMilliSecondsTop() throws Exception { // Thu, 06 Jun 2013 00:00:00.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); - assertEquals(1370476800, RollupUtils.getRollupBasetime(1370476800154L, interval)); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370476800154L, + tenmin_oneday)); } @Test public void getRollupBasetimeDaySecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); - assertEquals(1370476800, RollupUtils.getRollupBasetime(1370532925L, interval)); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370532925L, + tenmin_oneday)); } @Test public void getRollupBasetimeDayMilliSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); - assertEquals(1370476800, RollupUtils.getRollupBasetime(1370532925154L, interval)); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370532925154L, + tenmin_oneday)); } @Test public void getRollupBasetimeDaySecondsEnd() throws Exception { // Thu, 06 Jun 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); - assertEquals(1370476800, RollupUtils.getRollupBasetime(1370563199L, interval)); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370563199L, + tenmin_oneday)); } @Test public void getRollupBasetimeDayMilliSecondsEnd() throws Exception { // Thu, 06 Jun 2013 23:59:59.999 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); - assertEquals(1370476800, RollupUtils.getRollupBasetime(1370563199999L, interval)); + assertEquals(1370476800, RollupUtils.getRollupBasetime(1370563199999L, + tenmin_oneday)); } @Test public void getRollupBasetimeMonthSecondsTop() throws Exception { // Sat, 01 Jun 2013 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1370044800, RollupUtils.getRollupBasetime(1370044800L, interval)); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370044800L, + month_interval)); } @Test public void getRollupBasetimeMonthMilliSecondsTop() throws Exception { // Thu, 01 Jun 2013 00:00:00.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1370044800, RollupUtils.getRollupBasetime(1370044800154L, interval)); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370044800154L, + month_interval)); } @Test public void getRollupBasetimeMonthSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1370044800, RollupUtils.getRollupBasetime(1370532925L, interval)); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370532925L, + month_interval)); } @Test public void getRollupBasetimeMonthMilliSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1370044800, RollupUtils.getRollupBasetime(1370532925154L, interval)); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1370532925154L, + month_interval)); } @Test public void getRollupBasetimeMonthSecondsEnd30days() throws Exception { // Thu, 30 Jun 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1370044800, RollupUtils.getRollupBasetime(1372636799L, interval)); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1372636799L, + month_interval)); } @Test public void getRollupBasetimeMonthMilliSecondsEnd30days() throws Exception { // Thu, 30 Jun 2013 23:59:59.999 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1370044800, RollupUtils.getRollupBasetime(1372636799999L, interval)); + assertEquals(1370044800, RollupUtils.getRollupBasetime(1372636799999L, + month_interval)); } @Test public void getRollupBasetimeMonthSecondsEnd31days() throws Exception { // Wed, 31 Jul 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1372636800, RollupUtils.getRollupBasetime(1375315199L, interval)); + assertEquals(1372636800, RollupUtils.getRollupBasetime(1375315199L, + month_interval)); } @Test public void getRollupBasetimeMonthMilliSecondsEnd31days() throws Exception { // Wed, 31 Jul 2013 23:59:59.999 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1372636800, RollupUtils.getRollupBasetime(1375315199999L, interval)); + assertEquals(1372636800, RollupUtils.getRollupBasetime(1375315199999L, + month_interval)); } @Test public void getRollupBasetimeMonthSecondsEndFebruary() throws Exception { // Thu, 28 Feb 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1359676800, RollupUtils.getRollupBasetime(1362095999L, interval)); + assertEquals(1359676800, RollupUtils.getRollupBasetime(1362095999L, + month_interval)); } @Test public void getRollupBasetimeMonthMilliSecondsEndFebruary() throws Exception { // Thu, 28 Feb 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1359676800, RollupUtils.getRollupBasetime(1362095999999L, interval)); + assertEquals(1359676800, RollupUtils.getRollupBasetime(1362095999999L, + month_interval)); } @Test public void getRollupBasetimeMonthSecondsEndLeapFebruary() throws Exception { // Wed, 29 Feb 2012 23:59:59 GMT - final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "1h", "1m"); - assertEquals(1328054400, RollupUtils.getRollupBasetime(1330559999L, interval)); + assertEquals(1328054400, RollupUtils.getRollupBasetime(1330559999L, + month_interval)); } @Test public void getRollupBasetimeMonthMilliSecondsEndLeapFebruary() throws Exception { // Wed, 29 Feb 2012 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1328054400, RollupUtils.getRollupBasetime(1330559999999L, interval)); + assertEquals(1328054400, RollupUtils.getRollupBasetime(1330559999999L, + month_interval)); } // NOTE: This is system dependent and leap seconds will usually just bump @@ -276,118 +297,161 @@ public void getRollupBasetimeMonthMilliSecondsEndLeapFebruary() throws Exception @Test public void getRollupBasetimeMonthSecondsLeapSecond() throws Exception { // Tue, 30 Jun 2015 23:59:60 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1435708800, RollupUtils.getRollupBasetime(1435708800L, interval)); + assertEquals(1435708800, RollupUtils.getRollupBasetime(1435708800L, + month_interval)); } @Test public void getRollupBasetimeMonthSecondsLeapMilliSecond() throws Exception { // Tue, 30 Jun 2015 23:59:60.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); - assertEquals(1435708800, RollupUtils.getRollupBasetime(1435708800154L, interval)); + assertEquals(1435708800, RollupUtils.getRollupBasetime(1435708800154L, + month_interval)); } @Test public void getRollupBasetimeYearSecondsTop() throws Exception { // Tue, 01 Jan 2013 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "24h", "1y"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1356998400L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1356998400L, + interval)); } @Test public void getRollupBasetimeYearMilliSecondsTop() throws Exception { // Tue, 01 Jan 2013 00:00:00.154 GMT - final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "24h", "1y"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1356998400154L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1356998400154L, + interval)); } @Test public void getRollupBasetimeYearSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "24h", "1y"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1370532925L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1370532925L, + interval)); } @Test public void getRollupBasetimeYearMilliSecondsMid() throws Exception { // Thu, 06 Jun 2013 15:35:25.154 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "24h", "1y"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1370532925154L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1370532925154L, + interval)); } @Test public void getRollupBasetimeYearSecondsEnd() throws Exception { // Tue, 31 Dec 2013 23:59:59 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "24h", "1y"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399L, + interval)); } @Test public void getRollupBasetimeYearMilliSecondsEnd() throws Exception { // Tue, 31 Dec 2013 23:59:59.999 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "24h", "1y"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, + interval)); } @Test public void getRollupBasetimeHourZero() throws Exception { // Thu, 01 Jan 1970 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); + assertEquals(0, RollupUtils.getRollupBasetime(0L, hour_interval)); } @Test public void getRollupBasetimeDayZero() throws Exception { // Thu, 01 Jan 1970 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "10m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("10m") + .setRowSpan("1d") + .build(); assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); } @Test public void getRollupBasetimeMonthZero() throws Exception { // Thu, 01 Jan 1970 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1h", "1m"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1h") + .setRowSpan("1n") + .build(); assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); } @Test public void getRollupBasetimeYearZero() throws Exception { // Thu, 01 Jan 1970 00:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "24h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("24h") + .setRowSpan("1y") + .build(); assertEquals(0, RollupUtils.getRollupBasetime(0L, interval)); } @Test (expected = IllegalArgumentException.class) public void getRollupBasetimeNegativeTimestamp() throws Exception { // Thu, 06 Jun 2013 15:00:00 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1h"); - RollupUtils.getRollupBasetime(-1370530800L, interval); + RollupUtils.getRollupBasetime(-1370530800L, hour_interval); } @Test (expected = NullPointerException.class) public void getRollupBasetimeNullInterval() throws Exception { // Tue, 31 Dec 2013 23:59:59.999 GMT - assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, null)); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, + null)); } @Test (expected = IllegalArgumentException.class) public void getRollupBasetimeBadSpan() throws Exception { // Tue, 31 Dec 2013 23:59:59.999 GMT - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1s", "1w"); - assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, interval)); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1s") + .setRowSpan("1w") + .build(); + assertEquals(1356998400, RollupUtils.getRollupBasetime(1388534399999L, + interval)); } @Test @@ -441,8 +505,12 @@ public void buildRollupQualifier1SecondInHourOver() { @Test public void buildRollupQualifier30SecondInHourTop() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "30s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("30s") + .setRowSpan("1h") + .build(); final byte[] offset = {0, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -458,8 +526,12 @@ public void buildRollupQualifier30SecondInHourTop() { @Test public void buildRollupQualifier30SecondInHourMid() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "30s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("30s") + .setRowSpan("1h") + .build(); final byte[] offset = {4, (byte)0x67}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -475,8 +547,12 @@ public void buildRollupQualifier30SecondInHourMid() { @Test public void buildRollupQualifier30SecondInHourEnd() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "30s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("30s") + .setRowSpan("1h") + .build(); final byte[] offset = {7, (byte)0x77}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -492,8 +568,12 @@ public void buildRollupQualifier30SecondInHourEnd() { @Test (expected = IllegalArgumentException.class) public void buildRollupQualifier30SecondInHourOver() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "30s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("30s") + .setRowSpan("1h") + .build(); //Thu, 06 Jun 2013 16:00:00 GMT RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, @@ -502,8 +582,12 @@ public void buildRollupQualifier30SecondInHourOver() { @Test public void buildRollupQualifier1MinuteInHourTop() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1m", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1m") + .setRowSpan("1h") + .build(); final byte[] offset = {0, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -519,8 +603,12 @@ public void buildRollupQualifier1MinuteInHourTop() { @Test public void buildRollupQualifier1MinuteInHourMid() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1m", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1m") + .setRowSpan("1h") + .build(); final byte[] offset = {2, (byte)0x37}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -536,8 +624,12 @@ public void buildRollupQualifier1MinuteInHourMid() { @Test public void buildRollupQualifier1MinuteInHourEnd() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "1m", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("1m") + .setRowSpan("1h") + .build(); final byte[] offset = {3, (byte)0xB7}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -553,8 +645,12 @@ public void buildRollupQualifier1MinuteInHourEnd() { @Test (expected = IllegalArgumentException.class) public void buildRollupQualifier1MinuteInHourOver() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "30s", "1h"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("30s") + .setRowSpan("1h") + .build(); //Thu, 06 Jun 2013 16:00:00 GMT RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, @@ -563,8 +659,12 @@ public void buildRollupQualifier1MinuteInHourOver() { @Test public void buildRollupQualifier15MinutesInDayTop() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "15m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("15m") + .setRowSpan("1d") + .build(); final byte[] offset = {0, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -580,8 +680,12 @@ public void buildRollupQualifier15MinutesInDayTop() { @Test public void buildRollupQualifier15MinutesInDayMid() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "15m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("15m") + .setRowSpan("1d") + .build(); final byte[] offset = {3, (byte)0xE7}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -597,8 +701,12 @@ public void buildRollupQualifier15MinutesInDayMid() { @Test public void buildRollupQualifier15MinutesInDayEnd() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "15m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("15m") + .setRowSpan("1d") + .build(); final byte[] offset = {5, (byte)0xF7}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -614,8 +722,12 @@ public void buildRollupQualifier15MinutesInDayEnd() { @Test (expected = IllegalArgumentException.class) public void buildRollupQualifier15MinutesInDayOver() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "15m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("15m") + .setRowSpan("1d") + .build(); //Thu, 07 Jun 2013 00:00:00 GMT RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, "sum", @@ -624,8 +736,12 @@ public void buildRollupQualifier15MinutesInDayOver() { @Test public void buildRollupQualifier60MinutesInDayTop() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "60m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("60m") + .setRowSpan("1d") + .build(); final byte[] offset = {0, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -641,8 +757,12 @@ public void buildRollupQualifier60MinutesInDayTop() { @Test public void buildRollupQualifier60MinutesInDayMid() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "60m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("60m") + .setRowSpan("1d") + .build(); final byte[] offset = {0, (byte)0xF7}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -658,8 +778,12 @@ public void buildRollupQualifier60MinutesInDayMid() { @Test public void buildRollupQualifier60MinutesInDayEnd() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "60m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("60m") + .setRowSpan("1d") + .build(); final byte[] offset = {1, (byte)0x77}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -675,8 +799,12 @@ public void buildRollupQualifier60MinutesInDayEnd() { @Test (expected = IllegalArgumentException.class) public void buildRollupQualifier60MinutesInDayOver() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "60m", "1d"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("60m") + .setRowSpan("1d") + .build(); //Thu, 07 Jun 2013 00:00:00 GMT RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, "sum", @@ -685,8 +813,12 @@ public void buildRollupQualifier60MinutesInDayOver() { @Test public void buildRollupQualifier3HoursInMonthTop() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "3h", "1m"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("3h") + .setRowSpan("1n") + .build(); final byte[] offset = {0, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -702,8 +834,12 @@ public void buildRollupQualifier3HoursInMonthTop() { @Test public void buildRollupQualifier3HoursInMonthMid() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "3h", "1m"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("3h") + .setRowSpan("1n") + .build(); final byte[] offset = {2, (byte)0xD7}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -719,7 +855,12 @@ public void buildRollupQualifier3HoursInMonthMid() { @Test public void buildRollupQualifier3HoursInMonthEnd() { - final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "3h", "1m"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("3h") + .setRowSpan("1n") + .build(); final byte[] offset = {0x0E, (byte)0xF7}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -736,7 +877,12 @@ public void buildRollupQualifier3HoursInMonthEnd() { // NOTE this guy won't overflow since we max our monthlies on 31 days. @Test public void buildRollupQualifier3HoursInMonthOver30Days() { - final RollupInterval interval = new RollupInterval(temporal_table, groupby_table, "3h", "1m"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("3h") + .setRowSpan("1n") + .build(); final byte[] offset = {0x0F, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -753,8 +899,12 @@ public void buildRollupQualifier3HoursInMonthOver30Days() { // Still only overflows 3 days later @Test (expected = IllegalArgumentException.class) public void buildRollupQualifier3HoursInMonthOver() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "3h", "1m"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("3h") + .setRowSpan("1n") + .build(); //Wed, 03 Jul 2013 23:59:59 GMT RollupUtils.buildRollupQualifier(1372895999L, 1370044800, (byte)7, "sum", @@ -763,8 +913,12 @@ public void buildRollupQualifier3HoursInMonthOver() { @Test public void buildRollupQualifier6HoursInYearTop() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "6h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("6h") + .setRowSpan("1y") + .build(); final byte[] offset = {0, (byte)0x07}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -780,8 +934,12 @@ public void buildRollupQualifier6HoursInYearTop() { @Test public void buildRollupQualifier6HoursInYearMid() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "6h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("6h") + .setRowSpan("1y") + .build(); final byte[] offset = {0x27, (byte)0x27}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -797,8 +955,12 @@ public void buildRollupQualifier6HoursInYearMid() { @Test public void buildRollupQualifier6HoursInYearEnd() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "6h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("6h") + .setRowSpan("1y") + .build(); final byte[] offset = {0x5B, (byte)0x37}; byte[] expected_qual = new byte[SUM_COL.length + 2]; @@ -815,13 +977,18 @@ public void buildRollupQualifier6HoursInYearEnd() { // overflows since our max years are a little larger @Test (expected = IllegalArgumentException.class) public void buildRollupQualifier6HoursInYearOver() { - final RollupInterval interval = new RollupInterval( - temporal_table, groupby_table, "6h", "1y"); + final RollupInterval interval = RollupInterval.builder() + .setTable(temporal_table) + .setPreAggregationTable(groupby_table) + .setInterval("6h") + .setRowSpan("1y") + .build(); //Wed, 01 Jan 2014 00:00:00 GMT RollupUtils.buildRollupQualifier(1388620800, 1356998400, (byte)7, "sum", interval); } + // Flag tests ------------------ @Test public void buildRollupQualifier8BytesLong() { diff --git a/test/tsd/TestRollupRpc.java b/test/tsd/TestRollupRpc.java index d5a264ca2c..c29669fc2d 100644 --- a/test/tsd/TestRollupRpc.java +++ b/test/tsd/TestRollupRpc.java @@ -69,15 +69,31 @@ public void beforeLocal() throws Exception { final List families = new ArrayList(); families.add(FAMILY); - final List rollups = new ArrayList(); - rollups.add(new RollupInterval( - "tsdb", "tsdb-agg", "1m", "1h", true)); - rollups.add(new RollupInterval( - "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1m")); - - rollup_config = new RollupConfig(rollups); + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1m") + .setRowSpan("1h") + .setDefaultInterval(true)) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1n")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); - Whitebox.setInternalState(tsdb, "default_interval", rollups.get(0)); + Whitebox.setInternalState(tsdb, "default_interval", + rollup_config.getRollupInterval("1m")); storage = new MockBase(tsdb, client, true, true, true, true); storage.addTable("tsdb-rollup-1h".getBytes(), families); @@ -838,4 +854,4 @@ public void httpUnknownInterval() throws Exception { validateSEH(false); } -} +} \ No newline at end of file From 69570a497f534aacfd7c88a8e5016121829f8067 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Wed, 7 Jun 2017 11:36:55 -0700 Subject: [PATCH 643/826] Modify the Rollup code to write and read columns with one byte for the aggregation type instead of using something like "count:". This will save us 4 to 6 bytes per column. Yay! Signed-off-by: Chris Larsen --- src/core/SaltScanner.java | 35 +- src/core/Span.java | 3 +- src/core/TSDB.java | 9 +- src/core/TsdbQuery.java | 32 +- src/rollup/RollupConfig.java | 2 +- src/rollup/RollupSeq.java | 101 ++- src/rollup/RollupUtils.java | 42 +- test/core/TestRollupSpan.java | 26 + test/core/TestRollupSpanSalted.java | 46 ++ test/core/TestTSDBAddAggregatePoint.java | 398 +++------ .../core/TestTSDBAddAggregatePointSalted.java | 8 +- test/core/TestTsdbQueryRollup.java | 49 +- test/core/TestTsdbQueryRollupSalted.java | 74 ++ test/rollup/TestRollupSeq.java | 777 ++++++++++++------ test/rollup/TestRollupUtils.java | 297 ++++--- test/storage/MockBase.java | 2 +- test/tsd/TestRollupRpc.java | 29 +- 17 files changed, 1147 insertions(+), 783 deletions(-) create mode 100644 test/core/TestRollupSpanSalted.java create mode 100644 test/core/TestTsdbQueryRollupSalted.java diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 8775ec3fd2..68086fdc3f 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -109,6 +109,9 @@ public class SaltScanner { /** Index of the sub query in the main query list */ private final int query_index; + private final boolean is_rollup; + private final int rollup_agg_id; + private final int rollup_count_id; /** A latch used to determine how many scanners are still running */ private final CountDownLatch countdown; @@ -217,6 +220,20 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, this.query_stats = query_stats; this.query_index = query_index; countdown = new CountDownLatch(scanners.size()); + if (rollup_query != null && RollupQuery.isValidQuery(rollup_query)) { + is_rollup = true; + if (rollup_query.getRollupAgg() == Aggregators.AVG) { + rollup_agg_id = tsdb.getRollupConfig().getIdForAggregator("sum"); + rollup_count_id = tsdb.getRollupConfig().getIdForAggregator("count"); + } else { + rollup_agg_id = tsdb.getRollupConfig().getIdForAggregator( + rollup_query.getRollupAgg().toString()); + rollup_count_id = -1; + } + } else { + is_rollup = false; + rollup_agg_id = rollup_count_id = -1; + } } /** @@ -672,10 +689,9 @@ void processRow(final byte[] key, final ArrayList row) { final byte[] qual = kv.qualifier(); if (qual.length > 0) { - // TODO: Bug! Here we shouldn't use the first byte to check the - // type of this row. Instead should parse the byte array to find - // the suffix and determine the actual type - if (qual[0] == Annotation.PREFIX()) { + // TODO - allow rollups for annotations and histos? Probably will + // want to encode those on 4 bytes or something + if (!is_rollup && qual[0] == Annotation.PREFIX()) { // This could be a row with only an annotation in it final Annotation note = JSON.parseToObject(kv.value(), Annotation.class); @@ -687,7 +703,7 @@ void processRow(final byte[] key, final ArrayList row) { } map_notes.add(note); } - } else if (qual[0] == HistogramDataPoint.PREFIX) { + } else if (!is_rollup && qual[0] == HistogramDataPoint.PREFIX) { try { HistogramDataPoint histogram = Internal.decodeHistogramDataPoint(tsdb, kv); @@ -698,12 +714,15 @@ void processRow(final byte[] key, final ArrayList row) { } else { if (rollup_query.getGroupBy() == Aggregators.AVG || rollup_query.getGroupBy() == Aggregators.DEV) { - if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || + if (qual[0] == (byte) rollup_agg_id || + qual[0] == (byte) rollup_count_id || + Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { kvs.add(kv); } - } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), - qual, 0, rollup_query.getRollupAggPrefix().length) == 0) { + } else if (qual[0] == (byte) rollup_agg_id || + Bytes.memcmp(rollup_query.getRollupAggPrefix(), + qual, 0, rollup_query.getRollupAggPrefix().length) == 0) { kvs.add(kv); } } diff --git a/src/core/Span.java b/src/core/Span.java index 02a16e07f8..c54954cf47 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -227,7 +227,8 @@ protected void addRow(final KeyValue row) { */ static long lastTimestampInRow(final short metric_width, final KeyValue row) { - final long base_time = Bytes.getUnsignedInt(row.key(), metric_width); + final long base_time = Bytes.getUnsignedInt(row.key(), metric_width + + Const.SALT_WIDTH()); final byte[] qual = row.qualifier(); if (qual.length >= 4 && Internal.inMilliseconds(qual[qual.length - 4])) { return (base_time * 1000) + ((Bytes.getUnsignedInt(qual, qual.length - 4) & diff --git a/src/core/TSDB.java b/src/core/TSDB.java index e30d5af4af..e6a7d26808 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1477,9 +1477,10 @@ Deferred addAggregatePointInternal(final String metric, IncomingDataPoints.checkMetricAndTags(metric, tags); - final RollupInterval rollup_interval = interval == null || interval.isEmpty() - ? null : rollup_config.getRollupInterval(interval); - + final RollupInterval rollup_interval = (interval == null || interval.isEmpty() + ? null : rollup_config.getRollupInterval(interval)); + final int aggregator_id = rollup_interval == null ? -1 : + rollup_config.getIdForAggregator(rollup_aggregator); final byte[] row = IncomingDataPoints.rowKeyTemplate(this, metric, tags); final String rollup_agg = rollup_aggregator != null ? rollup_aggregator.toUpperCase() : null; @@ -1497,7 +1498,7 @@ Deferred addAggregatePointInternal(final String metric, final byte[] qualifier = interval == null || interval.isEmpty() ? Internal.buildQualifier(timestamp, flags) : RollupUtils.buildRollupQualifier( - timestamp, base_time, flags, rollup_agg, rollup_interval); + timestamp, base_time, flags, aggregator_id, rollup_interval); /** Callback executed for chaining filter calls to see if the value * should be written or not. */ diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index aa777672b9..0432276a74 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1316,23 +1316,43 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { // it. If not, then we can do this if (!rollup_query.getGroupBy().toString().equals("avg")) { if (existing != null) { - final List filters = new ArrayList(2); + final List filters = new ArrayList(3); filters.add(existing); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator(rollup_query.getGroupBy().toString() .getBytes(Const.ASCII_CHARSET)))); - scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ALL)); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(new byte[] { + (byte) tsdb.getRollupConfig().getIdForAggregator( + rollup_query.getRollupAgg().toString()) + }))); + scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ONE)); } else { - scanner.setFilter(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(rollup_query.getGroupBy().toString() - .getBytes(Const.ASCII_CHARSET)))); + final List filters = new ArrayList(2); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() + .getBytes(Const.ASCII_CHARSET)))); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(new byte[] { + (byte) tsdb.getRollupConfig().getIdForAggregator( + rollup_query.getRollupAgg().toString()) + }))); + scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ONE)); } } else { - final List filters = new ArrayList(2); + final List filters = new ArrayList(4); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator("sum".getBytes()))); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator("count".getBytes()))); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(new byte[] { + (byte) tsdb.getRollupConfig().getIdForAggregator("sum") + }))); + filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + new BinaryPrefixComparator(new byte[] { + (byte) tsdb.getRollupConfig().getIdForAggregator("count") + }))); if (existing != null) { final List combined = new ArrayList(2); diff --git a/src/rollup/RollupConfig.java b/src/rollup/RollupConfig.java index 55f0a84d50..f71824e178 100644 --- a/src/rollup/RollupConfig.java +++ b/src/rollup/RollupConfig.java @@ -282,7 +282,7 @@ public int getIdForAggregator(final String aggregator) { } Integer id = aggregations_to_ids.get(aggregator.toLowerCase()); if (id == null) { - throw new IllegalArgumentException("No ID found mapping to aggregator " + throw new IllegalArgumentException("No ID found mapping to aggregator: " + aggregator); } return id; diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java index 478d6a1571..82d242f383 100644 --- a/src/rollup/RollupSeq.java +++ b/src/rollup/RollupSeq.java @@ -38,8 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sun.reflect.generics.reflectiveObjects.NotImplementedException; - import com.stumbleupon.async.Deferred; /** @@ -63,6 +61,8 @@ public final class RollupSeq implements iRowSeq { /** Whether or not we need counts with our data, e.g. to compute the average */ private final boolean need_count; + private final int agg_id; + private final int count_id; /** First row key. */ protected byte[] key; @@ -108,8 +108,13 @@ public RollupSeq(final TSDB tsdb, final RollupQuery rollup_query) { count_qualifiers = new byte[rollup_query.getRollupInterval().getIntervals() * 2]; count_values = new byte[rollup_query.getRollupInterval().getIntervals()]; indices = new int[4]; + agg_id = tsdb.getRollupConfig().getIdForAggregator("sum"); + count_id = tsdb.getRollupConfig().getIdForAggregator("count"); } else { indices = new int[2]; + agg_id = tsdb.getRollupConfig() + .getIdForAggregator(rollup_query.getRollupAgg().toString()); + count_id = tsdb.getRollupConfig().getIdForAggregator("count"); } } @@ -129,22 +134,31 @@ public void setRow(final KeyValue column) { //Check whether the cell is generated by same rollup aggregator if (need_count) { - if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, RollupQuery.SUM.length) == 0) { - append(column, false); + System.out.println("AGG ID: " + agg_id + " COUNT ID: " + count_id + " MASK: " + (column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK)); + if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == agg_id) { + append(column, false, false); + } else if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == count_id) { + append(column, true, false); + // OLD style for Yahoo! + } else if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, RollupQuery.SUM.length) == 0) { + append(column, false, true); } else if (Bytes.memcmp(RollupQuery.COUNT, column.qualifier(), 0, RollupQuery.COUNT.length) == 0) { - append(column, true); + append(column, true, true); } else { throw new IllegalDataException("Attempt to add a different aggrregate cell =" + column + ", expected aggregator either SUM or COUNT"); } } else { - if (Bytes.memcmp(column.qualifier(), rollup_query.getRollupAggPrefix(), 0, - rollup_query.getRollupAggPrefix().length) != 0) { + if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == agg_id) { + append(column, false, false); + } else if (Bytes.memcmp(column.qualifier(), rollup_query.getRollupAggPrefix(), 0, + rollup_query.getRollupAggPrefix().length) == 0) { + append(column, false, true); + } else { throw new IllegalDataException("Attempt to add a different aggrregate cell =" + column + ", expected aggregator " + Bytes.pretty( rollup_query.getRollupAggPrefix())); } - append(column, false); } } @@ -167,33 +181,50 @@ public void addRow(final KeyValue column) { //Check whether the cell is generated by same rollup aggregator if (need_count) { - - if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, - RollupQuery.SUM.length) == 0) { - append(column, false); - } else if (Bytes.memcmp(RollupQuery.COUNT, column.qualifier(), 0, - RollupQuery.COUNT.length) == 0) { - append(column, true); + if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == agg_id) { + append(column, false, false); + } else if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == count_id) { + append(column, true, false); + // OLD style for Yahoo! + } else if (Bytes.memcmp(RollupQuery.SUM, column.qualifier(), 0, RollupQuery.SUM.length) == 0) { + append(column, false, true); + } else if (Bytes.memcmp(RollupQuery.COUNT, column.qualifier(), 0, RollupQuery.COUNT.length) == 0) { + append(column, true, true); } else { throw new IllegalDataException("Attempt to add a different aggrregate cell =" + column + ", expected aggregator either SUM or COUNT"); } } else { - if (Bytes.memcmp(column.qualifier(), rollup_query.getRollupAggPrefix(), 0, - rollup_query.getRollupAggPrefix().length) != 0) { + if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == agg_id) { + append(column, false, false); + } else if (Bytes.memcmp(column.qualifier(), rollup_query.getRollupAggPrefix(), 0, + rollup_query.getRollupAggPrefix().length) == 0) { + append(column, false, true); + } else { throw new IllegalDataException("Attempt to add a different aggrregate cell =" + column + ", expected aggregator " + Bytes.pretty( rollup_query.getRollupAggPrefix())); } - append(column, false); } } - private void append(KeyValue column, boolean is_count) { + /** + * Adds the column to the byte arrays. + * @param column The non-null key value to add. + * @param is_count Whether or not the column is for counts. + * @param strip_string Whether or not the column has the old style string + * header and needs cleaning. + */ + private void append(final KeyValue column, + final boolean is_count, + final boolean strip_string) { // for now assume we properly allocated our qualifiers if (is_count) { - int offset = Internal.getOffsetFromQualifier(column.qualifier(), - RollupQuery.COUNT.length + 1); + int offset = strip_string ? + Internal.getOffsetFromQualifier(column.qualifier(), + RollupQuery.COUNT.length + 1) : + Internal.getOffsetFromQualifier(column.qualifier(), + 1); if (last_count_offset > -1 && offset <= last_count_offset) { // only accept equivalent offsets. If somehow we get an earlier one, HBase is broke if (offset == last_count_offset && tsdb.getConfig().fix_duplicates()) { @@ -223,8 +254,12 @@ private void append(KeyValue column, boolean is_count) { } last_count_offset = offset; last_count_ts = column.timestamp(); - System.arraycopy(column.qualifier(), RollupQuery.COUNT.length + 1, - count_qualifiers, indices[2], 2); + if (strip_string) { + System.arraycopy(column.qualifier(), RollupQuery.COUNT.length + 1, + count_qualifiers, indices[2], 2); + } else { + System.arraycopy(column.qualifier(), 1, count_qualifiers, indices[2], 2); + } indices[2] += 2; if (indices[3] + column.value().length > count_values.length) { @@ -236,8 +271,10 @@ private void append(KeyValue column, boolean is_count) { column.value().length); indices[3] += column.value().length; } else { - int offset = Internal.getOffsetFromQualifier(column.qualifier(), - rollup_query.getRollupAggPrefix().length); + int offset = strip_string ? + Internal.getOffsetFromQualifier(column.qualifier(), + rollup_query.getRollupAggPrefix().length) + : Internal.getOffsetFromQualifier(column.qualifier(), 1); if (last_offset > -1 && offset <= last_offset) { // only accept equivalent offsets. If somehow we get an earlier one, HBase is broke if (offset == last_offset && tsdb.getConfig().fix_duplicates()) { @@ -266,8 +303,12 @@ private void append(KeyValue column, boolean is_count) { } last_offset = offset; last_value_ts = column.timestamp(); - System.arraycopy(column.qualifier(), - rollup_query.getRollupAggPrefix().length, qualifiers, indices[0], 2); + if (strip_string) { + System.arraycopy(column.qualifier(), + rollup_query.getRollupAggPrefix().length, qualifiers, indices[0], 2); + } else { + System.arraycopy(column.qualifier(), 1, qualifiers, indices[0], 2); + } indices[0] += 2; if (indices[1] + column.value().length > values.length) { @@ -423,17 +464,17 @@ public long timestamp(int i) { @Override public boolean isInteger(int i) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override public long longValue(int i) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override public double doubleValue(int i) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override diff --git a/src/rollup/RollupUtils.java b/src/rollup/RollupUtils.java index 0598ab42d2..8236171632 100644 --- a/src/rollup/RollupUtils.java +++ b/src/rollup/RollupUtils.java @@ -27,6 +27,10 @@ public final class RollupUtils { private static final Logger LOG = LoggerFactory.getLogger(RollupUtils.class); + public static final byte AGGREGATOR_MASK = (byte) 0x7F; + + public static final byte COMPACTED_MASK = (byte) 0x80; + /** The rollup qualifier delimiter character */ public static final String ROLLUP_QUAL_DELIM = ":"; @@ -46,7 +50,7 @@ private RollupUtils() { * has an unsupported span */ public static int getRollupBasetime(final long timestamp, - final RollupInterval interval) { + final RollupInterval interval) { if (timestamp < 0) { throw new IllegalArgumentException("Not supporting negative " + "timestamps at this time: " + timestamp); @@ -107,18 +111,18 @@ public static int getRollupBasetime(final long timestamp, * n : 2 bytes } * @param timestamp The data point timestamp * @param flags The length and type (float || int) flags for the value - * @param aggregator The aggregator used to generate the data + * @param aggregator_id The numeric ID of the aggregator the value maps to. * @param interval The RollupInterval object with data about the interval * @return An n byte array to use as the qualifier * @throws IllegalArgumentException if the aggregator is null or empty or the * timestamp is too far from the base time to fit within the interval. */ public static byte[] buildRollupQualifier(final long timestamp, - final short flags, - final String aggregator, - final RollupInterval interval) { + final short flags, + final int aggregator_id, + final RollupInterval interval) { return buildRollupQualifier(timestamp, - getRollupBasetime(timestamp, interval), flags, aggregator, interval); + getRollupBasetime(timestamp, interval), flags, aggregator_id, interval); } /** @@ -130,7 +134,7 @@ public static byte[] buildRollupQualifier(final long timestamp, * @param timestamp The data point timestamp * @param basetime The base timestamp to calculate the offset from * @param flags The length and type (float || int) flags for the value - * @param aggregator The aggregator used to generate the data + * @param aggregator_id The numeric ID of the aggregator the value maps to. * @param interval The RollupInterval object with data about the interval * @return An n byte array to use as the qualifier * @throws IllegalArgumentException if the aggregator is null or empty or the @@ -139,14 +143,9 @@ public static byte[] buildRollupQualifier(final long timestamp, public static byte[] buildRollupQualifier(final long timestamp, final int basetime, final short flags, - final String aggregator, + final int aggregator_id, final RollupInterval interval) { - if (aggregator == null || aggregator.isEmpty()) { - throw new IllegalArgumentException("Aggregator cannot be null or empty"); - } - - final byte[] agg = getRollupQualifierPrefix(aggregator); - final byte[] qualifier = new byte[agg.length + 2]; + final byte[] qualifier = new byte[3]; final int time_seconds = (int) ((timestamp & Const.SECOND_MASK) != 0 ? timestamp / 1000 : timestamp); @@ -162,10 +161,8 @@ public static byte[] buildRollupQualifier(final long timestamp, // shift the offset over 4 bits then apply the flag offset = offset << Const.FLAG_BITS; offset = offset | flags; - final byte[] offset_array = Bytes.fromShort((short) offset); - System.arraycopy(agg, 0, qualifier, 0, agg.length); - System.arraycopy(offset_array, 0, qualifier, agg.length, - offset_array.length); + qualifier[0] = (byte) aggregator_id; + System.arraycopy(Bytes.fromShort((short) offset), 0, qualifier, 1, 2); return qualifier; } @@ -255,4 +252,13 @@ public static byte[] getRollupQualifierPrefix(final String aggregator) { return (aggregator.toLowerCase() + ROLLUP_QUAL_DELIM) .getBytes(Const.ASCII_CHARSET); } + + /** + * Determines whether or not the column has been compacted or appended. + * @param qualifier A non-null and non-empty byte array. + * @return True if the column was compacted, false if not. + */ + public static boolean isCompacted(final byte[] qualifier) { + return (qualifier[0] & COMPACTED_MASK) != 0; + } } diff --git a/test/core/TestRollupSpan.java b/test/core/TestRollupSpan.java index e4cf442c9d..5aa177b216 100644 --- a/test/core/TestRollupSpan.java +++ b/test/core/TestRollupSpan.java @@ -19,7 +19,9 @@ import org.hbase.async.KeyValue; import org.junit.Before; import org.junit.Test; +import org.powermock.reflect.Whitebox; +import net.opentsdb.rollup.RollupConfig; import net.opentsdb.rollup.RollupInterval; import net.opentsdb.rollup.RollupQuery; import net.opentsdb.rollup.RollupSpan; @@ -29,6 +31,7 @@ public class TestRollupSpan extends BaseTsdbTest { protected byte[] hour1 = null; protected byte[] hour2 = null; protected byte[] hour3 = null; + protected RollupConfig rollup_config; protected static final Aggregator aggr_sum = Aggregators.SUM; protected static final RollupQuery rollup_query = @@ -47,6 +50,29 @@ public void beforeLocal() throws Exception { hour1 = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); hour2 = getRowKey(METRIC_STRING, 1357002000, TAGK_STRING, TAGV_STRING); hour3 = getRowKey(METRIC_STRING, 1357005600, TAGK_STRING, TAGV_STRING); + + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("6h")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); } @Test diff --git a/test/core/TestRollupSpanSalted.java b/test/core/TestRollupSpanSalted.java new file mode 100644 index 0000000000..49e602bfba --- /dev/null +++ b/test/core/TestRollupSpanSalted.java @@ -0,0 +1,46 @@ +package net.opentsdb.core; + +import org.junit.Before; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.reflect.Whitebox; + +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; + +public class TestRollupSpanSalted extends TestRollupSpan { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + hour1 = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); + hour2 = getRowKey(METRIC_STRING, 1357002000, TAGK_STRING, TAGV_STRING); + hour3 = getRowKey(METRIC_STRING, 1357005600, TAGK_STRING, TAGV_STRING); + + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("6h")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + } +} diff --git a/test/core/TestTSDBAddAggregatePoint.java b/test/core/TestTSDBAddAggregatePoint.java index f72fa32782..d1c1cfd2bf 100644 --- a/test/core/TestTSDBAddAggregatePoint.java +++ b/test/core/TestTSDBAddAggregatePoint.java @@ -1,15 +1,3 @@ -// 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 static org.junit.Assert.assertArrayEquals; @@ -70,6 +58,12 @@ public void beforeLocal() throws Exception { .addAggregationId("count", 1) .addAggregationId("max", 2) .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1m") + .setRowSpan("1h") + .setDefaultInterval(true)) .addInterval(RollupInterval.builder() .setTable("tsdb-rollup-10m") .setPreAggregationTable("tsdb-rollup-agg-10m") @@ -88,7 +82,7 @@ public void beforeLocal() throws Exception { .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); Whitebox.setInternalState(tsdb, "default_interval", - rollup_config.getRollupInterval("10m")); + rollup_config.getRollupInterval("1m")); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); @@ -101,7 +95,7 @@ public void beforeLocal() throws Exception { @Test public void addAggregatePointLong1Byte() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -115,7 +109,7 @@ public void addAggregatePointLong1Byte() throws Exception { @Test public void addAggregatePointLong1ByteNegative() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -129,7 +123,7 @@ public void addAggregatePointLong1ByteNegative() throws Exception { @Test public void addAggregatePointLong2Bytes() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 1}; + final byte[] qualifier = new byte[] {0, 0, 1}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 257, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -143,7 +137,7 @@ public void addAggregatePointLong2Bytes() throws Exception { @Test public void addAggregatePointLong2BytesNegative() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 1}; + final byte[] qualifier = new byte[] {0, 0, 1}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -257, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -157,7 +151,7 @@ public void addAggregatePointLong2BytesNegative() throws Exception { @Test public void addAggregatePointLong4Bytes() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 3}; + final byte[] qualifier = new byte[] {0, 0, 3}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 65537, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -171,7 +165,7 @@ public void addAggregatePointLong4Bytes() throws Exception { @Test public void addAggregatePointLong4BytesNegative() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 3}; + final byte[] qualifier = new byte[] {0, 0, 3}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -65537, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -186,7 +180,7 @@ public void addAggregatePointLong4BytesNegative() throws Exception { @Test public void addAggregatePointLong8Bytes() throws Exception { - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 7}; + final byte[] qualifier = new byte[] {0, 0, 7}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 4294967296L, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -200,8 +194,7 @@ public void addAggregatePointLong8Bytes() throws Exception { @Test public void addAggregatePointLong8BytesNegative() throws Exception { - RowKey.prefixKeyWithSalt(row); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 7}; + final byte[] qualifier = new byte[] {0, 0, 7}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -4294967296L, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -216,8 +209,7 @@ public void addAggregatePointLong8BytesNegative() throws Exception { @Test public void addAggregatePointFloat4Bytes() throws Exception { - RowKey.prefixKeyWithSalt(row); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + final byte[] qualifier = new byte[] {0, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42.5F, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -232,8 +224,7 @@ public void addAggregatePointFloat4Bytes() throws Exception { @Test public void addAggregatePointFloat4BytesNegative() throws Exception { - RowKey.prefixKeyWithSalt(row); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + final byte[] qualifier = new byte[] {0, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42.5F, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -248,8 +239,7 @@ public void addAggregatePointFloat4BytesNegative() throws Exception { @Test public void addAggregatePointFloat4BytesPrecision() throws Exception { - RowKey.prefixKeyWithSalt(row); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + final byte[] qualifier = new byte[] {0, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42.5123459999F, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -264,8 +254,7 @@ public void addAggregatePointFloat4BytesPrecision() throws Exception { @Test public void addAggregatePointFloat4BytesPrecisionNegative() throws Exception { - RowKey.prefixKeyWithSalt(row); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0x0B}; + final byte[] qualifier = new byte[] {0, 0, 0x0B}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, -42.5123459999F, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -294,11 +283,11 @@ public void addAggregatePointNoSuchRollup() throws Exception { @Test public void addAggregatePoint10mInDayTop() throws Exception { row = getRowKey(METRIC_STRING, 1370476800, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; - + final byte[] qualifier = new byte[] {0, 0, 0}; + tsdb.addAggregatePoint(METRIC_STRING, 1370476800, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); - + final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), row, FAMILY, qualifier); @@ -309,7 +298,7 @@ public void addAggregatePoint10mInDayTop() throws Exception { @Test public void addAggregatePoint10mInDayMid() throws Exception { row = getRowKey(METRIC_STRING, 1370476800, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 5, (byte) 0xD0}; + final byte[] qualifier = new byte[] {0, 5, (byte) 0xD0}; tsdb.addAggregatePoint(METRIC_STRING, 1370532925L, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -324,7 +313,7 @@ public void addAggregatePoint10mInDayMid() throws Exception { @Test public void addAggregatePoint10mInDayEnd() throws Exception { row = getRowKey(METRIC_STRING, 1370476800, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 5, (byte) 0xF0}; + final byte[] qualifier = new byte[] {0, 5, (byte) 0xF0}; tsdb.addAggregatePoint(METRIC_STRING, 1370534399L, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -339,7 +328,7 @@ public void addAggregatePoint10mInDayEnd() throws Exception { @Test public void addAggregatePoint10mInDayOver() throws Exception { row = getRowKey(METRIC_STRING, 1370563200, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1370563200L, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); @@ -354,8 +343,7 @@ public void addAggregatePoint10mInDayOver() throws Exception { @Test public void addAggregatePoint1hInMonthTop() throws Exception { row = getRowKey(METRIC_STRING, 1370044800, TAGK_STRING, TAGV_STRING); - RowKey.prefixKeyWithSalt(row); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1370044800L, 42, tags, false, "1h", "sum", null).joinUninterruptibly(); @@ -370,7 +358,7 @@ public void addAggregatePoint1hInMonthTop() throws Exception { @Test public void addAggregatePoint1hInMonthMid() throws Exception { row = getRowKey(METRIC_STRING, 1370044800, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0x2C, (byte) 0xF0}; + final byte[] qualifier = new byte[] {0, 0x2C, (byte) 0xF0}; tsdb.addAggregatePoint(METRIC_STRING, 1372636799L, 42, tags, false, "1h", "sum", null).joinUninterruptibly(); @@ -385,7 +373,7 @@ public void addAggregatePoint1hInMonthMid() throws Exception { @Test public void addAggregatePoint1hInMonthOver() throws Exception { row = getRowKey(METRIC_STRING, 1372636800, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1372636800L, 42, tags, false, "1h", "sum", null).joinUninterruptibly(); @@ -400,7 +388,7 @@ public void addAggregatePoint1hInMonthOver() throws Exception { @Test public void addAggregatePoint1dInYearTop() throws Exception { row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, "1d", "sum", null).joinUninterruptibly(); @@ -415,7 +403,7 @@ public void addAggregatePoint1dInYearTop() throws Exception { @Test public void addAggregatePoint1dInYearMid() throws Exception { row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 9, (byte) 0xC0}; + final byte[] qualifier = new byte[] {0, 9, (byte) 0xC0}; tsdb.addAggregatePoint(METRIC_STRING, 1370532925L, 42, tags, false, "1d", "sum", null).joinUninterruptibly(); @@ -430,7 +418,7 @@ public void addAggregatePoint1dInYearMid() throws Exception { @Test public void addAggregatePoint1dInYearEnd() throws Exception { row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0x16, (byte) 0xC0}; + final byte[] qualifier = new byte[] {0, 0x16, (byte) 0xC0}; tsdb.addAggregatePoint(METRIC_STRING, 1388534399L, 42, tags, false, "1d", "sum", null).joinUninterruptibly(); @@ -445,7 +433,7 @@ public void addAggregatePoint1dInYearEnd() throws Exception { @Test public void addAggregatePoint1dInYearOver() throws Exception { row = getRowKey(METRIC_STRING, 1388534400, TAGK_STRING, TAGV_STRING); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, "1d", "sum", null).joinUninterruptibly(); @@ -477,18 +465,11 @@ public void addAggregatePointNSUNTagV() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1388534400L, 42, tags, false, "1d", "sum", null).joinUninterruptibly(); } - - // This is allowed, we don't check the aggregation function in this method. - // It's up to the RPC level to check - @Test + + @Test (expected = IllegalArgumentException.class) public void addAggregatePointRollupNoSuchAgg() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", "nosuchagg", null).joinUninterruptibly(); - - final RollupInterval interval = rollup_config.getRollupInterval("10m"); - assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, - "nosuchagg", interval))[0]); } @Test (expected = IllegalArgumentException.class) @@ -508,7 +489,7 @@ public void addAggregatePointNegativeTimestamp() throws Exception { public void addAggregatePointEmptyTags() throws Exception { tags.put(TAGK_STRING, ""); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", - "nosuchagg", null).joinUninterruptibly(); + "sum", null).joinUninterruptibly(); } // not allowed at this time @@ -555,11 +536,11 @@ public void addAggregatePointRollupRouting() throws Exception { "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); // make sure it didn't get into the tsdb table assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); storage.flushStorage(); @@ -569,14 +550,14 @@ public void addAggregatePointRollupRouting() throws Exception { "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); assertNull(storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, rollup_config.getRollupInterval("10m")))); storage.flushStorage(); @@ -586,14 +567,14 @@ public void addAggregatePointRollupRouting() throws Exception { "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); assertNull(storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, rollup_config.getRollupInterval("1h")))); storage.flushStorage(); @@ -602,30 +583,20 @@ public void addAggregatePointRollupRouting() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", "max", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 2, interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", "min", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 3, interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", "count", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 1, interval))[0]); - - // derived not allowed by default - try { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "1h", - "avg", null).joinUninterruptibly(); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { } - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); } @Test @@ -636,58 +607,58 @@ public void addAggregatePointLongs() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 0, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(0, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -42, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(-42, storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); // 2 bytes tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 257, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(257, Bytes.getShort(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, 0, interval)))); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -257, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(-257, Bytes.getShort(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)1, 0, interval)))); // 4 bytes tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 65537, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(65537, Bytes.getInt(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, 0, interval)))); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -65537, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(-65537, Bytes.getInt(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)3, 0, interval)))); // 8 bytes tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 4294967296L, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(4294967296L, Bytes.getLong(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, 0, interval)))); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -4294967296L, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(-4294967296L, Bytes.getLong(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)7, 0, interval)))); } @@ -699,128 +670,31 @@ public void addAggregatePointFloats() throws Exception { "sum", null).joinUninterruptibly(); assertEquals(0.0, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, 0, interval)))), 0.0001); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42.5F, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(42.5, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, 0, interval)))), 0.0001); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, -42.5F, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(-42.5, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, 0, interval)))), 0.0001); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42.5123459999F, tags, false, "10m", "sum", null).joinUninterruptibly(); assertEquals(42.5123459999F, Float.intBitsToFloat(Bytes.getInt( storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)11, 0, interval)))), 0.0000001); } - @Test - public void addAggregatePointGroupByOnlyRouting() throws Exception { - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "SUM"); - - assertNull(tags.get(agg_tag_key)); - RollupInterval interval = rollup_config.getRollupInterval("10m"); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - null, "sum").joinUninterruptibly(); - - assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, new byte[] { 0, 0 })[0]); - // make sure it didn't get into the tsdb table OR rollup table - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", - interval))); - assertEquals("SUM", tags.get(agg_tag_key)); - - storage.flushStorage(); - tags.remove(agg_tag_key); - - // other aggs - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "MAX"); - - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - null, "max").joinUninterruptibly(); - assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, new byte[] { 0, 0 })[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", - interval))); - assertEquals("MAX", tags.get(agg_tag_key)); - - storage.flushStorage(); - tags.remove(agg_tag_key); - - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "MIN"); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - null, "min").joinUninterruptibly(); - assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, new byte[] { 0, 0 })[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", - interval))); - assertEquals("MIN", tags.get(agg_tag_key)); - - storage.flushStorage(); - tags.remove(agg_tag_key); - - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "COUNT"); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - null, "count").joinUninterruptibly(); - assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, new byte[] { 0, 0 })[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", - interval))); - assertEquals("COUNT", tags.get(agg_tag_key)); - - storage.flushStorage(); - tags.remove(agg_tag_key); - - // derived metrics blocked by default - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "AVG"); - try { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - null, "avg").joinUninterruptibly(); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { } - assertNull(storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); - assertEquals("AVG", tags.get(agg_tag_key)); - } - @Test public void addAggregatePointGroupByRollupRouting() throws Exception { row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, @@ -832,52 +706,47 @@ public void addAggregatePointGroupByRollupRouting() throws Exception { "sum", "sum").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); // make sure it didn't get into the tsdb table OR rollup table assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); - assertEquals("SUM", tags.get(agg_tag_key)); storage.flushStorage(); - tags.remove(agg_tag_key); interval = rollup_config.getRollupInterval("1h"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", "sum", "sum").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); - assertEquals("SUM", tags.get(agg_tag_key)); storage.flushStorage(); - tags.remove(agg_tag_key); interval = rollup_config.getRollupInterval("1d"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1d", "sum", "sum").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))[0]); assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 0, interval))); - assertEquals("SUM", tags.get(agg_tag_key)); storage.flushStorage(); tags.remove(agg_tag_key); @@ -885,112 +754,84 @@ public void addAggregatePointGroupByRollupRouting() throws Exception { // other aggs row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, agg_tag_key, "MAX"); + RowKey.prefixKeyWithSalt(row); interval = rollup_config.getRollupInterval("1h"); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", "max", "max").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 2, interval))[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "max", - interval))); - assertEquals("MAX", tags.get(agg_tag_key)); storage.flushStorage(); tags.remove(agg_tag_key); row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, agg_tag_key, "MIN"); + RowKey.prefixKeyWithSalt(row); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", "min", "min").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 3, interval))[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "min", - interval))); - assertEquals("MIN", tags.get(agg_tag_key)); storage.flushStorage(); tags.remove(agg_tag_key); row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, agg_tag_key, "COUNT"); + RowKey.prefixKeyWithSalt(row); tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", "count", "count").joinUninterruptibly(); assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", + row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, 1, interval))[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "count", - interval))); - assertEquals("COUNT", tags.get(agg_tag_key)); - - storage.flushStorage(); - tags.remove(agg_tag_key); - - // derivced metrics blocked by default - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "AVG"); - try { - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "1h", - "avg", "avg").joinUninterruptibly(); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { } - assertNull(storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "avg", - interval))); - assertEquals("AVG", tags.get(agg_tag_key)); } - - //This is allowed, we don't check the aggregation function in this method. - // It's up to the RPC level to check - @Test + + @Test (expected = IllegalArgumentException.class) public void addAggregatePointGroupByRollupNoSuchAgg() throws Exception { - row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, - agg_tag_key, "SUM"); - final RollupInterval interval = rollup_config.getRollupInterval("10m"); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, "10m", "nosuchagg", "sum").joinUninterruptibly(); - - assertEquals(42, storage.getColumn(interval.getGroupbyTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, - "nosuchagg", interval))[0]); - assertNull(storage.getColumn(TSDB_TABLE, - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", - interval))); - assertNull(storage.getColumn(interval.getTemporalTable(), - row, FAMILY, RollupUtils.buildRollupQualifier(1356998400, (short)0, "sum", - interval))); } @Test (expected = IllegalArgumentException.class) public void addAggregatePointGroupByNoSuchAgg() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, - "sum", "nosuchagg").joinUninterruptibly(); + null, "nosuchagg").joinUninterruptibly(); } - @Test (expected = IllegalArgumentException.class) - public void addAggregatePointGroupByRollupsDisabled() throws Exception { - Whitebox.setInternalState(tsdb, "rollup_config", (Object) null); - tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, - "10m", "sum", null).joinUninterruptibly(); + @Test + public void addAggregatePointGroupBy() throws Exception { + storage.flushStorage(); + tags.remove(agg_tag_key); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 42, tags, true, null, + null, "sum").joinUninterruptibly(); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "SUM"); + storage.dumpToSystemOut(); + assertEquals(42, storage.getColumn(AGG_TABLE, row, FAMILY, + new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, row, FAMILY, + new byte[] { 0, 0 })); + assertNull(storage.getColumn( + rollup_config.getRollupInterval("10m").getGroupbyTable(), row, FAMILY, + new byte[] { 0, 0 })); + + storage.flushStorage(); + tags.remove(agg_tag_key); + + tsdb.addAggregatePoint(METRIC_STRING, 1356998400, 24, tags, true, null, + null, "count").joinUninterruptibly(); + row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, + agg_tag_key, "COUNT"); + RowKey.prefixKeyWithSalt(row); + assertEquals(24, storage.getColumn(AGG_TABLE, row, FAMILY, + new byte[] { 0, 0 })[0]); + assertNull(storage.getColumn(TSDB_TABLE, row, FAMILY, + new byte[] { 0, 0 })); + assertNull(storage.getColumn( + rollup_config.getRollupInterval("10m").getGroupbyTable(), row, FAMILY, + new byte[] { 0, 0 })); } @Test @@ -1006,7 +847,7 @@ public void dpFilterOK() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), row, FAMILY, qualifier); @@ -1027,7 +868,10 @@ public void uidFilterBlocked() throws Exception { tsdb.addAggregatePoint(METRIC_STRING, 1356998400L, 42, tags, false, "10m", "sum", null).joinUninterruptibly(); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] row = new byte[] { 0, 0, 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), row, FAMILY, qualifier); @@ -1051,8 +895,10 @@ public void dpFilterReturnsException() throws Exception { deferred.join(); fail("Expected an UnitTestException"); } catch (UnitTestException e) { }; - - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] row = new byte[] { 0, 0, 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), row, FAMILY, qualifier); @@ -1074,8 +920,10 @@ public void dpFilterThrowsException() throws Exception { 1356998400L, 42, tags, false, "10m", "sum", null); fail("Expected an UnitTestException"); } catch (UnitTestException e) { }; - - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] row = new byte[] { 0, 0, 0, 0, 1, + 0x50, (byte) 0xE2, 0x27, 0, 0, 0, 0, 1, 0, 0, 0, 1}; + RowKey.prefixKeyWithSalt(row); + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("10m").getTemporalTable(), row, FAMILY, qualifier); diff --git a/test/core/TestTSDBAddAggregatePointSalted.java b/test/core/TestTSDBAddAggregatePointSalted.java index 824c5763b9..7bc7821af7 100644 --- a/test/core/TestTSDBAddAggregatePointSalted.java +++ b/test/core/TestTSDBAddAggregatePointSalted.java @@ -59,6 +59,12 @@ public void beforeLocal() throws Exception { .addAggregationId("count", 1) .addAggregationId("max", 2) .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb-agg") + .setInterval("1m") + .setRowSpan("1h") + .setDefaultInterval(true)) .addInterval(RollupInterval.builder() .setTable("tsdb-rollup-10m") .setPreAggregationTable("tsdb-rollup-agg-10m") @@ -77,7 +83,7 @@ public void beforeLocal() throws Exception { .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); Whitebox.setInternalState(tsdb, "default_interval", - rollup_config.getRollupInterval("10m")); + rollup_config.getRollupInterval("1m")); Whitebox.setInternalState(tsdb, "rollups_block_derived", true); Whitebox.setInternalState(tsdb, "agg_tag_key", config.getString("tsd.rollups.agg_tag_key")); diff --git a/test/core/TestTsdbQueryRollup.java b/test/core/TestTsdbQueryRollup.java index fd28b65b30..7e93c7ef2f 100644 --- a/test/core/TestTsdbQueryRollup.java +++ b/test/core/TestTsdbQueryRollup.java @@ -49,11 +49,11 @@ @PrepareForTest({ RowSeq.class, TSDB.class, UniqueId.class, KeyValue.class, Config.class, RowKey.class }) public class TestTsdbQueryRollup extends BaseTsdbTest { - private final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); - private TsdbQuery query = null; - private RollupConfig rollup_config; - private Map tags2; - private TSQuery ts_query; + protected final static byte[] FAMILY = "t".getBytes(MockBase.ASCII()); + protected TsdbQuery query = null; + protected RollupConfig rollup_config; + protected Map tags2; + protected TSQuery ts_query; @Before public void beforeLocal() throws Exception { @@ -72,15 +72,6 @@ public void beforeLocal() throws Exception { tags2 = new HashMap(1); tags2.put(TAGK_STRING, TAGV_B_STRING); -// final List rollups = new ArrayList(); -// rollups.add(new RollupInterval( -// "tsdb-rollup-10m", "tsdb-rollup-agg-10m", "10m", "6h")); -// rollups.add(new RollupInterval( -// "tsdb-rollup-1h", "tsdb-rollup-agg-1h", "1h", "1d")); -// rollups.add(new RollupInterval( -// "tsdb-rollup-1d", "tsdb-rollup-agg-1d", "1d", "1m")); -// -// rollup_config = new RollupConfig(rollups); rollup_config = RollupConfig.builder() .addAggregationId("sum", 0) .addAggregationId("count", 1) @@ -103,7 +94,6 @@ public void beforeLocal() throws Exception { .setRowSpan("1n")) .build(); Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); - } // This test shows us falling back to raw data if the requested downsample @@ -832,6 +822,35 @@ public void runDupes() throws Exception { assertEquals(42.5F, dp.toDouble(), 0.0001); } + @Test + public void oldStringPrefix() throws Exception { + final RollupInterval interval = rollup_config.getRollupInterval("10m"); + final Aggregator aggr = Aggregators.SUM; + long start_timestamp = 1356998400000L; + + storage.addColumn("tsdb-rollup-10m".getBytes(), + getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING), + "t".getBytes(), + new byte[] { 's', 'u', 'm', ':', 0, 0 }, new byte[] { 0x2A }); + + final int time_interval = interval.getIntervalSeconds(); + setQuery(interval.getInterval(), aggr, tags, aggr); + query.configureFromQuery(ts_query, 0); + + final DataPoints[] dps = query.run(); + assertEquals(1, dps.length); + assertEquals(METRIC_STRING, dps[0].metricName()); + assertTrue(dps[0].getAggregatedTags().isEmpty()); + assertNull(dps[0].getAnnotations()); + assertEquals(TAGV_STRING, dps[0].getTags().get(TAGK_STRING)); + + final DataPoint dp = dps[0].iterator().next(); + assertFalse(dp.isInteger()); + assertEquals(42, dp.doubleValue(), 0.001); + assertEquals(start_timestamp, dp.timestamp()); + assertEquals(1, dps[0].size()); + } + // ----------------- // // Helper functions. // // ----------------- // diff --git a/test/core/TestTsdbQueryRollupSalted.java b/test/core/TestTsdbQueryRollupSalted.java new file mode 100644 index 0000000000..0284baeaa1 --- /dev/null +++ b/test/core/TestTsdbQueryRollupSalted.java @@ -0,0 +1,74 @@ +// 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.junit.Before; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.reflect.Whitebox; + +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; + +public class TestTsdbQueryRollupSalted extends TestTsdbQueryRollup { + + @Before + public void beforeLocal() throws Exception { + PowerMockito.mockStatic(Const.class); + PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); + PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); + PowerMockito.when(Const.MAX_NUM_TAGS()).thenReturn((short) 8); + + storeLongTimeSeriesSeconds(false, false); + final List families = new ArrayList(); + families.add(FAMILY); + + storage.addTable("tsdb-rollup-10m".getBytes(), families); + storage.addTable("tsdb-rollup-agg-10m".getBytes(), families); + storage.addTable("tsdb-rollup-1h".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1h".getBytes(), families); + storage.addTable("tsdb-rollup-1d".getBytes(), families); + storage.addTable("tsdb-rollup-agg-1d".getBytes(), families); + + query = new TsdbQuery(tsdb); + tags2 = new HashMap(1); + tags2.put(TAGK_STRING, TAGV_B_STRING); + + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("6h")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + } + +} diff --git a/test/rollup/TestRollupSeq.java b/test/rollup/TestRollupSeq.java index bfd7f4b0b3..c1b0ff4c0f 100644 --- a/test/rollup/TestRollupSeq.java +++ b/test/rollup/TestRollupSeq.java @@ -49,9 +49,11 @@ public class TestRollupSeq { protected TSDB tsdb = mock(TSDB.class); protected Config config = mock(Config.class); protected UniqueId metrics = mock(UniqueId.class); + protected RollupConfig rollup_config; protected static final byte[] TABLE = { 't', 'a', 'b', 'l', 'e' }; public static final byte[] FAMILY = { 't' }; protected byte[] key = null; + protected static final RollupQuery rollup_query_sum = new RollupQuery(RollupInterval.builder() .setTable("tsdb") @@ -157,11 +159,36 @@ public void before() throws Exception { BaseTsdbTest.generateUID(UniqueIdType.TAGV, (byte) 1)); when(RowKey.metricNameAsync(tsdb, key)) .thenReturn(Deferred.fromResult("sys.cpu.user")); + + rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addAggregationId("avg", 4) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("6h")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); + when(tsdb.getRollupConfig()).thenReturn(rollup_config); } @Test public void setRow() throws Exception { - final KeyValue kv = getRollupKeyValue(key, 1356998400000L, 4L, rollup_query_sum); + final KeyValue kv = getRollupKeyValue(key, 1356998400000L, 4L, + rollup_config.getIdForAggregator("SUM"), rollup_query_sum); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); rs.setRow(kv); assertEquals(1, rs.size()); @@ -177,20 +204,24 @@ public void setRow() throws Exception { @Test (expected = IllegalStateException.class) public void setRowAlreadySet() throws Exception { - final KeyValue kv = getRollupKeyValue(key, 1356998400000L, 4L, rollup_query_sum); + final KeyValue kv = getRollupKeyValue(key, 1356998400000L, 4L, + rollup_config.getIdForAggregator("SUM"), rollup_query_sum); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); rs.setRow(kv); assertEquals(1, rs.size()); //Expects an IllegalStateException - final KeyValue kv1 = getRollupKeyValue(key, 1356998500000L, 5L, rollup_query_sum); + final KeyValue kv1 = getRollupKeyValue(key, 1356998500000L, 5L, + rollup_config.getIdForAggregator("SUM"), rollup_query_sum); rs.setRow(kv1); } @Test public void addRow() throws Exception { - final KeyValue kv1 = getRollupKeyValue(key, 1356998400000L, 4L, rollup_query_sum); - final KeyValue kv2 = getRollupKeyValue(key, 1356998500000L, 5L, rollup_query_sum); + final KeyValue kv1 = getRollupKeyValue(key, 1356998400000L, 4L, + rollup_config.getIdForAggregator("SUM"), rollup_query_sum); + final KeyValue kv2 = getRollupKeyValue(key, 1356998500000L, 5L, + rollup_config.getIdForAggregator("SUM"), rollup_query_sum); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum); rs.setRow(kv1); @@ -591,7 +622,8 @@ public void addRowMergeDifferentTime() throws Exception { @Test public void timestamp() throws Exception { - final KeyValue kv = getRollupKeyValue(key,1356998400000L, 7L, rollup_query_sum_mimmax); + final KeyValue kv = getRollupKeyValue(key,1356998400000L, 7L, + rollup_config.getIdForAggregator("max"), rollup_query_sum_mimmax); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_sum_mimmax); rs.setRow(kv); @@ -613,11 +645,16 @@ public void rollup10m() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -639,11 +676,16 @@ public void rollup10mDouble() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 0.50, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 0.75, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 1.00, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 1.25, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 0.50, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 0.75, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 1.00, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 1.25, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -662,14 +704,18 @@ public void rollup10mDouble() throws Exception { @Test public void rollup10mFloat() throws Exception { - Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 10.25F, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 10.75F, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 11.25F, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 11.75F, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 12.25F, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 10.25F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 10.75F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 11.25F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 11.75F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 12.25F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -688,15 +734,20 @@ public void rollup10mFloat() throws Exception { @Test public void rollup10mMixFloatAndLong() throws Exception { - Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 10.50F, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 11.50F, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420073400, 12.50F, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 10.50F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 11.50F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 12.50F, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); assertEquals(6, rs.size()); final SeekableView it = rs.iterator(); @@ -728,14 +779,22 @@ public void rollupAvg10mWithCount() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(4, rs.size()); final SeekableView it = rs.iterator(); @@ -756,14 +815,22 @@ public void rollupAvg10mWithCountFirst() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); assertEquals(4, rs.size()); final SeekableView it = rs.iterator(); @@ -784,10 +851,14 @@ public void rollupAvg10mMissingCount() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); assertEquals(0, rs.size()); final SeekableView it = rs.iterator(); @@ -799,14 +870,22 @@ public void rollupAvg10mSkipFirstCount() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, + // rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -827,14 +906,22 @@ public void rollupAvg10mSkipLastCount() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - //rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 2, + // rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -852,17 +939,24 @@ public void rollupAvg10mSkipLastCount() throws Exception { @Test public void rollupAvg10mSkipMiddleCount() throws Exception { - Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - //rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 2, + // rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -885,13 +979,16 @@ public void rollupAvg10mSkipMiddleCount() throws Exception { @Test public void rollupAvg10mMissingSum() throws Exception { - Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(0, rs.size()); final SeekableView it = rs.iterator(); @@ -899,17 +996,24 @@ public void rollupAvg10mMissingSum() throws Exception { } public void rollupAvg10mSkipFirstSum() throws Exception { - Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - //rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + //rs.setRow(getRollupKeyValue(key, 1420070400, 20, + // rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -927,17 +1031,24 @@ public void rollupAvg10mSkipFirstSum() throws Exception { @Test public void rollupAvg10mSkipLastSum() throws Exception { - Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 23, + // rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -958,14 +1069,22 @@ public void rollupAvg10mSkipMiddleSum() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, + // rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -991,14 +1110,22 @@ public void rollupAvg10mUnaligned() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420070400, 2, + // rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420071000, 21, + // rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + //rs.addRow(getRollupKeyValue(key, 1420071600, 2, + // rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + //rs.addRow(getRollupKeyValue(key, 1420072200, 23, + // rollup_config.getIdForAggregator("SUM"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(0, rs.size()); final SeekableView it = rs.iterator(); @@ -1063,11 +1190,16 @@ public void rollup1hLong() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420077600, 3L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420081200, 4L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420084800, 5L, rollup_query_1h_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 4L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 5L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -1089,11 +1221,16 @@ public void rollup1hFloat() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 0.5, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420077600, 0.75, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420081200, 1.0, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420084800, 1.25, rollup_query_1h_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 0.25, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 0.5, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 0.75, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 1.0, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 1.25, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -1115,16 +1252,26 @@ public void rollup1hLongWithCount() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2L, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420077600, 3L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420077600, 2L, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420081200, 4L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420081200, 2L, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420084800, 5L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420084800, 2L, rollup_query_1h_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420081200, 4L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420084800, 5L, + rollup_config.getIdForAggregator("SUM"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -1146,16 +1293,26 @@ public void rollup1hLongWithCountWithDoubles() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_1h_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2L, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420074000, 2L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 2D, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420077600, 3L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420077600, 2D, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420081200, 4L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420081200, 2D, rollup_query_1h_count)); - rs.addRow(getRollupKeyValue(key, 1420084800, 5L, rollup_query_1h_sum)); - rs.addRow(getRollupKeyValue(key, 1420084800, 2L, rollup_query_1h_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 2D, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420077600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420077600, 2D, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420081200, 4L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420081200, 2D, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); + rs.addRow(getRollupKeyValue(key, 1420084800, 5L, + rollup_config.getIdForAggregator("sum"), rollup_query_1h_sum)); + rs.addRow(getRollupKeyValue(key, 1420084800, 2L, + rollup_config.getIdForAggregator("count"), rollup_query_1h_count)); assertEquals(5, rs.size()); final SeekableView it = rs.iterator(); @@ -1177,14 +1334,22 @@ public void rollup10mSeekTop() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); assertEquals(8, rs.size()); final SeekableView it = rs.iterator(); @@ -1207,14 +1372,22 @@ public void rollup10mSeek() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); assertEquals(8, rs.size()); final SeekableView it = rs.iterator(); @@ -1237,14 +1410,22 @@ public void rollup10mSeekOOB() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); assertEquals(8, rs.size()); final SeekableView it = rs.iterator(); @@ -1257,14 +1438,22 @@ public void rollup10mSeekSeconds() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); assertEquals(8, rs.size()); final SeekableView it = rs.iterator(); @@ -1287,14 +1476,22 @@ public void rollup10mSeekUnaligned() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 4L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072800, 5L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420073400, 6L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074000, 7L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420074600, 8L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 3L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 4L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072800, 5L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420073400, 6L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074000, 7L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420074600, 8L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); assertEquals(8, rs.size()); final SeekableView it = rs.iterator(); @@ -1317,14 +1514,22 @@ public void rollup10mAvgSeekTopAligned() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(4, rs.size()); final SeekableView it = rs.iterator(); @@ -1348,14 +1553,20 @@ public void rollup10mAvgSeekTopUnaligned() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(2, rs.size()); final SeekableView it = rs.iterator(); @@ -1378,14 +1589,21 @@ public void rollup10mAvgSeekTopUnalignedMissingTop() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(3, rs.size()); final SeekableView it = rs.iterator(); @@ -1409,14 +1627,22 @@ public void rollup10mAvgSeekAligned() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(4, rs.size()); final SeekableView it = rs.iterator(); @@ -1440,14 +1666,20 @@ public void rollup10mAvgSeekUnaligned() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(2, rs.size()); final SeekableView it = rs.iterator(); @@ -1471,14 +1703,18 @@ public void rollup10mAvgSeekUnalignedEmpty() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(0, rs.size()); final SeekableView it = rs.iterator(); @@ -1491,14 +1727,22 @@ public void rollup10mAvgSeekTopAlignedOOB() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071000, 21, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071600, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(4, rs.size()); final SeekableView it = rs.iterator(); @@ -1511,14 +1755,20 @@ public void rollup10mAvgSeekTopUnalignedOOB() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420070400, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 23, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(2, rs.size()); final SeekableView it = rs.iterator(); @@ -1531,14 +1781,18 @@ public void rollup10mAvgSeekTopUnalignedEmptyOOB() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_avg); - rs.setRow(getRollupKeyValue(key, 1420070400, 20, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 20, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420070400, 2, rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420071000, 21, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2, rollup_query_10m_count)); - rs.addRow(getRollupKeyValue(key, 1420071600, 22, rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420071600, 22, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); //rs.addRow(getRollupKeyValue(key, 1420071600, 2, rollup_query_10m_count)); //rs.addRow(getRollupKeyValue(key, 1420072200, 23, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420072200, 2, rollup_query_10m_count)); + rs.addRow(getRollupKeyValue(key, 1420072200, 2, + rollup_config.getIdForAggregator("count"), rollup_query_10m_count)); assertEquals(0, rs.size()); final SeekableView it = rs.iterator(); @@ -1551,9 +1805,12 @@ public void rollup10mTimestamp() throws Exception { Internal.setBaseTime(key, 1420070400); final RollupSeq rs = new RollupSeq(tsdb, rollup_query_10m_sum); - rs.setRow(getRollupKeyValue(key, 1420070400, 1L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071000, 2L, rollup_query_10m_sum)); - rs.addRow(getRollupKeyValue(key, 1420071600, 3L, rollup_query_10m_sum)); + rs.setRow(getRollupKeyValue(key, 1420070400, 1L, + rollup_config.getIdForAggregator("sum"), rollup_query_10m_sum)); + rs.addRow(getRollupKeyValue(key, 1420071000, 2L, + 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)); @@ -1571,45 +1828,49 @@ public void rollup10mTimestamp() throws Exception { } private static KeyValue getRollupKeyValue(final byte[] key, - final long timestamp, - final long value, - final RollupQuery rollup_query) { + final long timestamp, + final long value, + final int agg_id, + final RollupQuery rollup_query) { final byte[] val = Internal.vleEncodeLong(value); final short flags = (short) (val.length - 1); // Just the length. return new KeyValue(key, TestRowSeq.FAMILY, getQualifier(timestamp, flags, - rollup_query), val); + agg_id, rollup_query), val); } private static KeyValue getRollupKeyValue(final byte[] key, - final long timestamp, - final float value, - final RollupQuery rollup_query) { + final long timestamp, + final float value, + final int agg_id, + final RollupQuery rollup_query) { final short flags = Const.FLAG_FLOAT | 0x3; // A float stored on 4 bytes. final byte[] val = Bytes.fromInt(Float.floatToRawIntBits(value)); return new KeyValue(key, TestRowSeq.FAMILY, getQualifier(timestamp, flags, - rollup_query), val); + agg_id, rollup_query), val); } private static KeyValue getRollupKeyValue(final byte[] key, - final long timestamp, - final double value, - final RollupQuery rollup_query) { + final long timestamp, + final double value, + final int agg_id, + final RollupQuery rollup_query) { final short flags = Const.FLAG_FLOAT | 0x7; // A double stored on 8 bytes. final byte[] val = Bytes.fromLong(Double.doubleToRawLongBits(value)); return new KeyValue(key, TestRowSeq.FAMILY, getQualifier(timestamp, flags, - rollup_query), val); + agg_id, rollup_query), val); } private static byte[] getQualifier(final long timestamp, - final short flags, RollupQuery rollup_query) { + final short flags, + final int agg_id, + final RollupQuery rollup_query) { final int base_time = RollupUtils.getRollupBasetime(timestamp, rollup_query.getRollupInterval()); return RollupUtils.buildRollupQualifier(timestamp, base_time, flags, - rollup_query.getRollupAgg().toString(), - rollup_query.getRollupInterval()); + agg_id, rollup_query.getRollupInterval()); } } diff --git a/test/rollup/TestRollupUtils.java b/test/rollup/TestRollupUtils.java index 7f33c7c593..b207fb2e50 100644 --- a/test/rollup/TestRollupUtils.java +++ b/test/rollup/TestRollupUtils.java @@ -14,6 +14,8 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; @@ -21,7 +23,6 @@ import net.opentsdb.core.Const; public class TestRollupUtils { - private static final byte[] SUM_COL = "sum:".getBytes(Const.ASCII_CHARSET); private static final String temporal_table = "tsdb-rollup-10m"; private static final String groupby_table = "tsdb-rollup-agg-10m"; @@ -457,13 +458,13 @@ public void getRollupBasetimeBadSpan() throws Exception { @Test public void buildRollupQualifier1SecondInHourTop() { final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, - (byte)7, "sum", hour_interval); + (byte)7, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -471,13 +472,13 @@ public void buildRollupQualifier1SecondInHourTop() { @Test public void buildRollupQualifier1SecondInHourMid() { final byte[] offset = {(byte) 0x84, (byte)0xD7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte)7, "sum", hour_interval); + (byte)7, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -485,13 +486,13 @@ public void buildRollupQualifier1SecondInHourMid() { @Test public void buildRollupQualifier1SecondInHourEnd() { final byte[] offset = {(byte) 0xE0, (byte)0xF7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:59:59 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370534399L, 1370530800, - (byte)7, "sum", hour_interval); + (byte)7, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -500,7 +501,7 @@ public void buildRollupQualifier1SecondInHourEnd() { public void buildRollupQualifier1SecondInHourOver() { //Thu, 06 Jun 2013 16:00:00 GMT RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, - "sum", hour_interval); + 42, hour_interval); } @Test @@ -513,13 +514,13 @@ public void buildRollupQualifier30SecondInHourTop() { .build(); final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -534,13 +535,13 @@ public void buildRollupQualifier30SecondInHourMid() { .build(); final byte[] offset = {4, (byte)0x67}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -555,13 +556,13 @@ public void buildRollupQualifier30SecondInHourEnd() { .build(); final byte[] offset = {7, (byte)0x77}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:59:59 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370534399L, 1370530800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -577,7 +578,7 @@ public void buildRollupQualifier30SecondInHourOver() { //Thu, 06 Jun 2013 16:00:00 GMT RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, - "sum", interval); + 42, interval); } @Test @@ -590,13 +591,13 @@ public void buildRollupQualifier1MinuteInHourTop() { .build(); final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -611,13 +612,13 @@ public void buildRollupQualifier1MinuteInHourMid() { .build(); final byte[] offset = {2, (byte)0x37}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -632,13 +633,13 @@ public void buildRollupQualifier1MinuteInHourEnd() { .build(); final byte[] offset = {3, (byte)0xB7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370534399L, 1370530800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -654,7 +655,7 @@ public void buildRollupQualifier1MinuteInHourOver() { //Thu, 06 Jun 2013 16:00:00 GMT RollupUtils.buildRollupQualifier(1370534400L, 1370530800, (byte)7, - "sum", interval); + 42, interval); } @Test @@ -667,13 +668,13 @@ public void buildRollupQualifier15MinutesInDayTop() { .build(); final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 00:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370476800L, 1370476800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -688,13 +689,13 @@ public void buildRollupQualifier15MinutesInDayMid() { .build(); final byte[] offset = {3, (byte)0xE7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370476800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -709,13 +710,13 @@ public void buildRollupQualifier15MinutesInDayEnd() { .build(); final byte[] offset = {5, (byte)0xF7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 23:59:59 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370563199L, 1370476800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -730,7 +731,7 @@ public void buildRollupQualifier15MinutesInDayOver() { .build(); //Thu, 07 Jun 2013 00:00:00 GMT - RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, "sum", + RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, 42, interval); } @@ -744,13 +745,13 @@ public void buildRollupQualifier60MinutesInDayTop() { .build(); final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 00:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370476800L, 1370476800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -765,13 +766,13 @@ public void buildRollupQualifier60MinutesInDayMid() { .build(); final byte[] offset = {0, (byte)0xF7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370476800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -786,13 +787,13 @@ public void buildRollupQualifier60MinutesInDayEnd() { .build(); final byte[] offset = {1, (byte)0x77}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 23:59:59 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370563199L, 1370476800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -807,7 +808,7 @@ public void buildRollupQualifier60MinutesInDayOver() { .build(); //Thu, 07 Jun 2013 00:00:00 GMT - RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, "sum", + RollupUtils.buildRollupQualifier(1370563200L, 1370476800, (byte)7, 42, interval); } @@ -821,13 +822,13 @@ public void buildRollupQualifier3HoursInMonthTop() { .build(); final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Sat, 01 Jun 2013 00:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370044800L, 1370044800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -842,13 +843,13 @@ public void buildRollupQualifier3HoursInMonthMid() { .build(); final byte[] offset = {2, (byte)0xD7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370044800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -863,13 +864,13 @@ public void buildRollupQualifier3HoursInMonthEnd() { .build(); final byte[] offset = {0x0E, (byte)0xF7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 30 Jun 2013 23:59:59 GMT final byte[] q = RollupUtils.buildRollupQualifier(1372636799L, 1370044800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -885,13 +886,13 @@ public void buildRollupQualifier3HoursInMonthOver30Days() { .build(); final byte[] offset = {0x0F, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 1 July 2013 00:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1372636800L, 1370044800, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -907,7 +908,7 @@ public void buildRollupQualifier3HoursInMonthOver() { .build(); //Wed, 03 Jul 2013 23:59:59 GMT - RollupUtils.buildRollupQualifier(1372895999L, 1370044800, (byte)7, "sum", + RollupUtils.buildRollupQualifier(1372895999L, 1370044800, (byte)7, 42, interval); } @@ -921,13 +922,13 @@ public void buildRollupQualifier6HoursInYearTop() { .build(); final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Tue, 01 Jan 2013 00:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1356998400L, 1356998400, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -942,13 +943,13 @@ public void buildRollupQualifier6HoursInYearMid() { .build(); final byte[] offset = {0x27, (byte)0x27}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1356998400, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -963,13 +964,13 @@ public void buildRollupQualifier6HoursInYearEnd() { .build(); final byte[] offset = {0x5B, (byte)0x37}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Tue, 31 Dec 2013 23:59:59 GMT final byte[] q = RollupUtils.buildRollupQualifier(1388534399, 1356998400, - (byte)7, "sum", interval); + (byte)7, 42, interval); assertArrayEquals(expected_qual, q); } @@ -985,7 +986,7 @@ public void buildRollupQualifier6HoursInYearOver() { .build(); //Wed, 01 Jan 2014 00:00:00 GMT - RollupUtils.buildRollupQualifier(1388620800, 1356998400, (byte)7, "sum", + RollupUtils.buildRollupQualifier(1388620800, 1356998400, (byte)7, 42, interval); } @@ -993,12 +994,12 @@ public void buildRollupQualifier6HoursInYearOver() { @Test public void buildRollupQualifier8BytesLong() { final byte[] offset = {(byte) 0x84, (byte)0xD7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte)7, "sum", hour_interval); + (byte)7, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1006,12 +1007,12 @@ public void buildRollupQualifier8BytesLong() { @Test public void buildRollupQualifierBytesLong() { final byte[] offset = {(byte) 0x84, (byte)0xD3}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte) 3, "sum", hour_interval); + (byte) 3, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1019,12 +1020,12 @@ public void buildRollupQualifierBytesLong() { @Test public void buildRollupQualifier2BytesLong() { final byte[] offset = {(byte) 0x84, (byte)0xD1}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte) 1, "sum", hour_interval); + (byte) 1, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1032,12 +1033,12 @@ public void buildRollupQualifier2BytesLong() { @Test public void buildRollupQualifierByteLong() { final byte[] offset = {(byte) 0x84, (byte)0xD0}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte) 0, "sum", hour_interval); + (byte) 0, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1045,12 +1046,12 @@ public void buildRollupQualifierByteLong() { @Test public void buildRollupQualifierTenMin8ByteFloat() { final byte[] offset = {(byte) 0x84, (byte)0xDF}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); - + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); + final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte) ( 7 | Const.FLAG_FLOAT), "sum", hour_interval); + (byte) ( 7 | Const.FLAG_FLOAT), 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1058,12 +1059,12 @@ public void buildRollupQualifierTenMin8ByteFloat() { @Test public void buildRollupQualifier4ByteFloat() { final byte[] offset = {(byte) 0x84, (byte)0xDB}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte) ( 3 | Const.FLAG_FLOAT), "sum", hour_interval); + (byte) ( 3 | Const.FLAG_FLOAT), 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1071,12 +1072,12 @@ public void buildRollupQualifier4ByteFloat() { @Test public void buildRollupQualifierTenMinZeroTime() { final byte[] offset = {0x0, 0x0}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = - RollupUtils.buildRollupQualifier(0, 0, (byte) 0, "sum", hour_interval); + RollupUtils.buildRollupQualifier(0, 0, (byte) 0, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1087,12 +1088,12 @@ public void buildRollupQualifierTenMinZeroTime() { @Test public void buildRollupQualifierNegativeTime() { final byte[] offset = {(byte) 0xF1, 0}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); final byte[] q = RollupUtils.buildRollupQualifier(1420062000L, -1420063200, - (byte) 0, "sum", hour_interval); + (byte) 0, 42, hour_interval); assertArrayEquals(expected_qual, q); } @@ -1100,42 +1101,40 @@ public void buildRollupQualifierNegativeTime() { @Test public void buildRollupQualifierAggCase() { final byte[] offset = {0, (byte)0x07}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:00:00 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370530800L, 1370530800, - (byte)7, "Sum", hour_interval); + (byte)7, 42, hour_interval); assertArrayEquals(expected_qual, q); } - @Test (expected = IllegalArgumentException.class) - public void buildRollupQualifierNullAggregator() { - RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte)7, null, hour_interval); - } - - @Test (expected = IllegalArgumentException.class) - public void buildRollupQualifierEmptyAggregator() { - RollupUtils.buildRollupQualifier(1370532925L, 1370530800, - (byte)7, "", hour_interval); - } - // verify we truncate the milliseconds @Test public void buildRollupQualifierMillisecond() { final byte[] offset = {(byte) 0x84, (byte)0xD7}; - byte[] expected_qual = new byte[SUM_COL.length + 2]; - System.arraycopy(SUM_COL, 0, expected_qual, 0, SUM_COL.length); - System.arraycopy(offset, 0, expected_qual, SUM_COL.length, 2); + byte[] expected_qual = new byte[3]; + expected_qual[0] = 42; + System.arraycopy(offset, 0, expected_qual, 1, 2); //Thu, 06 Jun 2013 15:35:25 GMT final byte[] q = RollupUtils.buildRollupQualifier(1370532925154L, 1370530800, - (byte)7, "sum", hour_interval); + (byte)7, 42, hour_interval); assertArrayEquals(expected_qual, q); } + @Test + public void mask() throws Exception { + byte[] header = { (byte) 0x81 }; + assertEquals(1, header[0] & RollupUtils.AGGREGATOR_MASK); + assertTrue(RollupUtils.isCompacted(header)); + + header = new byte[] { 0x01 }; + assertEquals(1, header[0] & RollupUtils.AGGREGATOR_MASK); + assertFalse(RollupUtils.isCompacted(header)); + } } diff --git a/test/storage/MockBase.java b/test/storage/MockBase.java index 5f0a78e863..72a6bb5f71 100644 --- a/test/storage/MockBase.java +++ b/test/storage/MockBase.java @@ -1707,7 +1707,7 @@ public Deferred>> answer( } - // handle qualifier filters. Just regexp for now + // handle qualifier filters. if (filter != null) { List qfs = Lists.newArrayList(); if (filter instanceof FilterList) { diff --git a/test/tsd/TestRollupRpc.java b/test/tsd/TestRollupRpc.java index c29669fc2d..23f5bf319f 100644 --- a/test/tsd/TestRollupRpc.java +++ b/test/tsd/TestRollupRpc.java @@ -132,7 +132,7 @@ public void constructor() { // validateSEH(false); // storage.dumpToSystemOut(); // System.out.println(MockBase.bytesToString(row)); -// final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; +// final byte[] qualifier = new byte[] {0, 0, 0}; // final byte[] value = storage.getColumn( // rollup_config.getRollupInterval("1h").getTemporalTable(), // row, FAMILY, qualifier); @@ -153,7 +153,7 @@ public void execute() throws Exception { verify(chan, never()).isConnected(); validateSEH(false); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), row, FAMILY, qualifier); @@ -212,7 +212,7 @@ public void executeRollupWithAgg() throws Exception { row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, agg_tag_key, "SUM"); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getGroupbyTable(), row, FAMILY, qualifier); @@ -544,7 +544,7 @@ public void executeNSUNTagV() throws Exception { // validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); // validateSEH(false); // -// final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; +// final byte[] qualifier = new byte[] {0, 0, 0}; // final byte[] value = storage.getColumn( // rollup_config.getRollupInterval("1h").getTemporalTable(), // row, FAMILY, qualifier); @@ -564,7 +564,7 @@ public void httpAddSingleRollupPoint() throws Exception { validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); validateSEH(false); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), row, FAMILY, qualifier); @@ -611,7 +611,7 @@ public void httpAddSingleRollupAndGroupByPoint() throws Exception { row = getRowKey(METRIC_STRING, 1356998400, TAGK_STRING, TAGV_STRING, agg_tag_key, "SUM"); - final byte[] qualifier = new byte[] { 0x73, 0x75, 0x6D, 0x3A, 0, 0 }; + final byte[] qualifier = new byte[] { 0, 0, 0 }; final byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getGroupbyTable(), row, FAMILY, qualifier); @@ -634,7 +634,7 @@ public void httpAddTwoRollupPoints() throws Exception { validateCounters(0, 1, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0); validateSEH(false); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), row, FAMILY, qualifier); @@ -664,7 +664,7 @@ public void httpAddTwoRollupPointsOneGoodOneBad() throws Exception { validateCounters(0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0); validateSEH(false); - final byte[] qualifier = new byte[] {0x73, 0x75, 0x6D, 0x3A, 0, 0}; + final byte[] qualifier = new byte[] {0, 0, 0}; byte[] value = storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), row, FAMILY, qualifier); @@ -741,17 +741,14 @@ public void httpInvalidAggregator() throws Exception { + "\"tags\":{\"" + TAGK_STRING + "\":\"" + TAGV_STRING + "\"}}"); final RollupDataPointRpc rollup = new RollupDataPointRpc(tsdb.getConfig()); rollup.execute(tsdb, query); - assertEquals(HttpResponseStatus.OK, query.response().getStatus()); - validateCounters(0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0); + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + validateCounters(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0); validateSEH(false); - final byte[] qualifier = new byte[] {0x6E, 0x6F, 0x73, 0x75, 0x63, 0x68, - 0x61, 0x67, 0x67, 0x3A, 0, 0}; - final byte[] value = storage.getColumn( + final byte[] qualifier = new byte[] {0, 0, 0}; + assertNull(storage.getColumn( rollup_config.getRollupInterval("1h").getTemporalTable(), - row, FAMILY, qualifier); - final byte[] expected = {0x2A}; - assertArrayEquals(expected, value); + row, FAMILY, qualifier)); } @Test From ce7593e5d9f1da80ceb0a403a33b9985b89074f6 Mon Sep 17 00:00:00 2001 From: sidhhu Date: Sun, 11 Jun 2017 14:02:48 -0700 Subject: [PATCH 644/826] Add a method to the SearchPlugin to resolve query tags using the search engine so we can perform multi-get queries for efficient data fetching. Move the SaltMultiGetter to MultiGetQuery and rename the config parameters. Signed-off-by: Chris Larsen --- src/core/MultiGetQuery.java | 1295 ++++++++++++++++++ src/core/SaltMultiGetter.java | 871 ------------ src/core/TSDB.java | 6 + src/core/TsdbQuery.java | 143 +- src/search/SearchPlugin.java | 11 + src/utils/Config.java | 29 +- test/core/TestMultiGetQuery.java | 1102 +++++++++++++++ test/core/TestTsdbQueryHistogramQueries.java | 17 - 8 files changed, 2534 insertions(+), 940 deletions(-) create mode 100644 src/core/MultiGetQuery.java delete mode 100644 src/core/SaltMultiGetter.java create mode 100644 test/core/TestMultiGetQuery.java diff --git a/src/core/MultiGetQuery.java b/src/core/MultiGetQuery.java new file mode 100644 index 0000000000..d12f4afd7e --- /dev/null +++ b/src/core/MultiGetQuery.java @@ -0,0 +1,1295 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see . +package net.opentsdb.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.hbase.async.Bytes.ByteMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import com.stumbleupon.async.Callback; +import com.stumbleupon.async.Deferred; + +import org.hbase.async.Bytes; +import org.hbase.async.GetRequest; +import org.hbase.async.GetResultOrException; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.hbase.async.KeyValue; + +import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.rollup.RollupSpan; +import net.opentsdb.rollup.RollupUtils; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.stats.QueryStats.QueryStat; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.ByteSet; +import net.opentsdb.utils.DateTime; +import net.opentsdb.utils.JSON; + +/** + * Class that handles fetching TSDB data from storage using GetRequests instead + * of scanning for the data. This is only applicable if the query is using the + * "explicit_tags" feature and specified literal filters for exact matching. + * It also works best when used for data that has high cardinality but the query + * is fetching a small subset of that data. + * + * @since 2.4 + */ +public class MultiGetQuery { + private static final Logger LOG = LoggerFactory.getLogger(MultiGetQuery.class); + private final TSDB tsdb; + private final byte[] metric; + private final List> tags; + private final long start_row_time; // in sec + private final long end_row_time; // in sec + private final byte[] table_to_fetch; + private final TreeMap spans; + private final TreeMap histogramSpans; + private final RollupQuery rollup_query; + private final QueryStats query_stats; + private final int query_index; + + private int multi_get_wait_cnt; + + private int multi_get_num_get_requests; + + private final Map> kvsmap = + new ConcurrentHashMap>(); + + private final Map> annotMap = Collections + .synchronizedMap(new TreeMap>(new RowKey.SaltCmp())); + + private final Map>>> histMap = Maps.newConcurrentMap(); + + private final Deferred> results = + new Deferred>(); + + private final Deferred> histogramResults = + new Deferred>(); + + private final ArrayList> multi_get_tasks; + private final ArrayList multi_get_indexs; + + private long prepare_multi_get_start_time; + + private long prepare_multi_get_end_time; + + // the timestamp of starting fetching data + private long fetch_start_time; + + // the number of data point fetched + private AtomicLong number_pre_filter_data_point; + + private AtomicLong num_post_filter_data_points; + + // the byte size of the data fetched + private AtomicLong byte_size_fetched; + + // the finished multi get number + private AtomicInteger finished_multi_get_cnt; + + private AtomicInteger multi_get_seq_id; + + private final int concurrency_multi_get; + + private final int batch_size; + + /** Whether or not to fetch all possible salts for the rows in case the + * salting has changed during the TSD's run. */ + private final boolean get_all_salts; + + /** A holder for storing the first exception thrown by a scanner if something + * goes pear shaped. Make sure to synchronize on this object when checking + * for null or assigning from a scanner's callback. */ + private volatile Exception exception; + + private long max_bytes; + + private boolean multiget_no_meta; + + private AtomicLong number_byte_fetched; + + private final boolean is_rollup; + private final int rollup_agg_id; + private final int rollup_count_id; + + public MultiGetQuery(final TSDB tsdb, + final TsdbQuery query, + final byte[] metric, + final List> tags, + final long start_row_time, + final long end_row_time, + final byte[] table_to_fetch, + final TreeMap spans, + final TreeMap histogramSpans, + final long timeout, + final RollupQuery rollup_query, + final QueryStats query_stats, + final int query_index, + final long max_bytes, + final boolean override_count_limit, + final boolean multiget_no_meta) { + this.tsdb = tsdb; + this.metric = metric; + this.tags = tags; + this.start_row_time = start_row_time; + this.end_row_time = end_row_time; + this.table_to_fetch = table_to_fetch; + this.spans = spans; + this.histogramSpans = histogramSpans; + this.rollup_query = rollup_query; + this.query_stats = query_stats; + this.query_index = query_index; + this.multiget_no_meta = multiget_no_meta; + + if (tags == null) { + throw new IllegalArgumentException("Tags list cannot be null or empty"); + } + if (tags.isEmpty()) { + query.setNoResults(true); + } + if (end_row_time <= start_row_time) { + throw new IllegalArgumentException("Start time cannot be later or " + + "equal to the end time"); + } + + concurrency_multi_get = tsdb.config + .getInt("tsd.query.multi_get.concurrent"); + batch_size = tsdb.config.getInt("tsd.query.multi_get.batch_size"); + get_all_salts = tsdb.config.getBoolean("tsd.query.multi_get.get_all_salts"); + multi_get_tasks = new ArrayList>(concurrency_multi_get); + multi_get_indexs = new ArrayList(concurrency_multi_get); + for (int i = 0; i < concurrency_multi_get; ++i) { + multi_get_tasks.add(new ArrayList()); + multi_get_indexs.add(new AtomicInteger(-1)); + } + + number_pre_filter_data_point = new AtomicLong(0); + num_post_filter_data_points = new AtomicLong(0); + byte_size_fetched = new AtomicLong(0); + finished_multi_get_cnt = new AtomicInteger(0); + multi_get_seq_id = new AtomicInteger(-1); + number_byte_fetched = new AtomicLong(0); + this.max_bytes = max_bytes; + if (rollup_query != null && RollupQuery.isValidQuery(rollup_query)) { + is_rollup = true; + if (rollup_query.getRollupAgg() == Aggregators.AVG) { + rollup_agg_id = tsdb.getRollupConfig().getIdForAggregator("sum"); + rollup_count_id = tsdb.getRollupConfig().getIdForAggregator("count"); + } else { + rollup_agg_id = tsdb.getRollupConfig().getIdForAggregator( + rollup_query.getRollupAgg().toString()); + rollup_count_id = -1; + } + } else { + is_rollup = false; + rollup_agg_id = rollup_count_id = -1; + } + } + + /** + * Helper container class to store a set of TSUIDs and GetRequests in the same + * object. + */ + final static class MultiGetTask { + private final Set tsuids; + private final List gets; + + /** + * Default Ctor + * @param tsuids Non-null set of TSUIDs. May be empty. + * @param gets Non-null list of GetRequests. May be empty. + */ + public MultiGetTask(final Set tsuids, final List gets) { + this.tsuids = tsuids; + this.gets = gets; + } + + public Set getTSUIDs() { + return tsuids; + } + + public List getGets() { + return gets; + } + } + + ////////////////////////////////////////////////////////////////////////////////////////// + //// Call back to handle result + ///////////////////////////////////////////////////////////////////////////////////////// + final class MulGetCB implements Callback> { + private final int concurrency_index; + private final Set tsuids; + private final List gets; + private final int seq_id; + + private List keyValues = new ArrayList(); + private final Map> annotations = + new ConcurrentHashMap>(); + + // use list here because we want to keep the rows in the scan order - + // timestamp order. + // i don't want to define an additional class to store the information of + // the row key and the histogram data points in the row, then use {@link SimpleEntry} + private List>> histograms = + new ArrayList>>(); + + /////////////////////////////////////////////////////////////////////////// + // nanosecond times - trace response metrics // + /////////////////////////////////////////////////////////////////////////// + + // the time to start the multi get request + private long mul_get_start_time = -1; + + // cumulation of time waiting on HBase + private long mul_get_time = 0; + + // cumulation of time resolving uid + private long mul_get_uid_resolved_time = 0; + + // how many uids is resolved + private long mul_get_uids_resolved = 0; + + // cumulation of time compacting + private long mul_get_compaction_time = 0; + + // how many data points after filtering + private long mul_get_dps_post_filter = 0; + + // how many rows after filtering + private long mul_get_rows_post_filter = 0; + + // how many rows fetched from hbase + private long mul_get_number_row_fetched = 0; + + // how many cells fetched from hbase + private long mul_get_number_column_fetched = 0; + + // how many bytes fetched from hbase + private long mul_get_number_byte_fetched = 0; + + /** The exception thrown by this get request set */ + private Exception get_exception; + + public MulGetCB(final int concurrency_index, final Set tsuids, + final List gets) { + this.concurrency_index = concurrency_index; + this.tsuids = tsuids; + this.gets = gets; + + if (query_stats != null) { + seq_id = multi_get_seq_id.incrementAndGet(); + StringBuilder sb = new StringBuilder(); + sb.append("Mulget_").append(concurrency_index).append("_").append(seq_id); + query_stats.addScannerId(query_index, seq_id, sb.toString()); + } else { + seq_id = 0; + } + } + + /** Error callback that will capture an exception from AsyncHBase and store + * it so we can bubble it up to the caller. + */ + class ErrorCb implements Callback { + @Override + public Object call(final Exception e) throws Exception { + LOG.error("Multi get threw an exception: " + this, e); + MulGetCB.this.get_exception = e; + close(false); + return null; + } + } + + public Object fetch() { + + mul_get_start_time = DateTime.nanoTime(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Trying to fetch data for concurrency index: " + + concurrency_index + "; with " + gets.size() + " gets"); + } + return tsdb.client.get(gets) + .addCallback(this) + .addErrback(new ErrorCb()); + } + + /** + * Iterate through each row of the multi get results, parses out data + * points (and optional meta data). + * @return null if no rows were found, otherwise the TreeMap with spans + */ + @Override + public Object call(final List results) throws Exception { + mul_get_time = (DateTime.nanoTime() - mul_get_start_time); + + try { + for (final GetResultOrException result : results) { + // handle an exception + if (result.getException() != null) { + get_exception = result.getException(); + handleException(get_exception); + close(false); + } + if (null != result.getCells()) { + final ArrayList row = result.getCells(); + if (row.isEmpty()) { + continue; + } + + number_pre_filter_data_point.addAndGet(row.size()); + ++mul_get_number_row_fetched; + mul_get_number_column_fetched += row.size(); + + final byte[] key = row.get(0).key(); + final byte[] tsuid_key = UniqueId.getTSUIDFromKey(key, + TSDB.metrics_width(), Const.TIMESTAMP_BYTES); + + if (!tsuids.contains(tsuid_key)) { + LOG.error("Multi getter fetched the wrong row " + result + + " when fetching metric: " + Bytes.pretty(metric)); + continue; + } + process(key, row); + } else { + // TODO we don't get cells for some get requests. This could be an + // error or the database just didn't have data. Gotta look into it. + } + } // end for + } catch (Exception e) { + get_exception = e; + close(false); + return null; + } + + close(true); + return null; + } + + /** + * Handles processing of row of data into the proper list + * @param key The row key, possibly mutated + * @param row The row of KVs to process + * @return True if processing should continue, false if an exception occurred + * or the scanner was already closed (possibly due to another scanner error) + */ + boolean process(final byte[] key, final ArrayList row) { + ++mul_get_rows_post_filter; + num_post_filter_data_points.addAndGet(row.size()); + + List notes = null; + if (annotMap != null) { + notes = annotations.get(key); + + if (notes == null) { + notes = new ArrayList(); + annotations.put(key, notes); + } + } + + List hists = new ArrayList(); + if (RollupQuery.isValidQuery(rollup_query)) { + processRollupQuery(key, row, notes, hists); + } else { + processNotRollupQuery(key, row, notes, hists); + } + + return true; + } + + private void processNotRollupQuery(final byte[] key, + final ArrayList row, + List notes, + List hists) { + KeyValue compacted = null; + try { + final long compaction_start = DateTime.nanoTime(); + compacted = tsdb.compact(row, notes, hists); + + // histogram row + if (hists.size() > 0) { + histograms.add(new SimpleEntry>(key, hists)); + mul_get_dps_post_filter += hists.size(); + } + + mul_get_compaction_time += (DateTime.nanoTime() - compaction_start); + if (compacted != null) { + final byte[] compact_value = compacted.value(); + final byte[] compact_qualifier = compacted.qualifier(); + mul_get_number_byte_fetched = mul_get_number_byte_fetched + + compacted.value().length + compacted.key().length; + number_byte_fetched.addAndGet(compacted.value().length + compacted.key().length); + if (number_byte_fetched.get() > max_bytes) { + handleException( + new QueryException(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, + "Sorry, you have attempted to fetch more than our maximum " + + "amount of " + (max_bytes / 1024 / 1024) + "MB from storage. " + + "Please try reducing your time range or adjust the query filters.")); + close(false); + return; + } + if (compact_qualifier.length % 2 == 0) { + // The length of the qualifier is even so this is a put type + // so the size of the data is the length of the qualifier by 2 + if (compact_value[compact_value.length - 1] == 0) { + // LOG.debug("All data points we have here are either in seconds + // or Ms"); + if (Internal.inMilliseconds(compact_qualifier[0])) { + mul_get_dps_post_filter += compact_qualifier.length / 4; + } else { + mul_get_dps_post_filter += compact_qualifier.length / 2; + } + } else { + // LOG.debug("Data Points we have here are stored in second and Ms + // precision"); + // We wil make a estimate here as iterating over each qualifer + // could be expensive. + // We will just divide the qualifier by 3 to estimate the value + mul_get_dps_post_filter += compact_qualifier.length / 3; + } + } + } + } catch (IllegalDataException idex) { + LOG.error("Caught IllegalDataException exception while parsing the " + "row " + key + ", skipping index", idex); + } + + if (compacted != null) { // Can be null if we ignored all KVs. + keyValues.add(compacted); + } + } + + private void processRollupQuery(final byte[] key, + final ArrayList row, + List notes, + final List hists) { + for (KeyValue kv : row) { + mul_get_number_byte_fetched = mul_get_number_byte_fetched + + kv.value().length + kv.key().length; + number_byte_fetched.addAndGet(kv.value().length + kv.key().length); + if (number_byte_fetched.get() > max_bytes) { + handleException( + new QueryException(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, + "Sorry, you have attempted to fetch more than our maximum " + + "amount of " + (max_bytes / 1024 / 1024) + "MB from storage. " + + "Please try reducing your time range or adjust the query filters.")); + close(false); + return; + } + final byte[] qual = kv.qualifier(); + + if (qual.length > 0) { + // Todo: Bug! Here we shouldn't use the first byte to check the type + // of this row + // Instead should parse the byte array to find the suffix and + // determine the actual type + if (qual[0] == Annotation.PREFIX()) { + // This could be a row with only an annotation in it + final Annotation note = JSON.parseToObject(kv.value(), Annotation.class); + notes.add(note); + } else if (qual[0] == HistogramDataPoint.PREFIX) { + try { + HistogramDataPoint histogram = Internal.decodeHistogramDataPoint(tsdb, kv); + hists.add(histogram); + } catch (Throwable t) { + LOG.error("Failed to decode histogram data point", t); + } + } else { + if (qual[0] == (byte) rollup_agg_id || + qual[0] == (byte) rollup_count_id || + rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV) { + if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 + || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { + keyValues.add(kv); + } + } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), qual, 0, + rollup_query.getRollupAggPrefix().length) == 0) { + keyValues.add(kv); + } + } + } + } // end for + + // histogram row + if (hists.size() > 0) { + histograms.add(new SimpleEntry>(key, hists)); + } + } + + void close(final boolean ok) { + if (LOG.isDebugEnabled()) { + LOG.debug("Finished multiget on concurrency index: " + concurrency_index + + ", seq id: " + seq_id + ". Fetched rows: " + mul_get_number_row_fetched + + ", cells: " + mul_get_number_column_fetched + ", mget time(ms): " + + mul_get_time / 1000000 + " and " + ok); + } + + if (query_stats != null) { + query_stats.addScannerStat(query_index, seq_id, QueryStat.SCANNER_TIME, + DateTime.nanoTime() - mul_get_start_time); + + // Scanner Stats + query_stats.addScannerStat(query_index, seq_id, QueryStat.ROWS_FROM_STORAGE, + mul_get_number_row_fetched); + + query_stats.addScannerStat(query_index, seq_id, QueryStat.COLUMNS_FROM_STORAGE, + mul_get_number_column_fetched); + + query_stats.addScannerStat(query_index, seq_id, QueryStat.BYTES_FROM_STORAGE, + mul_get_number_byte_fetched); + + query_stats.addScannerStat(query_index, seq_id, QueryStat.HBASE_TIME, + mul_get_time); + query_stats.addScannerStat(query_index, seq_id, QueryStat.SUCCESSFUL_SCAN, + ok ? 1 : 0); + + // Post Scan stats + query_stats.addScannerStat(query_index, seq_id, QueryStat.ROWS_POST_FILTER, + mul_get_rows_post_filter); + query_stats.addScannerStat(query_index, seq_id, QueryStat.DPS_POST_FILTER, + mul_get_dps_post_filter); + query_stats.addScannerStat(query_index, seq_id, QueryStat.SCANNER_UID_TO_STRING_TIME, + mul_get_uid_resolved_time); + query_stats.addScannerStat(query_index, seq_id, QueryStat.UID_PAIRS_RESOLVED, + mul_get_uids_resolved); + query_stats.addScannerStat(query_index, seq_id, QueryStat.COMPACTION_TIME, + mul_get_compaction_time); + } + + if (ok) { + validateMultigetData(keyValues, annotations, histograms); + } + else { + finished_multi_get_cnt.incrementAndGet(); + } + + // check we have finished all the multi get + if (!checkAllFinishAndTriggerCallback()) { + // check to fire a new multi get in this concurrency bucket + List salt_mul_get_tasks = multi_get_tasks.get(concurrency_index); + int task_index = multi_get_indexs.get(concurrency_index).incrementAndGet(); + if (task_index < salt_mul_get_tasks.size()) { + MultiGetTask task = salt_mul_get_tasks.get(task_index); + MulGetCB mgcb = new MulGetCB(concurrency_index, task.getTSUIDs(), task.getGets()); + mgcb.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> fetch() { + if(tags.isEmpty()) { + return Deferred.fromResult(null); + } + startFetch(); + return results; + } + + /** + * 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() { + startFetch(); + return histogramResults; + } + + /** + * Start the work of firing up X concurrent get requests. + */ + private void startFetch() { + prepareConcurrentMultiGetTasks(); + + // set the time of starting + fetch_start_time = System.currentTimeMillis(); + if (LOG.isDebugEnabled()) { + LOG.debug("Start to fetch data using multiget, there will be " + multi_get_wait_cnt + + " multigets to call"); + } + + for (int con_idx = 0; con_idx < concurrency_multi_get; ++con_idx) { + final List con_mul_get_tasks = multi_get_tasks.get(con_idx); + final int task_index = multi_get_indexs.get(con_idx).incrementAndGet(); + + if (task_index < con_mul_get_tasks.size()) { + final MultiGetTask task = con_mul_get_tasks.get(task_index); + final MulGetCB mgcb = new MulGetCB(con_idx, task.getTSUIDs(), task.getGets()); + mgcb.fetch(); + } + } // end for + } + + /** + * Compiles the list of TSUIDs and GetRequests to send to execute against + * storage. Each batch will only have requests for one salt, i.e a batch + * will not have requests with multiple salts. + */ + @VisibleForTesting + void prepareConcurrentMultiGetTasks() { + multi_get_wait_cnt = 0; + prepare_multi_get_start_time = DateTime.currentTimeMillis(); + + + final List row_base_time_list; + if (RollupQuery.isValidQuery(rollup_query)) { + row_base_time_list = prepareRowBaseTimesRollup(); + } else { + row_base_time_list = prepareRowBaseTimes(); + } + + int next_concurrency_index = 0; + List gets_to_prepare = new ArrayList(batch_size); + Set tsuids = new ByteSet(); + + final ByteMap>> all_tsuids_gets; + if (multiget_no_meta) { + // prepare the tagvs combinations and base time list + + final List tagv_compinations = prepareAllTagvCompounds(); + all_tsuids_gets = prepareRequestsNoMeta(tagv_compinations, row_base_time_list); + } else { + all_tsuids_gets = prepareRequests(row_base_time_list, tags); + + } + // Iterate over all salts + for (final Entry>> salts_entry : all_tsuids_gets.entrySet()) { + if (gets_to_prepare.size() > 0) { // if we have any gets_to_prepare for previous salt, create a + // request out of it. + final MultiGetTask task = new MultiGetTask(tsuids, gets_to_prepare); + final List mulget_task_list = + multi_get_tasks.get((next_concurrency_index++) % concurrency_multi_get); + mulget_task_list.add(task); + ++multi_get_wait_cnt; + multi_get_num_get_requests = multi_get_num_get_requests + gets_to_prepare.size(); + gets_to_prepare = new ArrayList(batch_size); + tsuids = new ByteSet(); + } + byte[] curr_salt = salts_entry.getKey(); + // Iterate over all tsuid's in curr_salt and add them to gets_to_prepare + for (final Entry> gets_entry : salts_entry.getValue()) { + byte[] tsuid = gets_entry.getKey(); + + for (GetRequest request : gets_entry.getValue()) { + if (gets_to_prepare.size() >= batch_size) { // close batch and create a MultiGetTask + final MultiGetTask task = new MultiGetTask(tsuids, gets_to_prepare); + final List mulget_task_list = + multi_get_tasks.get((next_concurrency_index++) % concurrency_multi_get); + mulget_task_list.add(task); + ++multi_get_wait_cnt; + multi_get_num_get_requests = multi_get_num_get_requests + gets_to_prepare.size(); + if (LOG.isDebugEnabled()) { + LOG.debug("Finished preparing MultiGetRequest with " + gets_to_prepare.size() + " requests for salt " + curr_salt + + " for tsuid " + Bytes.pretty(tsuid)); + } + // prepare a new task list and tsuids + gets_to_prepare = new ArrayList(batch_size); + tsuids = new ByteSet(); + } + gets_to_prepare.add(request); + tsuids.add(gets_entry.getKey()); + } + tsuids.add(gets_entry.getKey()); + } + } + + + if (gets_to_prepare.size() > 0) { + + final MultiGetTask task = new MultiGetTask(tsuids, gets_to_prepare); + final List mulget_task_list = + multi_get_tasks.get((next_concurrency_index++) % concurrency_multi_get); + mulget_task_list.add(task); + ++multi_get_wait_cnt; + multi_get_num_get_requests = multi_get_num_get_requests + gets_to_prepare.size(); + LOG.debug("Finished preparing MultiGetRequest with " + gets_to_prepare.size()); + gets_to_prepare = new ArrayList(batch_size); + tsuids = new ByteSet(); + } + + prepare_multi_get_end_time = DateTime.currentTimeMillis(); + if (LOG.isDebugEnabled()) { + LOG.debug("Finished preparing concurrency multi get task with " + + multi_get_wait_cnt + " tasks using " + + (prepare_multi_get_end_time - prepare_multi_get_start_time) + "ms"); + } + + } + + private void validateMultigetData(List kvs, + Map> annotations, + List>> histograms) { + int tasks = finished_multi_get_cnt.incrementAndGet(); + + if (kvs.size() > 0) { + kvsmap.put(tasks, kvs); + } + + if (annotMap != null) { + for (byte[] key : annotations.keySet()) { + List notes = annotations.get(key); + + if (notes.size() > 0) { + annotMap.put(key, notes); + } + } + } + + if (histograms.size() > 0) { + histMap.put(tasks, histograms); + } + } + + private boolean checkAllFinishAndTriggerCallback() { + if (multi_get_wait_cnt == finished_multi_get_cnt.get()) { + try { + if (exception == null) { + mergeAndReturnResults(); + } + } catch (Exception ex) { + LOG.error("Failed merging and returning results, calling back with " + + "exception", ex); + + if (!isHistogramScan()) { + results.callback(ex); + } else { + histogramResults.callback(ex); + } + } + return true; + } else { + return false; + } + } + + private void mergeAndReturnResults() throws Exception { + final long hbase_time = DateTime.currentTimeMillis(); + TsdbQuery.scanlatency.add((int) (hbase_time - fetch_start_time)); + if (LOG.isDebugEnabled()) { + LOG.debug("Finished fetching data for metric: " + Bytes.pretty(metric) + + " using " + (hbase_time - fetch_start_time) + "ms"); + } + LOG.info("Finished fetching data for metric: " + Bytes.pretty(metric) + + " using " + (hbase_time - fetch_start_time) + "ms"); + if (exception != null) { + LOG.error("After all of the multi-gets finished, at " + + "least one threw an exception", exception); + throw exception; + } + + final long merge_start = DateTime.nanoTime(); + + // Merge sorted spans together + if (!isHistogramScan()) { + mergeDataPoints(); + } else { + // Merge histogram data points + mergeHistogramDataPoints(); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("It took " + (DateTime.currentTimeMillis() - hbase_time) + " ms, " + + " to merge and sort the rows into a tree map"); + } + + if (query_stats != null) { + query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, + (DateTime.nanoTime() - merge_start)); + } + + if (!isHistogramScan()) { + results.callback(spans); + } else { + histogramResults.callback(histogramSpans); + } + } + + private boolean isHistogramScan() { + return histogramSpans != null; + } + + private void mergeHistogramDataPoints() { + if (histogramSpans != null) { + for (List>> rows : histMap.values()) { + if (null == rows || rows.isEmpty()) { + LOG.error("Found a histogram rows list that was null or empty"); + continue; + } + + // for all the rows with the same salt in the timestamp order + for (final SimpleEntry> row : rows) { + if (null == row) { + LOG.error("Found a histogram row item that was null"); + continue; + } + + HistogramSpan histSpan = null; + try { + histSpan = histogramSpans.get(row.getKey()); + } catch (RuntimeException e) { + LOG.error("Failed to fetch the histogram span", e); + } + + if (histSpan == null) { + histSpan = new HistogramSpan(tsdb); + histogramSpans.put(row.getKey(), histSpan); + } + + if (annotMap.containsKey(row.getKey())) { + histSpan.getAnnotations().addAll(annotMap.get(row.getKey())); + annotMap.remove(row.getKey()); + } + + try { + histSpan.addRow(row.getKey(), row.getValue()); + } catch (RuntimeException e) { + LOG.error("Exception adding row to histogram span", e); + } + } // end for + } // end for + + histMap.clear(); + + for (byte[] key : annotMap.keySet()) { + HistogramSpan histSpan = histogramSpans.get(key); + + if (histSpan == null) { + histSpan = new HistogramSpan(tsdb); + histogramSpans.put(key, histSpan); + } + + histSpan.getAnnotations().addAll(annotMap.get(key)); + } + + annotMap.clear(); + } + } + + private void mergeDataPoints() { + for (List kvs : kvsmap.values()) { + if (kvs == null || kvs.isEmpty()) { + LOG.error("Found a key value list that was null or empty"); + continue; + } + for (final KeyValue kv : kvs) { + + if (kv == null) { + LOG.error("Found a key value item that was null"); + continue; + } + if (kv.key() == null) { + LOG.error("A key for a kv was null"); + continue; + } + + Span datapoints = null; + try { + datapoints = spans.get(kv.key()); + } catch (RuntimeException e) { + LOG.error("Failed to fetch the span", e); + } + + // If this tsdb follows append logic, then there will not be any + // duplicates here. But if it is not, then there can be multiple + // non-compcated or out of order rows here + if (datapoints == null) { + datapoints = RollupQuery.isValidQuery(rollup_query) + ? new RollupSpan(tsdb, rollup_query) + : new Span(tsdb); + spans.put(kv.key(), datapoints); + } + + if (annotMap.containsKey(kv.key())) { + for (Annotation note : annotMap.get(kv.key())) { + datapoints.getAnnotations().add(note); + } + annotMap.remove(kv.key()); + } + try { + datapoints.addRow(kv); + } catch (RuntimeException e) { + LOG.error("Exception adding row to span", e); + } + } + } + + kvsmap.clear(); + + for (byte[] key : annotMap.keySet()) { + Span datapoints = (Span) spans.get(key); + + if (datapoints == null) { + datapoints = new Span(tsdb); + spans.put(key, datapoints); + } + + for (Annotation note : annotMap.get(key)) { + datapoints.getAnnotations().add(note); + } + } + + annotMap.clear(); + } + boolean exception1; + /** + * If one or more of the scanners throws an exception then we should close it + * and pass the exception here so that we can catch and return it to the + * caller. If all of the scanners have finished, this will callback to the + * caller immediately. + * @param e The exception to store. + */ + private void handleException(final Exception e) { + // make sure only one scanner can set the exception + finished_multi_get_cnt.incrementAndGet(); + if (exception == null) { + synchronized (this) { + if (exception == null) { + exception = e; + // fail once and fast on the first scanner to throw an exception + try { + if (exception != null) { + mergeAndReturnResults(); + } + } catch (Exception ex) { + + LOG.error("Failed merging and returning results, " + + "calling back with exception", ex); + results.callback(ex); + + } + } else { + // TODO - it would be nice to close and cancel the other scanners but + // for now we have to wait for them to finish and/or throw exceptions. + LOG.error("Another scanner threw an exception", e); + } + } + } + } + + /** + * Generates a list of Unix Epoch Timestamps for the row key base times given + * the start and end times of the query. + * @return A non-null list of at least one base row. + */ + @VisibleForTesting + List prepareRowBaseTimes() { + final ArrayList row_base_time_list = new ArrayList( + (int) ((end_row_time - start_row_time) / Const.MAX_TIMESPAN)); + // NOTE: inclusive end here + long ts = (start_row_time - (start_row_time % Const.MAX_TIMESPAN)); + while (ts <= end_row_time) { + row_base_time_list.add(ts); + ts += Const.MAX_TIMESPAN; + } + return row_base_time_list; + } + + /** + * Generates a list of Unix Epoch Timestamps for the row key base times given + * the start and end times of the query and rollup interval given. + * @return A non-null list of at least one base row. + */ + @VisibleForTesting + List prepareRowBaseTimesRollup() { + final RollupInterval interval = rollup_query.getRollupInterval(); + + // standard TSDB table format, i.e. we're using the default table and schema + if (interval.getUnits() == 'h') { + return prepareRowBaseTimes(); + } else { + final List row_base_times = new ArrayList( + (int) ((end_row_time - start_row_time) / interval.getIntervals())); + + long ts = RollupUtils.getRollupBasetime(start_row_time, interval); + while (ts <= end_row_time) { + row_base_times.add(ts); + // TODO - possible this could overshoot in some cases. It shouldn't + // if the rollups are properly configured, but... you know. Check it. + ts = RollupUtils.getRollupBasetime(ts + + (interval.getIntervalSeconds() * interval.getIntervals()), interval); + } + return row_base_times; + } + } + + /** + * We have multiple tagks and each tagk may has multiple possible values. + * This routine generates all the possible tagv compounds basing on the + * present sequence of the tagks. Each compound will be used to generate + * the final tsuid. + * For example, if there are two tag keys where the first tag key has 4 values + * and the second tagk has 2 values then the resulting list will have 6 + * entries, one for each permutation. + * + * @return a non-null list that contains all the possible compounds + */ + @VisibleForTesting + List prepareAllTagvCompounds() { + List pre_phase_tags = new LinkedList(); + pre_phase_tags.add(new byte[tags.get(0).size()][TSDB.tagv_width()]); + + List next_phase_tags = new LinkedList(); + int next_append_index = 0; + + for (final Map.Entry tag : tags.get(0)) { + byte[][] tagv = tag.getValue(); + for (int i = 0; i < tagv.length; ++i) { + for (byte[][] pre_phase_tag : pre_phase_tags) { + final byte[][] next_phase_tag = + new byte[tags.get(0).size()][tsdb.tag_values.width()]; + + // copy the tagv from index 0 ~ next_append_index - 1 + for (int k = 0; k < next_append_index; ++k) { + System.arraycopy(pre_phase_tag[k], 0, next_phase_tag[k], 0, + tsdb.tag_values.width()); + } + + // copy the tagv in next_append_index + System.arraycopy(tagv[i], 0, next_phase_tag[next_append_index], 0, + tsdb.tag_values.width()); + next_phase_tags.add(next_phase_tag); + } + } // end for + + ++next_append_index; + pre_phase_tags = next_phase_tags; + next_phase_tags = new LinkedList(); + } // end for + return pre_phase_tags; + } + + /** + * Generates a map of TSUIDs to get requests given the tag permutations. + * If all salts is enabled, each TSUID will have Const.SALT_BUCKETS() number + * of entries. Otherwise each TSUID will have one row key. + * @param tagv_compounds The permutations of tag key and value combinations to + * search for. + * @param base_time_list The list of base timestamps. + * @return A non-null map of TSUIDs to lists of get requests to send to HBase. + */ + @VisibleForTesting + ByteMap>> prepareRequestsNoMeta(final List tagv_compounds, + final List base_time_list) { + + final int row_size = (Const.SALT_WIDTH() + tsdb.metrics.width() + + Const.TIMESTAMP_BYTES + + (tsdb.tag_names.width() * tags.get(0).size()) + + (tsdb.tag_values.width() * tags.get(0).size())); + + final ByteMap> tsuid_rows = new ByteMap>(); + for (final byte[][] tagvs : tagv_compounds) { + // TSUID's don't have salts + // TODO: we reallly don't have to allocate tsuid's here. + // we can use the row_key array to fetch the tsuid. + // This will just double the memory utilization per time series. + final byte[] tsuid = new byte[tsdb.metrics.width() + + (tags.get(0).size() * tsdb.tag_names.width()) + + tags.get(0).size() * tsdb.tag_values.width()]; + final byte[] row_key = new byte[row_size]; + + // metric + System.arraycopy(metric, 0, row_key, Const.SALT_WIDTH(), tsdb.metrics.width()); + System.arraycopy(metric, 0, tsuid, 0, tsdb.metrics.width()); + + final List rows = + new ArrayList(base_time_list.size()); + + // copy tagks and tagvs to the row key + int tag_index = 0; + int row_key_copy_offset = Const.SALT_WIDTH() + tsdb.metrics.width() + + Const.TIMESTAMP_BYTES; + int tsuid_copy_offset = tsdb.metrics.width(); + for (Map.Entry tag : tags.get(0)) { + // tagk + byte[] tagk = tag.getKey(); + System.arraycopy(tagk, 0, row_key, row_key_copy_offset, tsdb.tag_names.width()); + System.arraycopy(tagk, 0, tsuid, tsuid_copy_offset, tsdb.tag_names.width()); + row_key_copy_offset += tsdb.tag_names.width(); + tsuid_copy_offset += tsdb.tag_names.width(); + + // tagv + System.arraycopy(tagvs[tag_index], 0, row_key, row_key_copy_offset, + tsdb.tag_values.width()); + System.arraycopy(tagvs[tag_index], 0, tsuid, tsuid_copy_offset, + tsdb.tag_values.width()); + row_key_copy_offset += tsdb.tag_values.width(); + tsuid_copy_offset += tsdb.tag_values.width(); + + // move to the next tag + ++tag_index; + } + + // iterate for each timestamp, making a copy of the key and tweaking it's + // timestamp. + for (final long row_base_time : base_time_list) { + final byte[] key_copy = Arrays.copyOf(row_key, row_key.length); + + // base time + Internal.setBaseTime(key_copy, (int) row_base_time); + + if (get_all_salts) { + for (int i = 0; i < Const.SALT_BUCKETS(); i++) { + final byte[] copy = Arrays.copyOf(key_copy, key_copy.length); + // TODO - handle multi byte salts + copy[0] = (byte) i; + rows.add(new GetRequest(table_to_fetch, copy, TSDB.FAMILY)); + } + } else { + // salt + RowKey.prefixKeyWithSalt(key_copy); + rows.add(new GetRequest(table_to_fetch, key_copy, TSDB.FAMILY)); + } + } // end for + + tsuid_rows.put(tsuid, rows); + } // end for + ByteMap>> return_obj = new ByteMap>>(); + // byte[] salt = new byte[Const.SALT_WIDTH()]; + // System.arraycopy("1".getBytes(), 0, salt, 0, Const.SALT_WIDTH()); + return_obj.put("0".getBytes(), tsuid_rows); + return return_obj; + } + + /** + * Generates a map of TSUIDs per salt to get requests given the tag permutations. + * If all salts is enabled, each TSUID will have Const.SALT_BUCKETS() number + * of entries. Otherwise each TSUID will have one row key. + * @param tagv_compounds The cardinality of tag key and value combinations to + * search for. + * @param base_time_list The list of base timestamps. + * @return A non-null map of TSUIDs to lists of get requests to send to HBase. + */ + @VisibleForTesting + ByteMap>> prepareRequests(final List base_time_list, List> tags) { + + final ByteMap>> tsuid_rows = new ByteMap>>(); + // TSUID's don't have salts + // final byte[] tsuid = new byte[tsdb.metrics.width() + // + (tags.size() * tsdb.tag_names.width()) + // + tags.size() * tsdb.tag_values.width()]; + // final byte[] row_key = new byte[row_size]; + + + // copy tagks and tagvs to the row key + for (ByteMap each_row_key : tags) { + + final int row_size = (Const.SALT_WIDTH() + tsdb.metrics.width() + + Const.TIMESTAMP_BYTES + + (tsdb.tag_names.width() * each_row_key.size()) + + (tsdb.tag_values.width() * each_row_key.size())); + byte[] tsuid = new byte[tsdb.metrics.width() + + (each_row_key.size() * tsdb.tag_names.width()) + + each_row_key.size() * tsdb.tag_values.width()]; + byte[] row_key = new byte[row_size]; + int row_key_copy_offset = Const.SALT_WIDTH() + tsdb.metrics.width() + + Const.TIMESTAMP_BYTES; + int tsuid_copy_offset = tsdb.metrics.width(); + + // metric + System.arraycopy(metric, 0, row_key, Const.SALT_WIDTH(), tsdb.metrics.width()); + System.arraycopy(metric, 0, tsuid, 0, tsdb.metrics.width()); + + final List rows = + new ArrayList(base_time_list.size()); + for (Map.Entry tag_arr : each_row_key.entrySet()) { + byte[] tagv = tag_arr.getValue()[0]; + // tagk + byte[] tagk = tag_arr.getKey(); + + System.arraycopy(tagk, 0, row_key, row_key_copy_offset, tsdb.tag_names.width()); + System.arraycopy(tagk, 0, tsuid, tsuid_copy_offset, tsdb.tag_names.width()); + row_key_copy_offset += tsdb.tag_names.width(); + tsuid_copy_offset += tsdb.tag_names.width(); + + // tagv + System.arraycopy(tagv, 0, row_key, row_key_copy_offset, + tsdb.tag_values.width()); + System.arraycopy(tagv, 0, tsuid, tsuid_copy_offset, + tsdb.tag_values.width()); + row_key_copy_offset += tsdb.tag_values.width(); + tsuid_copy_offset += tsdb.tag_values.width(); + + } + + // iterate for each timestamp, making a copy of the key and tweaking it's + // timestamp. + for (final long row_base_time : base_time_list) { + final byte[] key_copy = Arrays.copyOf(row_key, row_key.length); + + // base time + Internal.setBaseTime(key_copy, (int) row_base_time); + + if (get_all_salts) { + for (int i = 0; i < Const.SALT_BUCKETS(); i++) { + byte[] salt = RowKey.getSaltBytes(i); + final byte[] copy = Arrays.copyOf(key_copy, key_copy.length); + System.arraycopy(salt, 0, copy, 0, Const.SALT_WIDTH()); + rows.add(new GetRequest(table_to_fetch, copy, TSDB.FAMILY)); + } + } else { + // salt + RowKey.prefixKeyWithSalt(key_copy); + rows.add(new GetRequest(table_to_fetch, key_copy, TSDB.FAMILY)); + } + } // end for + for (GetRequest request : rows) { + byte[] salt = new byte[Const.SALT_WIDTH()]; + System.arraycopy(request.key(), 0, salt, 0, Const.SALT_WIDTH()); + if (tsuid_rows.containsKey(salt)) { + ByteMap> map = tsuid_rows.get(salt); + if (map.containsKey(tsuid)) { + List list = map.get(tsuid); + list.add(request); + } else { + List list = new ArrayList(); + list.add(request); + map.put(tsuid, list); + } + } else { + ByteMap> map = new ByteMap>(); + List list = new ArrayList(); + list.add(request); + map.put(tsuid, list); + tsuid_rows.put(salt, map); + } + } + } + + return tsuid_rows; + } + + @VisibleForTesting + List> getMultiGetTasks() { + return multi_get_tasks; + } +} diff --git a/src/core/SaltMultiGetter.java b/src/core/SaltMultiGetter.java deleted file mode 100644 index e225894f76..0000000000 --- a/src/core/SaltMultiGetter.java +++ /dev/null @@ -1,871 +0,0 @@ -// This file is part of OpenTSDB. -// Copyright (C) 20156 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.Calendar; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -import org.hbase.async.Bytes.ByteMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.stumbleupon.async.Callback; -import com.stumbleupon.async.Deferred; - -import org.hbase.async.Bytes; -import org.hbase.async.GetRequest; -import org.hbase.async.GetResultOrException; -import org.hbase.async.KeyValue; - -import net.opentsdb.meta.Annotation; -import net.opentsdb.rollup.RollupInterval; -import net.opentsdb.rollup.RollupQuery; -import net.opentsdb.rollup.RollupSpan; -import net.opentsdb.stats.QueryStats; -import net.opentsdb.stats.QueryStats.QueryStat; -import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.DateTime; -import net.opentsdb.utils.JSON; - -public class SaltMultiGetter { - private static final Logger LOG = LoggerFactory.getLogger(SaltMultiGetter.class); - private final TSDB tsdb; - private final byte[] metric; - private final ByteMap tags; - private final long start_row_time; // in sec - private final long end_row_time; // in sec - private final byte[] table_to_fetch; - private final TreeMap spans; - private final long timeout; - private final RollupQuery rollup_query; - private final QueryStats query_stats; - private final int query_index; - private final long max_bytes; - - private long max_pre_filter_dps; - private long max_dps; - private int mulget_wait_cnt; - - //////////////////////////////////////////////////////////////////////////////////////// - private final Map> kvsmap = new ConcurrentHashMap>(); - - private final Map> annotMap = Collections - .synchronizedMap(new TreeMap>(new RowKey.SaltCmp())); - - private final Deferred> results = new Deferred>(); - - private final ArrayList> mul_get_tasks; - private final ArrayList mul_get_indexs; - - //////////////////////////////////////////////////////////////////////////////////////// - private long prepare_multi_get_start_time; - - private long prepare_multi_get_end_time; - - // the timestamp of starting fetching data - private long fetch_start_time; - - // the number of data point fetched - private AtomicLong number_pre_filter_data_point; - - private AtomicLong num_post_filter_data_points; - - // the byte size of the data fetched - private AtomicLong byte_size_feted; - - // the finished multi get number - private AtomicInteger finished_mulget_cnt; - - private AtomicInteger multi_get_seq_id; - - public SaltMultiGetter(TSDB tsdb, - byte[] metric, - ByteMap tags, - final long start_row_time, - final long end_row_time, - final byte[] table_to_fetch, - TreeMap spans, - final long timeout, - final RollupQuery rollup_query, - final QueryStats query_stats, - final int query_index, - final long max_bytes, - final boolean override_count_limit) { - this.tsdb = tsdb; - this.metric = metric; - this.tags = tags; - this.start_row_time = start_row_time; - this.end_row_time = end_row_time; - this.table_to_fetch = table_to_fetch; - this.spans = spans; - this.timeout = timeout; - this.rollup_query = rollup_query; - this.query_stats = query_stats; - this.query_index = query_index; - this.max_bytes = max_bytes; - - if (override_count_limit) { - this.max_pre_filter_dps = 0; - this.max_dps = 0; - } else { - // TODO - //this.max_pre_filter_dps = tsdb.getConfig().getLong("tsd.core.scanner.max_pre_filter_dps"); - //this.max_dps = tsdb.getConfig().max_data_points(); - } - - int concurrency_multi_get = tsdb.config.mul_get_concurrency_number(); - mul_get_tasks = new ArrayList>(concurrency_multi_get); - mul_get_indexs = new ArrayList(concurrency_multi_get); - for (int i = 0; i < concurrency_multi_get; ++i) { - mul_get_tasks.add(new ArrayList()); - mul_get_indexs.add(new AtomicInteger(-1)); - } - - number_pre_filter_data_point = new AtomicLong(0); - num_post_filter_data_points = new AtomicLong(0); - byte_size_feted = new AtomicLong(0); - finished_mulget_cnt = new AtomicInteger(0); - multi_get_seq_id = new AtomicInteger(-1); - } - - final class TSUIDComparator implements Comparator { - - @Override - public int compare(byte[] left, byte[] right) { - for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) { - int a = (left[i] & 0xff); - int b = (right[j] & 0xff); - if (a != b) { - return a - b; - } - } - return left.length - right.length; - } - } - - final class MulgetTask { - private final Set tsuids; - private final List gets; - - public MulgetTask(final Set tsuids, final List gets) { - this.tsuids = tsuids; - this.gets = gets; - } - - public Set getTSUIDs() { - return this.tsuids; - } - - public List getGets() { - return this.gets; - } - } - - ////////////////////////////////////////////////////////////////////////////////////////// - //// Call back to handle result - ///////////////////////////////////////////////////////////////////////////////////////// - final class MulGetCB implements Callback> { - private final int concurrency_index; - private final Set tsuids; - private final List gets; - private final int seq_id; - - private List keyValues = new ArrayList(); - private final Map> annotations = new ConcurrentHashMap>(); - - ///////////////////////////////////////////////////////////////////////////////////////// - // nanosecond times - trace response metrics // - //////////////////////////////////////////////////////////////////////////////////////// - - // the time to start the multi get request - private long mul_get_start_time = -1; - - // cumulation of time waiting on HBase - private long mul_get_time = 0; - - // cumulation of time resolving uid - private long mul_get_uid_resolved_time = 0; - - // how many uids is resolved - private long mul_get_uids_resolved = 0; - - // cumulation of time compacting - private long mul_get_compaction_time = 0; - - // how many data points after filtering - private long mul_get_dps_post_filter = 0; - - // how many rows after filtering - private long mul_get_rows_post_filter = 0; - - // how many rows fetched from hbase - private long mul_get_number_row_fetched = 0; - - // how many cells fetched from hbase - private long mul_get_number_column_fetched = 0; - - // how many bytes fetched from hbase - private long mul_get_number_byte_fetched = 0; - - public MulGetCB(final int concurrency_index, final Set tsuids, final List gets) { - this.concurrency_index = concurrency_index; - this.tsuids = tsuids; - this.gets = gets; - - if (query_stats != null) { - seq_id = multi_get_seq_id.incrementAndGet(); - StringBuilder sb = new StringBuilder(); - sb.append("Mulget_").append(this.concurrency_index).append("_").append(seq_id); - query_stats.addScannerId(query_index, seq_id, sb.toString()); - } else { - seq_id = 0; - } - } - - /** Error callback that will capture an exception from AsyncHBase and store - * it so we can bubble it up to the caller. - */ - class ErrorCb implements Callback { - @Override - public Object call(final Exception e) throws Exception { - LOG.error("Multi get threw an exception : ", e); - close(false); - return null; - } - } - - public Object fetch() { - mul_get_start_time = DateTime.nanoTime(); - - if (LOG.isDebugEnabled()) { - LOG.debug("Try to fetch data for concurrency index: " - + this.concurrency_index + "; with " + this.gets.size() + " gets"); - } - return tsdb.client.get(this.gets) - .addCallback(this) - .addErrback(new ErrorCb()); - } - - /** - * Iterate through each row of the multi get results, parses out data - * points (and optional meta data). - * @return null if no rows were found, otherwise the TreeMap with spans - */ - @Override - public Object call(final List results) throws Exception { - mul_get_time = (DateTime.nanoTime() - this.mul_get_start_time); - - try { - for (final GetResultOrException result : results) { - if (null != result.getCells()) { - ArrayList row = result.getCells(); - if (row.size() == 0) { - continue; - } - - number_pre_filter_data_point.addAndGet(row.size()); - ++mul_get_number_row_fetched; - mul_get_number_column_fetched += row.size(); - - final byte[] key = row.get(0).key(); - final byte[] tsuid_key = UniqueId.getTSUIDFromKey(key, - TSDB.metrics_width(), Const.TIMESTAMP_BYTES); - - if (!this.tsuids.contains(tsuid_key)) { - LOG.error("Multi geter fetched the wrong row " + result + " when fetching metric: " + Bytes.pretty(metric)); - continue; - } - - process(key, row); - } else { - // TODO we don't get cells for some get requests - } - } // end for - } catch (Exception e) { - close(true); - return null; - } - - close(true); - return null; - } - - /** - * Handles processing of row of data into the proper list - * @param key The row key, possibly mutated - * @param row The row of KVs to process - * @return True if processing should continue, false if an exception occurred - * or the scanner was already closed (possibly due to another scanner error) - */ - boolean process(final byte[] key, final ArrayList row) { - ++mul_get_rows_post_filter; - num_post_filter_data_points.addAndGet(row.size()); - - List notes = null; - if (annotMap != null) { - notes = annotations.get(key); - - if (notes == null) { - notes = new ArrayList(); - annotations.put(key, notes); - } - } - - if (RollupQuery.isValidQuery(rollup_query)) { - processRollupQuery(key, row, notes); - } else { - processNotRollupQuery(key, row, notes); - } - - return true; - } - - private void processNotRollupQuery(final byte[] key, - final ArrayList row, - List notes) { - KeyValue compacted = null; - try { - final long compaction_start = DateTime.nanoTime(); - compacted = tsdb.compact(row, notes, null); - - mul_get_compaction_time += (DateTime.nanoTime() - compaction_start); - if (compacted != null) { - final byte[] compact_value = compacted.value(); - final byte[] compact_qualifier = compacted.qualifier(); - - if (compact_qualifier.length % 2 == 0) { - // The length of the qualifier is even so this is a put type - // so the size of the data is the length of the qualifier by 2 - if (compact_value[compact_value.length - 1] == 0) { - // LOG.debug("All data points we have here are either in seconds - // or Ms"); - if (Internal.inMilliseconds(compact_qualifier[0])) { - mul_get_dps_post_filter += compact_qualifier.length / 4; - } else { - mul_get_dps_post_filter += compact_qualifier.length / 2; - } - } else { - // LOG.debug("Data Points we have here are stored in second and Ms - // precision"); - // We wil make a estimate here as iterating over each qualifer - // could be expensive. - // We will just divide the qualifier by 3 to estimate the value - mul_get_dps_post_filter += compact_qualifier.length / 3; - } - } - } - } catch (IllegalDataException idex) { - LOG.error("Caught IllegalDataException exception while parsing the " + "row " + key + ", skipping index", idex); - } - - if (compacted != null) { // Can be null if we ignored all KVs. - keyValues.add(compacted); - } - } - - private void processRollupQuery(final byte[] key, final ArrayList row, List notes) { - for (KeyValue kv : row) { - final byte[] qual = kv.qualifier(); - - if (qual.length > 0) { - // Todo: Bug! Here we shouldn't use the first byte to check the type - // of this row - // Instead should parse the byte array to find the suffix and - // determine the actual type - if (qual[0] == Annotation.PREFIX()) { - // This could be a row with only an annotation in it - final Annotation note = JSON.parseToObject(kv.value(), Annotation.class); - notes.add(note); - } else { - if (rollup_query.getGroupBy() == Aggregators.AVG || rollup_query.getGroupBy() == Aggregators.DEV) { - if (Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 - || Bytes.memcmp(RollupQuery.COUNT, qual, 0, RollupQuery.COUNT.length) == 0) { - keyValues.add(kv); - } - } else if (Bytes.memcmp(rollup_query.getRollupAggPrefix(), qual, 0, - rollup_query.getRollupAggPrefix().length) == 0) { - keyValues.add(kv); - } - } - } - } // end for - } - - void close(final boolean ok) { - if (LOG.isDebugEnabled()) { - LOG.debug("Finished multiget on concurrency index: " + this.concurrency_index + ", seq id: " + this.seq_id - + ". Fetched rows: " + this.mul_get_number_row_fetched + ", cells: " + this.mul_get_number_column_fetched - + ", mget time(ms): " + this.mul_get_time / 1000000); - } - - if (query_stats != null) { - query_stats.addScannerStat(query_index, seq_id, QueryStat.SCANNER_TIME, - DateTime.nanoTime() - mul_get_start_time); - - // Scanner Stats - query_stats.addScannerStat(query_index, seq_id, QueryStat.ROWS_FROM_STORAGE, this.mul_get_number_row_fetched); - - query_stats.addScannerStat(query_index, seq_id, QueryStat.COLUMNS_FROM_STORAGE, - this.mul_get_number_column_fetched); - - query_stats.addScannerStat(query_index, seq_id, QueryStat.BYTES_FROM_STORAGE, this.mul_get_number_byte_fetched); - - query_stats.addScannerStat(query_index, seq_id, QueryStat.HBASE_TIME, mul_get_time); - query_stats.addScannerStat(query_index, seq_id, QueryStat.SUCCESSFUL_SCAN, ok ? 1 : 0); - - // Post Scan stats - query_stats.addScannerStat(query_index, seq_id, QueryStat.ROWS_POST_FILTER, mul_get_rows_post_filter); - query_stats.addScannerStat(query_index, seq_id, QueryStat.DPS_POST_FILTER, mul_get_dps_post_filter); - query_stats.addScannerStat(query_index, seq_id, QueryStat.SCANNER_UID_TO_STRING_TIME, - mul_get_uid_resolved_time); - query_stats.addScannerStat(query_index, seq_id, QueryStat.UID_PAIRS_RESOLVED, mul_get_uids_resolved); - query_stats.addScannerStat(query_index, seq_id, QueryStat.COMPACTION_TIME, mul_get_compaction_time); - } - - if (ok) { - validateMultigetData(keyValues, annotations); - } else { - finished_mulget_cnt.incrementAndGet(); - } - - // check we have finished all the multi get - if (!checkAllFinishAndTriggerCallback()) { - // check to fire a new multi get in this concurrency bucket - List salt_mul_get_tasks = mul_get_tasks.get(this.concurrency_index); - int task_index = mul_get_indexs.get(this.concurrency_index).incrementAndGet(); - if (task_index < salt_mul_get_tasks.size()) { - MulgetTask task = salt_mul_get_tasks.get(task_index); - MulGetCB mgcb = new MulGetCB(this.concurrency_index, task.getTSUIDs(), task.getGets()); - mgcb.fetch(); - } - } - } - } - - public Deferred> fetch() { - startFetch(); - return this.results; - } - - private void startFetch() { - prepareConcurrentMultiGetTasks(); - int concurrency_number = tsdb.config.mul_get_concurrency_number(); - - // set the time of starting - fetch_start_time = System.currentTimeMillis(); - if (LOG.isDebugEnabled()) { - LOG.debug("Start to fetch data using multiget, there will be " + mulget_wait_cnt - + " multigets to call"); - } - - for (int con_idx = 0; con_idx < concurrency_number; ++con_idx) { - List con_mul_get_tasks = mul_get_tasks.get(con_idx); - int task_index = this.mul_get_indexs.get(con_idx).incrementAndGet(); - - if (task_index < con_mul_get_tasks.size()) { - MulgetTask task = con_mul_get_tasks.get(task_index); - MulGetCB mgcb = new MulGetCB(con_idx, task.getTSUIDs(), task.getGets()); - mgcb.fetch(); - } - } // end for - } - - private void prepareConcurrentMultiGetTasks() { - int batch_size = tsdb.config.mul_get_batch_size(); - int concurrency_number = tsdb.config.mul_get_concurrency_number(); - - mulget_wait_cnt = 0; - prepare_multi_get_start_time = System.currentTimeMillis(); - - // prepare the tagvs combinations and base time list - List tagv_compinations = prepareAllTagvCompounds(); - List row_base_time_list = null; - if (RollupQuery.isValidQuery(rollup_query)) { - row_base_time_list = prepareRowBaseTimesRollup(); - } else { - row_base_time_list = prepareRowBaseTimesNotRollup(); - } - - int next_concurrency_index = 0; - List gets_to_prepare = new ArrayList(batch_size); - Set tsuids = new TreeSet(new TSUIDComparator()); - ByteMap> all_tsuids_gets = prepareGets(tagv_compinations, row_base_time_list); - - for (Map.Entry> gets_entry : all_tsuids_gets) { - byte[] tsuid = gets_entry.getKey(); - List gets = gets_entry.getValue(); - - for (int slice_offset = 0; slice_offset < gets.size();) { - int cur_sz = gets_to_prepare.size(); - int need_sz = batch_size - cur_sz; - int left_sz = gets.size() - slice_offset; - int slice_sz = (left_sz > need_sz ? need_sz : left_sz); - gets_to_prepare.addAll(gets.subList(slice_offset, slice_offset + slice_sz)); - tsuids.add(tsuid); - - // move the offset - slice_offset += slice_sz; - - // a new task is ready, add it to next concurrency task list - if (gets_to_prepare.size() == batch_size) { - MulgetTask task = new MulgetTask(tsuids, gets_to_prepare); - List mulget_task_list = mul_get_tasks.get((next_concurrency_index++) % concurrency_number); - mulget_task_list.add(task); - ++mulget_wait_cnt; - - // prepare a new task list and tsuids - gets_to_prepare = new ArrayList(batch_size); - tsuids = new TreeSet(new TSUIDComparator()); - } // end if - } // end for (int slice_offset) - } // end for (Map.Entry) - - - // add the uncompleted one - if (gets_to_prepare.size() > 0) { - MulgetTask task = new MulgetTask(tsuids, gets_to_prepare); - List mulget_task_list = mul_get_tasks.get((next_concurrency_index++) % concurrency_number); - mulget_task_list.add(task); - ++mulget_wait_cnt; - } - - prepare_multi_get_end_time = System.currentTimeMillis(); - if (LOG.isDebugEnabled()) { - LOG.debug("Finished preparing concurrency multi get task with " + mulget_wait_cnt + " tasks using " - + (prepare_multi_get_end_time - prepare_multi_get_start_time) + "ms"); - } - } - - private void validateMultigetData(List kvs, - Map> annotations) { - int tasks = finished_mulget_cnt.incrementAndGet(); - - if (kvs.size() > 0) { - kvsmap.put(tasks, kvs); - } - - if (annotMap != null) { - for (byte[] key : annotations.keySet()) { - List notes = annotations.get(key); - - if (notes.size() > 0) { - annotMap.put(key, notes); - } - } - } - } - - private boolean checkAllFinishAndTriggerCallback() { - if (this.mulget_wait_cnt == finished_mulget_cnt.get()) { - try { - mergeAndReturnResults(); - } catch (Exception ex) { - LOG.error("Failed merging and returning results, " + "calling back with exception", ex); - - this.results.callback(ex); - } - - return true; - } else { - return false; - } - } - - private void mergeAndReturnResults() { - final long hbase_time = DateTime.currentTimeMillis(); - TsdbQuery.scanlatency.add((int) (hbase_time - this.fetch_start_time)); - if (LOG.isDebugEnabled()) { - LOG.debug("Finished to fetch data for metric: " + Bytes.pretty(this.metric) - + " using " + (hbase_time - this.fetch_start_time) + "ms"); - } - - final long merge_start = DateTime.nanoTime(); - - mergeDataPoints(); - - if (LOG.isDebugEnabled()) { - LOG.debug("It took " + (DateTime.currentTimeMillis() - hbase_time) + " ms, " - + " to merge and sort the rows into a tree map"); - } - - if (query_stats != null) { - query_stats.addStat(query_index, QueryStat.SCANNER_MERGE_TIME, (DateTime.nanoTime() - merge_start)); - } - - results.callback(this.spans); - } - - private void mergeDataPoints() { - for (List kvs : kvsmap.values()) { - if (kvs == null || kvs.isEmpty()) { - LOG.error("Found a key value list that was null or empty"); - continue; - } - for (final KeyValue kv : kvs) { - - if (kv == null) { - LOG.error("Found a key value item that was null"); - continue; - } - if (kv.key() == null) { - LOG.error("A key for a kv was null"); - continue; - } - - Span datapoints = null; - try { - datapoints = spans.get(kv.key()); - } catch (RuntimeException e) { - LOG.error("Failed to fetch the span", e); - } - - // If this tsdb follows append logic, then there will not be any - // duplicates here. But if it is not, then there can be multiple - // non-compcated or out of order rows here - if (datapoints == null) { - datapoints = RollupQuery.isValidQuery(rollup_query) ? new RollupSpan(tsdb, this.rollup_query) - : new Span(tsdb); - spans.put(kv.key(), datapoints); - } - - if (annotMap.containsKey(kv.key())) { - for (Annotation note : annotMap.get(kv.key())) { - datapoints.getAnnotations().add(note); - } - annotMap.remove(kv.key()); - } - try { - datapoints.addRow(kv); - } catch (RuntimeException e) { - LOG.error("Exception adding row to span", e); - } - } - } - - kvsmap.clear(); - - for (byte[] key : annotMap.keySet()) { - Span datapoints = (Span) spans.get(key); - - if (datapoints == null) { - datapoints = new Span(tsdb); - spans.put(key, datapoints); - } - - for (Annotation note : annotMap.get(key)) { - datapoints.getAnnotations().add(note); - } - } - - annotMap.clear(); - } - - protected ByteMap> prepareGets(final List tagv_compounds, - final List row_base_time_list) { - ByteMap> tsuids_rows = prepareTsuidRowKeys(tags, tagv_compounds, row_base_time_list); - ByteMap> tsuids_gets = new ByteMap>(); - for (Map.Entry> tsuid_rows : tsuids_rows) { - byte[] tsuid = tsuid_rows.getKey(); - List rows = tsuid_rows.getValue(); - List rows_gets = new ArrayList(); - - for (byte[] row : rows) { - GetRequest get = new GetRequest(this.table_to_fetch, row, TSDB.FAMILY); - rows_gets.add(get); - } // end for - - tsuids_gets.put(tsuid, rows_gets); - } // end for - - return tsuids_gets; - } - - private List prepareRowBaseTimesNotRollup() { - ArrayList row_base_time_list = new ArrayList(); - for (long row_base_time = start_row_time; row_base_time <= end_row_time; row_base_time += Const.MAX_TIMESPAN) { - row_base_time_list.add(row_base_time - row_base_time % Const.MAX_TIMESPAN); - } // end for - - return row_base_time_list; - } - - private List prepareRowBaseTimesRollup() { - RollupInterval interval = rollup_query.getRollupInterval(); - List rows_base_times = new ArrayList(); - - if (interval.getUnits() == 'h') { - int modulo = Const.MAX_TIMESPAN; - if (interval.getUnitMultiplier() > 1) { - modulo = interval.getUnitMultiplier() * 60 * 60; - } - - for (long row_base_time = this.start_row_time; row_base_time <= this.end_row_time; row_base_time += modulo) { - rows_base_times.add(row_base_time - row_base_time % modulo); - } // end for - } else { - Calendar pre_calendar = Calendar.getInstance(Const.UTC_TZ); - pre_calendar.setTimeInMillis(this.start_row_time * 1000); - rows_base_times.add(this.start_row_time); - - while(true) { - final Calendar calendar = Calendar.getInstance(Const.UTC_TZ); - calendar.setTimeInMillis(pre_calendar.getTimeInMillis()); - - // zero out the hour, minutes, seconds - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - - switch (interval.getUnits()) { - case 'd': - int day_of_month = pre_calendar.get(Calendar.DAY_OF_MONTH); - calendar.set(Calendar.DAY_OF_MONTH, ++day_of_month); - break; - case 'm': - calendar.set(Calendar.DAY_OF_MONTH, 1); - int month = pre_calendar.get(Calendar.MONTH); - calendar.set(Calendar.MONTH, ++month); - break; - case 'y': - calendar.set(Calendar.DAY_OF_MONTH, 1); - calendar.set(Calendar.MONTH, 0); // 0 for January - int year = pre_calendar.get(Calendar.YEAR); - calendar.set(Calendar.YEAR, ++year); - break; - default: - throw new IllegalArgumentException("Unrecogznied span: " + interval); - } - - long base_time = (long)(calendar.getTimeInMillis() / 1000); - if (base_time <= this.end_row_time) { - rows_base_times.add(base_time); - - // current calendar becomes the baseline for next one - pre_calendar = calendar; - } else { - break; - } - } // end while - } - - return rows_base_times; - } - - /** - * We have multiple tagks and each tagk may has multiple possible values. - * This routine generates all the possible tagv compounds basing on the - * presence sequence of the tagks. Each compound will be used to generate - * the final tsuid. - * - * @return a list contains all the possible compounds - */ - private List prepareAllTagvCompounds() { - List pre_phase_tags = new LinkedList(); - pre_phase_tags.add(new byte[tags.size()][tsdb.tag_values.width()]); - - List next_phase_tags = new LinkedList(); - int next_append_index = 0; - - for (Map.Entry tag : tags) { - byte[][] tagv = tag.getValue(); - - for (int i = 0; i < tagv.length; ++i) { - for (byte[][] pre_phase_tag : pre_phase_tags) { - byte[][] next_phase_tag = new byte[tags.size()][tsdb.tag_values.width()]; - - // copy the tagv from index 0 ~ next_append_index - 1 - for (int k = 0; k < next_append_index; ++k) { - System.arraycopy(pre_phase_tag[k], 0, next_phase_tag[k], 0, tsdb.tag_values.width()); - } - - // copy the tagv in next_append_index - System.arraycopy(tagv[i], 0, next_phase_tag[next_append_index], 0, tsdb.tag_values.width()); - next_phase_tags.add(next_phase_tag); - } - } // end for - - ++next_append_index; - pre_phase_tags = next_phase_tags; - next_phase_tags = new LinkedList(); - } // end for - return pre_phase_tags; - } - - private ByteMap> prepareTsuidRowKeys(final ByteMap tags, - final List tagv_compounds, final List base_time_list) { - - int row_size = (Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES - + tsdb.tag_names.width() * tags.size() + tsdb.tag_values.width() * tags.size()); - - ByteMap> tsuid_rows = new ByteMap>(); - for (byte[][] tagvs : tagv_compounds) { - byte[] tsuid = new byte[tsdb.metrics.width() + tags.size() * tsdb.tag_names.width() - + tags.size() * tsdb.tag_values.width()]; - List rows = new ArrayList(); - - for (Long row_base_time : base_time_list) { - byte[] row_key = new byte[row_size]; - // salt will prefix basing the hash value of the other part - - // metric - System.arraycopy(metric, 0, row_key, Const.SALT_WIDTH(), tsdb.metrics.width()); - System.arraycopy(metric, 0, tsuid, 0, tsdb.metrics.width()); - - // base time - Internal.setBaseTime(row_key, row_base_time.intValue()); - - // copy tagks and tagvs to the row key - int tag_index = 0; - int row_key_copy_offset = Const.SALT_WIDTH() + tsdb.metrics.width() + Const.TIMESTAMP_BYTES; - int tsuid_copy_offset = tsdb.metrics.width(); - for (Map.Entry tag : tags) { - // tagk - byte[] tagk = tag.getKey(); - System.arraycopy(tagk, 0, row_key, row_key_copy_offset, tsdb.tag_names.width()); - System.arraycopy(tagk, 0, tsuid, tsuid_copy_offset, tsdb.tag_names.width()); - row_key_copy_offset += tsdb.tag_names.width(); - tsuid_copy_offset += tsdb.tag_names.width(); - - // tagv - System.arraycopy(tagvs[tag_index], 0, row_key, row_key_copy_offset, tsdb.tag_values.width()); - System.arraycopy(tagvs[tag_index], 0, tsuid, tsuid_copy_offset, tsdb.tag_values.width()); - row_key_copy_offset += tsdb.tag_values.width(); - tsuid_copy_offset += tsdb.tag_values.width(); - - // move to the next tag - ++tag_index; - } - - // salt - RowKey.prefixKeyWithSalt(row_key); - - rows.add(row_key); - } // end for - - tsuid_rows.put(tsuid, rows); - } // end for - - return tsuid_rows; - } -} diff --git a/src/core/TSDB.java b/src/core/TSDB.java index e6a7d26808..98b52869e6 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -2094,6 +2094,12 @@ public HistogramCodecManager histogramManager() { return histogram_manager; } + /** @return The search plugin if configured and loaded. May be null. + * @since 2.4 */ + public SearchPlugin getSearchPlugin() { + return this.search; + } + private final boolean isHistogram(final byte[] qualifier) { return (qualifier.length & 0x1) == 1; } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 0432276a74..6dbeed1169 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -85,6 +85,9 @@ final class TsdbQuery implements Query { /** The time, in ns, when we start scanning for data **/ private long scan_start_time; + + /** Whether or not the query has any results. */ + private Boolean no_results; /** Value used for timestamps that are uninitialized. */ private static final int UNSET = -1; @@ -107,6 +110,9 @@ final class TsdbQuery implements Query { /** 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. @@ -118,6 +124,7 @@ final class TsdbQuery implements Query { * Tag key and values to use in the row key filter, all pre-sorted */ private ByteMap row_key_literals; + private List> row_key_literals_list; /** If true, use rate of change instead of actual values. */ private boolean rate; @@ -160,12 +167,22 @@ final class TsdbQuery implements Query { /** Whether or not to match series with ONLY the given tags */ private boolean explicit_tags; - private boolean has_filter_cannot_use_get = false; - 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; + /** * Enum for rollup fallback control. * @since 2.4 @@ -212,6 +229,7 @@ public TsdbQuery(final TSDB tsdb) { this.downsampler = DownsamplingSpecification.NO_DOWNSAMPLER; enable_fuzzy_filter = tsdb.getConfig() .getBoolean("tsd.query.enable_fuzzy_filter"); + use_multi_gets = tsdb.getConfig().getBoolean("tsd.query.multi_get.enable"); } /** Which rollup table it scanned to get the final result. @@ -434,6 +452,8 @@ public Deferred configureFromQuery(final TSQuery query, rollup_usage = sub_query.getRollupUsage(); filters = sub_query.getFilters(); explicit_tags = sub_query.getExplicitTags(); + override_fuzzy_filter = sub_query.getUseFuzzyFilter(); + override_multi_get = sub_query.getUseMultiGets(); // set percentile options percentiles = sub_query.getPercentiles(); @@ -445,6 +465,10 @@ public Deferred configureFromQuery(final TSQuery query, } sub_query.setTsdbQuery(this); + if (use_multi_gets && override_multi_get && multiget_with_search) { + row_key_literals_list = Lists.newArrayList(); + } + // if we have tsuids set, that takes precedence if (sub_query.getTsuids() != null && !sub_query.getTsuids().isEmpty()) { tsuids = new ArrayList(sub_query.getTsuids()); @@ -481,26 +505,58 @@ class MetricCB implements Callback, byte[]> { public Deferred call(final byte[] uid) throws Exception { metric = uid; if (filters != null) { - final List> deferreds = - new ArrayList>(filters.size()); - for (final TagVFilter filter : filters) { - // determine if the user is asking for pre-agg data - if (filter instanceof TagVLiteralOrFilter && tsdb.getAggTagKey() != null) { - if (filter.getTagk().equals(tsdb.getAggTagKey())) { - if (tsdb.getRawTagValue() != null && - !filter.getFilter().equals(tsdb.getRawTagValue())) { - pre_aggregate = true; - } + if (use_multi_gets && override_multi_get && multiget_with_search) { + class ErrorCB implements Callback, Exception> { + @Override + public Deferred call(Exception arg) throws Exception { + LOG.info("Doing scans because meta query is failed", arg); + if (explicit_tags) { + search_query_failure = true; + } else { + override_multi_get = false; + use_multi_gets = false; + } + return Deferred.group(resolveTagFilters()).addCallback(new FilterCB()); + } + } + + class SuccessCB implements Callback, List>> { + @Override + public Deferred call(final List> results) throws Exception { + row_key_literals_list.addAll(results); + return Deferred.fromResult(null); } } - deferreds.add(filter.resolveTagkName(tsdb)); + tsdb.getSearchPlugin().resolveTSQuery(query, index) + .addCallbackDeferring(new SuccessCB()) + .addErrback(new ErrorCB()); } - return Deferred.group(deferreds).addCallback(new FilterCB()); + + return Deferred.group(resolveTagFilters()).addCallback(new FilterCB()); } else { return Deferred.fromResult(null); } } + + private List> resolveTagFilters() { + final List> deferreds = + new ArrayList>(filters.size()); + for (final TagVFilter filter : filters) { + // determine if the user is asking for pre-agg data + if (filter instanceof TagVLiteralOrFilter && tsdb.getAggTagKey() != null) { + if (filter.getTagk().equals(tsdb.getAggTagKey())) { + if (tsdb.getRawTagValue() != null && + !filter.getFilter().equals(tsdb.getRawTagValue())) { + pre_aggregate = true; + } + } + } + + deferreds.add(filter.resolveTagkName(tsdb)); + } + return deferreds; + } } // fire off the callback chain by resolving the metric first @@ -541,7 +597,13 @@ private void findGroupBys() { return; } + if ((use_multi_gets && override_multi_get) && !search_query_failure) { + + } + row_key_literals = new ByteMap(); + final int expansion_limit = tsdb.getConfig().getInt( + "tsd.query.filter.expansion_limit"); Collections.sort(filters); final Iterator current_iterator = filters.iterator(); @@ -588,11 +650,10 @@ private void findGroupBys() { } if (literals.size() > 0) { - if (literals.size() + row_key_literals_count > - tsdb.getConfig().getInt("tsd.query.filter.expansion_limit")) { + if (literals.size() + row_key_literals_count > expansion_limit) { LOG.debug("Skipping literals for " + current.getTagk() + " as it exceedes the limit"); - has_filter_cannot_use_get = true; + //has_filter_cannot_use_get = true; } else { final byte[][] values = new byte[literals.size()][]; literals.keySet().toArray(values); @@ -605,7 +666,23 @@ private void findGroupBys() { } } else { row_key_literals.put(current.getTagkBytes(), null); - has_filter_cannot_use_get = true; + // no literal values, just keys, so we can't multi-get + if (search_query_failure) { + use_multi_gets = false; + } + } + + // make sure the multi-get cardinality doesn't exceed our limit (or disable + // multi-gets) + if ((use_multi_gets && override_multi_get)) { + int multi_get_limit = tsdb.getConfig().getInt("tsd.query.multi_get.limit"); + int cardinality = filters.size() * row_key_literals_count; + if (cardinality > multi_get_limit) { + use_multi_gets = false; + } else if (search_query_failure) { + row_key_literals_list.add(row_key_literals); + } + // TODO - account for time as well } } } @@ -646,7 +723,7 @@ public DataPoints[] runHistogram() throws HBaseException { @Override public Deferred runAsync() throws HBaseException { Deferred result = null; - if (!this.has_filter_cannot_use_get && this.explicit_tags) { + if (use_multi_gets && override_multi_get) { result = this.findSpansWithMultiGetter().addCallback(new GroupByAndAggregateCB()); } else { result = findSpans().addCallback(new GroupByAndAggregateCB()); @@ -666,7 +743,7 @@ public Deferred runHistogramAsync() throws HBaseException { } Deferred result = null; - if (!this.has_filter_cannot_use_get && this.explicit_tags) { + if (use_multi_gets && override_multi_get) { result = findHistogramSpansWithMultiGetter() .addCallback(new HistogramGroupByAndAggregateCB()); } else { @@ -741,9 +818,11 @@ private Deferred> findSpansWithMultiGetter() throws HBaseE new TreeMap(new SpanCmp(metric_width)); scan_start_time = System.nanoTime(); - return new SaltMultiGetter(tsdb, metric, row_key_literals, getScanStartTimeSeconds(), getScanEndTimeSeconds(), - tableToBeScanned(), spans, 0, rollup_query, query_stats, query_index, 0, - false).fetch(); + + 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(); } /** @@ -802,11 +881,11 @@ private Deferred> findHistogramSpansWithMultiGett final TreeMap histSpans = new TreeMap(new SpanCmp(metric_width)); scan_start_time = System.nanoTime(); - return Deferred.fromError(new UnsupportedOperationException("Not implemented yet.")); - //return new SaltMultiGetter(tsdb, metric, row_key_literals, getScanStartTimeSeconds(), getScanEndTimeSeconds(), - // tableToBeScanned(), null, histSpans, rollup_query, query_stats, query_index).fetchHistogram(); + return new MultiGetQuery(tsdb, this, metric, row_key_literals_list, + getScanStartTimeSeconds(), getScanEndTimeSeconds(), + tableToBeScanned(), null, histSpans, 0, rollup_query, query_stats, query_index, 0, + false, search_query_failure).fetchHistogram(); } - /** * Callback that should be attached the the output of @@ -1667,11 +1746,19 @@ public String toString() { .append("))"); return buf.toString(); } + + public Boolean getNoResults() { + return no_results; + } + public void setNoResults(Boolean noResults) { + this.no_results = noResults; + } + /** * Comparator that ignores timestamps in row keys. */ - private static final class SpanCmp implements Comparator { + static final class SpanCmp implements Comparator { private final short metric_width; diff --git a/src/search/SearchPlugin.java b/src/search/SearchPlugin.java index d9bad4eb47..e72783a152 100644 --- a/src/search/SearchPlugin.java +++ b/src/search/SearchPlugin.java @@ -13,11 +13,16 @@ package net.opentsdb.search; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; import net.opentsdb.meta.Annotation; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; import net.opentsdb.stats.StatsCollector; +import java.util.List; + +import org.hbase.async.Bytes.ByteMap; + import com.stumbleupon.async.Deferred; /** @@ -144,6 +149,12 @@ public abstract class SearchPlugin { */ public abstract Deferred deleteAnnotation(final Annotation note); + public Deferred>> resolveTSQuery(final TSQuery query, + final int sub_query_index) { + throw new UnsupportedOperationException("Not implemented by this plugin: " + + this); + } + /** * Executes a very basic search query, returning the results in the SearchQuery * object passed in. diff --git a/src/utils/Config.java b/src/utils/Config.java index 1ff7cc0a95..3e425aa6be 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -105,10 +105,6 @@ public class Config { /** tsd.storage.hbase.scanner.maxNumRows */ private int scanner_max_num_rows = 128; - - private int mul_get_batch_size = 1024; - - private int mul_get_cocurrency_number = 16; /** tsd.storage.use_otsdb_timestamp */ /** Sets the HBase cell timestamp equal to metric timestamp */ @@ -118,8 +114,6 @@ public class Config { /** Used for resolving between data coming in at same timestamp */ /** If set to true, the maximum value will be returned, minimum */ private boolean use_max_value = true; - - private String hist_decoder_name; /** * The list of properties configured to their defaults or modified by users @@ -274,14 +268,6 @@ public void setFixDuplicates(final boolean fix_duplicates) { public boolean enable_tree_processing() { return enable_tree_processing; } - - public int mul_get_batch_size() { - return mul_get_batch_size; - } - - public int mul_get_concurrency_number() { - return mul_get_cocurrency_number; - } public boolean use_otsdb_timestamp() { return use_otsdb_timestamp; @@ -291,11 +277,6 @@ public boolean use_max_value() { return use_max_value; } - /** @return The full class name of the decoder for histogram data points */ - public String hist_decoder_name() { - return hist_decoder_name; - } - /** * Allows for modifying properties after creation or loading. * @@ -553,6 +534,11 @@ protected void setDefaults() { default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.query.enable_fuzzy_filter", "true"); + default_map.put("tsd.query.multi_get.enable", "false"); + default_map.put("tsd.query.multi_get.limit", "131072"); + default_map.put("tsd.query.multi_get.batch_size", "1024"); + default_map.put("tsd.query.multi_get.concurrent", "20"); + default_map.put("tsd.query.multi_get.get_all_salts", "false"); default_map.put("tsd.rpc.telnet.return_errors", "true"); // Rollup related settings default_map.put("tsd.rollups.enable", "false"); @@ -596,8 +582,6 @@ protected void setDefaults() { + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, " + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since"); default_map.put("tsd.query.timeout", "0"); - default_map.put("tsd.core.mul_get_batch_size", "1024"); - default_map.put("tsd.core.mul_get_cocurrency_number", "20"); default_map.put("tsd.storage.use_otsdb_timestamp", "true"); default_map.put("tsd.storage.use_max_value", "true"); @@ -713,11 +697,8 @@ public void loadStaticVariables() { enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); - mul_get_batch_size = this.getInt("tsd.core.mul_get_batch_size"); - mul_get_cocurrency_number = this.getInt("tsd.core.mul_get_cocurrency_number"); use_otsdb_timestamp = this.getBoolean("tsd.storage.use_otsdb_timestamp"); use_max_value = this.getBoolean("tsd.storage.use_max_value"); - hist_decoder_name = this.getString("tsd.core.hist_decoder"); } /** diff --git a/test/core/TestMultiGetQuery.java b/test/core/TestMultiGetQuery.java new file mode 100644 index 0000000000..217bb613fd --- /dev/null +++ b/test/core/TestMultiGetQuery.java @@ -0,0 +1,1102 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyList; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import org.hbase.async.Bytes.ByteMap; +import org.hbase.async.GetRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import com.google.common.collect.Lists; + +import net.opentsdb.core.MultiGetQuery.MultiGetTask; +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.stats.QueryStats; +import net.opentsdb.utils.ByteSet; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*", "javax.net.ssl.*"}) +public class TestMultiGetQuery extends BaseTsdbTest { + protected List> q_tags; + protected List> q_tags_nometa; + protected List> q_tags_AD; + + protected long start_ts; + protected long end_ts; + protected TreeMap spans; + protected TreeMap histogramSpans; + protected QueryStats query_stats; + protected Aggregator aggregator; + protected int max_bytes; + protected boolean multiget_no_meta; + protected TsdbQuery query; + + @Before + public void localBefore() { + max_bytes = 1000000; + multiget_no_meta = false; + start_ts = 1481227200; + end_ts = 1481284800; + q_tags = new ArrayList>(); + q_tags_AD = new ArrayList>(); + ByteMap q_tags1; + q_tags1 = new ByteMap(); + byte[][] val; + val = new byte[1][]; + val[0] = UIDS.get("A"); + + q_tags1.put(TAGK_BYTES, val); + val = new byte[1][]; + val[0] = UIDS.get("D"); + q_tags1.put(TAGK_B_BYTES, val); + ByteMap q_tags2; + q_tags2 = new ByteMap(); + val = new byte[1][]; + val[0] = UIDS.get("B"); + q_tags2.put(TAGK_BYTES, val); + val = new byte[1][]; + val[0] = UIDS.get("D"); + q_tags2.put(TAGK_B_BYTES, val); + ByteMap q_tags3; + q_tags3 = new ByteMap(); + val = new byte[1][]; + val[0] = UIDS.get("C"); + q_tags3.put(TAGK_BYTES, val); + val = new byte[1][]; + val[0] = UIDS.get("D"); + q_tags3.put(TAGK_B_BYTES, val); + + q_tags.add(q_tags1); + q_tags.add(q_tags2); + q_tags.add(q_tags3); + + q_tags_AD.add(q_tags1); + + q_tags_nometa = new ArrayList>(); + ByteMap q_tags_map = new ByteMap(); + q_tags_map.put(TAGK_BYTES, new byte[][] { UIDS.get("A"), UIDS.get("B"), UIDS.get("C") }); + q_tags_map.put(TAGK_B_BYTES, new byte[][] { UIDS.get("D") }); + q_tags_map.put(UIDS.get("E"), new byte[][] { UIDS.get("F"), UIDS.get("G") }); + q_tags_nometa.add(q_tags_map); +// q_tags1.put(TAGK_BYTES, new byte[][] { UIDS.get("A"), UIDS.get("B"), UIDS.get("C") }); +// q_tags1.put(TAGK_B_BYTES, new byte[][] { UIDS.get("D") }); +// q_tags1.put(UIDS.get("E"), new byte[][] { UIDS.get("F"), UIDS.get("G") }); + + aggregator = Aggregators.get("sum"); + + RollupConfig rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("max", 2) + .addAggregationId("min", 3) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-10m") + .setPreAggregationTable("tsdb-rollup-agg-10m") + .setInterval("10m") + .setRowSpan("6h")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1h") + .setPreAggregationTable("tsdb-rollup-agg-1h") + .setInterval("1h") + .setRowSpan("1d")) + .addInterval(RollupInterval.builder() + .setTable("tsdb-rollup-1d") + .setPreAggregationTable("tsdb-rollup-agg-1d") + .setInterval("1d") + .setRowSpan("1n")) + .build(); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + } + + @Test + public void ctor() throws Exception { + TsdbQuery query = new TsdbQuery(tsdb); + final 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); + // TODO - validations + } + + @Test + public void prepareAllTagvCompounds() throws Exception { + multiget_no_meta = true; + MultiGetQuery mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags_nometa, + start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, + 0, 0, false, multiget_no_meta); + + List tagvs = mgq.prepareAllTagvCompounds(); + assertEquals(6, tagvs.size()); + assertEquals(3, tagvs.get(0).length); + assertArrayEquals(UIDS.get("A"), tagvs.get(0)[0]); + assertArrayEquals(UIDS.get("D"), tagvs.get(0)[1]); + assertArrayEquals(UIDS.get("F"), tagvs.get(0)[2]); + + assertEquals(3, tagvs.get(1).length); + assertArrayEquals(UIDS.get("B"), tagvs.get(1)[0]); + assertArrayEquals(UIDS.get("D"), tagvs.get(1)[1]); + assertArrayEquals(UIDS.get("F"), tagvs.get(1)[2]); + + assertEquals(3, tagvs.get(2).length); + assertArrayEquals(UIDS.get("C"), tagvs.get(2)[0]); + assertArrayEquals(UIDS.get("D"), tagvs.get(2)[1]); + assertArrayEquals(UIDS.get("F"), tagvs.get(2)[2]); + + assertEquals(3, tagvs.get(3).length); + assertArrayEquals(UIDS.get("A"), tagvs.get(3)[0]); + assertArrayEquals(UIDS.get("D"), tagvs.get(3)[1]); + assertArrayEquals(UIDS.get("G"), tagvs.get(3)[2]); + + assertEquals(3, tagvs.get(4).length); + assertArrayEquals(UIDS.get("B"), tagvs.get(4)[0]); + assertArrayEquals(UIDS.get("D"), tagvs.get(4)[1]); + assertArrayEquals(UIDS.get("G"), tagvs.get(4)[2]); + + assertEquals(3, tagvs.get(5).length); + assertArrayEquals(UIDS.get("C"), tagvs.get(5)[0]); + assertArrayEquals(UIDS.get("D"), tagvs.get(5)[1]); + assertArrayEquals(UIDS.get("G"), tagvs.get(5)[2]); + + // simple test + q_tags_nometa = new ArrayList>(); + ByteMap q_tags_nometa_map = new ByteMap(); + q_tags_nometa_map.put(TAGK_BYTES, new byte[][] { UIDS.get("A") }); + q_tags_nometa.add(q_tags_nometa_map); + mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags_nometa, + start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, + 0, 0, false, multiget_no_meta); + tagvs = mgq.prepareAllTagvCompounds(); + assertEquals(1, tagvs.size()); + assertEquals(1, tagvs.get(0).length); + assertArrayEquals(UIDS.get("A"), tagvs.get(0)[0]); + } + + @Test + public void prepareRowBaseTimes() throws Exception { + // aligned timestamps + 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); + List timestamps = mgq.prepareRowBaseTimes(); + assertEquals(17, timestamps.size()); + long expected = 1481227200; + for (final long ts : timestamps) { + assertEquals(expected, ts); + expected += 3600; + } + + // unaligned + start_ts = 1481229792; + end_ts = 1481284801; + 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); + timestamps = mgq.prepareRowBaseTimes(); + assertEquals(17, timestamps.size()); + expected = 1481227200; + for (final long ts : timestamps) { + assertEquals(expected, ts); + expected += 3600; + } + + // short interval + start_ts = 1481229792; + end_ts = 1481229961; + 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); + timestamps = mgq.prepareRowBaseTimes(); + assertEquals(1, timestamps.size()); + assertEquals(1481227200, (long) timestamps.get(0)); + } + + @Test + public void prepareRowBaseTimesRollup() throws Exception { + RollupInterval interval = RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb_agg") + .setInterval("1m") + .setRowSpan("1h") + .build(); + RollupQuery rq = new RollupQuery(interval, aggregator, 0, aggregator); + MultiGetQuery mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags, + start_ts, end_ts, tsdb.dataTable(), spans, null, 0, rq, query_stats, + 0, max_bytes, false, multiget_no_meta); + List timestamps = mgq.prepareRowBaseTimesRollup(); + assertEquals(17, timestamps.size()); + long expected = 1481227200; + for (final long ts : timestamps) { + assertEquals(expected, ts); + expected += 3600; + } + + interval = RollupInterval.builder() + .setTable("tsdb") + .setPreAggregationTable("tsdb_agg") + .setInterval("1m") + .setRowSpan("1d") + .build(); + rq = new RollupQuery(interval, aggregator, 0, aggregator); + mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags, + start_ts, end_ts, tsdb.dataTable(), spans, null, 0, rq, query_stats, + 0, max_bytes, false, multiget_no_meta); + timestamps = mgq.prepareRowBaseTimesRollup(); + timestamps = mgq.prepareRowBaseTimesRollup(); + assertEquals(2, timestamps.size()); + expected = 1481155200; + for (final long ts : timestamps) { + assertEquals(expected, ts); + expected += 86400; + } + } + + @Test + public void prepareRequests() 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); + List timestamps = mgq.prepareRowBaseTimes(); + ByteMap>> row_map = mgq.prepareRequests(timestamps, q_tags); + ByteSet tsuids = new ByteSet(); + for (ByteMap> rows : row_map.values()) { + tsuids.addAll(rows.keySet()); + } + assertEquals(3, tsuids.size()); + + List rows = new ArrayList(); + for (Entry>> salt_entry : row_map.entrySet()) { + System.out.println(salt_entry.getValue()); + rows.addAll(salt_entry.getValue().get(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D"))); + } + + assertEquals(timestamps.size(), rows.size()); + for (int i = 0; i < timestamps.size(); i++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "A", TAGK_B_STRING, "D"); + assertArrayEquals(key, rows.get(i).key()); + } + + rows = new ArrayList(); + for (Entry>> salt_entry : row_map.entrySet()) { + rows.addAll(salt_entry.getValue().get(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + } + assertEquals(timestamps.size(), rows.size()); + for (int i = 0; i < timestamps.size(); i++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "B", TAGK_B_STRING, "D"); + assertArrayEquals(key, rows.get(i).key()); + } + + rows = new ArrayList(); + for (Entry>> salt_entry : row_map.entrySet()) { + rows.addAll(salt_entry.getValue().get(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + } + assertEquals(timestamps.size(), rows.size()); + for (int i = 0; i < timestamps.size(); i++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "C", TAGK_B_STRING, "D"); + assertArrayEquals(key, rows.get(i).key()); + } + + rows = new ArrayList(); + for (Entry>> salt_entry : row_map.entrySet()) { + rows.addAll(salt_entry.getValue().get(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D"))); + } + + assertEquals(timestamps.size(), rows.size()); + for (int i = 0; i < timestamps.size(); i++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "A", TAGK_B_STRING, "D"); + assertArrayEquals(key, rows.get(i).key()); + } + + rows = new ArrayList(); + for (Entry>> salt_entry : row_map.entrySet()) { + rows.addAll(salt_entry.getValue().get(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + } + assertEquals(timestamps.size(), rows.size()); + for (int i = 0; i < timestamps.size(); i++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "B", TAGK_B_STRING, "D"); + assertArrayEquals(key, rows.get(i).key()); + } + + rows = new ArrayList(); + for (Entry>> salt_entry : row_map.entrySet()) { + rows.addAll(salt_entry.getValue().get(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + } + assertEquals(timestamps.size(), rows.size()); + for (int i = 0; i < timestamps.size(); i++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "C", TAGK_B_STRING, "D"); + assertArrayEquals(key, rows.get(i).key()); + } + } + + @Test + public void prepareRowKeysAllSalts() throws Exception { + multiget_no_meta = true; + if (Const.SALT_WIDTH() == 0) { + return; + } + config.overrideConfig("tsd.query.multi_get.get_all_salts", "true"); + + MultiGetQuery mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags_nometa, + start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, + 0, 0, false, multiget_no_meta); + List timestamps = Lists.newArrayList(1481227200L, 1481230800L); + List q_tags_compounds = mgq.prepareAllTagvCompounds(); + ByteMap>> row_map_map = mgq.prepareRequestsNoMeta( q_tags_compounds, timestamps); + ByteMap> row_map = row_map_map.get("0".getBytes()); + assertEquals(6, row_map.size()); + + List rows = row_map.get(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D", "E", "F")); + assertEquals(timestamps.size() * Const.SALT_BUCKETS(), rows.size()); + for (int i = 0; i < timestamps.size(); i += Const.SALT_BUCKETS()) { + for (int x = 0; x < Const.SALT_BUCKETS(); x++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "A", TAGK_B_STRING, "D", "E", "F"); + key[0] = (byte) x; + assertArrayEquals(key, rows.get(i + x).key()); + } + } + + rows = row_map.get(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D", "E", "F")); + assertEquals(timestamps.size() * Const.SALT_BUCKETS(), rows.size()); + for (int i = 0; i < timestamps.size(); i += Const.SALT_BUCKETS()) { + for (int x = 0; x < Const.SALT_BUCKETS(); x++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "B", TAGK_B_STRING, "D", "E", "F"); + key[0] = (byte) x; + assertArrayEquals(key, rows.get(i + x).key()); + } + } + + rows = row_map.get(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D", "E", "F")); + assertEquals(timestamps.size() * Const.SALT_BUCKETS(), rows.size()); + for (int i = 0; i < timestamps.size(); i += Const.SALT_BUCKETS()) { + for (int x = 0; x < Const.SALT_BUCKETS(); x++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "C", TAGK_B_STRING, "D", "E", "F"); + key[0] = (byte) x; + assertArrayEquals(key, rows.get(i + x).key()); + } + } + + rows = row_map.get(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D", "E", "G")); + assertEquals(timestamps.size() * Const.SALT_BUCKETS(), rows.size()); + for (int i = 0; i < timestamps.size(); i += Const.SALT_BUCKETS()) { + for (int x = 0; x < Const.SALT_BUCKETS(); x++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "B", TAGK_B_STRING, "D", "E", "G"); + key[0] = (byte) x; + assertArrayEquals(key, rows.get(i + x).key()); + } + } + rows = row_map.get(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D", "E", "G")); + assertEquals(timestamps.size() * Const.SALT_BUCKETS(), rows.size()); + for (int i = 0; i < timestamps.size(); i += Const.SALT_BUCKETS()) { + for (int x = 0; x < Const.SALT_BUCKETS(); x++) { + byte[] key = getRowKey(METRIC_STRING, timestamps.get(i).intValue(), + TAGK_STRING, "C", TAGK_B_STRING, "D", "E", "G"); + key[0] = (byte) x; + assertArrayEquals(key, rows.get(i + x).key()); + } + } + } + + @Test + public void prepareConcurrentMultiGetTasks() 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); + mgq.prepareConcurrentMultiGetTasks(); + final List> tasks = mgq.getMultiGetTasks(); + + assertEquals(config.getInt("tsd.query.multi_get.concurrent"), tasks.size()); + for (int i = 1; i < tasks.size(); i++) { + assertTrue(tasks.get(i).isEmpty()); + } + + for (List taskList : tasks) { + for (MultiGetTask task : taskList) { + byte salt = task.getGets().get(0).key()[0]; + for (GetRequest request : task.getGets()) { + assertEquals(salt, request.key()[0]); + } + } + } + + assertEquals(1, tasks.get(0).size()); + MultiGetTask task = tasks.get(0).get(0); + assertEquals(3, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + + assertEquals(51, task.getGets().size()); + + + // 6 sets of 17 timestamps. Ugly UT + int idx = 0; + int ts = 1481227200; + while (idx < 17) { + + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "A", TAGK_B_STRING, "D")); + ts += 3600; + } + + ts = 1481227200; + while (idx < 17) { + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "B", TAGK_B_STRING, "D")); + ts += 3600; + } + ts = 1481227200; + + while (idx < 17) { + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "C", TAGK_B_STRING, "D")); + ts += 3600; + } + + } + + @Test + public void prepareConcurrentMultiGetTasksSmallBatch() throws Exception { + config.overrideConfig("tsd.query.multi_get.batch_size", "17"); + 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); + mgq.prepareConcurrentMultiGetTasks(); + final List> tasks = mgq.getMultiGetTasks(); + assertEquals(config.getInt("tsd.query.multi_get.concurrent"), tasks.size()); + assertEquals(1, tasks.get(0).size()); + + // first batch + MultiGetTask task = tasks.get(0).get(0); + Set tsuids = task.getTSUIDs(); + assertEquals(1, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D"))); + assertEquals(17, task.getGets().size()); + int idx = 0; + int ts = 1481227200; + while (idx < 17) { // notice the early cut off. The last hour should spill over. + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "A", TAGK_B_STRING, "D")); + ts += 3600; + } + + // next batch + task = tasks.get(1).get(0); + assertEquals(1, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + assertEquals(17, task.getGets().size()); + idx = 0; + ts = 1481227200; + + + while (idx < 17) { + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "B", TAGK_B_STRING, "D")); + ts += 3600; + } + + // next batch + task = tasks.get(2).get(0); + assertEquals(1, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + assertEquals(17, task.getGets().size()); + idx = 0; + ts = 1481227200; + while (idx < 17) { + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "C", TAGK_B_STRING, "D")); + ts += 3600; + } + + } + + + @Test + public void prepareConcurrentMultiSortedSalts() + throws Exception { + config.overrideConfig("tsd.query.multi_get.concurrent", "2"); + config.overrideConfig("tsd.query.multi_get.batch_size", "2"); + Const.setSaltWidth(1); + 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); + mgq.prepareConcurrentMultiGetTasks(); + final List> tasks = mgq.getMultiGetTasks(); + assertEquals(config.getInt("tsd.query.multi_get.concurrent"), tasks.size()); + + for (List taskList : tasks) { + for (MultiGetTask task : taskList) { + byte salt = task.getGets().get(0).key()[0]; + for (GetRequest request : task.getGets()) { + assertEquals(salt, request.key()[0]); + } + } + } + Const.setSaltWidth(0); + + } + + @Test + public void prepareConcurrentMultiGetTasksSmallBatchAndSmallConcurrent() + throws Exception { + config.overrideConfig("tsd.query.multi_get.concurrent", "2"); + config.overrideConfig("tsd.query.multi_get.batch_size", "17"); + 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); + mgq.prepareConcurrentMultiGetTasks(); + final List> tasks = mgq.getMultiGetTasks(); + assertEquals(config.getInt("tsd.query.multi_get.concurrent"), tasks.size()); + assertEquals(2, tasks.get(0).size()); + assertEquals(1, tasks.get(1).size()); + + for (List taskList : tasks) { + for (MultiGetTask task : taskList) { + byte salt = task.getGets().get(0).key()[0]; + for (GetRequest request : task.getGets()) { + assertEquals(salt, request.key()[0]); + } + } + } + + // first batch + MultiGetTask task = tasks.get(0).get(0); + assertEquals(1, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "A", TAGK_B_STRING, "D"))); + assertEquals(17, task.getGets().size()); + int idx = 0; + int ts = 1481227200; + while (idx < 16) { // notice the early cut off. The last hour should spill over. + + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "A", TAGK_B_STRING, "D")); + ts += 3600; + } + // 17th request + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "A", TAGK_B_STRING, "D")); + ts += 3600; + // next batch + task = tasks.get(1).get(0); + assertEquals(1, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "B", TAGK_B_STRING, "D"))); + assertEquals(17, task.getGets().size()); + idx = 0; + ts = 1481227200; + +// assertArrayEquals(task.getGets().get(idx++).key(), +// getRowKey(METRIC_STRING, ts, TAGK_STRING, "B", TAGK_B_STRING, "D")); + + while (idx < 17) { + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "B", TAGK_B_STRING, "D")); + ts += 3600; + } + + // next batch + task = tasks.get(0).get(1); + assertEquals(1, task.getTSUIDs().size()); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + assertNotNull(task.getTSUIDs().contains(getTSUID(METRIC_STRING, TAGK_STRING, + "C", TAGK_B_STRING, "D"))); + assertEquals(17, task.getGets().size()); + idx = 0; + ts = 1481227200; + while (idx < 17) { + assertArrayEquals(task.getGets().get(idx++).key(), + getRowKey(METRIC_STRING, ts, TAGK_STRING, "C", TAGK_B_STRING, "D")); + ts += 3600; + } + + + } + + @Test + public void fetch() throws Exception { + setupStorage(); + 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(); + assertSame(spans, results); + verify(client, times(1)).get(anyList()); + System.out.println(spans); + validateSpans(); + } + + @Test + public void fetchMultigetNoMeta() throws Exception { + setupStorageNoMeta(); + multiget_no_meta = true; + MultiGetQuery mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags_nometa, + 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(); + assertSame(spans, results); + verify(client, times(1)).get(anyList()); + System.out.println(spans); + validateSpansNometa(); + } + + @Test (expected=QueryException.class) + public void fetchMoreThanMaxBytes() throws Exception { + setupStorage(); + max_bytes = 0; + 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(); + } + + @Test + public void fetchEmptyTable() throws Exception { + setDataPointStorage(); + spans = new TreeMap(new TsdbQuery.SpanCmp(TSDB.metrics_width())); + 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(); + assertSame(spans, results); + assertTrue(spans.isEmpty()); + verify(client, times(1)).get(anyList()); + } + + @Test + public void fetchSmallBatch() throws Exception { + setupStorage(); + config.overrideConfig("tsd.query.multi_get.batch_size", "16"); + + 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(); + assertSame(spans, results); + verify(client, times(4)).get(anyList()); + validateSpans(); + } + + @Test + public void fetchSmallBatchAndSmallConcurrent() throws Exception { + setupStorage(); + config.overrideConfig("tsd.query.multi_get.concurrent", "2"); + config.overrideConfig("tsd.query.multi_get.batch_size", "16"); + + 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(); + assertSame(spans, results); + verify(client, times(4)).get(anyList()); + validateSpans(); + } + + @Test + public void fetchException() throws Exception { + setupStorage(); + final RuntimeException e = new RuntimeException("Boo!"); + storage.throwException(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "A", TAGK_B_STRING, "D"), e); + MultiGetQuery mgq = new MultiGetQuery(tsdb, query, METRIC_BYTES, q_tags_AD, + start_ts, end_ts, tsdb.dataTable(), spans, null, 0, null, query_stats, + 0, 10000000, false, false); + + try { + mgq.fetch().join(); + fail("Expected RuntimeException"); + } catch (RuntimeException ex) { + assertSame(ex, e); + } + verify(client, times(1)).get(anyList()); + } + + /** + * Validates the data setup in {@link #setupStorage()} is returned in the requests. + * @throws Exception if something went pear shaped. + */ + protected void validateSpansNometa() throws Exception { + assertEquals(6, spans.size()); + Span span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "A", TAGK_B_STRING, "D", "E", "F")); + SeekableView view = span.iterator(); + long ts = start_ts * 1000; + long v = 1; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "B", TAGK_B_STRING, "D", "E", "F")); + view = span.iterator(); + ts = start_ts * 1000; + v = 11; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "C", TAGK_B_STRING, "D", "E", "F")); + view = span.iterator(); + ts = start_ts * 1000; + v = 111; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "A", TAGK_B_STRING, "D", "E", "G")); + view = span.iterator(); + ts = start_ts * 1000; + v = 1111; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "B", TAGK_B_STRING, "D", "E", "G")); + view = span.iterator(); + ts = start_ts * 1000; + v = 11111; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "C", TAGK_B_STRING, "D", "E", "G")); + view = span.iterator(); + ts = start_ts * 1000; + v = 111111; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + } + + /** + * Validates the data setup in {@link #setupStorage()} is returned in the requests. + * @throws Exception if something went pear shaped. + */ + protected void validateSpans() throws Exception { + System.out.println(spans); + assertEquals(3, spans.size()); + Span span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "A", TAGK_B_STRING, "D")); + SeekableView view = span.iterator(); + long ts = start_ts * 1000; + long v = 1; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "B", TAGK_B_STRING, "D")); + view = span.iterator(); + ts = start_ts * 1000; + v = 11; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "C", TAGK_B_STRING, "D")); + view = span.iterator(); + ts = start_ts * 1000; + v = 111; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "A", TAGK_B_STRING, "D")); + view = span.iterator(); + ts = start_ts * 1000; + v = 1; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "B", TAGK_B_STRING, "D")); + view = span.iterator(); + ts = start_ts * 1000; + v = 11; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + + span = spans.get(getRowKey(METRIC_STRING, (int) start_ts, TAGK_STRING, + "C", TAGK_B_STRING, "D")); + view = span.iterator(); + ts = start_ts * 1000; + v = 111; + while (view.hasNext()) { + DataPoint dp = view.next(); + assertEquals(ts, dp.timestamp()); + assertEquals(v++, dp.longValue()); + ts += 3600000; + } + } + + /** + * Helper that writes a data point for each row that the get request should + * cover. + * @throws Exception if something went pear shaped. + */ + protected void setupStorageNoMeta() throws Exception { + setDataPointStorage(); + spans = new TreeMap(new TsdbQuery.SpanCmp(TSDB.metrics_width())); + + int value = 1; + int ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "A"); + tags.put(TAGK_B_STRING, "D"); + tags.put("E", "F"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 11; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "B"); + tags.put(TAGK_B_STRING, "D"); + tags.put("E", "F"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 111; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "C"); + tags.put(TAGK_B_STRING, "D"); + tags.put("E", "F"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 1111; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "A"); + tags.put(TAGK_B_STRING, "D"); + tags.put("E", "G"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 11111; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "B"); + tags.put(TAGK_B_STRING, "D"); + tags.put("E", "G"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 111111; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "C"); + tags.put(TAGK_B_STRING, "D"); + tags.put("E", "G"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + } + + + /** + * Helper that writes a data point for each row that the get request should + * cover. + * @throws Exception if something went pear shaped. + */ + protected void setupStorage() throws Exception { + setDataPointStorage(); + spans = new TreeMap(new TsdbQuery.SpanCmp(TSDB.metrics_width())); + + int value = 1; + int ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "A"); + tags.put(TAGK_B_STRING, "D"); + //tags.put("E", "F"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 11; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "B"); + tags.put(TAGK_B_STRING, "D"); + // tags.put("E", "F"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + + value = 111; + ts = (int) start_ts; + tags.clear(); + tags.put(TAGK_STRING, "C"); + tags.put(TAGK_B_STRING, "D"); + // tags.put("E", "F"); + while (ts <= (int) end_ts) { + tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); + ts += 3600; + } + +// value = 1111; +// ts = (int) start_ts; +// tags.clear(); +// tags.put(TAGK_STRING, "A"); +// tags.put(TAGK_B_STRING, "D"); +// // tags.put("E", "G"); +// while (ts <= (int) end_ts) { +// tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); +// ts += 3600; +// } +// +// value = 11111; +// ts = (int) start_ts; +// tags.clear(); +// tags.put(TAGK_STRING, "B"); +// tags.put(TAGK_B_STRING, "D"); +// // tags.put("E", "G"); +// while (ts <= (int) end_ts) { +// tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); +// ts += 3600; +// } +// +// value = 111111; +// ts = (int) start_ts; +// tags.clear(); +// tags.put(TAGK_STRING, "C"); +// tags.put(TAGK_B_STRING, "D"); +// // tags.put("E", "G"); +// while (ts <= (int) end_ts) { +// tsdb.addPoint(METRIC_STRING, (long) ts, (long) value++, tags).join(); +// ts += 3600; +// } + } +} + + diff --git a/test/core/TestTsdbQueryHistogramQueries.java b/test/core/TestTsdbQueryHistogramQueries.java index 7a1866adbb..118cfd5c72 100644 --- a/test/core/TestTsdbQueryHistogramQueries.java +++ b/test/core/TestTsdbQueryHistogramQueries.java @@ -142,9 +142,6 @@ public void runSingleTsMsSinglePercentile() throws Exception { @Test public void runSingleTsMsDoulePercentile() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); - this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(1); tags.put("host", "web01"); @@ -194,8 +191,6 @@ public void runSingleTsMsDoulePercentile() throws Exception { @Test public void runSingleTsMsTwoAggSum() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(); @@ -227,8 +222,6 @@ public void runSingleTsMsTwoAggSum() throws Exception { @Test public void runSingleTsMsAggNone() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(); @@ -279,8 +272,6 @@ public void runSingleTsMsAggNone() throws Exception { @Test public void runSingleTsMsAggSumTwoGroups() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesMs(); HashMap tags = new HashMap(); @@ -332,8 +323,6 @@ public void runSingleTsMsAggSumTwoGroups() throws Exception { @Test public void runWithAnnotation() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); final Annotation note = new Annotation(); @@ -369,8 +358,6 @@ public void runWithAnnotation() throws Exception { @Test public void runWithOnlyAnnotation() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); byte[] key = getRowKey(HISTOGRAM_METRIC_STRING, 1357002000, TAGK_STRING, TAGV_STRING); @@ -412,8 +399,6 @@ public void runWithOnlyAnnotation() throws Exception { @Test public void runTSUIDQuery() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); query.setStartTime(1356998400); @@ -445,8 +430,6 @@ public void runTSUIDQuery() throws Exception { @Test public void runTSUIDsAggSum() throws Exception { - Whitebox.setInternalState(config, "hist_decoder_name", - "net.opentsdb.core.LongHistogramDataPointForTestDecoder"); this.storeTestHistogramTimeSeriesSeconds(false); query.setStartTime(1356998400); From 797c10191ef700720471a8e6cbf89181c6886caf Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 16:34:10 -0700 Subject: [PATCH 645/826] Fix #994 by filtering on the annotation start time when serializing so that we skip any that do not start within the query timespan. Signed-off-by: Chris Larsen --- src/tsd/HttpJsonSerializer.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index b29a834bab..675e66cb5c 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -32,6 +32,7 @@ import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; +import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; import net.opentsdb.core.FillPolicy; @@ -733,6 +734,13 @@ public Object call(final ArrayList deferreds) throws Exception { Collections.sort(annotations); json.writeArrayFieldStart("annotations"); for (Annotation note : annotations) { + long ts = note.getStartTime(); + if (!((ts & Const.SECOND_MASK) != 0)) { + ts *= 1000; + } + if (ts < data_query.startTime() || ts > data_query.endTime()) { + continue; + } json.writeObject(note); } json.writeEndArray(); @@ -742,6 +750,13 @@ public Object call(final ArrayList deferreds) throws Exception { Collections.sort(globals); json.writeArrayFieldStart("globalAnnotations"); for (Annotation note : globals) { + long ts = note.getStartTime(); + if (!((ts & Const.SECOND_MASK) != 0)) { + ts *= 1000; + } + if (ts < data_query.startTime() || ts > data_query.endTime()) { + continue; + } json.writeObject(note); } json.writeEndArray(); From bf4b565cf342f248cd0fa192ff6176c0ec56b16e Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 16:34:10 -0700 Subject: [PATCH 646/826] Fix #994 by filtering on the annotation start time when serializing so that we skip any that do not start within the query timespan. Signed-off-by: Chris Larsen --- src/tsd/HttpJsonSerializer.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 3bcc6ef4b9..b3e7d6937b 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -33,6 +33,7 @@ import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; +import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; import net.opentsdb.core.FillPolicy; @@ -779,6 +780,13 @@ public Object call(final ArrayList deferreds) throws Exception { Collections.sort(annotations); json.writeArrayFieldStart("annotations"); for (Annotation note : annotations) { + long ts = note.getStartTime(); + if (!((ts & Const.SECOND_MASK) != 0)) { + ts *= 1000; + } + if (ts < data_query.startTime() || ts > data_query.endTime()) { + continue; + } json.writeObject(note); } json.writeEndArray(); @@ -788,6 +796,13 @@ public Object call(final ArrayList deferreds) throws Exception { Collections.sort(globals); json.writeArrayFieldStart("globalAnnotations"); for (Annotation note : globals) { + long ts = note.getStartTime(); + if (!((ts & Const.SECOND_MASK) != 0)) { + ts *= 1000; + } + if (ts < data_query.startTime() || ts > data_query.endTime()) { + continue; + } json.writeObject(note); } json.writeEndArray(); From 38c6f6708cf94d07ca491da8f02fb8776e11b069 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 17:15:10 -0700 Subject: [PATCH 647/826] Fix #967 by changing the conversions to UTF-8 for the static byte methods in the UID class. This should properly decode the UTF strings now. Also remove the CHARSET from the UID class, use Const instead. Signed-off-by: Chris Larsen --- src/tools/CliUtils.java | 21 ++++++++------------- src/tools/UidManager.java | 3 ++- src/uid/UniqueId.java | 15 ++++++--------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/tools/CliUtils.java b/src/tools/CliUtils.java index f67c966dd4..17b74b0292 100644 --- a/src/tools/CliUtils.java +++ b/src/tools/CliUtils.java @@ -41,8 +41,6 @@ final class CliUtils { static final Method toBytes; /** Function used to convert a byte[] to a String. */ static final Method fromBytes; - /** Charset used to convert Strings to byte arrays and back. */ - static final Charset CHARSET; /** The single column family used by this class. */ static final byte[] ID_FAMILY; /** The single column family used by this class. */ @@ -58,9 +56,6 @@ final class CliUtils { // "THIS IS INTERNAL DO NOT USE". If only Java had C++'s "friend" or // a less stupid notion of a package. Field f; - f = uidclass.getDeclaredField("CHARSET"); - f.setAccessible(true); - CHARSET = (Charset) f.get(null); f = uidclass.getDeclaredField("ID_FAMILY"); f.setAccessible(true); ID_FAMILY = (byte[]) f.get(null); @@ -79,17 +74,17 @@ final class CliUtils { } } /** Qualifier for metrics meta data */ - static final byte[] METRICS_META = "metric_meta".getBytes(CHARSET); + static final byte[] METRICS_META = "metric_meta".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagk meta data */ - static final byte[] TAGK_META = "tagk_meta".getBytes(CHARSET); + static final byte[] TAGK_META = "tagk_meta".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagv meta data */ - static final byte[] TAGV_META = "tagv_meta".getBytes(CHARSET); + static final byte[] TAGV_META = "tagv_meta".getBytes(Const.ASCII_CHARSET); /** Qualifier for metrics UIDs */ - static final byte[] METRICS = "metrics".getBytes(CHARSET); + static final byte[] METRICS = "metrics".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagk UIDs */ - static final byte[] TAGK = "tagk".getBytes(CHARSET); + static final byte[] TAGK = "tagk".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagv UIDs */ - static final byte[] TAGV = "tagv".getBytes(CHARSET); + static final byte[] TAGV = "tagv".getBytes(Const.ASCII_CHARSET); /** * Returns the max metric ID from the UID table @@ -103,8 +98,8 @@ static long getMaxMetricID(final TSDB tsdb) { // first up, we need the max metric ID so we can split up the data table // amongst threads. final GetRequest get = new GetRequest(tsdb.uidTable(), new byte[] { 0 }); - get.family("id".getBytes(CHARSET)); - get.qualifier("metrics".getBytes(CHARSET)); + get.family("id".getBytes(Const.ASCII_CHARSET)); + get.qualifier("metrics".getBytes(Const.ASCII_CHARSET)); ArrayList row; try { row = tsdb.getClient().get(get).joinUninterruptibly(); diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 5e4185d4f4..bcd01453f6 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -35,6 +35,7 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; import net.opentsdb.uid.NoSuchUniqueId; @@ -300,7 +301,7 @@ private static int grep(final HBaseClient client, if (ignorecase) { regexp = "(?i)" + regexp; } - scanner.setKeyRegexp(regexp, CliUtils.CHARSET); + scanner.setKeyRegexp(regexp, Const.ASCII_CHARSET); boolean found = false; try { ArrayList> rows; diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 22a384fc82..274ed04dee 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.uid; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -65,8 +64,6 @@ public enum UniqueIdType { TAGV } - /** Charset used to convert Strings to byte arrays and back. */ - private static final Charset CHARSET = Charset.forName("ISO-8859-1"); /** The single column family used by this class. */ private static final byte[] ID_FAMILY = toBytes("id"); /** The single column family used by this class. */ @@ -1260,11 +1257,11 @@ private void hbasePutWithRetry(final PutRequest put, short attempts, short wait) } private static byte[] toBytes(final String s) { - return s.getBytes(CHARSET); + return s.getBytes(Const.UTF8_CHARSET); } private static String fromBytes(final byte[] b) { - return new String(b, CHARSET); + return new String(b, Const.UTF8_CHARSET); } /** Returns a human readable string representation of the object. */ @@ -1588,21 +1585,21 @@ public Map call(final ArrayList row) // and the user hasn't put any metrics in, so log and return 0s LOG.info("Could not find the UID assignment row"); for (final byte[] kind : kinds) { - results.put(new String(kind, CHARSET), 0L); + results.put(new String(kind, Const.ASCII_CHARSET), 0L); } return results; } for (final KeyValue column : row) { - results.put(new String(column.qualifier(), CHARSET), + results.put(new String(column.qualifier(), Const.ASCII_CHARSET), Bytes.getLong(column.value())); } // if the user is starting with a fresh UID table, we need to account // for missing columns for (final byte[] kind : kinds) { - if (results.get(new String(kind, CHARSET)) == null) { - results.put(new String(kind, CHARSET), 0L); + if (results.get(new String(kind, Const.ASCII_CHARSET)) == null) { + results.put(new String(kind, Const.ASCII_CHARSET), 0L); } } return results; From cbb44a4827b106be6bdd8f6945f6ad31349de922 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 17:15:10 -0700 Subject: [PATCH 648/826] Fix #967 by changing the conversions to UTF-8 for the static byte methods in the UID class. This should properly decode the UTF strings now. Also remove the CHARSET from the UID class, use Const instead. Signed-off-by: Chris Larsen --- src/tools/CliUtils.java | 21 ++++++++------------- src/tools/UidManager.java | 3 ++- src/uid/UniqueId.java | 15 ++++++--------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/tools/CliUtils.java b/src/tools/CliUtils.java index f67c966dd4..17b74b0292 100644 --- a/src/tools/CliUtils.java +++ b/src/tools/CliUtils.java @@ -41,8 +41,6 @@ final class CliUtils { static final Method toBytes; /** Function used to convert a byte[] to a String. */ static final Method fromBytes; - /** Charset used to convert Strings to byte arrays and back. */ - static final Charset CHARSET; /** The single column family used by this class. */ static final byte[] ID_FAMILY; /** The single column family used by this class. */ @@ -58,9 +56,6 @@ final class CliUtils { // "THIS IS INTERNAL DO NOT USE". If only Java had C++'s "friend" or // a less stupid notion of a package. Field f; - f = uidclass.getDeclaredField("CHARSET"); - f.setAccessible(true); - CHARSET = (Charset) f.get(null); f = uidclass.getDeclaredField("ID_FAMILY"); f.setAccessible(true); ID_FAMILY = (byte[]) f.get(null); @@ -79,17 +74,17 @@ final class CliUtils { } } /** Qualifier for metrics meta data */ - static final byte[] METRICS_META = "metric_meta".getBytes(CHARSET); + static final byte[] METRICS_META = "metric_meta".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagk meta data */ - static final byte[] TAGK_META = "tagk_meta".getBytes(CHARSET); + static final byte[] TAGK_META = "tagk_meta".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagv meta data */ - static final byte[] TAGV_META = "tagv_meta".getBytes(CHARSET); + static final byte[] TAGV_META = "tagv_meta".getBytes(Const.ASCII_CHARSET); /** Qualifier for metrics UIDs */ - static final byte[] METRICS = "metrics".getBytes(CHARSET); + static final byte[] METRICS = "metrics".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagk UIDs */ - static final byte[] TAGK = "tagk".getBytes(CHARSET); + static final byte[] TAGK = "tagk".getBytes(Const.ASCII_CHARSET); /** Qualifier for tagv UIDs */ - static final byte[] TAGV = "tagv".getBytes(CHARSET); + static final byte[] TAGV = "tagv".getBytes(Const.ASCII_CHARSET); /** * Returns the max metric ID from the UID table @@ -103,8 +98,8 @@ static long getMaxMetricID(final TSDB tsdb) { // first up, we need the max metric ID so we can split up the data table // amongst threads. final GetRequest get = new GetRequest(tsdb.uidTable(), new byte[] { 0 }); - get.family("id".getBytes(CHARSET)); - get.qualifier("metrics".getBytes(CHARSET)); + get.family("id".getBytes(Const.ASCII_CHARSET)); + get.qualifier("metrics".getBytes(Const.ASCII_CHARSET)); ArrayList row; try { row = tsdb.getClient().get(get).joinUninterruptibly(); diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 5e4185d4f4..bcd01453f6 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -35,6 +35,7 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; import net.opentsdb.uid.NoSuchUniqueId; @@ -300,7 +301,7 @@ private static int grep(final HBaseClient client, if (ignorecase) { regexp = "(?i)" + regexp; } - scanner.setKeyRegexp(regexp, CliUtils.CHARSET); + scanner.setKeyRegexp(regexp, Const.ASCII_CHARSET); boolean found = false; try { ArrayList> rows; diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 22a384fc82..274ed04dee 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -12,7 +12,6 @@ // see . package net.opentsdb.uid; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -65,8 +64,6 @@ public enum UniqueIdType { TAGV } - /** Charset used to convert Strings to byte arrays and back. */ - private static final Charset CHARSET = Charset.forName("ISO-8859-1"); /** The single column family used by this class. */ private static final byte[] ID_FAMILY = toBytes("id"); /** The single column family used by this class. */ @@ -1260,11 +1257,11 @@ private void hbasePutWithRetry(final PutRequest put, short attempts, short wait) } private static byte[] toBytes(final String s) { - return s.getBytes(CHARSET); + return s.getBytes(Const.UTF8_CHARSET); } private static String fromBytes(final byte[] b) { - return new String(b, CHARSET); + return new String(b, Const.UTF8_CHARSET); } /** Returns a human readable string representation of the object. */ @@ -1588,21 +1585,21 @@ public Map call(final ArrayList row) // and the user hasn't put any metrics in, so log and return 0s LOG.info("Could not find the UID assignment row"); for (final byte[] kind : kinds) { - results.put(new String(kind, CHARSET), 0L); + results.put(new String(kind, Const.ASCII_CHARSET), 0L); } return results; } for (final KeyValue column : row) { - results.put(new String(column.qualifier(), CHARSET), + results.put(new String(column.qualifier(), Const.ASCII_CHARSET), Bytes.getLong(column.value())); } // if the user is starting with a fresh UID table, we need to account // for missing columns for (final byte[] kind : kinds) { - if (results.get(new String(kind, CHARSET)) == null) { - results.put(new String(kind, CHARSET), 0L); + if (results.get(new String(kind, Const.ASCII_CHARSET)) == null) { + results.put(new String(kind, Const.ASCII_CHARSET), 0L); } } return results; From 0f0531892d209b825481276e18414b2cabe999cf Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 21:02:03 -0700 Subject: [PATCH 649/826] Take a stab at fixing #953 by at least hunting for back-ticks before passing parameters to Gnuplot. Metrics and tags are already handled by the char list. Thanks @gsocgsoc Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 2669d6b8c6..206b5b1857 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -179,6 +179,15 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) } else if (options.size() != tsdbqueries.length) { throw new BadRequestException(options.size() + " `o' parameters, but " + tsdbqueries.length + " `m' parameters."); + } else { + for (final String option : options) { + // TODO - far from perfect, should help a little. + if (option.contains("`") || option.contains("%60") || + option.contains("`")) { + throw new BadRequestException("Option contained a back-tick. " + + "That's a no-no."); + } + } } for (final Query tsdbquery : tsdbqueries) { try { @@ -627,6 +636,12 @@ private HashMap loadCachedJson(final HttpQuery query, static void setPlotDimensions(final HttpQuery query, final Plot plot) { final String wxh = query.getQueryStringParam("wxh"); if (wxh != null && !wxh.isEmpty()) { + // TODO - far from perfect, should help a little. + if (wxh.contains("`") || wxh.contains("%60") || + wxh.contains("`")) { + throw new BadRequestException("WXH contained a back-tick. " + + "That's a no-no."); + } final int wxhlength = wxh.length(); if (wxhlength < 7) { // 100x100 minimum. throw new BadRequestException("Parameter wxh too short: " + wxh); @@ -677,7 +692,14 @@ private static String popParam(final Map> querystring, if (params == null) { return null; } - return params.get(params.size() - 1); + final String given = params.get(params.size() - 1); + // TODO - far from perfect, should help a little. + if (given.contains("`") || given.contains("%60") || + given.contains("`")) { + throw new BadRequestException("Parameter " + param + " contained a " + + "back-tick. That's a no-no."); + } + return given; } /** From a02dcc1529396f1b54ad671bf754bc8fc579a77b Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 21:02:03 -0700 Subject: [PATCH 650/826] Take a stab at fixing #953 by at least hunting for back-ticks before passing parameters to Gnuplot. Metrics and tags are already handled by the char list. Thanks @gsocgsoc Signed-off-by: Chris Larsen --- src/tsd/GraphHandler.java | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index ba2b797064..229cff97af 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -180,6 +180,15 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) } else if (options.size() != tsdbqueries.length) { throw new BadRequestException(options.size() + " `o' parameters, but " + tsdbqueries.length + " `m' parameters."); + } else { + for (final String option : options) { + // TODO - far from perfect, should help a little. + if (option.contains("`") || option.contains("%60") || + option.contains("`")) { + throw new BadRequestException("Option contained a back-tick. " + + "That's a no-no."); + } + } } for (final Query tsdbquery : tsdbqueries) { try { @@ -628,6 +637,12 @@ private HashMap loadCachedJson(final HttpQuery query, static void setPlotDimensions(final HttpQuery query, final Plot plot) { final String wxh = query.getQueryStringParam("wxh"); if (wxh != null && !wxh.isEmpty()) { + // TODO - far from perfect, should help a little. + if (wxh.contains("`") || wxh.contains("%60") || + wxh.contains("`")) { + throw new BadRequestException("WXH contained a back-tick. " + + "That's a no-no."); + } final int wxhlength = wxh.length(); if (wxhlength < 7) { // 100x100 minimum. throw new BadRequestException("Parameter wxh too short: " + wxh); @@ -678,7 +693,14 @@ private static String popParam(final Map> querystring, if (params == null) { return null; } - return params.get(params.size() - 1); + final String given = params.get(params.size() - 1); + // TODO - far from perfect, should help a little. + if (given.contains("`") || given.contains("%60") || + given.contains("`")) { + throw new BadRequestException("Parameter " + param + " contained a " + + "back-tick. That's a no-no."); + } + return given; } /** From fb8755e2309f757fc089ca6335ec7bd3b2181608 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 21:45:20 -0700 Subject: [PATCH 651/826] Bump AsyncHBase client to 1.8.0 for HBase 1.3.x compat. Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.8.0.jar.md5 | 1 + third_party/hbase/include.mk | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 third_party/hbase/asynchbase-1.8.0.jar.md5 diff --git a/third_party/hbase/asynchbase-1.8.0.jar.md5 b/third_party/hbase/asynchbase-1.8.0.jar.md5 new file mode 100644 index 0000000000..daf07749fd --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.0.jar.md5 @@ -0,0 +1 @@ +84ce0ce7f048a80755105f001352e14e diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index e46d86205a..cb2b4f58c9 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2016 The OpenTSDB Authors. +# Copyright (C) 2011-2017 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.8.0-20161127.193259-5 +ASYNCHBASE_VERSION := 1.8.0 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar -ASYNCHBASE_BASE_URL := https://oss.sonatype.org/content/repositories/snapshots/org/hbase/asynchbase/1.8.0-SNAPSHOT/ +ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) $(ASYNCHBASE): $(ASYNCHBASE).md5 set dummy "$(ASYNCHBASE_BASE_URL)" "$(ASYNCHBASE)"; shift; $(FETCH_DEPENDENCY) From 7e882849133efb002b051ad1b03b86310b2d76b5 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 21:45:20 -0700 Subject: [PATCH 652/826] Bump AsyncHBase client to 1.8.0 for HBase 1.3.x compat. Signed-off-by: Chris Larsen --- third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 | 1 - third_party/hbase/asynchbase-1.7.1.jar.md5 | 1 - third_party/hbase/asynchbase-1.8.0.jar.md5 | 1 + third_party/hbase/include.mk | 4 ++-- 4 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 delete mode 100644 third_party/hbase/asynchbase-1.7.1.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.8.0.jar.md5 diff --git a/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 b/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 deleted file mode 100644 index 75abc13db6..0000000000 --- a/third_party/hbase/asynchbase-1.7.1-20151004.015637-1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -898d34a463b52e570addf0f0160add48 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.7.1.jar.md5 b/third_party/hbase/asynchbase-1.7.1.jar.md5 deleted file mode 100644 index 45ad0e9669..0000000000 --- a/third_party/hbase/asynchbase-1.7.1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -f236854721eac6d40b6710ec7d59f4a8 diff --git a/third_party/hbase/asynchbase-1.8.0.jar.md5 b/third_party/hbase/asynchbase-1.8.0.jar.md5 new file mode 100644 index 0000000000..daf07749fd --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.0.jar.md5 @@ -0,0 +1 @@ +84ce0ce7f048a80755105f001352e14e diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index 7cb693bb8a..cb2b4f58c9 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2014 The OpenTSDB Authors. +# Copyright (C) 2011-2017 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see . -ASYNCHBASE_VERSION := 1.7.2 +ASYNCHBASE_VERSION := 1.8.0 ASYNCHBASE := third_party/hbase/asynchbase-$(ASYNCHBASE_VERSION).jar ASYNCHBASE_BASE_URL := http://central.maven.org/maven2/org/hbase/asynchbase/$(ASYNCHBASE_VERSION) From bedc084c7e86e0d2672f4a8a4f5c66a152964632 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 22:38:58 -0700 Subject: [PATCH 653/826] Add the Kryo third_party dep and update the Makefile with all of the proper files for building 2.4 Signed-off-by: Chris Larsen --- Makefile.am | 71 ++++++++++++++----- third_party/include.mk | 1 + third_party/kryo/asm-4.0.jar.md5 | 1 + third_party/kryo/include.mk | 44 ++++++++++++ third_party/kryo/kryo-2.21.1.jar.md5 | 1 + third_party/kryo/minlog-1.2.jar.md5 | 1 + .../kryo/reflectasm-1.07-shaded.jar.md5 | 1 + 7 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 third_party/kryo/asm-4.0.jar.md5 create mode 100644 third_party/kryo/include.mk create mode 100644 third_party/kryo/kryo-2.21.1.jar.md5 create mode 100644 third_party/kryo/minlog-1.2.jar.md5 create mode 100644 third_party/kryo/reflectasm-1.07-shaded.jar.md5 diff --git a/Makefile.am b/Makefile.am index fcee869859..084e7895ce 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,20 +47,43 @@ tsdb_SRC := \ src/core/DownsamplingSpecification.java \ src/core/FillingDownsampler.java \ src/core/FillPolicy.java \ + src/core/Histogram.java \ + src/core/HistogramAggregation.java \ + src/core/HistogramAggregationIterator.java \ + src/core/HistogramAggregator.java \ + src/core/HistogramBucketDataPointsAdaptor.java \ + src/core/HistogramCodecManager.java \ + src/core/HistogramDataPoint.java \ + src/core/HistogramDataPointCodec.java \ + src/core/HistogramDataPoints.java \ + src/core/HistogramDataPointsToDataPointsAdaptor.java \ + src/core/HistogramDownsampler.java \ + src/core/HistogramPojo.java \ + src/core/HistogramRowSeq.java \ + src/core/HistogramSeekableView.java \ + src/core/HistogramSpan.java \ + src/core/HistogramSpanGroup.java \ + src/core/HistogramRowSeq.java \ + src/core/iHistogramRowSeq.java \ src/core/IncomingDataPoint.java \ src/core/IncomingDataPoints.java \ src/core/IllegalDataException.java \ src/core/Internal.java \ + src/core/MultiGetQuery.java \ src/core/MutableDataPoint.java \ src/core/Query.java \ src/core/QueryException.java \ - src/core/RateOptions.java \ - src/core/RateSpan.java \ + src/core/RateOptions.java \ + src/core/RateSpan.java \ + src/core/RequestBuilder.java \ src/core/RowKey.java \ src/core/RowSeq.java \ + src/core/iRowSeq.java \ src/core/SaltScanner.java \ - src/core/SaltMultiGetter.java \ src/core/SeekableView.java \ + src/core/SimpleHistogram.java \ + src/core/SimpleHistogramDataPointAdapter.java \ + src/core/SimpleHistogramDecoder.java \ src/core/Span.java \ src/core/SpanGroup.java \ src/core/TSDB.java \ @@ -72,7 +95,9 @@ tsdb_SRC := \ src/core/WriteableDataPointFilterPlugin.java \ src/graph/Plot.java \ src/auth/AuthenticationChannelHandler.java \ - src/auth/AuthenticationPlugin.java \ + src/auth/Authentication.java \ + src/auth/Authorization.java \ + src/auth/AuthState.java \ src/meta/Annotation.java \ src/meta/MetaDataCache.java \ src/meta/TSMeta.java \ @@ -120,6 +145,15 @@ tsdb_SRC := \ src/query/pojo/Query.java \ src/query/pojo/Timespan.java \ src/query/pojo/Validatable.java \ + src/rollup/NoSuchRollupForIntervalException.java \ + src/rollup/NoSuchRollupForTableException.java \ + src/rollup/RollupConfig.java \ + src/rollup/RollUpDataPoint.java \ + src/rollup/RollupInterval.java \ + src/rollup/RollupQuery.java \ + src/rollup/RollupSeq.java \ + src/rollup/RollupSpan.java \ + src/rollup/RollupUtils.java \ src/search/SearchPlugin.java \ src/search/SearchQuery.java \ src/search/TimeSeriesLookup.java \ @@ -158,6 +192,7 @@ tsdb_SRC := \ src/tsd/DropCachesRpc.java \ src/tsd/GnuplotException.java \ src/tsd/GraphHandler.java \ + src/tsd/HistogramDataPointRpc.java \ src/tsd/HttpJsonSerializer.java \ src/tsd/HttpSerializer.java \ src/tsd/HttpQuery.java \ @@ -202,20 +237,10 @@ tsdb_SRC := \ src/utils/JSONException.java \ src/utils/Pair.java \ src/utils/PluginLoader.java \ - src/utils/Threads.java \ - src/core/iRowSeq.java \ - src/core/RequestBuilder.java - src/rollup/NoSuchRollupForIntervalException.java \ - src/rollup/NoSuchRollupForTableException.java \ - src/rollup/RollUpDataPoint.java \ - src/rollup/RollupConfig.java \ - src/rollup/RollupInterval.java \ - src/rollup/RollupQuery.java \ - src/rollup/RollupSeq.java \ - src/rollup/RollupSpan.java \ - src/rollup/RollupUtils.java + src/utils/Threads.java tsdb_DEPS = \ + $(ASM) \ $(COMMONS_LOGGING) \ $(GUAVA) \ $(LOG4J_OVER_SLF4J) \ @@ -227,7 +252,10 @@ tsdb_DEPS = \ $(JAVACC) \ $(JEXL) \ $(JGRAPHT) \ + $(KRYO) \ + $(MINLOG) \ $(NETTY) \ + $(REFLECTASM) \ $(SLF4J_API) \ $(SUASYNC) \ $(APACHE_MATH) @@ -259,6 +287,9 @@ endif test_SRC := \ test/core/SeekableViewsForTest.java \ test/core/BaseTsdbTest.java \ + test/core/HistogramSeekableViewForTest.java \ + test/core/LongHistogramDataPointForTest.java \ + test/core/LongHistogramDataPointForTestDecoder.java \ test/core/TestAggregationIterator.java \ test/core/TestAggregators.java \ test/core/TestAppendDataPoints.java \ @@ -267,6 +298,14 @@ test_SRC := \ test/core/TestDownsampler.java \ test/core/TestDownsamplingSpecification.java \ test/core/TestFillingDownsampler.java \ + test/core/TestHistogramAggregationIterator.java \ + test/core/TestHistogramCodecManager.java \ + test/core/TestHistogramDataPointsToDataPointsAdaptor.java \ + test/core/TestHistogramDownsampler.java \ + test/core/TestHistogramPojo.java \ + test/core/TestHistogramRowSeq.java \ + test/core/TestHistogramSpan.java \ + test/core/TestHistogramSpanGroup.java \ test/core/TestIncomingDataPoints.java \ test/core/TestInternal.java \ test/core/TestMutableDataPoint.java \ diff --git a/third_party/include.mk b/third_party/include.mk index 01743e4e28..b44b12416a 100644 --- a/third_party/include.mk +++ b/third_party/include.mk @@ -27,6 +27,7 @@ include third_party/javassist/include.mk include third_party/jexl/include.mk include third_party/jgrapht/include.mk include third_party/junit/include.mk +include third_party/kryo/include.mk include third_party/logback/include.mk include third_party/mockito/include.mk include third_party/netty/include.mk diff --git a/third_party/kryo/asm-4.0.jar.md5 b/third_party/kryo/asm-4.0.jar.md5 new file mode 100644 index 0000000000..2cfea76019 --- /dev/null +++ b/third_party/kryo/asm-4.0.jar.md5 @@ -0,0 +1 @@ +322d8f88c5111af612df838c0191cd7e diff --git a/third_party/kryo/include.mk b/third_party/kryo/include.mk new file mode 100644 index 0000000000..d9343cc4fc --- /dev/null +++ b/third_party/kryo/include.mk @@ -0,0 +1,44 @@ +# Copyright (C) 2017 The OpenTSDB Authors. +# +# This library 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 library 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 library. If not, see . + +KRYO_VERSION := 2.21.1 +KRYO := third_party/kryo/kryo-$(KRYO_VERSION).jar +KRYO_BASE_URL := http://central.maven.org/maven2/com/esotericsoftware/kryo/kryo/$(KRYO_VERSION) + +$(KRYO): $(KRYO).md5 + set dummy "$(KRYO_BASE_URL)" "$(KRYO)"; shift; $(FETCH_DEPENDENCY) + +REFLECTASM_VERSION := 1.07 +REFLECTASM := third_party/kryo/reflectasm-$(REFLECTASM_VERSION)-shaded.jar +REFLECTASM_BASE_URL := http://central.maven.org/maven2/com/esotericsoftware/reflectasm/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): $(ASM).md5 + set dummy "$(ASM_BASE_URL)" "$(ASM)"; shift; $(FETCH_DEPENDENCY) + +MINLOG_VERSION := 1.2 +MINLOG := third_party/kryo/minlog-$(MINLOG_VERSION).jar +MINLOG_BASE_URL := http://central.maven.org/maven2/com/esotericsoftware/minlog/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 diff --git a/third_party/kryo/kryo-2.21.1.jar.md5 b/third_party/kryo/kryo-2.21.1.jar.md5 new file mode 100644 index 0000000000..7924c1ab25 --- /dev/null +++ b/third_party/kryo/kryo-2.21.1.jar.md5 @@ -0,0 +1 @@ +aa44f411a986ed6130dee951766bbba6 diff --git a/third_party/kryo/minlog-1.2.jar.md5 b/third_party/kryo/minlog-1.2.jar.md5 new file mode 100644 index 0000000000..6286bd611f --- /dev/null +++ b/third_party/kryo/minlog-1.2.jar.md5 @@ -0,0 +1 @@ +f7cfbdf63b67df0bbfa4c7cb260885bc diff --git a/third_party/kryo/reflectasm-1.07-shaded.jar.md5 b/third_party/kryo/reflectasm-1.07-shaded.jar.md5 new file mode 100644 index 0000000000..ababaf5e81 --- /dev/null +++ b/third_party/kryo/reflectasm-1.07-shaded.jar.md5 @@ -0,0 +1 @@ +2042c222840fb21e3d13cef7c3965831 From d9659789ad53b7f5d906ffe3c4f1948c20757ea2 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 22:48:45 -0700 Subject: [PATCH 654/826] Fix the TestCompactionQueue class for JDK 6 compat. This'll be the last release. Signed-off-by: Chris Larsen --- test/core/TestCompactionQueue.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/core/TestCompactionQueue.java b/test/core/TestCompactionQueue.java index 651150304d..6080802e37 100644 --- a/test/core/TestCompactionQueue.java +++ b/test/core/TestCompactionQueue.java @@ -23,8 +23,8 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.Random; import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; import org.hbase.async.Bytes; import org.hbase.async.KeyValue; @@ -107,17 +107,18 @@ public void before() throws Exception { @Test public void useMaxTsWhileCompacting() throws Exception { + Random rnd = new Random(); ArrayList kvs = new ArrayList(2); ArrayList annotations = new ArrayList(0); - long ts1 = Math.abs(ThreadLocalRandom.current().nextLong()); + long ts1 = Math.abs(rnd.nextLong()); final byte[] qual1 = { (byte) 0xF0, 0x00, 0x00, 0x07 }; final byte[] val1 = Bytes.fromLong(4L); kvs.add(makekvWithTs(qual1, ts1, val1)); - long ts2 = Math.abs(ThreadLocalRandom.current().nextLong()); + long ts2 = Math.abs(rnd.nextLong()); final byte[] qual2 = { (byte) 0xF0, 0x00, 0x01, 0x07 }; final byte[] val2 = Bytes.fromLong(5L); kvs.add(makekvWithTs(qual2, ts2, val2)); - long ts3 = Math.abs(ThreadLocalRandom.current().nextLong()); + long ts3 = Math.abs(rnd.nextLong()); final byte[] qual3 = { (byte) 0xF0, 0x00, 0x02, 0x07 }; final byte[] val3 = Bytes.fromLong(2L); kvs.add(makekvWithTs(qual3, ts3, val3)); From a32a5f912480d1a92a0c471444a73b55c8c7a861 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 11 Jun 2017 23:05:12 -0700 Subject: [PATCH 655/826] Cut relase 2.4.0RC1 Signed-off-by: Chris Larsen --- NEWS | 20 +++++++++++++++++++- THANKS | 9 +++++++++ configure.ac | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index e2e4426532..e697dc9c96 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,23 @@ -OpenTSDB - User visible changes. +OpenTSDB - Changelog +* Version 2.4.0 RC1 (2017-06-11) + +Noteworthy Changes: + - Rollup and pre-aggregated data storage for writing and querying lower resolution + data for queries with extremely high cardinality or low cardinality but wide time + ranges that would overwhelm the JVM normally. + - Support for storing and querying histograms, digest and sketches. These provide + distributed percentile calculations that are much more accurate than trying to + average percentiles from multiple sources. + - A new authentication plugin for determining whether or not users should have access + to various queries. + - Support HBase's Date Tiered Compaction as an option by writing the cell timestamp + as the data point's timestamp. This can also help with TTL'd tables where really old + data written to the table will be dropped immediately. + - Allow HBase/Bigtable batched get requests for queries with the option of using the + search plugin to pre-resolve all of the time series. This can greatly improve + performance for queries when selecting a small subset of data from high cardinality + metrics. * Version 2.3.0 (2016-12-31) diff --git a/THANKS b/THANKS index e7262c951e..a06a0d1a7f 100644 --- a/THANKS +++ b/THANKS @@ -9,6 +9,7 @@ copyright assignment. Adrian Muraru +Adrian Goll Adrien Mogenet Alex Ioffe Andre Pech @@ -19,6 +20,7 @@ Aravind Gottipati Arvind Jayaprakash Berk D. Demir Bikrant Neupane +Bizhu Qiu Bryan Hernandez Bryan Zubrod Camden Narzt @@ -26,6 +28,7 @@ Can Zhang Carlos Devoto Chris McClymont Cristian Sechel +Christos Soulios Christophe Furmaniak Dave Barr Davide D Amico @@ -40,6 +43,7 @@ Hong Dai Thanh Hugo M Fernandes Hugo Trippaers Isaiah Choe +Ioan Szilagyi Ivan Babrou Jacek Masiulaniec Jari Takkala @@ -53,6 +57,8 @@ Johan Zeeck Johannes Meixner Jonathan Works Josh Thomas +JSBali +Karan Mehta Kevin Bowling Kevin Landreth Kieren Hynd @@ -68,12 +74,14 @@ Lou Yunlong Matt Jibson Matt Schallert Marc Tamsky +Marcin Januszkiewicz Mark Smith Martin Jansen Max Meng Michal Kimle Mike Bryant Mike Kobyakov +Misha Brukman Nathan Owens Nicole Nagele Nikhil Benesch @@ -85,6 +93,7 @@ Peter Edwards Ping Yong Pradeep Chhetri Rajesh G +Rohan Nog Ryan Berdeen Sean Miller Siddartha Guthikonda diff --git a/configure.ac b/configure.ac index e5389aaab2..1bc3c05a4d 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.0-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.4.0RC1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 07f96d9341a5563c80cea37a6ce2be6786e1282f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 7 Jul 2017 16:24:58 -0700 Subject: [PATCH 656/826] Revert "Fix #967 by changing the conversions to UTF-8 for the static byte methods" This reverts commit 38c6f6708cf94d07ca491da8f02fb8776e11b069 and fixes #1002. We'll have to dig deeper to see if it's possible to properly handle existing UID data with UTF8 encoding. --- src/tools/CliUtils.java | 21 +++++++++++++-------- src/tools/UidManager.java | 3 +-- src/uid/UniqueId.java | 15 +++++++++------ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/tools/CliUtils.java b/src/tools/CliUtils.java index 17b74b0292..f67c966dd4 100644 --- a/src/tools/CliUtils.java +++ b/src/tools/CliUtils.java @@ -41,6 +41,8 @@ final class CliUtils { static final Method toBytes; /** Function used to convert a byte[] to a String. */ static final Method fromBytes; + /** Charset used to convert Strings to byte arrays and back. */ + static final Charset CHARSET; /** The single column family used by this class. */ static final byte[] ID_FAMILY; /** The single column family used by this class. */ @@ -56,6 +58,9 @@ final class CliUtils { // "THIS IS INTERNAL DO NOT USE". If only Java had C++'s "friend" or // a less stupid notion of a package. Field f; + f = uidclass.getDeclaredField("CHARSET"); + f.setAccessible(true); + CHARSET = (Charset) f.get(null); f = uidclass.getDeclaredField("ID_FAMILY"); f.setAccessible(true); ID_FAMILY = (byte[]) f.get(null); @@ -74,17 +79,17 @@ final class CliUtils { } } /** Qualifier for metrics meta data */ - static final byte[] METRICS_META = "metric_meta".getBytes(Const.ASCII_CHARSET); + static final byte[] METRICS_META = "metric_meta".getBytes(CHARSET); /** Qualifier for tagk meta data */ - static final byte[] TAGK_META = "tagk_meta".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGK_META = "tagk_meta".getBytes(CHARSET); /** Qualifier for tagv meta data */ - static final byte[] TAGV_META = "tagv_meta".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGV_META = "tagv_meta".getBytes(CHARSET); /** Qualifier for metrics UIDs */ - static final byte[] METRICS = "metrics".getBytes(Const.ASCII_CHARSET); + static final byte[] METRICS = "metrics".getBytes(CHARSET); /** Qualifier for tagk UIDs */ - static final byte[] TAGK = "tagk".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGK = "tagk".getBytes(CHARSET); /** Qualifier for tagv UIDs */ - static final byte[] TAGV = "tagv".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGV = "tagv".getBytes(CHARSET); /** * Returns the max metric ID from the UID table @@ -98,8 +103,8 @@ static long getMaxMetricID(final TSDB tsdb) { // first up, we need the max metric ID so we can split up the data table // amongst threads. final GetRequest get = new GetRequest(tsdb.uidTable(), new byte[] { 0 }); - get.family("id".getBytes(Const.ASCII_CHARSET)); - get.qualifier("metrics".getBytes(Const.ASCII_CHARSET)); + get.family("id".getBytes(CHARSET)); + get.qualifier("metrics".getBytes(CHARSET)); ArrayList row; try { row = tsdb.getClient().get(get).joinUninterruptibly(); diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index bcd01453f6..5e4185d4f4 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -35,7 +35,6 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; -import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; import net.opentsdb.uid.NoSuchUniqueId; @@ -301,7 +300,7 @@ private static int grep(final HBaseClient client, if (ignorecase) { regexp = "(?i)" + regexp; } - scanner.setKeyRegexp(regexp, Const.ASCII_CHARSET); + scanner.setKeyRegexp(regexp, CliUtils.CHARSET); boolean found = false; try { ArrayList> rows; diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 274ed04dee..22a384fc82 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.uid; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -64,6 +65,8 @@ public enum UniqueIdType { TAGV } + /** Charset used to convert Strings to byte arrays and back. */ + private static final Charset CHARSET = Charset.forName("ISO-8859-1"); /** The single column family used by this class. */ private static final byte[] ID_FAMILY = toBytes("id"); /** The single column family used by this class. */ @@ -1257,11 +1260,11 @@ private void hbasePutWithRetry(final PutRequest put, short attempts, short wait) } private static byte[] toBytes(final String s) { - return s.getBytes(Const.UTF8_CHARSET); + return s.getBytes(CHARSET); } private static String fromBytes(final byte[] b) { - return new String(b, Const.UTF8_CHARSET); + return new String(b, CHARSET); } /** Returns a human readable string representation of the object. */ @@ -1585,21 +1588,21 @@ public Map call(final ArrayList row) // and the user hasn't put any metrics in, so log and return 0s LOG.info("Could not find the UID assignment row"); for (final byte[] kind : kinds) { - results.put(new String(kind, Const.ASCII_CHARSET), 0L); + results.put(new String(kind, CHARSET), 0L); } return results; } for (final KeyValue column : row) { - results.put(new String(column.qualifier(), Const.ASCII_CHARSET), + results.put(new String(column.qualifier(), CHARSET), Bytes.getLong(column.value())); } // if the user is starting with a fresh UID table, we need to account // for missing columns for (final byte[] kind : kinds) { - if (results.get(new String(kind, Const.ASCII_CHARSET)) == null) { - results.put(new String(kind, Const.ASCII_CHARSET), 0L); + if (results.get(new String(kind, CHARSET)) == null) { + results.put(new String(kind, CHARSET), 0L); } } return results; From 0473f2089bcea3491fff292f0996275f0ad0e81c Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 7 Jul 2017 16:24:58 -0700 Subject: [PATCH 657/826] Revert "Fix #967 by changing the conversions to UTF-8 for the static byte methods" This reverts commit 38c6f6708cf94d07ca491da8f02fb8776e11b069 and fixes #1002. We'll have to dig deeper to see if it's possible to properly handle existing UID data with UTF8 encoding. --- src/tools/CliUtils.java | 21 +++++++++++++-------- src/tools/UidManager.java | 3 +-- src/uid/UniqueId.java | 15 +++++++++------ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/tools/CliUtils.java b/src/tools/CliUtils.java index 17b74b0292..f67c966dd4 100644 --- a/src/tools/CliUtils.java +++ b/src/tools/CliUtils.java @@ -41,6 +41,8 @@ final class CliUtils { static final Method toBytes; /** Function used to convert a byte[] to a String. */ static final Method fromBytes; + /** Charset used to convert Strings to byte arrays and back. */ + static final Charset CHARSET; /** The single column family used by this class. */ static final byte[] ID_FAMILY; /** The single column family used by this class. */ @@ -56,6 +58,9 @@ final class CliUtils { // "THIS IS INTERNAL DO NOT USE". If only Java had C++'s "friend" or // a less stupid notion of a package. Field f; + f = uidclass.getDeclaredField("CHARSET"); + f.setAccessible(true); + CHARSET = (Charset) f.get(null); f = uidclass.getDeclaredField("ID_FAMILY"); f.setAccessible(true); ID_FAMILY = (byte[]) f.get(null); @@ -74,17 +79,17 @@ final class CliUtils { } } /** Qualifier for metrics meta data */ - static final byte[] METRICS_META = "metric_meta".getBytes(Const.ASCII_CHARSET); + static final byte[] METRICS_META = "metric_meta".getBytes(CHARSET); /** Qualifier for tagk meta data */ - static final byte[] TAGK_META = "tagk_meta".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGK_META = "tagk_meta".getBytes(CHARSET); /** Qualifier for tagv meta data */ - static final byte[] TAGV_META = "tagv_meta".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGV_META = "tagv_meta".getBytes(CHARSET); /** Qualifier for metrics UIDs */ - static final byte[] METRICS = "metrics".getBytes(Const.ASCII_CHARSET); + static final byte[] METRICS = "metrics".getBytes(CHARSET); /** Qualifier for tagk UIDs */ - static final byte[] TAGK = "tagk".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGK = "tagk".getBytes(CHARSET); /** Qualifier for tagv UIDs */ - static final byte[] TAGV = "tagv".getBytes(Const.ASCII_CHARSET); + static final byte[] TAGV = "tagv".getBytes(CHARSET); /** * Returns the max metric ID from the UID table @@ -98,8 +103,8 @@ static long getMaxMetricID(final TSDB tsdb) { // first up, we need the max metric ID so we can split up the data table // amongst threads. final GetRequest get = new GetRequest(tsdb.uidTable(), new byte[] { 0 }); - get.family("id".getBytes(Const.ASCII_CHARSET)); - get.qualifier("metrics".getBytes(Const.ASCII_CHARSET)); + get.family("id".getBytes(CHARSET)); + get.qualifier("metrics".getBytes(CHARSET)); ArrayList row; try { row = tsdb.getClient().get(get).joinUninterruptibly(); diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index bcd01453f6..5e4185d4f4 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -35,7 +35,6 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; -import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.meta.TSMeta; import net.opentsdb.uid.NoSuchUniqueId; @@ -301,7 +300,7 @@ private static int grep(final HBaseClient client, if (ignorecase) { regexp = "(?i)" + regexp; } - scanner.setKeyRegexp(regexp, Const.ASCII_CHARSET); + scanner.setKeyRegexp(regexp, CliUtils.CHARSET); boolean found = false; try { ArrayList> rows; diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 274ed04dee..22a384fc82 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -12,6 +12,7 @@ // see . package net.opentsdb.uid; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -64,6 +65,8 @@ public enum UniqueIdType { TAGV } + /** Charset used to convert Strings to byte arrays and back. */ + private static final Charset CHARSET = Charset.forName("ISO-8859-1"); /** The single column family used by this class. */ private static final byte[] ID_FAMILY = toBytes("id"); /** The single column family used by this class. */ @@ -1257,11 +1260,11 @@ private void hbasePutWithRetry(final PutRequest put, short attempts, short wait) } private static byte[] toBytes(final String s) { - return s.getBytes(Const.UTF8_CHARSET); + return s.getBytes(CHARSET); } private static String fromBytes(final byte[] b) { - return new String(b, Const.UTF8_CHARSET); + return new String(b, CHARSET); } /** Returns a human readable string representation of the object. */ @@ -1585,21 +1588,21 @@ public Map call(final ArrayList row) // and the user hasn't put any metrics in, so log and return 0s LOG.info("Could not find the UID assignment row"); for (final byte[] kind : kinds) { - results.put(new String(kind, Const.ASCII_CHARSET), 0L); + results.put(new String(kind, CHARSET), 0L); } return results; } for (final KeyValue column : row) { - results.put(new String(column.qualifier(), Const.ASCII_CHARSET), + results.put(new String(column.qualifier(), CHARSET), Bytes.getLong(column.value())); } // if the user is starting with a fresh UID table, we need to account // for missing columns for (final byte[] kind : kinds) { - if (results.get(new String(kind, Const.ASCII_CHARSET)) == null) { - results.put(new String(kind, Const.ASCII_CHARSET), 0L); + if (results.get(new String(kind, CHARSET)) == null) { + results.put(new String(kind, CHARSET), 0L); } } return results; From caf605a84e04eefd7146daa7b89c56c15def9121 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 15 Jul 2017 15:16:49 -0700 Subject: [PATCH 658/826] Modify the plugin HTTP RPC handler so that it parses only the first part of the URI, i.e. from "/plugin/kafka/version" it would parse out "kafka" to route to the proper plugin who's path was "kafka". Modify the AbstractHttpQuery handler so that on badRequest or internal error it at least prints the stack trace for the consumer as a string. TODO - modify it so that it uses the serializer to give us JSON or what not. Signed-off-by: Chris Larsen --- src/tsd/AbstractHttpQuery.java | 12 ++++++++++-- src/tsd/HttpRpcPluginQuery.java | 13 ++----------- test/tsd/TestHttpRpcPluginQuery.java | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index b09bc8b8d7..09f90d3269 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -23,6 +23,7 @@ import com.stumbleupon.async.Deferred; import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; @@ -36,6 +37,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.stats.QueryStats; @@ -360,7 +362,10 @@ public void done() { */ public void internalError(final Exception cause) { logError("Internal Server Error on " + request().getUri(), cause); - sendStatusOnly(HttpResponseStatus.INTERNAL_SERVER_ERROR); + sendBuffer(HttpResponseStatus.INTERNAL_SERVER_ERROR, + ChannelBuffers.wrappedBuffer( + cause.toString().getBytes(Const.UTF8_CHARSET)), + "text/plain"); } /** @@ -369,7 +374,10 @@ public void internalError(final Exception cause) { */ public void badRequest(final BadRequestException exception) { logWarn("Bad Request on " + request().getUri() + ": " + exception.getMessage()); - sendStatusOnly(HttpResponseStatus.BAD_REQUEST); + sendBuffer(HttpResponseStatus.BAD_REQUEST, + ChannelBuffers.wrappedBuffer( + exception.toString().getBytes(Const.UTF8_CHARSET)), + "text/plain"); } /** diff --git a/src/tsd/HttpRpcPluginQuery.java b/src/tsd/HttpRpcPluginQuery.java index 15e1cc928e..55464e99af 100644 --- a/src/tsd/HttpRpcPluginQuery.java +++ b/src/tsd/HttpRpcPluginQuery.java @@ -29,7 +29,7 @@ public HttpRpcPluginQuery(final TSDB tsdb, final HttpRequest request, final Chan } /** - * Return the base route with no plugin prefix in it. This is matched with + * Return the base route with no plugin prefix in it. This is matched with * values returned by {@link HttpRpcPlugin#getPath()}. * @return the base route path (no query parameters, etc.) */ @@ -39,15 +39,6 @@ public String getQueryBaseRoute() { if (parts.length < 2) { // Must be at least something like: /plugin/blah throw new BadRequestException("Invalid plugin request path: " + getQueryPath()); } - // Lop off the first element (which is the "plugin" base path). - // The remaining elements are the base route. - final StringBuilder joined = new StringBuilder(); - for (int i=1; i Date: Sun, 16 Jul 2017 12:16:38 -0700 Subject: [PATCH 659/826] Remove the "final" modifier from the meta classes (TSMeta, UIDMeta and Annotations) so that plugins can extend them as needed. This can be useful in the search plugin. Signed-off-by: Chris Larsen --- src/meta/Annotation.java | 2 +- src/meta/TSMeta.java | 2 +- src/meta/UIDMeta.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index b595ed4057..9fa255a976 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -75,7 +75,7 @@ @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public final class Annotation implements Comparable { +public class Annotation implements Comparable { private static final Logger LOG = LoggerFactory.getLogger(Annotation.class); /** Charset used to convert Strings to byte arrays and back. */ diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 65e0030dd0..cefd86c02b 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -70,7 +70,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) -public final class TSMeta { +public class TSMeta { private static final Logger LOG = LoggerFactory.getLogger(TSMeta.class); /** Charset used to convert Strings to byte arrays and back. */ diff --git a/src/meta/UIDMeta.java b/src/meta/UIDMeta.java index 21f0e5470b..e96eb25aec 100644 --- a/src/meta/UIDMeta.java +++ b/src/meta/UIDMeta.java @@ -67,7 +67,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) -public final class UIDMeta { +public class UIDMeta { private static final Logger LOG = LoggerFactory.getLogger(UIDMeta.class); /** Charset used to convert Strings to byte arrays and back. */ From 8b2fa5ec9d27a4c1c37376b490a99b81ff2f0c5a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 16 Jul 2017 12:16:38 -0700 Subject: [PATCH 660/826] Remove the "final" modifier from the meta classes (TSMeta, UIDMeta and Annotations) so that plugins can extend them as needed. This can be useful in the search plugin. Signed-off-by: Chris Larsen --- src/meta/Annotation.java | 2 +- src/meta/TSMeta.java | 2 +- src/meta/UIDMeta.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/meta/Annotation.java b/src/meta/Annotation.java index d44a6df385..d00988aece 100644 --- a/src/meta/Annotation.java +++ b/src/meta/Annotation.java @@ -76,7 +76,7 @@ @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public final class Annotation implements Comparable { +public class Annotation implements Comparable { private static final Logger LOG = LoggerFactory.getLogger(Annotation.class); /** Charset used to convert Strings to byte arrays and back. */ diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index 65e0030dd0..cefd86c02b 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -70,7 +70,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) -public final class TSMeta { +public class TSMeta { private static final Logger LOG = LoggerFactory.getLogger(TSMeta.class); /** Charset used to convert Strings to byte arrays and back. */ diff --git a/src/meta/UIDMeta.java b/src/meta/UIDMeta.java index 21f0e5470b..e96eb25aec 100644 --- a/src/meta/UIDMeta.java +++ b/src/meta/UIDMeta.java @@ -67,7 +67,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY) -public final class UIDMeta { +public class UIDMeta { private static final Logger LOG = LoggerFactory.getLogger(UIDMeta.class); /** Charset used to convert Strings to byte arrays and back. */ From 15978fb8f30a4d72c6119867a0115df4eb32b588 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 5 Aug 2017 15:16:59 -0700 Subject: [PATCH 661/826] Fix #1032 by bumping Javacc maven plugin to version 2.8.2. Thanks @FavorMylikes. --- pom.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml.in b/pom.xml.in index c44b692ff9..c82087a2ed 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -317,7 +317,7 @@ com.helger.maven ph-javacc-maven-plugin - 2.8.0 + 2.8.2 jjc From c9a27bcb06e76a1d3d0f3c6fd8ba3d28ad93462d Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 5 Aug 2017 15:16:59 -0700 Subject: [PATCH 662/826] Fix #1032 by bumping Javacc maven plugin to version 2.8.2. Thanks @FavorMylikes. --- pom.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml.in b/pom.xml.in index 235e8a1df1..71b7647cb8 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -317,7 +317,7 @@ com.helger.maven ph-javacc-maven-plugin - 2.8.0 + 2.8.2 jjc From a89d1847831d34d4edd3b6fa059ad60c5e29020f Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sat, 5 Aug 2017 15:39:17 -0700 Subject: [PATCH 663/826] Fix #1027 by adding Kryo to the Fat jar pom. Thanks @bearrito! Signed-off-by: Chris Larsen --- Makefile.am | 1 + fat-jar/fat-jar-pom.xml.in | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Makefile.am b/Makefile.am index 084e7895ce..7db1f2636b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -895,6 +895,7 @@ fat-jar-pom.xml: ./fat-jar/fat-jar-pom.xml.in Makefile -e 's/@JACKSON_VERSION@/$(JACKSON_VERSION)/' \ -e 's/@JAVASSIST_VERSION@/$(JAVASSIST_VERSION)/' \ -e 's/@JUNIT_VERSION@/$(JUNIT_VERSION)/' \ + -e 's/@KRYO_VERSION@/$(KRYO_VERSION)/' \ -e 's/@LOG4J_OVER_SLF4J_VERSION@/$(LOG4J_OVER_SLF4J_VERSION)/' \ -e 's/@LOGBACK_CLASSIC_VERSION@/$(LOGBACK_CLASSIC_VERSION)/' \ -e 's/@LOGBACK_CORE_VERSION@/$(LOGBACK_CORE_VERSION)/' \ diff --git a/fat-jar/fat-jar-pom.xml.in b/fat-jar/fat-jar-pom.xml.in index 9d2246f601..314a12e224 100644 --- a/fat-jar/fat-jar-pom.xml.in +++ b/fat-jar/fat-jar-pom.xml.in @@ -73,6 +73,7 @@ @ZOOKEEPER_VERSION@ @SLF4J_API_VERSION@ @ASYNCHBASE_VERSION@ + @KRYO_VERSION@ @LOG4J_OVER_SLF4J_VERSION@ @LOGBACK_CORE_VERSION@ @LOGBACK_CLASSIC_VERSION@ @@ -523,6 +524,11 @@ ${jgrapht.version} + + com.esotericsoftware.kryo + kryo + ${kryo.version} + From 7ed83cdc1748f4a6664d865e5b4fee3fbed6cd97 Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Sun, 6 Aug 2017 13:16:42 -0700 Subject: [PATCH 664/826] Javadoc fixes to make Java 8 happy. Signed-off-by: Chris Larsen --- src/core/AggregationIterator.java | 2 +- src/core/Aggregators.java | 4 ++-- src/core/HistogramCodecManager.java | 1 - src/core/HistogramDataPointCodec.java | 1 - src/core/Internal.java | 18 ++++++++++-------- src/core/RateOptions.java | 2 +- src/core/RowKey.java | 4 +++- src/core/Span.java | 1 + src/core/TSDB.java | 1 + src/core/TSSubQuery.java | 2 +- src/core/WriteableDataPointFilterPlugin.java | 2 +- src/meta/MetaDataCache.java | 2 +- src/meta/TSMeta.java | 2 ++ src/meta/TSUIDQuery.java | 3 ++- src/meta/UIDMeta.java | 1 + src/query/QueryUtil.java | 2 +- src/query/expression/ExpressionIterator.java | 6 ++++-- src/query/expression/ExpressionReader.java | 2 +- src/query/expression/Expressions.java | 2 +- src/query/expression/ITimeSyncedIterator.java | 4 ++-- src/query/expression/IntersectionIterator.java | 4 ++-- src/query/expression/NumericFillPolicy.java | 2 +- src/query/expression/VariableIterator.java | 4 ++-- src/query/filter/TagVFilter.java | 8 ++++---- src/query/filter/TagVRegexFilter.java | 1 + src/rollup/RollUpDataPoint.java | 4 ++-- src/search/SearchPlugin.java | 2 +- src/search/TimeSeriesLookup.java | 9 ++++----- src/stats/QueryStats.java | 4 ++-- src/tools/ConfigArgP.java | 18 +++++++++++------- src/tools/StartupPlugin.java | 5 +++-- src/tree/Leaf.java | 1 + src/tree/Tree.java | 1 + src/tree/TreeRule.java | 1 + src/tsd/AbstractHttpQuery.java | 2 +- src/tsd/HttpRpcPlugin.java | 12 ++++++------ src/tsd/HttpSerializer.java | 11 ++++++----- src/tsd/PipelineFactory.java | 6 +++--- src/tsd/RTPublisher.java | 2 +- src/tsd/RpcPlugin.java | 2 +- src/tsd/StorageExceptionHandler.java | 2 +- src/uid/RandomUniqueId.java | 4 ++-- src/uid/UniqueId.java | 8 ++++---- src/uid/UniqueIdFilterPlugin.java | 2 +- src/utils/Config.java | 2 +- src/utils/DateTime.java | 6 +++--- src/utils/JSON.java | 8 ++------ src/utils/PluginLoader.java | 3 ++- 48 files changed, 106 insertions(+), 90 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index b58d121232..99bb540c6b 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -166,7 +166,7 @@ public class AggregationIterator implements SeekableView, DataPoint, *
  • No: for {@code iterators[i]} the timestamp of the current data * point is {@code timestamps[i]} and the timestamp of the next data * point is {@code timestamps[iterators.length + i]}.
  • - * + * *

    * Each timestamp can have the {@code FLAG_FLOAT} applied so it's important * to use the {@code TIME_MASK} when getting the actual timestamp value diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index a84351dc92..9c2992d775 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -87,12 +87,12 @@ public enum Interpolation { public static final Aggregator ZIMSUM = new Sum( Interpolation.ZIM, "zimsum"); - /** Returns the minimum data point, causing SpanGroup to set .MaxValue + /** Returns the minimum data point, causing SpanGroup to set <type>.MaxValue * if timestamps don't line up instead of interpolating. */ public static final Aggregator MIMMIN = new Min( Interpolation.MAX, "mimmin"); - /** Returns the maximum data point, causing SpanGroup to set .MinValue + /** Returns the maximum data point, causing SpanGroup to set <type>.MinValue * if timestamps don't line up instead of interpolating. */ public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); diff --git a/src/core/HistogramCodecManager.java b/src/core/HistogramCodecManager.java index faf4518dc0..8376e4c1b4 100644 --- a/src/core/HistogramCodecManager.java +++ b/src/core/HistogramCodecManager.java @@ -195,7 +195,6 @@ public byte[] encode(final int id, * @param id The ID of the histogram type to search for. * @param raw_data The non-null and non-empty byte array to parse. Should NOT * include the first byte of the ID in the data. - * @param timestamp The timestamp associated with the data point. * @param includes_id Whether or not the data includes the ID prefix. * @return A non-null data point if decoding was successful. */ diff --git a/src/core/HistogramDataPointCodec.java b/src/core/HistogramDataPointCodec.java index 0956b989ba..7ac01c0cd2 100644 --- a/src/core/HistogramDataPointCodec.java +++ b/src/core/HistogramDataPointCodec.java @@ -46,7 +46,6 @@ public void setId(final int id) { * Creates {@code HistogramDataPoint} from raw data and timestamp. Note that * the data point identifier is separate. * @param raw_data The encoded byte array of the histogram data - * @param timestamp The timestamp of this data point * @param includes_id Whether or not to include the id prefix. * @return The decoded histogram data point instance */ diff --git a/src/core/Internal.java b/src/core/Internal.java index fb0292d671..3d1b82171e 100644 --- a/src/core/Internal.java +++ b/src/core/Internal.java @@ -30,8 +30,10 @@ /** * This class is not part of the public API. - *

    - * ,____________________________,
    + */
    +
    +/**-
    + *  ,____________________________,
      * | This class is reserved for |
      * | OpenTSDB's internal usage! |
      * `----------------------------'
    @@ -52,8 +54,10 @@
      *                ///-._ _ _ _ _ _ _}^ - - - - ~                     ~-- ,.-~
      *                                                                   /.-~
      *              You've been warned by the dragon!
    - * 

    - * This class is reserved for OpenTSDB's own internal usage only. If you use + * + */ + +/** This class is reserved for OpenTSDB's own internal usage only. If you use * anything from this package outside of OpenTSDB, a dragon will spontaneously * appear and eat you. You've been warned. *

    @@ -816,7 +820,7 @@ public static byte[] extractQualifier(final byte[] qualifier, * the timestamp is in seconds, this returns a 2 byte qualifier. If it's in * milliseconds, returns a 4 byte qualifier * @param timestamp A Unix epoch timestamp in seconds or milliseconds - * @param flags Flags to set on the qualifier (length &| float) + * @param flags Flags to set on the qualifier (length &| float) * @return A 2 or 4 byte qualifier for storage in column or compacted column * @since 2.0 */ @@ -912,7 +916,7 @@ public static void createAndSetTSUIDFilter(final Scanner scanner, * Simple helper to calculate the max value for any width of long from 0 to 8 * bytes. * @param width The width of the byte array we're comparing - * @return The maximum unsigned integer value on {@link width} bytes. Note: + * @return The maximum unsigned integer value on {@code width} bytes. Note: * If you ask for 8 bytes, it will return the max signed value. This is due * to Java lacking unsigned integers... *sigh*. * @since 2.2 @@ -1051,8 +1055,6 @@ public static long getTimeStampFromNonDP(final long base_time, byte[] quantifier /** * Decode the histogram point from the given key value * @param kv the key value that contains a histogram - * @param config config object of TSDB, will use {@code "tsd.core.hist_decoder"} - * to get the decoder * @return the decoded {@code HistogramDataPoint} */ public static HistogramDataPoint decodeHistogramDataPoint(final TSDB tsdb, diff --git a/src/core/RateOptions.java b/src/core/RateOptions.java index 07ae817f6a..aa1370d37c 100644 --- a/src/core/RateOptions.java +++ b/src/core/RateOptions.java @@ -19,7 +19,7 @@ * options are useful when working with metrics that are raw counter values, * where a counter is defined by a value that always increases until it hits * a maximum value and then it "rolls over" to start back at 0. - *

    + *

    * These options will only be utilized if the query is for a rate calculation * and if the "counter" options is set to true. * @since 2.0 diff --git a/src/core/RowKey.java b/src/core/RowKey.java index 3563d8e566..2a8a5f7064 100644 --- a/src/core/RowKey.java +++ b/src/core/RowKey.java @@ -15,6 +15,8 @@ import java.util.Arrays; import java.util.Comparator; +import net.opentsdb.uid.NoSuchUniqueId; + import org.hbase.async.Bytes; import com.stumbleupon.async.Deferred; @@ -128,7 +130,7 @@ public static byte[] getSaltBytes(final int bucket) { * tag UIDs and returning a modulo based on the number of salt buckets. * The result will always be a positive integer from 0 to salt buckets. * - * NOTE: The row key passed in MUST have allocated the {@link width} number of + * NOTE: The row key passed in MUST have allocated the {@code width} number of * bytes at the front of the row key or this call will overwrite data. * * WARNING: If the width is set to a positive value, then the bucket must be diff --git a/src/core/Span.java b/src/core/Span.java index c54954cf47..ad413193db 100644 --- a/src/core/Span.java +++ b/src/core/Span.java @@ -21,6 +21,7 @@ import net.opentsdb.meta.Annotation; import net.opentsdb.rollup.RollupQuery; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import org.hbase.async.Bytes; diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 98b52869e6..34a6997e3f 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -42,6 +42,7 @@ import org.hbase.async.HBaseException; import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; +import org.hbase.async.TableNotFoundException; import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.Timeout; import org.jboss.netty.util.Timer; diff --git a/src/core/TSSubQuery.java b/src/core/TSSubQuery.java index 4adf11ff8c..1803364142 100644 --- a/src/core/TSSubQuery.java +++ b/src/core/TSSubQuery.java @@ -457,7 +457,7 @@ public void setFilters(List filters) { this.filters = filters; } - /** @param whether or not to match series with ONLY the given tags + /** @param explicit_tags whether or not to match series with ONLY the given tags * @since 2.3 */ public void setExplicitTags(final boolean explicit_tags) { this.explicit_tags = explicit_tags; diff --git a/src/core/WriteableDataPointFilterPlugin.java b/src/core/WriteableDataPointFilterPlugin.java index 0d97dd2bc9..3202951ede 100644 --- a/src/core/WriteableDataPointFilterPlugin.java +++ b/src/core/WriteableDataPointFilterPlugin.java @@ -42,7 +42,7 @@ public abstract class WriteableDataPointFilterPlugin { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/meta/MetaDataCache.java b/src/meta/MetaDataCache.java index 96504b1ce7..05efcc0d8e 100644 --- a/src/meta/MetaDataCache.java +++ b/src/meta/MetaDataCache.java @@ -38,7 +38,7 @@ public abstract class MetaDataCache { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/meta/TSMeta.java b/src/meta/TSMeta.java index cefd86c02b..517a3247c9 100644 --- a/src/meta/TSMeta.java +++ b/src/meta/TSMeta.java @@ -22,6 +22,8 @@ import java.util.Map; import net.opentsdb.core.TSDB; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.JSON; diff --git a/src/meta/TSUIDQuery.java b/src/meta/TSUIDQuery.java index 95d51f93f7..51eb4667c0 100644 --- a/src/meta/TSUIDQuery.java +++ b/src/meta/TSUIDQuery.java @@ -25,6 +25,7 @@ import net.opentsdb.core.RowKey; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; @@ -523,7 +524,7 @@ public String toString() { /** * Attempts to retrieve the last data point for the given TSUID. - * This operates by checking the meta table for the {@link #COUNTER_QUALIFIER} + * This operates by checking the meta table for the {@code COUNTER_QUALIFIER} * and if found, parses the HBase timestamp for the counter (i.e. the time when * the counter was written) and tries to load the row in the data table for * the hour where that timestamp would have landed. If the counter does not diff --git a/src/meta/UIDMeta.java b/src/meta/UIDMeta.java index e96eb25aec..9d2f9a29ca 100644 --- a/src/meta/UIDMeta.java +++ b/src/meta/UIDMeta.java @@ -36,6 +36,7 @@ import com.stumbleupon.async.Deferred; import net.opentsdb.core.TSDB; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.JSON; diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index c7b9782b2a..203616bd22 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -183,7 +183,7 @@ public static String getRowKeyUIDRegex( * be null. * @param row_key_literals An optional list of key value pairs to filter on. * May be null. - * @param explicit_tag sWhether or not explicit tags are enabled so that the + * @param explicit_tags Whether or not explicit tags are enabled so that the * regex only picks out series with the specified tags * @param enable_fuzzy_filter Whether or not a fuzzy filter should be used * in combination with the explicit tags param. If explicit tags is disabled diff --git a/src/query/expression/ExpressionIterator.java b/src/query/expression/ExpressionIterator.java index dc32576708..76a93b860c 100644 --- a/src/query/expression/ExpressionIterator.java +++ b/src/query/expression/ExpressionIterator.java @@ -21,11 +21,13 @@ import java.util.Set; import net.opentsdb.core.FillPolicy; +import net.opentsdb.core.IllegalDataException; import net.opentsdb.query.expression.VariableIterator.SetOperator; import net.opentsdb.utils.ByteSet; import org.apache.commons.jexl2.JexlContext; import org.apache.commons.jexl2.JexlEngine; +import org.apache.commons.jexl2.JexlException; import org.apache.commons.jexl2.MapContext; import org.apache.commons.jexl2.Script; import org.apache.commons.jexl2.scripting.JexlScriptEngineFactory; @@ -46,7 +48,7 @@ * intersection of the series. * - Call {@link #values()} and store the reference. Results for each * series will be written here as you iterate. - * - Call {@link #hasNext()} and {@link #next()} to iterate over results. + * - Call {@link #hasNext()} and {@link #next(int)} to iterate over results. * - At each iteration, fetch the timestamp and value from the data points array. *

    * Iteration is performed across all series supplied to the iterator, synchronizing @@ -356,7 +358,7 @@ public ExpressionDataPoint[] next(final long timestamp) { } /** @return a list of expression results. You can keep this list and check the - * results on each call to {@link #next()} */ + * results on each call to {@link #next(int)} */ @Override public ExpressionDataPoint[] values() { return dps; diff --git a/src/query/expression/ExpressionReader.java b/src/query/expression/ExpressionReader.java index 01cf7e0105..68805dd90f 100644 --- a/src/query/expression/ExpressionReader.java +++ b/src/query/expression/ExpressionReader.java @@ -61,7 +61,7 @@ public char next() { return chars[mark++]; } - /** @param the number of characters to skip */ + /** @param num the number of characters to skip */ public void skip(final int num) { if (num < 0) { throw new UnsupportedOperationException("Skipping backwards is not allowed"); diff --git a/src/query/expression/Expressions.java b/src/query/expression/Expressions.java index 1d7e2de954..e49943d6b6 100644 --- a/src/query/expression/Expressions.java +++ b/src/query/expression/Expressions.java @@ -70,7 +70,7 @@ public static ExpressionTree parse(final String expression, /** * Parses a list of string expressions into the proper trees, adding the - * metrics to the {@link metric_queries} list. + * metrics to the {@code metric_queries} list. * @param expressions A list of zero or more expressions (if empty, you get an * empty tree list back) * @param ts_query The original query with timestamps diff --git a/src/query/expression/ITimeSyncedIterator.java b/src/query/expression/ITimeSyncedIterator.java index b72a0df401..77aaec3eae 100644 --- a/src/query/expression/ITimeSyncedIterator.java +++ b/src/query/expression/ITimeSyncedIterator.java @@ -65,7 +65,7 @@ public interface ITimeSyncedIterator { /** @return the index in the ExpressionIterator */ public int getIndex(); - /** @param the index in the ExpressionIterator */ + /** @param index the index in the ExpressionIterator */ public void setIndex(final int index); /** @return the ID of this set given by the user */ @@ -75,7 +75,7 @@ public interface ITimeSyncedIterator { * were defined then the set may be empty. */ public ByteSet getQueryTagKs(); - /** @param A fill policy for the iterator. Iterators should implement a default */ + /** @param policy A fill policy for the iterator. Iterators should implement a default */ public void setFillPolicy(final NumericFillPolicy policy); /** @return the fill policy for the iterator */ diff --git a/src/query/expression/IntersectionIterator.java b/src/query/expression/IntersectionIterator.java index 40d01068a0..0c5c4213e3 100644 --- a/src/query/expression/IntersectionIterator.java +++ b/src/query/expression/IntersectionIterator.java @@ -43,8 +43,8 @@ *

    * The {@link #current_values} map will map the expression "variables" to the * proper iterator for each serie's array. E.g. - * <"A", [1, 2, 3, 4]> - * <"B", [1, 2, 3, 4]> + * <"A", [1, 2, 3, 4]> + * <"B", [1, 2, 3, 4]> *

    * So to use it's you simply fetch the result map, call {@link #hasNext()} and * {@link #next()} to iterate and in a for loop, iterate {@link #getSeriesSize()} diff --git a/src/query/expression/NumericFillPolicy.java b/src/query/expression/NumericFillPolicy.java index cf0f4673d7..1ea9fa9ba6 100644 --- a/src/query/expression/NumericFillPolicy.java +++ b/src/query/expression/NumericFillPolicy.java @@ -60,7 +60,7 @@ public String toString() { return "policy=" + policy + ", value=" + value; } - /** @returns a NumericFillPolicy builder */ + /** @return a NumericFillPolicy builder */ public static Builder Builder() { return new Builder(); } diff --git a/src/query/expression/VariableIterator.java b/src/query/expression/VariableIterator.java index 9bfba0b889..82fedcfce6 100644 --- a/src/query/expression/VariableIterator.java +++ b/src/query/expression/VariableIterator.java @@ -82,7 +82,7 @@ public static SetOperator fromString(final String name) { public void next(); /** - * Determines whether the individual series in the {@link values} array has + * Determines whether the individual series in the {@code values} array has * another value. This may be used for non-synchronous iteration. * @param index The index of the series in the values array to check for * @return True if the series has another value, false if not @@ -90,7 +90,7 @@ public static SetOperator fromString(final String name) { public boolean hasNext(final int index); /** - * Fetches the next value for an individual series in the {@link values} array. + * Fetches the next value for an individual series in the {@code values} array. * @param index The index of the series in the values array to advance */ public void next(final int index); diff --git a/src/query/filter/TagVFilter.java b/src/query/filter/TagVFilter.java index 611a6c8266..94c38f5d2f 100644 --- a/src/query/filter/TagVFilter.java +++ b/src/query/filter/TagVFilter.java @@ -141,7 +141,7 @@ public TagVFilter() { * The ctor that validates we have a good tag key to work with * @param tagk The tag key to associate with this filter * @param filter The unparsed filter - * @throws IlleglArgumentException if the tag was empty or null. + * @throws IllegalArgumentException if the tag was empty or null. */ public TagVFilter(final String tagk, final String filter) { this.tagk = tagk; @@ -161,7 +161,7 @@ public TagVFilter(final String tagk, final String filter) { /** * The name of this filter as used in queries. When used in URL queries the - * value will be in parentheses, e.g. filter() + * value will be in parentheses, e.g. filter(<exp>) * The name will also be lowercased before storing it in the lookup map. * @return The name of the filter. */ @@ -169,7 +169,7 @@ public TagVFilter(final String tagk, final String filter) { /** * A simple string of the filter settings for printing in toString() calls. - * @return A string with the format "{settings=, ...}" + * @return A string with the format "{settings=<val>, ...}" */ @JsonIgnore public abstract String debugInfo(); @@ -492,7 +492,7 @@ public String getTagk() { } /** @return the tag key UID associated with this filter. - * Call {@link resolveName} first */ + * Call {@link #resolveTagkName(TSDB)} first */ @JsonIgnore public byte[] getTagkBytes() { return tagk_bytes; diff --git a/src/query/filter/TagVRegexFilter.java b/src/query/filter/TagVRegexFilter.java index 249ec23ed8..5be4ac2340 100644 --- a/src/query/filter/TagVRegexFilter.java +++ b/src/query/filter/TagVRegexFilter.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import com.google.common.base.Objects; import com.stumbleupon.async.Deferred; diff --git a/src/rollup/RollUpDataPoint.java b/src/rollup/RollUpDataPoint.java index 3ad91e708a..39f816cdca 100644 --- a/src/rollup/RollUpDataPoint.java +++ b/src/rollup/RollUpDataPoint.java @@ -86,8 +86,8 @@ public final String getGroupByAggregator() { return groupby_aggregator; } - /** @param an optional aggregation function if the data point was - * pre-aggregated */ + /** @param groupby_aggregator an optional aggregation function if the data + * point was pre-aggregated */ public final void setGroupByAggregator(final String groupby_aggregator) { this.groupby_aggregator = groupby_aggregator; } diff --git a/src/search/SearchPlugin.java b/src/search/SearchPlugin.java index e72783a152..f1fdbed615 100644 --- a/src/search/SearchPlugin.java +++ b/src/search/SearchPlugin.java @@ -60,7 +60,7 @@ public abstract class SearchPlugin { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/search/TimeSeriesLookup.java b/src/search/TimeSeriesLookup.java index 38b66efb9f..dfc829651c 100644 --- a/src/search/TimeSeriesLookup.java +++ b/src/search/TimeSeriesLookup.java @@ -50,9 +50,9 @@ * This class doesn't handle wild-card searching yet. * * When dealing with tags, we can lookup on tagks, tagvs or pairs. Thus: - * tagk, null <- lookup all series with a tagk - * tagk, tagv <- lookup all series with a tag pair - * null, tagv <- lookup all series with a tag value somewhere + * tagk, null <- lookup all series with a tagk + * tagk, tagv <- lookup all series with a tag pair + * null, tagv <- lookup all series with a tag value somewhere * * The user can supply multiple tags in a query so the logic is a little goofy * but here it is: @@ -114,8 +114,7 @@ public class TimeSeriesLookup { /** * Default ctor * @param tsdb The TSD to which we belong - * @param metric A metric to match on, may be null - * @param tags One or more tags to match on, may be null + * @param query The search query to execute. */ public TimeSeriesLookup(final TSDB tsdb, final SearchQuery query) { this.tsdb = tsdb; diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 1bf23c5d4e..c8db96bfed 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -45,7 +45,7 @@ * stats can be observed via /api/query/stats. * * When a query is executed, it should instantiate an object of this class. - * Once the query is completed, make sure to call {@link markComplete}. + * Once the query is completed, make sure to call {@link #markSent()}. * * The cache will store each query based on the combination of the client, query * and the result code. If the same query was executed multiple times then it @@ -890,7 +890,7 @@ public double getTimeStat(final QueryStat stat) { return DateTime.msFromNano(overall_stats.get(stat)); } - /** @param whether or not to allow duplicate queries to run */ + /** @param enable_dupes whether or not to allow duplicate queries to run */ public static void setEnableDuplicates(final boolean enable_dupes) { ENABLE_DUPLICATES = enable_dupes; } diff --git a/src/tools/ConfigArgP.java b/src/tools/ConfigArgP.java index aff886766a..92779b3348 100644 --- a/src/tools/ConfigArgP.java +++ b/src/tools/ConfigArgP.java @@ -73,9 +73,13 @@ public class ConfigArgP { /** The raw configuration items loaded from the json file */ protected final TreeSet configItemsByCl = new TreeSet(); - /** The regex pattern to perform a substitution for

    ${<sysprop>:<default>}
    patterns in strings */ + /** The regex pattern to perform a substitution for + * ${<sysprop>:<default>} + * patterns in strings */ public static final Pattern SYS_PROP_PATTERN = Pattern.compile("\\$\\{(.*?)(?::(.*?))??\\}"); - /** The regex pattern to perform a substitution for
    $[<javascript snippet>]
    patterns in strings */ + /** The regex pattern to perform a substitution for + * $[<javascript snippet>] + * patterns in strings */ public static final Pattern JS_PATTERN = Pattern.compile("\\$\\[(.*?)\\]", Pattern.MULTILINE); /** The config key for the TSD RPC addin classes */ @@ -503,9 +507,9 @@ public static String evaluate(CharSequence text) { /** * Attempts to decode the passed dot delimited as a system property, and if not found, attempts a decode as an - * environmental variable, replacing the dots with underscores. e.g. for the key: buffer.size.max, - * a system property named buffer.size.max will be looked up, and then an environmental variable - * named buffer.size.max will be looked up. + * environmental variable, replacing the dots with underscores. e.g. for the key: buffer.size.max, + * a system property named buffer.size.max will be looked up, and then an environmental variable + * named buffer.size.max will be looked up. * @param key The dot delimited key to decode * @param defaultValue The default value returned if neither source can decode the key * @return the decoded value or the default value if neither source can decode the key @@ -807,10 +811,10 @@ public boolean isClArg(final String arg) { } /** - * Checks the opentsdb.conf.json document to see if it has a bindings segment + * Checks the opentsdb.conf.json document to see if it has a bindings segment * which contains JS statements to evaluate which will prime variables used by the configuration. * @param jsonMapper The JSON mapper - * @param root The root opentsdb.conf.json document + * @param root The root opentsdb.conf.json document */ protected void processBindings(ObjectMapper jsonMapper, JsonNode root) { String script = null; diff --git a/src/tools/StartupPlugin.java b/src/tools/StartupPlugin.java index cbfa040522..e8aae0d5e5 100644 --- a/src/tools/StartupPlugin.java +++ b/src/tools/StartupPlugin.java @@ -23,8 +23,9 @@ * as soon as it is completely parsed, just before OpenTSDB begins to use it. *

    * Note: Implementations must have a parameterless constructor. The - * {@link #initialize(TSDB)} method will be called immediately after the plugin is + * {@link #initialize(Config)} method will be called immediately after the plugin is * instantiated and before any other methods are called. + * * @since 2.3 */ public abstract class StartupPlugin { @@ -36,7 +37,7 @@ public abstract class StartupPlugin { * Note: Implementations should throw exceptions if they can't start * up properly. The TSD will then shutdown so the operator can fix the * problem. Please use IllegalArgumentException for configuration issues. - * @param tsdb The parent TSDB object + * @param config The OpenTSDDB config object. * @return A reference to the same configuration object passed in the parameters * on success. * @throws IllegalArgumentException if required configuration parameters are diff --git a/src/tree/Leaf.java b/src/tree/Leaf.java index 3614930468..9d1c0817be 100644 --- a/src/tree/Leaf.java +++ b/src/tree/Leaf.java @@ -33,6 +33,7 @@ import com.stumbleupon.async.Deferred; import net.opentsdb.core.TSDB; +import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import net.opentsdb.uid.UniqueId.UniqueIdType; import net.opentsdb.utils.JSON; diff --git a/src/tree/Tree.java b/src/tree/Tree.java index 834555d571..017fdd10c8 100644 --- a/src/tree/Tree.java +++ b/src/tree/Tree.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.regex.PatternSyntaxException; import net.opentsdb.core.TSDB; import net.opentsdb.uid.UniqueId; diff --git a/src/tree/TreeRule.java b/src/tree/TreeRule.java index a864e42325..76d255ad8b 100644 --- a/src/tree/TreeRule.java +++ b/src/tree/TreeRule.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 09f90d3269..f83d7f0682 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -115,7 +115,7 @@ public Channel channel() { return chan; } - /** @return The remote address and port in the format : */ + /** @return The remote address and port in the format <ip>:<port> */ public String getRemoteAddress() { return chan.getRemoteAddress().toString(); } diff --git a/src/tsd/HttpRpcPlugin.java b/src/tsd/HttpRpcPlugin.java index 17c41dc195..940393a68f 100644 --- a/src/tsd/HttpRpcPlugin.java +++ b/src/tsd/HttpRpcPlugin.java @@ -49,7 +49,7 @@ public abstract class HttpRpcPlugin { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception + * @throws RuntimeException */ public abstract void initialize(TSDB tsdb); @@ -89,10 +89,10 @@ public abstract class HttpRpcPlugin { * plugin will fail to load. * *

    Here are some examples where - * path --(is available at)--> server path + * path --(is available at)--> server path *

      - *
    • /myAwesomePlugin --> /plugin/myAwesomePlugin - *
    • /myOtherPlugin/operation --> /plugin/myOtherPlugin/operation + *
    • /myAwesomePlugin --> /plugin/myAwesomePlugin + *
    • /myOtherPlugin/operation --> /plugin/myOtherPlugin/operation *
    * * @return a slash separated path @@ -101,8 +101,8 @@ public abstract class HttpRpcPlugin { /** * Executes the plugin for the given query received on the path derived from - * {@link #getPath()}. This method will be called by multiple threads - * simultaneously and must be thread-safe. + * {@link #getPath()}. This method will be called by multiple threads + * simultaneously and must be thread-safe. * * @param tsdb the owning TSDB instance. * @param query the parsed query diff --git a/src/tsd/HttpSerializer.java b/src/tsd/HttpSerializer.java index 8b9a343c9b..f6108466f3 100644 --- a/src/tsd/HttpSerializer.java +++ b/src/tsd/HttpSerializer.java @@ -44,6 +44,7 @@ import net.opentsdb.tsd.AnnotationRpc.AnnotationBulkDelete; import net.opentsdb.tsd.QueryRpc.LastPointQuery; import net.opentsdb.utils.Config; +import net.opentsdb.utils.JSONException; /** * Abstract base class for Serializers; plugins that handle converting requests @@ -329,7 +330,7 @@ public List parseTreeRulesV1() { * Parses a tree ID and optional list of TSUIDs to search for collisions or * not matched TSUIDs. * @return A map with "treeId" as an integer and optionally "tsuids" as a - * List + * List<String> * @throws BadRequestException if the plugin has not implemented this method */ public Map parseTreeTSUIDsListV1() { @@ -380,7 +381,7 @@ public AnnotationBulkDelete parseAnnotationBulkDeleteV1() { * @param results A map of results. The map will consist of: *
    • success - (long) the number of successfully parsed datapoints
    • *
    • failed - (long) the number of datapoint parsing failures
    • - *
    • errors - (ArrayList>) an optional list of + *
    • errors - (ArrayList<HashMap<String, Object>>) an optional list of * datapoints that had errors. The nested map has these fields: *
      • error - (String) the error that occurred
      • *
      • datapoint - (IncomingDatapoint) the datapoint that generated the error @@ -563,7 +564,7 @@ public ChannelBuffer formatTSMetaV1(final TSMeta meta) { /** * Format a a list of TSMeta objects - * @param meta The list of TSMeta objects to serialize + * @param metas The list of TSMeta objects to serialize * @return A JSON structure * @throws JSONException if serialization failed */ @@ -650,7 +651,7 @@ public ChannelBuffer formatTreeCollisionNotMatchedV1( * @param results The list of results. Main map key is the tsuid. Child map: * "branch" : Parsed branch result, may be null * "meta" : TSMeta object, may be null - * "messages" : An ArrayList of one or more messages + * "messages" : An ArrayList<String> of one or more messages * @return A ChannelBuffer object to pass on to the caller * @throws BadRequestException if the plugin has not implemented this method */ @@ -690,7 +691,7 @@ public ChannelBuffer formatAnnotationsV1(final List notes) { /** * Format the results of a bulk annotation deletion - * @param notes The annotation deletion request to return + * @param request The request to handle. * @return A ChannelBuffer object to pass on to the caller * @throws BadRequestException if the plugin has not implemented this method */ diff --git a/src/tsd/PipelineFactory.java b/src/tsd/PipelineFactory.java index 063f6a3c6f..5e9544d1c4 100644 --- a/src/tsd/PipelineFactory.java +++ b/src/tsd/PipelineFactory.java @@ -68,7 +68,7 @@ public final class PipelineFactory implements ChannelPipelineFactory { * plugins. This constructor creates its own {@link RpcManager}. * @param tsdb The TSDB to use. * @throws RuntimeException if there is an issue loading plugins - * @throws Exception if the HttpQuery handler is unable to load + * @throws RuntimeException if the HttpQuery handler is unable to load * serializers */ public PipelineFactory(final TSDB tsdb) { @@ -82,7 +82,7 @@ public PipelineFactory(final TSDB tsdb) { * @param tsdb The TSDB to use. * @param manager instance of a ready-to-use {@link RpcManager}. * @throws RuntimeException if there is an issue loading plugins - * @throws Exception if the HttpQuery handler is unable to load serializers + * throws Exception if the HttpQuery handler is unable to load serializers */ public PipelineFactory(final TSDB tsdb, final RpcManager manager) { this(tsdb, RpcManager.instance(tsdb), @@ -97,7 +97,7 @@ public PipelineFactory(final TSDB tsdb, final RpcManager manager) { * @param connections_limit The maximum number of concurrent connections * supported by the TSD. * @throws RuntimeException if there is an issue loading plugins - * @throws Exception if the HttpQuery handler is unable to load serializers + * throws Exception if the HttpQuery handler is unable to load serializers * @since 2.3 */ public PipelineFactory(final TSDB tsdb, final RpcManager manager, diff --git a/src/tsd/RTPublisher.java b/src/tsd/RTPublisher.java index 877c14d784..194a36e353 100644 --- a/src/tsd/RTPublisher.java +++ b/src/tsd/RTPublisher.java @@ -48,7 +48,7 @@ public abstract class RTPublisher { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/tsd/RpcPlugin.java b/src/tsd/RpcPlugin.java index 8b053945cf..fd97ea9759 100644 --- a/src/tsd/RpcPlugin.java +++ b/src/tsd/RpcPlugin.java @@ -45,7 +45,7 @@ public abstract class RpcPlugin { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/tsd/StorageExceptionHandler.java b/src/tsd/StorageExceptionHandler.java index 07b2e15feb..7c7a2ee42a 100644 --- a/src/tsd/StorageExceptionHandler.java +++ b/src/tsd/StorageExceptionHandler.java @@ -40,7 +40,7 @@ public abstract class StorageExceptionHandler { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/uid/RandomUniqueId.java b/src/uid/RandomUniqueId.java index ef4fa5ce10..58e560dfc4 100644 --- a/src/uid/RandomUniqueId.java +++ b/src/uid/RandomUniqueId.java @@ -40,7 +40,7 @@ public class RandomUniqueId { * and 2^31-1. * NOTE: The caller is responsible for assuring that the UID hasn't been * assigned yet. - * @return a random UID up to {@link TSDB.metrics_width} wide + * @return a random UID up to {@link TSDB#metrics_width()} wide */ public static long getRandomUID() { return getRandomUID(TSDB.metrics_width()); @@ -52,7 +52,7 @@ public static long getRandomUID() { * @param width Number of bytes to randomize, it can not be larger * than {@link MAX_WIDTH} bytes wide * @return a randomly UID - * @throws throws IllegalArgumentException if the width is larger than + * @throws IllegalArgumentException if the width is larger than * {@link MAX_WIDTH} bytes */ public static long getRandomUID(final int width) { diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 22a384fc82..5585d26055 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -144,7 +144,7 @@ public UniqueId(final HBaseClient client, final byte[] table, final String kind, * @param table The name of the HBase table to use. * @param kind The kind of Unique ID this instance will deal with. * @param width The number of bytes on which Unique IDs should be encoded. - * @param Whether or not to randomize new UIDs + * @param randomize_id Whether or not to randomize new UIDs * @throws IllegalArgumentException if width is negative or too small/large * or if kind is an empty string. * @since 2.2 @@ -171,7 +171,7 @@ public UniqueId(final HBaseClient client, final byte[] table, final String kind, * @param table The name of the HBase table to use. * @param kind The kind of Unique ID this instance will deal with. * @param width The number of bytes on which Unique IDs should be encoded. - * @param Whether or not to randomize new UIDs + * @param randomize_id Whether or not to randomize new UIDs * @throws IllegalArgumentException if width is negative or too small/large * or if kind is an empty string. * @since 2.3 @@ -233,7 +233,7 @@ public void setTSDB(final TSDB tsdb) { /** The largest possible ID given the number of bytes the IDs are * represented on. - * @deprecated Use {@link Internal.getMaxUnsignedValueOnBytes} + * @deprecated Use {@link Internal#getMaxUnsignedValueOnBytes(int)} */ public long maxPossibleId() { return Internal.getMaxUnsignedValueOnBytes(id_width); @@ -1623,7 +1623,7 @@ public Map call(final ArrayList row) * @param uid_cache_map A map of {@link UniqueId} objects keyed on the kind. * @throws HBaseException Passes any HBaseException from HBase scanner. * @throws RuntimeException Wraps any non HBaseException from HBase scanner. - * @2.1 + * @since 2.1 */ public static void preloadUidCache(final TSDB tsdb, final ByteMap uid_cache_map) throws HBaseException { diff --git a/src/uid/UniqueIdFilterPlugin.java b/src/uid/UniqueIdFilterPlugin.java index b0fd0c5e2a..60006c0742 100644 --- a/src/uid/UniqueIdFilterPlugin.java +++ b/src/uid/UniqueIdFilterPlugin.java @@ -43,7 +43,7 @@ public abstract class UniqueIdFilterPlugin { * @param tsdb The parent TSDB object * @throws IllegalArgumentException if required configuration parameters are * missing - * @throws Exception if something else goes wrong + * @throws RuntimeException if something else goes wrong */ public abstract void initialize(final TSDB tsdb); diff --git a/src/utils/Config.java b/src/utils/Config.java index 3e425aa6be..e15a106842 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -38,7 +38,7 @@ * Wherever you need to access the config value, use the proper helper to fetch * the value, accounting for exceptions that may be thrown if necessary. * - * The get number helpers will return NumberFormatExceptions if the + * The get<type> number helpers will return NumberFormatExceptions if the * requested property is null or unparseable. The {@link #getString(String)} * helper will return a NullPointerException if the property isn't found. *

        diff --git a/src/utils/DateTime.java b/src/utils/DateTime.java index 3d0e1eed52..4649fc3097 100644 --- a/src/utils/DateTime.java +++ b/src/utils/DateTime.java @@ -342,7 +342,7 @@ public static void setDefaultTimezone(final String tzname) { } /** - * Pass through to {@link System.currentTimeMillis} for use in classes to + * Pass through to {@link System#currentTimeMillis()} for use in classes to * make unit testing easier. Mocking System.class is a bad idea in general * so placing this here and mocking DateTime.class is MUCH cleaner. * @return The current epoch time in milliseconds @@ -353,7 +353,7 @@ public static long currentTimeMillis() { } /** - * Pass through to {@link System.nanoTime} for use in classes to + * Pass through to {@link System#nanoTime()} for use in classes to * make unit testing easier. Mocking System.class is a bad idea in general * so placing this here and mocking DateTime.class is MUCH cleaner. * @return The current epoch time in milliseconds @@ -608,7 +608,7 @@ public static Calendar previousInterval(final long ts, final int interval, /** * Return the proper Calendar time unit as an integer given the string * @param units The unit to parse - * @return An integer matching a Calendar. enum + * @return An integer matching a Calendar.<UNIT> enum * @throws IllegalArgumentException if the unit is null, empty or doesn't * match one of the configured units. * @since 2.3 diff --git a/src/utils/JSON.java b/src/utils/JSON.java index dd72c12125..eef88b4f20 100644 --- a/src/utils/JSON.java +++ b/src/utils/JSON.java @@ -269,7 +269,6 @@ public static final JsonParser parseToStream(final InputStream json) { * @return A JSON formatted string * @throws IllegalArgumentException if the object was null * @throws JSONException if the object could not be serialized - * @throws IOException Thrown when there was an issue reading the object */ public static final String serializeToString(final Object object) { if (object == null) @@ -287,7 +286,6 @@ public static final String serializeToString(final Object object) { * @return A JSON formatted byte array * @throws IllegalArgumentException if the object was null * @throws JSONException if the object could not be serialized - * @throws IOException Thrown when there was an issue reading the object */ public static final byte[] serializeToBytes(final Object object) { if (object == null) @@ -301,7 +299,7 @@ public static final byte[] serializeToBytes(final Object object) { /** * Serializes the given object and wraps it in a callback function - * i.e. <callback>(<json>) + * i.e. <callback>(<json>) * Note: This will not append a trailing semicolon * @param callback The name of the Javascript callback to prepend * @param object The object to serialize @@ -309,7 +307,6 @@ public static final byte[] serializeToBytes(final Object object) { * @throws IllegalArgumentException if the callback method name was missing * or object was null * @throws JSONException if the object could not be serialized - * @throws IOException Thrown when there was an issue reading the object */ public static final String serializeToJSONPString(final String callback, final Object object) { @@ -326,7 +323,7 @@ public static final String serializeToJSONPString(final String callback, /** * Serializes the given object and wraps it in a callback function - * i.e. <callback>(<json>) + * i.e. <callback>(<json>) * Note: This will not append a trailing semicolon * @param callback The name of the Javascript callback to prepend * @param object The object to serialize @@ -334,7 +331,6 @@ public static final String serializeToJSONPString(final String callback, * @throws IllegalArgumentException if the callback method name was missing * or object was null * @throws JSONException if the object could not be serialized - * @throws IOException Thrown when there was an issue reading the object */ public static final byte[] serializeToJSONPBytes(final String callback, final Object object) { diff --git a/src/utils/PluginLoader.java b/src/utils/PluginLoader.java index d66c75f0c9..2d0b8b3bc5 100644 --- a/src/utils/PluginLoader.java +++ b/src/utils/PluginLoader.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import org.slf4j.Logger; @@ -88,7 +89,7 @@ public final class PluginLoader { * @return An instantiated object of the given type if found, null if the * class could not be found * @throws ServiceConfigurationError if the plugin cannot be instantiated - * @throws IllegalArgumentName if the plugin name is null or empty + * @throws IllegalArgumentException if the plugin name is null or empty */ public static T loadSpecificPlugin(final String name, final Class type) { From f371ba02ba296125b334cb45b0d210e3f5dd3a1a Mon Sep 17 00:00:00 2001 From: Chris Larsen Date: Fri, 11 Aug 2017 15:45:58 -0700 Subject: [PATCH 665/826] Fix #12 by switching to the straight HTML DOCTYPE for the built-in UI. Signed-off-by: Chris Larsen --- src/tsd/HttpQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index 49c30268d6..2a7d81fa44 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -957,7 +957,7 @@ protected Logger logger() { // -------------------------------------------- // private static final String PAGE_HEADER_START = - "" + "" + "" + "" + ""; From e7d58270d11958fb9729f6a14512c44ee5870a84 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Fri, 11 Aug 2017 22:58:28 -0700 Subject: [PATCH 666/826] Fix the UT's after changing the raw HTML header. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- test/tsd/TestHttpQuery.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/tsd/TestHttpQuery.java b/test/tsd/TestHttpQuery.java index a87fa66e3b..5660ce0461 100644 --- a/test/tsd/TestHttpQuery.java +++ b/test/tsd/TestHttpQuery.java @@ -791,9 +791,9 @@ public void internalErrorDeprecated() { assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR, query.response().getStatus()); assertEquals( - "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">", + "<!DOCTYPE html>", query.response().getContent().toString(Charset.forName("UTF-8")) - .substring(0, 63)); + .substring(0, 15)); } @Test @@ -845,9 +845,9 @@ public void badRequestDeprecated() { } assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); assertEquals( - "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">", + "<!DOCTYPE html>", query.response().getContent().toString(Charset.forName("UTF-8")) - .substring(0, 63)); + .substring(0, 15)); } @Test @@ -926,9 +926,9 @@ public void badRequestDeprecatedString() { query.badRequest("Bad user error"); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); assertEquals( - "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">", + "<!DOCTYPE html>", query.response().getContent().toString(Charset.forName("UTF-8")) - .substring(0, 63)); + .substring(0, 15)); } @Test @@ -967,9 +967,9 @@ public void notFoundDeprecated() { query.notFound(); assertEquals(HttpResponseStatus.NOT_FOUND, query.response().getStatus()); assertEquals( - "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">", + "<!DOCTYPE html>", query.response().getContent().toString(Charset.forName("UTF-8")) - .substring(0, 63)); + .substring(0, 15)); } @Test From 7bc9075d215debec1b83b5960ae6c058599631ff Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 12 Aug 2017 10:36:04 -0700 Subject: [PATCH 667/826] Remove Oracle JDK7 from the travis build as the installer was EOL'd by Oracle. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index be2280bb9c..0c51b3a4f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet addons: hostname: short-hostname jdk: - - oraclejdk7 - oraclejdk8 notifications: email: false From 37297587b7b2b9b377a39607cce3020da4e83cb5 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Fri, 11 Aug 2017 22:52:59 -0700 Subject: [PATCH 668/826] Add a limit to the number of bytes or data points retrieved from storage per metric in each query. This will help keep massive queries from OOMing TSDs by returning an error to the caller asking them to adjust their query. Add the QueryLimitOverride class that allows loading overrides to the query limits from a JSON config file using regular expressions to match on a metric. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/SaltScanner.java | 67 ++++- src/core/TSDB.java | 13 +- src/core/TSQuery.java | 29 ++ src/core/TsdbQuery.java | 37 ++- src/query/QueryLimitOverride.java | 337 ++++++++++++++++++++++++ src/utils/Config.java | 5 + test/core/TestSaltScanner.java | 36 +++ test/core/TestSaltScannerHistogram.java | 8 +- test/core/TestTsdbQuery.java | 74 ++++++ test/query/TestQueryLimitOverride.java | 318 ++++++++++++++++++++++ 10 files changed, 913 insertions(+), 11 deletions(-) create mode 100644 src/query/QueryLimitOverride.java create mode 100644 test/query/TestQueryLimitOverride.java diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 68086fdc3f..a09a5d1d74 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -22,6 +22,8 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import net.opentsdb.meta.Annotation; import net.opentsdb.query.filter.TagVFilter; @@ -34,6 +36,7 @@ import net.opentsdb.utils.JSON; import org.hbase.async.Bytes.ByteMap; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; import org.hbase.async.KeyValue; @@ -113,6 +116,13 @@ public class SaltScanner { private final int rollup_agg_id; private final int rollup_count_id; + /** Settings and counters to determine when we need to cancel a query. */ + private final AtomicLong num_data_points; + private final AtomicBoolean max_data_points_flag; + private AtomicLong bytes_fetched = new AtomicLong(); + private final long max_data_points; + private final long max_bytes; + /** A latch used to determine how many scanners are still running */ private final CountDownLatch countdown; @@ -149,7 +159,7 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final List<Scanner> scanners, final TreeMap<byte[], Span> spans, final List<TagVFilter> filters) { - this(tsdb, metric, scanners, spans, filters, false, null, null, 0, null); + this(tsdb, metric, scanners, spans, filters, false, null, null, 0, null, 0, 0); } /** @@ -165,6 +175,10 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, * @param query_stats A stats object for tracking timing * @param query_index The index of the sub query in the main query list * @param histogramSpans The histo map to populate. + * @param max_bytes The maximum number of bytes pulled out from all scanners + * combined. + * @param max_data_points The maximum number of data points pulled out from all + * scanners (estimated). * @throws IllegalArgumentException if any required data was missing or * we had invalid parameters. */ @@ -176,7 +190,9 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, final RollupQuery rollup_query, final QueryStats query_stats, final int query_index, - final TreeMap<byte[], HistogramSpan> histogramSpans) { + final TreeMap<byte[], HistogramSpan> histogramSpans, + final long max_bytes, + final long max_data_points) { if (tsdb == null) { throw new IllegalArgumentException("The TSDB argument was null."); } @@ -234,6 +250,11 @@ public SaltScanner(final TSDB tsdb, final byte[] metric, is_rollup = false; rollup_agg_id = rollup_count_id = -1; } + this.max_bytes = max_bytes; + this.max_data_points = max_data_points; + num_data_points = new AtomicLong(); + bytes_fetched = new AtomicLong(); + max_data_points_flag = new AtomicBoolean(); } /** @@ -532,9 +553,46 @@ public Object call(final ArrayList<ArrayList<KeyValue>> rows) filters != null && !filters.isEmpty() ? new ArrayList<Deferred<Object>>(rows.size()) : null; + // 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 + // end of a scan call. + if (max_data_points > 0 && num_data_points.get() >= max_data_points) { + max_data_points_flag.getAndSet(true); + try { + close(false); + handleException( + new QueryException(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, + "Sorry, you have attempted to fetch more than our limit of " + + max_data_points + " data points. 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; + } + } + + if (max_bytes > 0 && bytes_fetched.get() > max_bytes) { + max_data_points_flag.getAndSet(true); + try { + close(false); + handleException( + new QueryException(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, + "Sorry, you have attempted to fetch more than our maximum " + + "amount of " + (max_bytes / 1024 / 1024) + "MB from storage. " + + "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; + } + } + rows_pre_filter += rows.size(); for (final ArrayList<KeyValue> row : rows) { final byte[] key = row.get(0).key(); + num_data_points.addAndGet(row.size()); if (RowKey.rowKeyContainsMetric(metric, key) != 0) { close(false); handleException(new IllegalDataException( @@ -547,7 +605,12 @@ public Object call(final ArrayList<ArrayList<KeyValue>> rows) // calculate estimated data point count. We don't want to deserialize // the byte arrays so we'll just get a rough estimate of compacted // columns. + long bytes = 0; for (final KeyValue kv : row) { + // rough estimate of the # of bytes returned from storage. + bytes += key.length + kv.qualifier().length | kv.value().length; + bytes_fetched.addAndGet(bytes); + if (kv.qualifier().length % 2 == 0) { if (kv.qualifier().length == 2 || kv.qualifier().length == 4) { ++dps_pre_filter; diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 34a6997e3f..50a458af56 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// 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 @@ -65,6 +65,7 @@ import net.opentsdb.meta.MetaDataCache; import net.opentsdb.meta.TSMeta; import net.opentsdb.meta.UIDMeta; +import net.opentsdb.query.QueryLimitOverride; import net.opentsdb.query.expression.ExpressionFactory; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.rollup.RollupConfig; @@ -178,6 +179,9 @@ public final class TSDB { * {@link #initializePlugins(boolean)} was called.*/ private HistogramCodecManager histogram_manager; + /** A list of query overrides for the scanners */ + private final QueryLimitOverride query_limits; + /** Writes rejected by the filter */ private final AtomicLong rejected_dps = new AtomicLong(); private final AtomicLong rejected_aggregate_dps = new AtomicLong(); @@ -310,6 +314,8 @@ public TSDB(final HBaseClient client, final Config config) { if (config.getString("tsd.core.tag.allow_specialchars") != null) { Tags.setAllowSpecialChars(config.getString("tsd.core.tag.allow_specialchars")); } + + query_limits = new QueryLimitOverride(this); // load up the functions that require the TSDB object ExpressionFactory.addTSDBFunctions(this); @@ -2101,6 +2107,11 @@ public SearchPlugin getSearchPlugin() { return this.search; } + /** @return The byte limit class for queries */ + public QueryLimitOverride getQueryByteLimits() { + return query_limits; + } + private final boolean isHistogram(final byte[] qualifier) { return (qualifier.length & 0x1) == 1; } diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 4500b0aecf..e571004d66 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -102,6 +102,12 @@ public final class TSQuery { /** The query status for tracking over all performance of this query */ private QueryStats query_stats; + /** Override default max byte limit */ + private boolean override_byte_limit; + + /** Override default max row count limit */ + private boolean override_data_point_limit; + /** * Default constructor necessary for POJO de/serialization */ @@ -484,4 +490,27 @@ public void setUseCalendar(boolean use_calendar) { public void setQueryStats(final QueryStats query_stats) { this.query_stats = query_stats; } + + /** @return Whether or not the query would like to override the byte limiter. */ + public boolean overrideByteLimit() { + return override_byte_limit; + } + + /** @param override_byte_limit Whether or not the query would like to override + * the byte limiter. */ + public void setOverrideByteLimit(boolean override_byte_limit) { + this.override_byte_limit = override_byte_limit; + } + + /** @return Whether or not the query would like to override the data point limit. */ + public boolean overrideDataPointLimit() { + return override_data_point_limit; + } + + /** @param override_data_point_limit Whether or not the query would like to + * override the data point limit. */ + public void setOverrideDataPointLimit(boolean override_data_point_limit) { + this.override_data_point_limit = override_data_point_limit; + } + } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 6dbeed1169..c61f652e5a 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -183,6 +183,12 @@ final class TsdbQuery implements Query { /** 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 @@ -455,6 +461,17 @@ public Deferred<Object> configureFromQuery(final TSQuery query, override_fuzzy_filter = sub_query.getUseFuzzyFilter(); override_multi_get = sub_query.getUseMultiGets(); + max_bytes = tsdb.getQueryByteLimits().getByteLimit(sub_query.getMetric()); + if (tsdb.getConfig().getBoolean("tsd.query.limits.bytes.allow_override") && + query.overrideByteLimit()) { + max_bytes = 0; + } + max_data_points = tsdb.getQueryByteLimits().getDataPointLimit(sub_query.getMetric()); + if (tsdb.getConfig().getBoolean("tsd.query.limits.data_points.allow_override") && + query.overrideDataPointLimit()) { + max_data_points = 0; + } + // set percentile options percentiles = sub_query.getPercentiles(); show_histogram_buckets = sub_query.getShowHistogramBuckets(); @@ -802,13 +819,15 @@ private Deferred<TreeMap<byte[], Span>> findSpans() throws HBaseException { } scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, - delete, rollup_query, query_stats, query_index, null).scan(); + delete, rollup_query, query_stats, query_index, null, + max_bytes, max_data_points).scan(); } else { final List<Scanner> scanners = new ArrayList<Scanner>(1); scanners.add(getScanner(0)); scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, spans, scanner_filters, - delete, rollup_query, query_stats, query_index, null).scan(); + delete, rollup_query, query_stats, query_index, null, max_bytes, + max_data_points).scan(); } } @@ -866,12 +885,14 @@ private Deferred<TreeMap<byte[], HistogramSpan>> findHistogramSpans() throws HBa } scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, null, scanner_filters, - delete, rollup_query, query_stats, query_index, histSpans).scanHistogram(); + delete, rollup_query, query_stats, query_index, histSpans, + max_bytes, max_data_points).scanHistogram(); } else { scanners = Lists.newArrayList(getScanner()); scan_start_time = DateTime.nanoTime(); return new SaltScanner(tsdb, metric, scanners, null, scanner_filters, - delete, rollup_query, query_stats, query_index, histSpans).scanHistogram(); + delete, rollup_query, query_stats, query_index, histSpans, + max_bytes, max_data_points).scanHistogram(); } } @@ -1829,5 +1850,13 @@ static ByteMap<byte[][]> getRowKeyLiterals(final TsdbQuery query) { return query.row_key_literals; } + static long maxBytes(final TsdbQuery query) { + return query.max_bytes; + } + + static long maxDataPoints(final TsdbQuery query) { + return query.max_data_points; + } + } } diff --git a/src/query/QueryLimitOverride.java b/src/query/QueryLimitOverride.java new file mode 100644 index 0000000000..dad9bb140a --- /dev/null +++ b/src/query/QueryLimitOverride.java @@ -0,0 +1,337 @@ +// 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 <http://www.gnu.org/licenses/>. +package net.opentsdb.query; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.jboss.netty.util.HashedWheelTimer; +import org.jboss.netty.util.Timeout; +import org.jboss.netty.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import com.google.common.io.Files; + +import net.opentsdb.core.Const; +import net.opentsdb.core.TSDB; +import net.opentsdb.utils.JSON; + +/** + * A class used for numeric query overrides using regular expression matching. + * The class will attempt to load from a locally cached file on construction if + * a file is given. Each time one or more items are added or removed to the list + * the file will be updated if it was configured. + * This class is thread safe. + * + * @since 2.4 + */ +public class QueryLimitOverride implements TimerTask { + private static final Logger LOG = LoggerFactory.getLogger(QueryLimitOverride.class); + + /** Used for deserialization the proper object. Without this the hash codes + /* won't function properly as Jackson may just create a hash map for our simple + /* object. */ + public static TypeReference<HashSet<QueryLimitOverrideItem>> TR_OVERRIDES = + new TypeReference<HashSet<QueryLimitOverrideItem>>() {}; + + /** The list of overrides */ + + /** Keyed on the raw regex so we can update objects properly. */ + private final Map<String, QueryLimitOverrideItem> overrides; + + /** The default byte limit to use if the string didn't match. */ + private long default_byte_limit; + + /** The default data points limit to use if the string didn't match. */ + private long default_data_points_limit; + + /** The optional file location to read/write */ + private String file_location; + + /** How often, in seconds, to reload from the file */ + private int reload_interval; + + /** A timer for refreshing data from the config */ + private HashedWheelTimer timer; + + /** + * Default ctor. If a file location is given, it will be read on construction + * and reloaded every interval seconds. Note that if there is a problem reading + * or parsing the config file (when set) the ctor will continue loading with + * the defaults and if an interval is set, will attempt to reload at a later + * time. + * @param tsdb The TSDB to which we belong. + * @throws IllegalArgumentException if the default limits are less than zero. + */ + public QueryLimitOverride(final TSDB tsdb) { + overrides = Maps.newConcurrentMap(); + default_byte_limit = tsdb.getConfig().getLong("tsd.query.limits.bytes.default"); + default_data_points_limit = tsdb.getConfig() + .getLong("tsd.query.limits.data_points.default"); + if (tsdb.getConfig().hasProperty("tsd.query.limits.overrides.interval")) { + reload_interval = tsdb.getConfig().getInt("tsd.query.limits.overrides.interval"); + } else { + reload_interval = 0; + } + file_location = tsdb.getConfig().getString("tsd.query.limits.overrides.config"); + timer = (HashedWheelTimer) tsdb.getTimer(); + if (default_byte_limit < 0) { + throw new IllegalArgumentException("The default byte limit cannot be negative"); + } + if (default_data_points_limit < 0) { + throw new IllegalArgumentException("The default data points limit cannot" + + " be negative"); + } + + if (!Strings.isNullOrEmpty(file_location)) { + loadFromFile(); + if (reload_interval > 0) { + timer.newTimeout(this, reload_interval, TimeUnit.SECONDS); + } + } + } + + /** @return The default byte limit used when a match fails */ + public long getDefaultByteLimit() { + return default_byte_limit; + } + + /** @return The default data points limit used when a match fails */ + public long getDefaultDataPointsLimit() { + return default_data_points_limit; + } + + /** + * Iterates over the list of overrides and return the first that matches or + * the default if no match is found. + * NOTE: The set of expressions is not sorted so if more than one regex + * matches the string, the result is indeterministic. + * If the metric is null or empty, the default limit is returned. + * @param metric The string to match + * @return The matched or default limit. + */ + public synchronized long getByteLimit(final String metric) { + if (metric == null || metric.isEmpty()) { + return default_byte_limit; + } + for (final QueryLimitOverrideItem item : overrides.values()) { + if (item.matches(metric)) { + return item.getByteLimit(); + } + } + return default_byte_limit; + } + + /** + * Iterates over the list of overrides and return the first that matches or + * the default if no match is found. + * NOTE: The set of expressions is not sorted so if more than one regex + * matches the string, the result is indeterministic. + * If the metric is null or empty, the default limit is returned. + * @param metric The string to match + * @return The matched or default limit. + */ + public synchronized long getDataPointLimit(final String metric) { + if (metric == null || metric.isEmpty()) { + return default_data_points_limit; + } + for (final QueryLimitOverrideItem item : overrides.values()) { + if (item.matches(metric)) { + return item.getDataPointsLimit(); + } + } + return default_data_points_limit; + } + + /** @return An unmodifiable collection of the items. WARNING: Don't modify the items! + * They are not duplicates (right now)*/ + public Collection<QueryLimitOverrideItem> getLimits() { + return Collections.unmodifiableCollection(overrides.values()); + } + + @Override + public String toString() { + return JSON.serializeToString(this); + } + + /** @param timeout The timeout reference. */ + @Override + public void run(final Timeout timeout) { + try { + loadFromFile(); + } catch (RuntimeException e) { + LOG.error("Failed to read cache file on auto reload: " + this, e); + } finally { + timer.newTimeout(this, reload_interval, TimeUnit.SECONDS); + } + } + + /** + * Attempts to load the file from disk + */ + private void loadFromFile() { + // load from disk if the caller gave us a file + if (file_location != null && !file_location.isEmpty()) { + final File file = new File(file_location); + if (!file.exists()) { + LOG.warn("Query override file " + file_location + " does not exist"); + return; + } + try { + final String raw_json = Files.toString(file, Const.UTF8_CHARSET); + if (raw_json != null && !raw_json.isEmpty()) { + final Set<QueryLimitOverrideItem> cached_items = + JSON.parseToObject(raw_json, TR_OVERRIDES); + + // iterate so we only change bits that are different. + for (final QueryLimitOverrideItem override : cached_items) { + QueryLimitOverrideItem existing = overrides.get(override.getRegex()); + if (existing == null || !existing.equals(override)) { + overrides.put(override.getRegex(), override); + } + } + + // reverse quadratic, woot! Ugly but if the limit file is so big that + // this takes over 60 seconds or starts blocking queries on modifications + // to the map then something is really wrong. + final Iterator<Entry<String, QueryLimitOverrideItem>> iterator = + overrides.entrySet().iterator(); + while (iterator.hasNext()) { + final Entry<String, QueryLimitOverrideItem> entry = iterator.next(); + boolean matched = false; + for (final QueryLimitOverrideItem override : cached_items) { + if (override.getRegex().equals(entry.getKey())) { + matched = true; + break; + } + } + if (!matched) { + iterator.remove(); + } + } + } + LOG.info("Successfully loaded query overrides: " + this); + } catch (Exception e) { + LOG.error("Failed to read cache file for query limit override: " + + this, e); + } + } + } + + /** A simple class for ser/des of the items along with some validation */ + public static class QueryLimitOverrideItem { + + /** The regular expression provided by the user */ + private String regex; + + /** A compiled pattern for speedier operation */ + private Pattern pattern; + + /** The byte limit to use when matched */ + private long byte_limit = 0; + + /** The data points limit to use when matched */ + private long data_points_limit = 0; + + @Override + public int hashCode() { + return Objects.hashCode(regex, byte_limit, data_points_limit); + } + + @Override + public boolean equals(final Object item) { + if (item == this) { + return true; + } + if (item == null) { + return false; + } + return regex.equals(((QueryLimitOverrideItem)item).regex) && + byte_limit == ((QueryLimitOverrideItem)item).byte_limit && + data_points_limit == ((QueryLimitOverrideItem)item).data_points_limit; + } + + /** @return The regular expression */ + public String getRegex() { + return regex; + } + + /** @param regex The regular expression + * @throws PatternSyntaxException if the regex can't be compiled */ + public void setRegex(final String regex) { + this.regex = regex; + pattern = Pattern.compile(regex); + } + + /** @return The byte limit to use for this override */ + public long getByteLimit() { + return byte_limit; + } + + /** @param byte_limit The byte limit to use for this override */ + public void setByteLimit(final long byte_limit) { + this.byte_limit = byte_limit; + } + + /** @return The data points limit to use for this override. */ + public long getDataPointsLimit() { + return data_points_limit; + } + + /** @param data_points_limit The data points limit to use for this override. */ + public void setDataPointsLimit(final long data_points_limit) { + this.data_points_limit = data_points_limit; + } + + /** @param string The string to match. + * @return true if the string matches this regex pattern, false if not. + * If the string is null or empty we return false. */ + public boolean matches(final String string) { + if (string == null || string.isEmpty()) { + return false; + } + if (pattern == null || regex == null || regex.isEmpty()) { + return false; + } + return pattern.matcher(string).find(); + } + + /** @throws IllegalArgumentException if the limit is less than zero or + * the regular expression is null or empty */ + public void validate() { + if (byte_limit < 0) { + throw new IllegalArgumentException("The byte limit must be 0 or greater"); + } + if (data_points_limit < 0) { + throw new IllegalArgumentException("The data points limit must be 0 or greater"); + } + if (regex == null || regex.isEmpty()) { + throw new IllegalArgumentException("The regex cannot be empty or null"); + } + } + } +} diff --git a/src/utils/Config.java b/src/utils/Config.java index e15a106842..b5ce690853 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -534,6 +534,11 @@ protected void setDefaults() { default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); default_map.put("tsd.query.enable_fuzzy_filter", "true"); + default_map.put("tsd.query.limits.bytes.default", "0"); + default_map.put("tsd.query.limits.bytes.allow_override", "false"); + default_map.put("tsd.query.limits.data_points.default", "0"); + default_map.put("tsd.query.limits.data_points.allow_override", "false"); + default_map.put("tsd.query.limits.overrides.interval", "60000"); default_map.put("tsd.query.multi_get.enable", "false"); default_map.put("tsd.query.multi_get.limit", "131072"); default_map.put("tsd.query.multi_get.batch_size", "1024"); diff --git a/test/core/TestSaltScanner.java b/test/core/TestSaltScanner.java index 118fa6b294..0e3dfebed0 100644 --- a/test/core/TestSaltScanner.java +++ b/test/core/TestSaltScanner.java @@ -31,10 +31,12 @@ import java.util.TreeMap; import net.opentsdb.query.filter.TagVFilter; +import net.opentsdb.query.filter.TagVWildcardFilter; import net.opentsdb.uid.UniqueId; 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; @@ -411,6 +413,40 @@ public void scanCompactionRuntimeException() throws Exception { scanner.scan().joinUninterruptibly(); } + @Test + public void scanTooManyDps() throws Exception { + setupMockScanners(false); + List<TagVFilter> filters = new ArrayList<TagVFilter>(1); + filters.add(new TagVWildcardFilter(TAGK_STRING, "web*")); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, null, false, null, null, 0, null, 0, 1); + try { + scanner.scan().joinUninterruptibly(); + fail("Excpected a QueryException"); + } catch (QueryException e) { + assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, e.getStatus()); + } + } + + @Test + public void scanTooManyBytes() throws Exception { + setupMockScanners(false); + List<TagVFilter> filters = new ArrayList<TagVFilter>(1); + filters.add(new TagVWildcardFilter(TAGK_STRING, "web*")); + + final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, + spans, null, false, null, null, 0, null, 2, 0); + try { + scanner.scan().joinUninterruptibly(); + fail("Excpected a QueryException"); + } catch (QueryException e) { + assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, e.getStatus()); + } + config.overrideConfig("tsd.core.scanner.max_bytes", "0"); + } + + /** * Sets up a pair of scanners with either a list of values or no data * @param no_data Whether or not to return 0 data. diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index 51d023bf8d..0d63fa1bda 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -196,7 +196,7 @@ public void scan() throws Exception { PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, null, false, null, query_stats, 0, spans); + null, null, false, null, query_stats, 0, spans, 0, 0); assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); assertEquals(3, spans.size()); @@ -228,7 +228,7 @@ public void scanWithFilter() throws Exception { PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, null, false, null, query_stats, 0, spans); + null, null, false, null, query_stats, 0, spans, 0, 0); assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); assertEquals(3, spans.size()); @@ -263,7 +263,7 @@ public void scanWithFiltersOnSameTag() throws Exception { PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, null, false, null, query_stats, 0, spans); + null, null, false, null, query_stats, 0, spans, 0, 0); assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); assertEquals(3, spans.size()); @@ -297,7 +297,7 @@ public void scanWithFiltersOnSameTagOneFail() throws Exception { PowerMockito.whenNew(SimpleHistogram.class).withAnyArguments().thenReturn(y1Hist); final SaltScanner scanner = new SaltScanner(tsdb, METRIC_BYTES, scanners, - null, filters, false, null, query_stats, 0, spans); + null, filters, false, null, query_stats, 0, spans, 0, 0); assertTrue(spans == scanner.scanHistogram().joinUninterruptibly()); assertEquals(0, spans.size()); diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 190c2a7b1c..366e9951b2 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -25,6 +25,7 @@ import java.util.List; 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.storage.MockBase; @@ -37,6 +38,7 @@ 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 com.stumbleupon.async.DeferredGroupException; @@ -457,6 +459,78 @@ public void configureFromQueryGroupByPipeNSUTagvSkipUnresolved() ForTesting.getGroupBys(query).get(0)); } + @Test + public void configureFromQueryMaxBytes() throws Exception { + TSQuery ts_query = getTSQuery(); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(config.getInt("tsd.query.limits.bytes.default"), + ForTesting.maxBytes(query)); + + config.overrideConfig("tsd.query.limits.bytes.default", "128"); + config.overrideConfig("tsd.query.limits.data_points.default", "16"); + Whitebox.setInternalState(tsdb, "query_limits", + new QueryLimitOverride(tsdb)); + ts_query = getTSQuery(); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(128, ForTesting.maxBytes(query)); + + // disabled by default + ts_query = getTSQuery(); + ts_query.setOverrideByteLimit(true); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(128, ForTesting.maxBytes(query)); + + config.overrideConfig("tsd.query.limits.bytes.allow_override", "true"); + ts_query = getTSQuery(); + ts_query.setOverrideByteLimit(true); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(0, ForTesting.maxBytes(query)); + } + + @Test + public void configureFromQueryMaxDataPoints() throws Exception { + TSQuery ts_query = getTSQuery(); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(config.getInt("tsd.query.limits.data_points.default"), + ForTesting.maxDataPoints(query)); + + config.overrideConfig("tsd.query.limits.bytes.default", "128"); + config.overrideConfig("tsd.query.limits.data_points.default", "16"); + Whitebox.setInternalState(tsdb, "query_limits", + new QueryLimitOverride(tsdb)); + ts_query = getTSQuery(); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(16, ForTesting.maxDataPoints(query)); + + // disabled by default + ts_query = getTSQuery(); + ts_query.setOverrideDataPointLimit(true); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(16, ForTesting.maxDataPoints(query)); + + config.overrideConfig("tsd.query.limits.data_points.allow_override", "true"); + ts_query = getTSQuery(); + ts_query.setOverrideDataPointLimit(true); + ts_query.validateAndSetQuery(); + query = new TsdbQuery(tsdb); + query.configureFromQuery(ts_query, 0).joinUninterruptibly(); + assertEquals(0, ForTesting.maxDataPoints(query)); + } + @Test public void deleteDatapoints() throws Exception { setDataPointStorage(); diff --git a/test/query/TestQueryLimitOverride.java b/test/query/TestQueryLimitOverride.java new file mode 100644 index 0000000000..351da751e7 --- /dev/null +++ b/test/query/TestQueryLimitOverride.java @@ -0,0 +1,318 @@ +// 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 <http://www.gnu.org/licenses/>. +package net.opentsdb.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + +import org.jboss.netty.util.HashedWheelTimer; +import org.jboss.netty.util.TimerTask; +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.google.common.io.Files; + +import net.opentsdb.core.TSDB; +import net.opentsdb.query.QueryLimitOverride.QueryLimitOverrideItem; +import net.opentsdb.utils.Config; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, HashedWheelTimer.class, QueryLimitOverride.class, + File.class, Files.class }) +public class TestQueryLimitOverride { + + private TSDB tsdb; + private Config config; + private HashedWheelTimer timer; + private File file; + + @Before + public void before() throws Exception { + tsdb = PowerMockito.mock(TSDB.class); + config = new Config(false); + timer = mock(HashedWheelTimer.class); + PowerMockito.when(tsdb.getTimer()).thenReturn(timer); + when(tsdb.getConfig()).thenReturn(config); + PowerMockito.mockStatic(Files.class); + file = mock(File.class); + PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(file); + + config.overrideConfig("tsd.query.limits.bytes.default", "42"); + config.overrideConfig("tsd.query.limits.data_points.default", "24"); + config.overrideConfig("tsd.query.limits.overrides.config", "/tmp/overrides.json"); + } + + @Test + public void ctorNoFileConfigured() throws Exception { + config.overrideConfig("tsd.query.limits.overrides.config", null); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + assertEquals(0, limits.getLimits().size()); + verify(file, never()).exists(); + verify(timer, never()) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void ctorFileNoReload() throws Exception { + config.overrideConfig("tsd.query.limits.overrides.interval", null); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + verify(file, times(1)).exists(); + verify(timer, never()) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void ctorFileDoesntExistException() throws Exception { + PowerMockito.when(Files.getFileExtension(anyString())) + .thenThrow(new IllegalStateException("Boo!")); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + verify(file, times(1)).exists(); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + assertEquals(0, limits.getLimits().size()); + verify(timer, times(1)) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void ctorNegativeDefaultsLimit() throws Exception { + config.overrideConfig("tsd.query.limits.bytes.default", "-42"); + config.overrideConfig("tsd.query.limits.data_points.default", "24"); + try { + new QueryLimitOverride(tsdb); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + config.overrideConfig("tsd.query.limits.bytes.default", "42"); + config.overrideConfig("tsd.query.limits.data_points.default", "-24"); + try { + new QueryLimitOverride(tsdb); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } + + @Test + public void ctorWithFile() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24,\"dataPointsLimit\":16}]"); + when(file.exists()).thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + verify(file, times(1)).exists(); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + assertEquals(1, limits.getLimits().size()); + final QueryLimitOverrideItem item = limits.getLimits().iterator().next(); + assertEquals(24, item.getByteLimit()); + assertEquals(16, item.getDataPointsLimit()); + assertEquals(".*sys$", item.getRegex()); + assertTrue(item.matches("namespace.app.sys")); + assertFalse(item.matches("namespace.app.sys.cpu")); + verify(timer, times(1)) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void ctorWithFileException() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenThrow(new IOException("Boo!")); + when(file.exists()).thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + verify(file, times(1)).exists(); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + assertEquals(0, limits.getLimits().size()); + verify(timer, times(1)) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void ctorWithFileBadJSON() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLim"); + when(file.exists()).thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + verify(file, times(1)).exists(); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + assertEquals(0, limits.getLimits().size()); + verify(timer, times(1)) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void ctorWithFileBadRegex() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sy(notclosed\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]"); + when(file.exists()).thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + verify(file, times(1)).exists(); + assertEquals(42, limits.getDefaultByteLimit()); + assertEquals(24, limits.getDefaultDataPointsLimit()); + assertEquals(0, limits.getLimits().size()); + verify(timer, times(1)) + .newTimeout(any(TimerTask.class), anyLong(), any(TimeUnit.class)); + } + + @Test + public void timerTaskEmptySet() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]"); + when(file.exists()) + .thenReturn(false) + .thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(0, limits.getLimits().size()); + limits.run(null); + assertEquals(1, limits.getLimits().size()); + final QueryLimitOverrideItem item = limits.getLimits().iterator().next(); + assertEquals(24, item.getByteLimit()); + assertEquals(16, item.getDataPointsLimit()); + assertEquals(".*sys$", item.getRegex()); + } + + @Test + public void timerTaskSame() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]"); + when(file.exists()) + .thenReturn(true) + .thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(1, limits.getLimits().size()); + final QueryLimitOverrideItem item = limits.getLimits().iterator().next(); + assertEquals(24, item.getByteLimit()); + assertEquals(16, item.getDataPointsLimit()); + assertEquals(".*sys$", item.getRegex()); + + limits.run(null); + assertEquals(1, limits.getLimits().size()); + // same obj/address + assertSame(item, limits.getLimits().iterator().next()); + } + + @Test + public void timerTaskDiffLimit() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]") + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":60," + + "\"dataPointsLimit\":16}]"); + when(file.exists()) + .thenReturn(true) + .thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(1, limits.getLimits().size()); + QueryLimitOverrideItem item = limits.getLimits().iterator().next(); + assertEquals(24, item.getByteLimit()); + assertEquals(16, item.getDataPointsLimit()); + assertEquals(".*sys$", item.getRegex()); + + limits.run(null); + assertEquals(1, limits.getLimits().size()); + item = limits.getLimits().iterator().next(); + assertEquals(60, item.getByteLimit()); + assertEquals(16, item.getDataPointsLimit()); + assertEquals(".*sys$", item.getRegex()); + } + + @Test + public void timerTaskCleared() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]") + .thenReturn("[]"); + when(file.exists()) + .thenReturn(true) + .thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(1, limits.getLimits().size()); + limits.run(null); + assertEquals(0, limits.getLimits().size()); + } + + @Test + public void timerTaskAddOne() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]") + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}," + + "{\"regex\":\".*if$\",\"byteLimit\":96," + + "\"dataPointsLimit\":32}]"); + when(file.exists()) + .thenReturn(true) + .thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(1, limits.getLimits().size()); + limits.run(null); + assertEquals(2, limits.getLimits().size()); + } + + @Test + public void timerTaskRemoveOne() throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}," + + "{\"regex\":\".*if$\",\"byteLimit\":96," + + "\"dataPointsLimit\":32}]") + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24," + + "\"dataPointsLimit\":16}]"); + when(file.exists()) + .thenReturn(true) + .thenReturn(true); + final QueryLimitOverride limits = new QueryLimitOverride(tsdb); + assertEquals(2, limits.getLimits().size()); + limits.run(null); + assertEquals(1, limits.getLimits().size()); + } + + /** @return an override object with a couple of items */ + public QueryLimitOverride getTestObject(final boolean with_file) throws Exception { + PowerMockito.when(Files.class, "toString", any(File.class), any(Charset.class)) + .thenReturn("[{\"regex\":\".*sys$\",\"byteLimit\":24,\"dataPointsLimit\":16}," + + "{\"regex\":\".*perf.*\",\"byteLimit\":84,\"dataPointsLimit\":42}]"); + when(file.exists()).thenReturn(true); + return new QueryLimitOverride(tsdb); + } +} From e3cba734f1f7476f51cf27f198eaa55706abe012 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 29 Aug 2017 22:24:35 -0700 Subject: [PATCH 669/826] Fix #1050 by seeking to the start timestamp of the query when the time zone aligned timestamp may be earlier than the start time. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/AggregationIterator.java | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 4c4a917c3d..ff7b99c990 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -361,7 +361,7 @@ public AggregationIterator(final SeekableView[] iterators, for (int i = 0; i < size; i++) { SeekableView it = iterators[i]; it.seek(start_time); - final DataPoint dp; + DataPoint dp; if (!it.hasNext()) { ++num_empty_spans; endReached(i); @@ -374,12 +374,23 @@ public AggregationIterator(final SeekableView[] iterators, // + dp.timestamp() + " >= " + start_time); putDataPoint(size + i, dp); } else { - if (LOG.isDebugEnabled()) { - LOG.debug(String.format("No DP in range for #%d: %d < %d", i, - dp.timestamp(), start_time)); + // if there is data, advance to the start time if applicable. + while (dp != null && dp.timestamp() < start_time) { + if (it.hasNext()) { + dp = it.next(); + } else { + dp = null; + } } - endReached(i); - continue; + if (dp == null) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("No DP in range for #%d: start time %d", i, + start_time)); + } + endReached(i); + continue; + } + putDataPoint(size + i, dp); } if (rate) { // The first rate against the time zero should be populated From d2ca10ae805e76aaec3f495e64b67bf365b34fb2 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 29 Aug 2017 22:24:35 -0700 Subject: [PATCH 670/826] Fix #1050 by seeking to the start timestamp of the query when the time zone aligned timestamp may be earlier than the start time. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/AggregationIterator.java | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/core/AggregationIterator.java b/src/core/AggregationIterator.java index 99bb540c6b..a5d0f541fa 100644 --- a/src/core/AggregationIterator.java +++ b/src/core/AggregationIterator.java @@ -414,7 +414,7 @@ public AggregationIterator(final SeekableView[] iterators, for (int i = 0; i < size; i++) { SeekableView it = iterators[i]; it.seek(start_time); - final DataPoint dp; + DataPoint dp; if (!it.hasNext()) { ++num_empty_spans; endReached(i); @@ -427,12 +427,23 @@ public AggregationIterator(final SeekableView[] iterators, // + dp.timestamp() + " >= " + start_time); putDataPoint(size + i, dp); } else { - if (LOG.isDebugEnabled()) { - LOG.debug(String.format("No DP in range for #%d: %d < %d", i, - dp.timestamp(), start_time)); + // if there is data, advance to the start time if applicable. + while (dp != null && dp.timestamp() < start_time) { + if (it.hasNext()) { + dp = it.next(); + } else { + dp = null; + } } - endReached(i); - continue; + if (dp == null) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("No DP in range for #%d: start time %d", i, + start_time)); + } + endReached(i); + continue; + } + putDataPoint(size + i, dp); } if (rate) { // The first rate against the time zero should be populated From 8feeb581bd80521dabb87d4be5ace7c785921d3b Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Wed, 30 Aug 2017 11:55:47 -0700 Subject: [PATCH 671/826] Remove the JDK 6 and 7 builds from Travis as they're now deprecated. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fccf400e41..9354fb4ea2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,6 @@ script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet addons: hostname: short-hostname jdk: - - oraclejdk7 - - openjdk6 - oraclejdk8 notifications: email: false From cf1f8c9a5c50dfa70bc4bfc03338188c8119bc0d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 2 Sep 2017 20:26:43 -0700 Subject: [PATCH 672/826] Bump AsyncBigtable to 0.3.1-SNAPSHOT so that we can use Bigtable with 2.4 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- ...gtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 | 1 - ...gtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 | 1 - ...gtable-0.3.1-20170903.031804-2-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 2 +- 4 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 delete mode 100644 third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 create mode 100644 third_party/asyncbigtable/asyncbigtable-0.3.1-20170903.031804-2-jar-with-dependencies.jar.md5 diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 deleted file mode 100644 index dbfa539ba6..0000000000 --- a/third_party/asyncbigtable/asyncbigtable-0.2.1-20151029.214823-2-jar-with-dependencies.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -e07097fbc7023fd0ee108368a7ad7c73 \ No newline at end of file diff --git a/third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 deleted file mode 100644 index 78c394f1d3..0000000000 --- a/third_party/asyncbigtable/asyncbigtable-0.2.1-20160228.235952-3-jar-with-dependencies.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -512cc4c7ba345a11aa8d6662d03bb3ed diff --git a/third_party/asyncbigtable/asyncbigtable-0.3.1-20170903.031804-2-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.3.1-20170903.031804-2-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..c6f344387e --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.3.1-20170903.031804-2-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +0c3d205142ae73fd2b72614fa4d7fde0 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index 9fd3bc0449..1b90ba5d39 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -ASYNCBIGTABLE_VERSION := 0.3.0 +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/releases/com/pythian/opentsdb/asyncbigtable/0.3.0/ From 607f7d89f78ac75620404e827f42d0612ff4b28a Mon Sep 17 00:00:00 2001 From: hkousha <kousha1367@gmail.com> Date: Thu, 28 Sep 2017 21:39:18 -0700 Subject: [PATCH 673/826] Adding QueryLimitOverride to the list of build files Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index 7db1f2636b..61b0ca42ad 100644 --- a/Makefile.am +++ b/Makefile.am @@ -104,6 +104,7 @@ tsdb_SRC := \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ src/query/QueryUtil.java \ + src/query/QueryLimitOverride.java \ src/query/expression/Absolute.java \ src/query/expression/Alias.java \ src/query/expression/DiffSeries.java \ From bbb687a533a701f17f99ceec7a8ff2e10399eae8 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sun, 8 Oct 2017 14:33:44 -0700 Subject: [PATCH 674/826] Add the TSDB.OperationMode to track the mode of the tsd, adding a write- only mode. Add the option to have a Guava LRU cache for UIDs to limit the size of UID caches. Add an option to only write the one or the other of the UID caches if the TSD is in read-only or write-only mode. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/TSDB.java | 27 ++++ src/uid/UniqueId.java | 187 +++++++++++++++++++++----- src/utils/Config.java | 4 + test/uid/TestUniqueId.java | 261 ++++++++++++++++++++++++++++++++++++- 4 files changed, 444 insertions(+), 35 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 50a458af56..e2ed27380a 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -99,9 +99,19 @@ public final class TSDB { private static short TAG_VALUE_WIDTH = 3; private static final int MIN_HISTOGRAM_BYTES = 1; + /** The operation mode (role) of the TSD. */ + public enum OperationMode { + READWRITE, + READONLY, + WRITEONLY + } + /** Client for the HBase cluster to use. */ final HBaseClient client; + /** The operation mode (role) of the TSD. */ + final OperationMode mode; + /** Name of the table in which timeseries are stored. */ final byte[] table; /** Name of the table in which UID information is stored. */ @@ -218,6 +228,17 @@ public TSDB(final HBaseClient client, final Config config) { this.client = client; } + String string_mode = config.getString("tsd.mode"); + if (Strings.isNullOrEmpty(string_mode)) { + mode = OperationMode.READWRITE; + } else if (string_mode.toLowerCase().equals("ro")) { + mode = OperationMode.READONLY; + } else if (string_mode.toLowerCase().equals("wo")) { + mode = OperationMode.WRITEONLY; + } else { + mode = OperationMode.READWRITE; + } + // SALT AND UID WIDTHS // Users really wanted this to be set via config instead of having to // compile. Hopefully they know NOT to change these after writing data. @@ -2112,6 +2133,12 @@ public QueryLimitOverride getQueryByteLimits() { return query_limits; } + /** @return The mode of operation for this TSD. + * @since 2.4 */ + public OperationMode getMode() { + return mode; + } + private final boolean isHistogram(final byte[] qualifier) { return (qualifier.length & 0x1) == 1; } diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 5585d26055..35b9c6ec0a 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -26,6 +26,9 @@ import javax.xml.bind.DatatypeConverter; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -45,6 +48,7 @@ import net.opentsdb.core.Const; import net.opentsdb.core.Internal; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSDB.OperationMode; import net.opentsdb.meta.UIDMeta; /** @@ -98,12 +102,17 @@ public enum UniqueIdType { private final boolean randomize_id; /** Cache for forward mappings (name to ID). */ - private final ConcurrentHashMap<String, byte[]> name_cache = - new ConcurrentHashMap<String, byte[]>(); + private final ConcurrentHashMap<String, byte[]> name_cache; /** Cache for backward mappings (ID to name). * The ID in the key is a byte[] converted to a String to be Comparable. */ - private final ConcurrentHashMap<String, String> id_cache = - new ConcurrentHashMap<String, String>(); + private final ConcurrentHashMap<String, String> id_cache; + + /** Cache for forward mappings (name to ID). */ + private final Cache<String, byte[]> lru_name_cache; + /** Cache for backward mappings (ID to name). + * The ID in the key is a byte[] converted to a String to be Comparable. */ + private final Cache<String, String> lru_id_cache; + /** Map of pending UID assignments */ private final HashMap<String, Deferred<byte[]>> pending_assignments = new HashMap<String, Deferred<byte[]>>(); @@ -121,6 +130,15 @@ public enum UniqueIdType { /** How many times assignments have been rejected by the UID filter */ private volatile int rejected_assignments; + /** The mode of operation for this TSD. */ + private OperationMode mode; + + /** Whether or not to use the mode for caching IDs. */ + private boolean use_mode; + + /** Whether or not to use the Guava LRU cache for IDs. */ + private boolean use_lru; + /** TSDB object used for filtering and/or meta generation. */ private TSDB tsdb; @@ -163,6 +181,12 @@ public UniqueId(final HBaseClient client, final byte[] table, final String kind, } this.id_width = (short) width; this.randomize_id = randomize_id; + mode = OperationMode.READWRITE; + name_cache = new ConcurrentHashMap<String, byte[]>(); + id_cache = new ConcurrentHashMap<String, String>(); + lru_name_cache = null; + lru_id_cache = null; + use_lru = false; } /** @@ -191,6 +215,24 @@ public UniqueId(final TSDB tsdb, final byte[] table, final String kind, } this.id_width = (short) width; this.randomize_id = randomize_id; + mode = tsdb.getMode(); + use_mode = tsdb.getConfig().getBoolean("tsd.uid.use_mode"); + use_lru = tsdb.getConfig().getBoolean("tsd.uid.lru.enable"); + if (use_lru) { + name_cache = null; + id_cache = null; + lru_name_cache = CacheBuilder.newBuilder() + .maximumSize(tsdb.getConfig().getInt("tsd.uid.lru.name.size")) + .build(); + lru_id_cache = CacheBuilder.newBuilder() + .maximumSize(tsdb.getConfig().getInt("tsd.uid.lru.id.size")) + .build(); + } else { + name_cache = new ConcurrentHashMap<String, byte[]>(); + id_cache = new ConcurrentHashMap<String, String>(); + lru_name_cache = null; + lru_id_cache = null; + } } /** The number of times we avoided reading from HBase thanks to the cache. */ @@ -205,6 +247,9 @@ public int cacheMisses() { /** Returns the number of elements stored in the internal cache. */ public int cacheSize() { + if (use_lru) { + return (int) (lru_name_cache.size() + lru_id_cache.size()); + } return name_cache.size() + id_cache.size(); } @@ -229,6 +274,8 @@ public short width() { /** @param tsdb Whether or not to track new UIDMeta objects */ public void setTSDB(final TSDB tsdb) { this.tsdb = tsdb; + mode = tsdb.getMode(); + use_mode = tsdb.getConfig().getBoolean("tsd.uid.use_mode"); } /** The largest possible ID given the number of bytes the IDs are @@ -244,8 +291,13 @@ public long maxPossibleId() { * @since 1.1 */ public void dropCaches() { - name_cache.clear(); - id_cache.clear(); + if (use_lru) { + lru_name_cache.invalidateAll(); + lru_id_cache.invalidateAll(); + } else { + name_cache.clear(); + id_cache.clear(); + } } /** @@ -300,8 +352,21 @@ public String call(final String name) { if (name == null) { throw new NoSuchUniqueId(kind(), id); } - addNameToCache(id, name); - addIdToCache(name, id); + if (use_mode) { + switch(mode) { + case READONLY: + addNameToCache(id, name); + break; + case WRITEONLY: + break; + default: + addNameToCache(id, name); + addIdToCache(name, id); + } + } else { + addNameToCache(id, name); + addIdToCache(name, id); + } return name; } } @@ -309,7 +374,8 @@ public String call(final String name) { } private String getNameFromCache(final byte[] id) { - return id_cache.get(fromBytes(id)); + return use_lru ? lru_id_cache.getIfPresent(fromBytes(id)) : + id_cache.get(fromBytes(id)); } private Deferred<String> getNameFromHBase(final byte[] id) { @@ -323,9 +389,13 @@ public String call(final byte[] name) { private void addNameToCache(final byte[] id, final String name) { final String key = fromBytes(id); - String found = id_cache.get(key); + String found = use_lru ? lru_id_cache.getIfPresent(key) : id_cache.get(key); if (found == null) { - found = id_cache.putIfAbsent(key, name); + if (use_lru) { + lru_id_cache.put(key, name); + } else { + found = id_cache.putIfAbsent(key, name); + } } if (found != null && !found.equals(name)) { throw new IllegalStateException("id=" + Arrays.toString(id) + " => name=" @@ -360,8 +430,21 @@ public byte[] call(final byte[] id) { + " which is != " + id_width + " required for '" + kind() + '\''); } - addIdToCache(name, id); - addNameToCache(id, name); + if (use_mode) { + switch(mode) { + case READONLY: + break; + case WRITEONLY: + addIdToCache(name, id); + break; + default: + addNameToCache(id, name); + addIdToCache(name, id); + } + } else { + addIdToCache(name, id); + addNameToCache(id, name); + } return id; } } @@ -370,7 +453,7 @@ public byte[] call(final byte[] id) { } private byte[] getIdFromCache(final String name) { - return name_cache.get(name); + return use_lru ? lru_name_cache.getIfPresent(name) : name_cache.get(name); } private Deferred<byte[]> getIdFromHBase(final String name) { @@ -378,13 +461,18 @@ private Deferred<byte[]> getIdFromHBase(final String name) { } private void addIdToCache(final String name, final byte[] id) { - byte[] found = name_cache.get(name); + byte[] found = use_lru ? lru_name_cache.getIfPresent(name) : + name_cache.get(name); if (found == null) { - found = name_cache.putIfAbsent(name, - // Must make a defensive copy to be immune - // to any changes the caller may do on the - // array later on. - Arrays.copyOf(id, id.length)); + if (use_lru) { + lru_name_cache.put(name, Arrays.copyOf(id, id.length)); + } else { + found = name_cache.putIfAbsent(name, + // Must make a defensive copy to be immune + // to any changes the caller may do on the + // array later on. + Arrays.copyOf(id, id.length)); + } } if (found != null && !Arrays.equals(found, id)) { throw new IllegalStateException("name=" + name + " => id=" @@ -939,7 +1027,8 @@ public Object call(final ArrayList<ArrayList<KeyValue>> rows) { final byte[] key = row.get(0).key(); final String name = fromBytes(key); final byte[] id = row.get(0).value(); - final byte[] cached_id = name_cache.get(name); + final byte[] cached_id = use_lru ? lru_name_cache.getIfPresent(name) : + name_cache.get(name); if (cached_id == null) { cacheMapping(name, id); } else if (!Arrays.equals(id, cached_id)) { @@ -1042,8 +1131,13 @@ public void rename(final String oldname, final String newname) { // Update cache. addIdToCache(newname, row); // add new name -> ID - id_cache.put(fromBytes(row), newname); // update ID -> new name - name_cache.remove(oldname); // remove old name -> ID + if (use_lru) { + lru_id_cache.put(fromBytes(row), newname); + lru_name_cache.invalidate(oldname); + } else { + id_cache.put(fromBytes(row), newname); // update ID -> new name + name_cache.remove(oldname); // remove old name -> ID + } // Delete the old forward mapping. try { @@ -1103,8 +1197,13 @@ public Deferred<Object> deleteAsync(final String name) { class ErrCB implements Callback<Object, Exception> { @Override public Object call(final Exception ex) throws Exception { - name_cache.remove(name); - id_cache.remove(fromBytes(uid)); + if (use_lru) { + lru_name_cache.invalidate(name); + lru_id_cache.invalidate(fromBytes(uid)); + } else { + name_cache.remove(name); + id_cache.remove(fromBytes(uid)); + } LOG.error("Failed to delete " + fromBytes(kind) + " UID " + name + " but still cleared the cache", ex); return ex; @@ -1116,8 +1215,13 @@ class GroupCB implements Callback<Deferred<Object>, ArrayList<Object>> { @Override public Deferred<Object> call(final ArrayList<Object> response) throws Exception { - name_cache.remove(name); - id_cache.remove(fromBytes(uid)); + if (use_lru) { + lru_name_cache.invalidate(name); + lru_id_cache.invalidate(fromBytes(uid)); + } else { + name_cache.remove(name); + id_cache.remove(fromBytes(uid)); + } LOG.info("Successfully deleted " + fromBytes(kind) + " UID " + name); return Deferred.fromResult(null); } @@ -1146,7 +1250,8 @@ public Deferred<Object> call(final byte[] stored_uid) throws Exception { } } - final byte[] cached_uid = name_cache.get(name); + final byte[] cached_uid = use_lru ? lru_name_cache.getIfPresent(name) : + name_cache.get(name); if (cached_uid == null) { return getIdFromHBase(name).addCallbackDeferring(new LookupCB()) .addErrback(new ErrCB()); @@ -1663,8 +1768,10 @@ public static void preloadUidCache(final TSDB tsdb, for (UniqueId unique_id_table : uid_cache_map.values()) { LOG.info("After preloading, uid cache '{}' has {} ids and {} names.", unique_id_table.kind(), - unique_id_table.id_cache.size(), - unique_id_table.name_cache.size()); + unique_id_table.use_lru ? unique_id_table.lru_id_cache.size() : + unique_id_table.id_cache.size(), + unique_id_table.use_lru ? unique_id_table.lru_name_cache.size() : + unique_id_table.name_cache.size()); } } catch (Exception e) { if (e instanceof HBaseException) { @@ -1680,4 +1787,24 @@ public static void preloadUidCache(final TSDB tsdb, } } } + + @VisibleForTesting + Map<String, byte[]> nameCache() { + return name_cache; + } + + @VisibleForTesting + Map<String, String> idCache() { + return id_cache; + } + + @VisibleForTesting + Cache<String, byte[]> lruNameCache() { + return lru_name_cache; + } + + @VisibleForTesting + Cache<String, String> lruIdCache() { + return lru_id_cache; + } } diff --git a/src/utils/Config.java b/src/utils/Config.java index b5ce690853..8f8cb77286 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -576,6 +576,10 @@ protected void setDefaults() { default_map.put("tsd.storage.compaction.max_concurrent_flushes", "10000"); default_map.put("tsd.storage.compaction.flush_speed", "2"); default_map.put("tsd.timeseriesfilter.enable", "false"); + default_map.put("tsd.uid.use_mode", "false"); + default_map.put("tsd.uid.lru.enable", "false"); + default_map.put("tsd.uid.lru.name.size", "5000000"); + default_map.put("tsd.uid.lru.id.size", "5000000"); default_map.put("tsd.uidfilter.enable", "false"); default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); diff --git a/test/uid/TestUniqueId.java b/test/uid/TestUniqueId.java index 0b0393f855..0ce2a1224f 100644 --- a/test/uid/TestUniqueId.java +++ b/test/uid/TestUniqueId.java @@ -22,6 +22,7 @@ import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSDB.OperationMode; import net.opentsdb.core.BaseTsdbTest.UnitTestException; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.UniqueId.UniqueIdType; @@ -36,6 +37,7 @@ import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -83,11 +85,21 @@ public final class TestUniqueId { private static final String TAGV = "tagv"; private static final byte[] TAGV_ARRAY = { 't', 'a', 'g', 'v' }; private static final byte[] UID = new byte[] { 0, 0, 1 }; - private TSDB tsdb = mock(TSDB.class); - private HBaseClient client = mock(HBaseClient.class); + private TSDB tsdb; + private Config config; + private HBaseClient client; private UniqueId uid; private MockBase storage; + @Before + public void before() throws Exception { + tsdb = mock(TSDB.class); + client = mock(HBaseClient.class); + config = new Config(false); + when(tsdb.getClient()).thenReturn(client); + when(tsdb.getConfig()).thenReturn(config); + } + @Test(expected=IllegalArgumentException.class) public void testCtorZeroWidth() { uid = new UniqueId(client, table, METRIC, 0); @@ -1465,14 +1477,253 @@ public void deleteNoSuchUniqueName() throws Exception { assertEquals("sys.cpu.user", uid.getName(UID)); } + @Test + public void useLru() throws Exception { + config.overrideConfig("tsd.uid.lru.enable", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("foo", uid.getName(id)); + // Should be a cache hit ... + assertEquals("foo", uid.getName(id)); + + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + + // ... so verify there was only one HBase Get. + verify(client).get(anyGet()); + assertNotNull(uid.lruNameCache()); + assertNotNull(uid.lruIdCache()); + } + + @Test + public void useLruLimit() throws Exception { + config.overrideConfig("tsd.uid.lru.name.size", "2"); + config.overrideConfig("tsd.uid.lru.id.size", "2"); + config.overrideConfig("tsd.uid.lru.enable", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + byte[] id = { 0, 'a', 0x42 }; + byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("foo", uid.getName(id)); + // Should be a cache hit ... + assertEquals("foo", uid.getName(id)); + + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + + // ... so verify there was only one HBase Get. + verify(client).get(anyGet()); + + id = new byte[] { 0, 0, 1 }; + byte_name = new byte[] { 'b', 'a', 'r' }; + + kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("bar", uid.getName(id)); + assertEquals(1, uid.cacheHits()); + assertEquals(2, uid.cacheMisses()); + assertEquals(4, uid.cacheSize()); + verify(client, times(2)).get(anyGet()); + + // now one should be bumped out + id = new byte[] { 0, 0, 2 }; + byte_name = new byte[] { 'd', 'o', 'g' }; + + kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("dog", uid.getName(id)); + assertEquals(1, uid.cacheHits()); + assertEquals(3, uid.cacheMisses()); + assertEquals(4, uid.cacheSize()); + verify(client, times(3)).get(anyGet()); + } + + @Test + public void useModeRWGetName() throws Exception { + when(tsdb.getMode()).thenReturn(OperationMode.READWRITE); + config.overrideConfig("tsd.uid.use_mode", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("foo", uid.getName(id)); + // Should be a cache hit ... + assertEquals("foo", uid.getName(id)); + + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + + // ... so verify there was only one HBase Get. + verify(client).get(anyGet()); + } + + @Test + public void useModeROGetName() throws Exception { + when(tsdb.getMode()).thenReturn(OperationMode.READONLY); + config.overrideConfig("tsd.uid.use_mode", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("foo", uid.getName(id)); + // Should be a cache hit ... + assertEquals("foo", uid.getName(id)); + + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(1, uid.cacheSize()); + + // ... so verify there was only one HBase Get. + verify(client).get(anyGet()); + assertTrue(uid.nameCache().isEmpty()); + assertEquals(1, uid.idCache().size()); + } + + @Test + public void useModeWOGetName() throws Exception { + when(tsdb.getMode()).thenReturn(OperationMode.WRITEONLY); + config.overrideConfig("tsd.uid.use_mode", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(id, ID, METRIC_ARRAY, byte_name)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)) + .thenReturn(Deferred.fromResult(kvs)); + + assertEquals("foo", uid.getName(id)); + // NOT a cache hit since we didn't cache the first result. + assertEquals("foo", uid.getName(id)); + + assertEquals(0, uid.cacheHits()); + assertEquals(2, uid.cacheMisses()); + assertEquals(0, uid.cacheSize()); + + // 2 hbase hits this time + verify(client, times(2)).get(anyGet()); + assertTrue(uid.nameCache().isEmpty()); + assertTrue(uid.idCache().isEmpty()); + } + + @Test + public void useModeRWGetId() throws Exception { + when(tsdb.getMode()).thenReturn(OperationMode.READWRITE); + config.overrideConfig("tsd.uid.use_mode", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)); + + assertArrayEquals(id, uid.getId("foo")); + // Should be a cache hit ... + assertArrayEquals(id, uid.getId("foo")); + + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(2, uid.cacheSize()); + + // ... so verify there was only one HBase Get. + verify(client).get(anyGet()); + } + + @Test + public void useModeROGetId() throws Exception { + when(tsdb.getMode()).thenReturn(OperationMode.READONLY); + config.overrideConfig("tsd.uid.use_mode", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)) + .thenReturn(Deferred.fromResult(kvs)); + + assertArrayEquals(id, uid.getId("foo")); + // NOT a cache hit since we didn't cache the first time. + assertArrayEquals(id, uid.getId("foo")); + + assertEquals(0, uid.cacheHits()); + assertEquals(2, uid.cacheMisses()); + assertEquals(0, uid.cacheSize()); + + // two HBase hits + verify(client, times(2)).get(anyGet()); + assertTrue(uid.nameCache().isEmpty()); + assertTrue(uid.idCache().isEmpty()); + } + + @Test + public void useModeWOGetId() throws Exception { + when(tsdb.getMode()).thenReturn(OperationMode.WRITEONLY); + config.overrideConfig("tsd.uid.use_mode", "true"); + uid = new UniqueId(tsdb, table, METRIC, 3, false); + final byte[] id = { 0, 'a', 0x42 }; + final byte[] byte_name = { 'f', 'o', 'o' }; + + ArrayList<KeyValue> kvs = new ArrayList<KeyValue>(1); + kvs.add(new KeyValue(byte_name, ID, METRIC_ARRAY, id)); + when(client.get(anyGet())) + .thenReturn(Deferred.fromResult(kvs)) + .thenReturn(Deferred.fromResult(kvs)); + + assertArrayEquals(id, uid.getId("foo")); + // Should be a cache hit ... + assertArrayEquals(id, uid.getId("foo")); + + assertEquals(1, uid.cacheHits()); + assertEquals(1, uid.cacheMisses()); + assertEquals(1, uid.cacheSize()); + + // ... so verify there was only one HBase Get. + verify(client, times(1)).get(anyGet()); + assertEquals(1, uid.nameCache().size()); + assertTrue(uid.idCache().isEmpty()); + } + // ----------------- // // Helper functions. // // ----------------- // private void setupStorage() throws Exception { - final Config config = mock(Config.class); - when(tsdb.getConfig()).thenReturn(config); - when(tsdb.getClient()).thenReturn(client); storage = new MockBase(tsdb, client, true, true, true, true); final List<byte[]> families = new ArrayList<byte[]>(); From 47614a5f133a4e10ceb4d8b415f2726950ea9b9b Mon Sep 17 00:00:00 2001 From: rgidwani <rgidwani@salesforce.com> Date: Fri, 15 Sep 2017 15:07:54 -0700 Subject: [PATCH 675/826] DateTieredCompaction for existing tsdb tables #1065 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/TsdbQuery.java | 4 +- src/tools/CliOptions.java | 4 +- src/utils/Config.java | 16 ++ test/core/TestTsdbTSConfig.java | 324 +++++++++++++++++--------------- 4 files changed, 197 insertions(+), 151 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index c61f652e5a..0df64585f0 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1400,7 +1400,9 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { if(tsdb.getConfig().use_otsdb_timestamp()) { long stTime = (getScanStartTimeSeconds() * 1000); long endTime = end_time == UNSET ? -1 : (getScanEndTimeSeconds() * 1000); - scanner.setTimeRange(stTime, endTime); + if (tsdb.getConfig().get_date_tiered_compaction_start() <= stTime) { + scanner.setTimeRange(stTime, endTime); + } } if (tsuids != null && !tsuids.isEmpty()) { createAndSetTSUIDFilter(scanner); diff --git a/src/tools/CliOptions.java b/src/tools/CliOptions.java index f78684850a..60ddefc179 100644 --- a/src/tools/CliOptions.java +++ b/src/tools/CliOptions.java @@ -67,7 +67,7 @@ static void addAutoMetricFlag(final ArgP argp) { /** * Parse the command line arguments with the given options. - * @param options Options to parse in the given args. + * @param opt,ions Options to parse in the given args. * @param args Command line arguments to parse. * @return The remainder of the command line or * {@code null} if {@code args} were invalid and couldn't be parsed. @@ -154,6 +154,8 @@ static void overloadConfig(final ArgP argp, final Config config) { config.overrideConfig("tsd.network.worker_threads", entry.getValue()); } else if(entry.getKey().toLowerCase().equals("--use-otsdb-ts")) { config.overrideConfig("tsd.storage.use_otsdb_timestamp", "true"); + } else if (entry.getKey().toLowerCase().equals("--dtc-ts")) { + config.overrideConfig("tsd.storage.get_date_tiered_compaction_start", entry.getValue()); } } } diff --git a/src/utils/Config.java b/src/utils/Config.java index 8f8cb77286..db4780708b 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -110,6 +110,15 @@ public class Config { /** Sets the HBase cell timestamp equal to metric timestamp */ private boolean use_otsdb_timestamp = true; + /** tsd.storage.get_date_tiered_compaction_start */ + /** Sets the time at which you started using use_otsdb_timestamp + * this value is overriden if you have existing data in your tsdb table + * but don't want to re-write the cell timestamps, therefore you can start + * set this timestamp to when you start using use_otsdb_timestamp + * */ + private long get_date_tiered_compaction_start = 0; + + /** tsd.storage.use_max_value */ /** Used for resolving between data coming in at same timestamp */ /** If set to true, the maximum value will be returned, minimum */ @@ -273,6 +282,11 @@ public boolean use_otsdb_timestamp() { return use_otsdb_timestamp; } + /** @return the time at which you started storing data using otsdb_timestamp(), if not set defaults to 0 */ + public long get_date_tiered_compaction_start() { + return get_date_tiered_compaction_start; + } + public boolean use_max_value() { return use_max_value; } @@ -593,6 +607,7 @@ protected void setDefaults() { default_map.put("tsd.query.timeout", "0"); default_map.put("tsd.storage.use_otsdb_timestamp", "true"); default_map.put("tsd.storage.use_max_value", "true"); + default_map.put("tsd.storage.get_date_tiered_compaction_start", "0"); for (Map.Entry<String, String> entry : default_map.entrySet()) { if (!properties.containsKey(entry.getKey())) @@ -707,6 +722,7 @@ public void loadStaticVariables() { fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); use_otsdb_timestamp = this.getBoolean("tsd.storage.use_otsdb_timestamp"); + get_date_tiered_compaction_start = this.getLong("tsd.storage.get_date_tiered_compaction_start"); use_max_value = this.getBoolean("tsd.storage.use_max_value"); } diff --git a/test/core/TestTsdbTSConfig.java b/test/core/TestTsdbTSConfig.java index 1010b83512..5261a627c9 100644 --- a/test/core/TestTsdbTSConfig.java +++ b/test/core/TestTsdbTSConfig.java @@ -12,17 +12,14 @@ // see <http://www.gnu.org/licenses/>. package net.opentsdb.core; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - +import com.google.common.collect.ImmutableMap; +import com.stumbleupon.async.Deferred; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; import org.hbase.async.HBaseClient; import org.hbase.async.Scanner; import org.jboss.netty.util.HashedWheelTimer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; @@ -33,11 +30,13 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; -import com.stumbleupon.async.Deferred; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; -import net.opentsdb.storage.MockBase; -import net.opentsdb.uid.UniqueId; -import net.opentsdb.utils.Config; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; /** * Sets up a real TSDB with mocked client, compaction queue and timer along @@ -45,145 +44,172 @@ */ @RunWith(PowerMockRunner.class) @PowerMockIgnore({"javax.management.*", "javax.xml.*", - "ch.qos.*", "org.slf4j.*", - "com.sum.*", "org.xml.*"}) + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, UniqueId.class, HBaseClient.class, - HashedWheelTimer.class, Scanner.class, Const.class }) + HashedWheelTimer.class, Scanner.class, Const.class }) public class TestTsdbTSConfig { - /** A list of UIDs from A to Z for unit testing UIDs values */ - public static final Map<String, byte[]> METRIC_UIDS = - new HashMap<String, byte[]>(26); - public static final Map<String, byte[]> TAGK_UIDS = - new HashMap<String, byte[]>(26); - public static final Map<String, byte[]> TAGV_UIDS = - new HashMap<String, byte[]>(26); - static { - char letter = 'A'; - int uid = 10; - for (int i = 0; i < 26; i++) { - METRIC_UIDS.put(Character.toString(letter), - UniqueId.longToUID(uid, TSDB.metrics_width())); - TAGK_UIDS.put(Character.toString(letter), - UniqueId.longToUID(uid, TSDB.tagk_width())); - TAGV_UIDS.put(Character.toString(letter++), - UniqueId.longToUID(uid++, TSDB.tagv_width())); + /** A list of UIDs from A to Z for unit testing UIDs values */ + public static final Map<String, byte[]> METRIC_UIDS = + new HashMap<String, byte[]>(26); + public static final Map<String, byte[]> TAGK_UIDS = + new HashMap<String, byte[]>(26); + public static final Map<String, byte[]> TAGV_UIDS = + new HashMap<String, byte[]>(26); + static { + char letter = 'A'; + int uid = 10; + for (int i = 0; i < 26; i++) { + METRIC_UIDS.put(Character.toString(letter), + UniqueId.longToUID(uid, TSDB.metrics_width())); + TAGK_UIDS.put(Character.toString(letter), + UniqueId.longToUID(uid, TSDB.tagk_width())); + TAGV_UIDS.put(Character.toString(letter++), + UniqueId.longToUID(uid++, TSDB.tagv_width())); + } + } + + public static final String METRIC_STRING = "sys.cpu.user"; + public static final byte[] METRIC_BYTES = new byte[] { 0, 0, 1 }; + + public static final String TAGK_STRING = "host"; + public static final byte[] TAGK_BYTES = new byte[] { 0, 0, 1 }; + + public static final String TAGV_STRING = "web01"; + public static final byte[] TAGV_BYTES = new byte[] { 0, 0, 1 }; + + protected Config config; + protected TSDB tsdb; + protected HBaseClient client = mock(HBaseClient.class); + protected UniqueId metrics = mock(UniqueId.class); + protected UniqueId tag_names = mock(UniqueId.class); + protected UniqueId tag_values = mock(UniqueId.class); + protected Map<String, String> tags = new HashMap<String, String>(1); + protected MockBase storage; + + public void before(Map<String, String> overrideConfigs) throws Exception { + config = new Config(false); + for (Map.Entry<String, String> entry : overrideConfigs.entrySet()) { + config.overrideConfig(entry.getKey(), entry.getValue()); + } + + tsdb = PowerMockito.spy(new TSDB(config)); + + config.setAutoMetric(true); + + Whitebox.setInternalState(tsdb, "metrics", metrics); + Whitebox.setInternalState(tsdb, "tag_names", tag_names); + Whitebox.setInternalState(tsdb, "tag_values", tag_values); + + setupMetricMaps(); + setupTagkMaps(); + setupTagvMaps(); + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + tags.put(TAGK_STRING, TAGV_STRING); + } + + /** Adds the static UIDs to the metrics UID mock object */ + void setupMetricMaps() { + when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); + when(metrics.getIdAsync(METRIC_STRING)) + .thenAnswer(new Answer<Deferred<byte[]>>() { + @Override + public Deferred<byte[]> answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(METRIC_BYTES); + } + }); + when(metrics.getOrCreateId(METRIC_STRING)) + .thenReturn(METRIC_BYTES); + } + + /** Adds the static UIDs to the tag keys UID mock object */ + void setupTagkMaps() { + when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); + when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); + when(tag_names.getIdAsync(TAGK_STRING)) + .thenAnswer(new Answer<Deferred<byte[]>>() { + @Override + public Deferred<byte[]> answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGK_BYTES); + } + }); + when(tag_names.getOrCreateIdAsync(TAGK_STRING)) + .thenReturn(Deferred.fromResult(TAGK_BYTES)); + + } + + /** Adds the static UIDs to the tag values UID mock object */ + void setupTagvMaps() { + when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); + when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); + when(tag_values.getIdAsync(TAGV_STRING)) + .thenAnswer(new Answer<Deferred<byte[]>>() { + @Override + public Deferred<byte[]> answer(InvocationOnMock invocation) + throws Throwable { + return Deferred.fromResult(TAGV_BYTES); + } + }); + when(tag_values.getOrCreateIdAsync(TAGV_STRING)) + .thenReturn(Deferred.fromResult(TAGV_BYTES)); + + } + + @Test + public void scannerTimestampsEqualToQueryTimestamps() throws Exception { + before(ImmutableMap.of("tsd.storage.enable_compaction", "false")); + TSQuery q = new TSQuery(); + q.setStart("1h-ago"); + + TSSubQuery subQuery = new TSSubQuery(); + subQuery.setMetric(METRIC_STRING); + subQuery.setAggregator("none"); + + ArrayList<TSSubQuery> list = new ArrayList<TSSubQuery>(); + list.add(subQuery); + q.setQueries(list); + q.validateAndSetQuery(); + + TsdbQuery query = new TsdbQuery(tsdb); + query.configureFromQuery(q, 0); + + Scanner scanner = query.getScanner(); + long minTs = scanner.getMinTimestamp(); + long maxTs = scanner.getMaxTimestamp(); + // For 1h - ago, the TSDB will create a window of 2 hrs, For example if the current time is 2:45 PM, the window will be 1 PM to 3 PM + assert((maxTs - minTs) == (2 * 3600000)); + } + + @Test + public void scannerTimestampsAreOnlySetWhenDTCTimestampIsOlder() throws Exception { + String now = String.valueOf(System.currentTimeMillis()); + before(ImmutableMap.of("tsd.storage.get_date_tiered_compaction_start", now)); + TSQuery q = new TSQuery(); + q.setStart("1h-ago"); + + TSSubQuery subQuery = new TSSubQuery(); + subQuery.setMetric(METRIC_STRING); + subQuery.setAggregator("none"); + + ArrayList<TSSubQuery> list = new ArrayList<TSSubQuery>(); + list.add(subQuery); + q.setQueries(list); + q.validateAndSetQuery(); + + TsdbQuery query = new TsdbQuery(tsdb); + query.configureFromQuery(q, 0); + + Scanner scanner = query.getScanner(); + long minTs = scanner.getMinTimestamp(); + long maxTs = scanner.getMaxTimestamp(); + assertEquals(0L, minTs); + assertEquals(Long.MAX_VALUE, maxTs); } - } - - public static final String METRIC_STRING = "sys.cpu.user"; - public static final byte[] METRIC_BYTES = new byte[] { 0, 0, 1 }; - - public static final String TAGK_STRING = "host"; - public static final byte[] TAGK_BYTES = new byte[] { 0, 0, 1 }; - - public static final String TAGV_STRING = "web01"; - public static final byte[] TAGV_BYTES = new byte[] { 0, 0, 1 }; - - protected Config config; - protected TSDB tsdb; - protected HBaseClient client = mock(HBaseClient.class); - protected UniqueId metrics = mock(UniqueId.class); - protected UniqueId tag_names = mock(UniqueId.class); - protected UniqueId tag_values = mock(UniqueId.class); - protected Map<String, String> tags = new HashMap<String, String>(1); - protected MockBase storage; - - @Before - public void before() throws Exception { - - config = new Config(false); - config.overrideConfig("tsd.storage.enable_compaction", "false"); - tsdb = PowerMockito.spy(new TSDB(config)); - - config.setAutoMetric(true); - - Whitebox.setInternalState(tsdb, "metrics", metrics); - Whitebox.setInternalState(tsdb, "tag_names", tag_names); - Whitebox.setInternalState(tsdb, "tag_values", tag_values); - - setupMetricMaps(); - setupTagkMaps(); - setupTagvMaps(); - - when(metrics.width()).thenReturn((short)3); - when(tag_names.width()).thenReturn((short)3); - when(tag_values.width()).thenReturn((short)3); - - tags.put(TAGK_STRING, TAGV_STRING); - } - - /** Adds the static UIDs to the metrics UID mock object */ - void setupMetricMaps() { - when(metrics.getId(METRIC_STRING)).thenReturn(METRIC_BYTES); - when(metrics.getIdAsync(METRIC_STRING)) - .thenAnswer(new Answer<Deferred<byte[]>>() { - @Override - public Deferred<byte[]> answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(METRIC_BYTES); - } - }); - when(metrics.getOrCreateId(METRIC_STRING)) - .thenReturn(METRIC_BYTES); - } - - /** Adds the static UIDs to the tag keys UID mock object */ - void setupTagkMaps() { - when(tag_names.getId(TAGK_STRING)).thenReturn(TAGK_BYTES); - when(tag_names.getOrCreateId(TAGK_STRING)).thenReturn(TAGK_BYTES); - when(tag_names.getIdAsync(TAGK_STRING)) - .thenAnswer(new Answer<Deferred<byte[]>>() { - @Override - public Deferred<byte[]> answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGK_BYTES); - } - }); - when(tag_names.getOrCreateIdAsync(TAGK_STRING)) - .thenReturn(Deferred.fromResult(TAGK_BYTES)); - - } - - /** Adds the static UIDs to the tag values UID mock object */ - void setupTagvMaps() { - when(tag_values.getId(TAGV_STRING)).thenReturn(TAGV_BYTES); - when(tag_values.getOrCreateId(TAGV_STRING)).thenReturn(TAGV_BYTES); - when(tag_values.getIdAsync(TAGV_STRING)) - .thenAnswer(new Answer<Deferred<byte[]>>() { - @Override - public Deferred<byte[]> answer(InvocationOnMock invocation) - throws Throwable { - return Deferred.fromResult(TAGV_BYTES); - } - }); - when(tag_values.getOrCreateIdAsync(TAGV_STRING)) - .thenReturn(Deferred.fromResult(TAGV_BYTES)); - - } - - @Test - public void scannerTimestampsEqualToQueryTimestamps() { - - TSQuery q = new TSQuery(); - q.setStart("1h-ago"); - - TSSubQuery subQuery = new TSSubQuery(); - subQuery.setMetric(METRIC_STRING); - subQuery.setAggregator("none"); - - ArrayList<TSSubQuery> list = new ArrayList<TSSubQuery>(); - list.add(subQuery); - q.setQueries(list); - q.validateAndSetQuery(); - - TsdbQuery query = new TsdbQuery(tsdb); - query.configureFromQuery(q, 0); - - Scanner scanner = query.getScanner(); - long minTs = scanner.getMinTimestamp(); - long maxTs = scanner.getMaxTimestamp(); - // For 1h - ago, the TSDB will create a window of 2 hrs, For example if the current time is 2:45 PM, the window will be 1 PM to 3 PM - assert((maxTs - minTs) == (2 * 3600000)); - } } \ No newline at end of file From 84b77c567e6bd6fd2c8a572e10ed5d1513961b94 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sun, 8 Oct 2017 16:00:57 -0700 Subject: [PATCH 676/826] Fix #1059 by enabling standard APIs for all roles (including api/stats) and then using the new OperationMode to determine what APIs should be enabled. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tsd/RpcHandler.java | 3 +- src/tsd/RpcManager.java | 118 +++++++++++++++++++++++------------ test/tsd/TestRpcManager.java | 4 ++ 3 files changed, 82 insertions(+), 43 deletions(-) diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index 69224ca8b5..0a89c99e0e 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -91,9 +91,8 @@ public RpcHandler(final TSDB tsdb, final RpcManager manager) { this.rpc_manager = manager; final String cors = tsdb.getConfig().getString("tsd.http.request.cors_domains"); - final String mode = tsdb.getConfig().getString("tsd.mode"); - LOG.info("TSD is in " + mode + " mode"); + LOG.info("TSD is in " + tsdb.getMode() + " mode"); if (cors == null || cors.isEmpty()) { cors_domains = null; diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 7a7dfb16b7..d757d686dc 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -43,6 +43,7 @@ import net.opentsdb.tools.BuildData; import net.opentsdb.core.Aggregators; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSDB.OperationMode; import net.opentsdb.query.filter.TagVFilter; import net.opentsdb.stats.StatsCollector; import net.opentsdb.utils.Config; @@ -132,8 +133,7 @@ public static synchronized RpcManager instance(final TSDB tsdb) { } final RpcManager manager = new RpcManager(tsdb); - final String mode = Strings.nullToEmpty(tsdb.getConfig().getString("tsd.mode")); - + // Load any plugins that are enabled via Config. Fail if any plugin cannot be loaded. final ImmutableList.Builder<RpcPlugin> rpcBuilder = ImmutableList.builder(); @@ -145,14 +145,14 @@ public static synchronized RpcManager instance(final TSDB tsdb) { final ImmutableMap.Builder<String, TelnetRpc> telnetBuilder = ImmutableMap.builder(); final ImmutableMap.Builder<String, HttpRpc> httpBuilder = ImmutableMap.builder(); - manager.initializeBuiltinRpcs(mode, telnetBuilder, httpBuilder); + manager.initializeBuiltinRpcs(tsdb.getMode(), telnetBuilder, httpBuilder); manager.telnet_commands = telnetBuilder.build(); manager.http_commands = httpBuilder.build(); final ImmutableMap.Builder<String, HttpRpcPlugin> httpPluginsBuilder = ImmutableMap.builder(); if (tsdb.getConfig().hasProperty("tsd.http.rpc.plugins")) { final String[] plugins = tsdb.getConfig().getString("tsd.http.rpc.plugins").split(","); - manager.initializeHttpRpcPlugins(mode, plugins, httpPluginsBuilder); + manager.initializeHttpRpcPlugins(tsdb.getMode(), plugins, httpPluginsBuilder); } manager.http_plugin_commands = httpPluginsBuilder.build(); @@ -248,7 +248,7 @@ boolean isHttpRpcPluginPath(final String uri) { * instances. * @param http a map of API endpoints to {@link HttpRpc} instances. */ - private void initializeBuiltinRpcs(final String mode, + private void initializeBuiltinRpcs(final OperationMode mode, final ImmutableMap.Builder<String, TelnetRpc> telnet, final ImmutableMap.Builder<String, HttpRpc> http) { @@ -258,64 +258,100 @@ private void initializeBuiltinRpcs(final String mode, LOG.info("Mode: {}, HTTP UI Enabled: {}, HTTP API Enabled: {}", mode, enableUi, enableApi); - if (mode.equals("rw") || mode.equals("wo")) { - final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); - final RollupDataPointRpc rollups = new RollupDataPointRpc(tsdb.getConfig()); - final HistogramDataPointRpc histos = new HistogramDataPointRpc(tsdb.getConfig()); + // defaults common to every mode + final StatsRpc stats = new StatsRpc(); + final ListAggregators aggregators = new ListAggregators(); + final DropCachesRpc dropcaches = new DropCachesRpc(); + final Version version = new Version(); + + telnet.put("stats", stats); + telnet.put("dropcaches", dropcaches); + telnet.put("version", version); + telnet.put("exit", new Exit()); + telnet.put("help", new Help()); + + if (enableUi) { + http.put("aggregators", aggregators); + http.put("logs", new LogsRpc()); + http.put("stats", stats); + http.put("version", version); + } + + if (enableApi) { + http.put("api/aggregators", aggregators); + http.put("api/config", new ShowConfig()); + http.put("api/dropcaches", dropcaches); + http.put("api/stats", stats); + http.put("api/version", version); + } + + final PutDataPointRpc put = new PutDataPointRpc(tsdb.getConfig()); + final RollupDataPointRpc rollups = new RollupDataPointRpc(tsdb.getConfig()); + final HistogramDataPointRpc histos = new HistogramDataPointRpc(tsdb.getConfig()); + final SuggestRpc suggest_rpc = new SuggestRpc(); + final AnnotationRpc annotation_rpc = new AnnotationRpc(); + final StaticFileRpc staticfile = new StaticFileRpc(); + + switch(mode) { + case WRITEONLY: telnet.put("put", put); telnet.put("rollup", rollups); telnet.put("histogram", histos); + if (enableApi) { + http.put("api/annotation", annotation_rpc); + http.put("api/annotations", annotation_rpc); http.put("api/put", put); http.put("api/rollup", rollups); http.put("api/histogram", histos); + http.put("api/tree", new TreeRpc()); + http.put("api/uid", new UniqueIdRpc()); } - } - - if (mode.equals("rw") || mode.equals("ro")) { - final StaticFileRpc staticfile = new StaticFileRpc(); - final StatsRpc stats = new StatsRpc(); - final DropCachesRpc dropcaches = new DropCachesRpc(); - final ListAggregators aggregators = new ListAggregators(); - final SuggestRpc suggest_rpc = new SuggestRpc(); - final AnnotationRpc annotation_rpc = new AnnotationRpc(); - final Version version = new Version(); - - telnet.put("stats", stats); - telnet.put("dropcaches", dropcaches); - telnet.put("version", version); - telnet.put("exit", new Exit()); - telnet.put("help", new Help()); - + break; + case READONLY: if (enableUi) { http.put("", new HomePage()); - http.put("aggregators", aggregators); - http.put("dropcaches", dropcaches); + http.put("s", staticfile); http.put("favicon.ico", staticfile); - http.put("logs", new LogsRpc()); + http.put("suggest", suggest_rpc); http.put("q", new GraphHandler()); + } + + if (enableApi) { + http.put("api/query", new QueryRpc()); + http.put("api/search", new SearchRpc()); + http.put("api/suggest", suggest_rpc); + } + + break; + case READWRITE: + telnet.put("put", put); + telnet.put("rollup", rollups); + telnet.put("histogram", histos); + + if (enableUi) { + http.put("", new HomePage()); http.put("s", staticfile); - http.put("stats", stats); + http.put("favicon.ico", staticfile); http.put("suggest", suggest_rpc); - http.put("version", version); + http.put("q", new GraphHandler()); } - + if (enableApi) { - http.put("api/aggregators", aggregators); - http.put("api/annotation", annotation_rpc); - http.put("api/annotations", annotation_rpc); - http.put("api/config", new ShowConfig()); - http.put("api/dropcaches", dropcaches); http.put("api/query", new QueryRpc()); http.put("api/search", new SearchRpc()); - http.put("api/serializers", new Serializers()); - http.put("api/stats", stats); + http.put("api/annotation", annotation_rpc); + http.put("api/annotations", annotation_rpc); http.put("api/suggest", suggest_rpc); + http.put("api/put", put); + 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/version", version); } } + + if (enableDieDieDie) { final DieDieDie diediedie = new DieDieDie(); @@ -338,7 +374,7 @@ private void initializeBuiltinRpcs(final String mode, * to {@link HttpRpcPlugin} instance. */ @VisibleForTesting - protected void initializeHttpRpcPlugins(final String mode, + protected void initializeHttpRpcPlugins(final OperationMode mode, final String[] pluginClassNames, final ImmutableMap.Builder<String, HttpRpcPlugin> http) { for (final String plugin : pluginClassNames) { diff --git a/test/tsd/TestRpcManager.java b/test/tsd/TestRpcManager.java index fc062bc506..e97cadfc06 100644 --- a/test/tsd/TestRpcManager.java +++ b/test/tsd/TestRpcManager.java @@ -29,6 +29,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSDB.OperationMode; import net.opentsdb.utils.Config; import net.opentsdb.utils.PluginLoader; @@ -53,6 +54,7 @@ public void before() { when(config.getString("tsd.no_diediedie")) .thenReturn("false"); TSDB tsdb = mock(TSDB.class); + when(tsdb.getMode()).thenReturn(OperationMode.READWRITE); when(tsdb.getConfig()).thenReturn(config); mock_tsdb_no_plugins = tsdb; } @@ -79,6 +81,7 @@ public void loadHttpRpcPlugins() throws Exception { .thenReturn("false"); TSDB tsdb = mock(TSDB.class); + when(tsdb.getMode()).thenReturn(OperationMode.READWRITE); when(tsdb.getConfig()).thenReturn(config); PluginLoader.loadJAR("plugin_test.jar"); @@ -109,6 +112,7 @@ public void loadRpcPlugin() throws Exception { .thenReturn("false"); TSDB tsdb = mock(TSDB.class); + when(tsdb.getMode()).thenReturn(OperationMode.READWRITE); when(tsdb.getConfig()).thenReturn(config); PluginLoader.loadJAR("plugin_test.jar"); From 2a98502d8f842e7fed28aae5d25eb415a7483ace Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sun, 8 Oct 2017 21:50:58 -0700 Subject: [PATCH 677/826] Cut release of 2.4RC2 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- NEWS | 27 +++++++++++++++++++++++++++ THANKS | 1 + configure.ac | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index e697dc9c96..d2ea179e19 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,32 @@ OpenTSDB - Changelog +* Version 2.4.0 RC2 (2017-10-08) + +Noteworthy Changes: + - Modify the RPC handler plugin system so that it parses only the first part of + the URI instead of the entire path. Now plugins can implement sub-paths. + - Return the HTML 5 doctype for built-in UI pages + - Add an optional byte and/or data point limit to the amount of data fetched + from storage. This allows admins to prevent OOMing TSDs due to massive queries. + - Allow a start time via config when enabling the date tiered compaction in HBase + - Provide the option of using an LRU for caching UIDs to avoid OOMing writers and + readers with too many strings + - Optionally avoid writing to the forward or reverse UID maps when a specific TSD + operational mode is enabled to avoid wasting memory on maps that will never be + used. + +Bug Fixes: + - Roll back UTF8 issue with UIDs in RC1 wherein the stored bytes weren't converting + properly and vice-versa. We'll have to work on full UTF8 support in 3.x + - Fix a build issue for Javacc + - Add Kryo as a dependency to the fat jar + - Javadoc fixes + - Fix an issue with calendar aligned downsampling by seeking to the start time of + the query when the zone-aligned timestamp may be earlier than the query start time + - Add the missing QueryLimitOverride to the makefile + - Fix compatibility with Bigtable for 2.4 + - Enable standard read-only APIs when the TSD is in write only mode + * Version 2.4.0 RC1 (2017-06-11) Noteworthy Changes: diff --git a/THANKS b/THANKS index a06a0d1a7f..b100703a7f 100644 --- a/THANKS +++ b/THANKS @@ -63,6 +63,7 @@ Kevin Bowling Kevin Landreth Kieren Hynd <kieren.hynd@ticketmaster.co.uk> Kimoon Kim <kimoon@pepperdata.com> +Kousha Hamidi Kris Beevers <beevek@gmail.com> Kyle Brandt Lex Herbert <lex.herbert@gmail.com> diff --git a/configure.ac b/configure.ac index 1bc3c05a4d..659a5cd976 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.4.0RC1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.4.0RC2], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From f68fc404804d29f11287b1f0aaacef170a48594e Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Tue, 28 Nov 2017 18:42:22 -0600 Subject: [PATCH 678/826] Fixing deb postrm (#1115) --- build-aux/deb/control/postrm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-aux/deb/control/postrm b/build-aux/deb/control/postrm index 8cd9f45e92..aa12a2d040 100644 --- a/build-aux/deb/control/postrm +++ b/build-aux/deb/control/postrm @@ -7,7 +7,7 @@ case "$1" in rm -rf /var/log/opentsdb # remove **only** empty data dir - rmdir -p --ignore-fail-on-non-empty /tmp/opentsdb + rmdir --ignore-fail-on-non-empty /tmp/opentsdb ;; purge) From 70e8ac4b618a61fb44a5056a6618282f51565df6 Mon Sep 17 00:00:00 2001 From: Suman <sumannewton@gmail.com> Date: Wed, 29 Nov 2017 06:18:39 +0530 Subject: [PATCH 679/826] Support for downsample fill policies (#1109) --- tools/check_tsd | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index 0159db4d82..db888b82fa 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -36,6 +36,8 @@ AGGREGATORS = ('avg', 'count', 'dev', 'p50', 'p75', 'p90', 'p95', 'p99', 'p999', 'sum', 'zimsum') +FILL_POLICIES = ('none','nan','null','zero') + def main(argv): """Pulls data out of the TSDB and do very simple alerting from Nagios.""" @@ -54,6 +56,8 @@ def main(argv): help='Downsample function, e.g. one of avg, min, sum, or max.') parser.add_option('-W', '--downsample-window', type='int', default=60, metavar='SECONDS', help='Window size over which to downsample.') + parser.add_option('-F', '--downsample-fill-policy', default='none', metavar='POLICY', + help='Fill Policies, e.g. one of none, nan, null, or zero.') parser.add_option('-a', '--aggregator', default='sum', metavar='METHOD', help='Aggregation method: avg, min, sum (default), max.') parser.add_option('-x', '--method', dest='comparator', default='gt', @@ -93,6 +97,8 @@ def main(argv): parser.error("Comparator '%s' not valid." % options.comparator) elif options.downsample not in ('none',)+AGGREGATORS: parser.error("Downsample '%s' not valid." % options.downsample) + elif options.downsample_fill_policy not in FILL_POLICIES: + parser.error("Downsample Fill policy '%s' not valid." % options.downsample_fill_policy) elif options.aggregator not in AGGREGATORS: parser.error("Aggregator '%s' not valid." % options.aggregator) elif not options.metric: @@ -125,8 +131,8 @@ def main(argv): if options.downsample == 'none': downsampling = '' else: - downsampling = '%ds-%s:' % (options.downsample_window, - options.downsample) + downsampling = '%ds-%s-%s:' % (options.downsample_window, + options.downsample, options.downsample_fill_policy) if options.rate: rate = 'rate:' else: From 95790c73e09727905e2b971802c657c29c178efc Mon Sep 17 00:00:00 2001 From: Jeffrey 'jf' Lim <jf@users.noreply.github.com> Date: Wed, 29 Nov 2017 08:50:33 +0800 Subject: [PATCH 680/826] Update scripts + config in build-aux/deb (#1103) * build-aux/deb/init.d/opentsdb: fix JDK_DIRS list for JDK8 (oracle ain't the only JDK) * build-aux/deb/init.d/opentsdb: tidy up JDK_DIRS list * Remove JDK 6 and 7 if they're now deprecated (see 8feeb581bd80521dabb87d4be5ace7c785921d3b) * build-aux/deb/opentsdb.conf: remove trailing spaces * Harmonize build-aux/{rpm,deb}/opentsdb.conf (there is no reason for them to diverge) * build-aux/deb/init.d/opentsdb: put back JDK 7 (thanks, @IDerr) --- build-aux/deb/init.d/opentsdb | 14 ++++++++------ build-aux/deb/opentsdb.conf | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/build-aux/deb/init.d/opentsdb b/build-aux/deb/init.d/opentsdb index 9e46e9037e..f0e69b2d25 100644 --- a/build-aux/deb/init.d/opentsdb +++ b/build-aux/deb/init.d/opentsdb @@ -29,12 +29,14 @@ MAX_OPEN_FILES=65535 # The first existing directory is used for JAVA_HOME # (if JAVA_HOME is not defined in $DEFAULT) -JDK_DIRS="/usr/lib/jvm/java-8-oracle \ - /usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ - /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ - /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-6-openjdk \ - /usr/lib/jvm/java-6-openjdk-amd64 /usr/lib/jvm/java-6-openjdk-i386 \ - /usr/lib/jvm/default-java" +JDK_DIRS="\ + /usr/lib/jvm/java-8-oracle /usr/lib/jvm/java-8-openjdk \ + /usr/lib/jvm/java-8-openjdk-amd64/ /usr/lib/jvm/java-8-openjdk-i386/ \ + \ + /usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ + /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ + \ + /usr/lib/jvm/default-java" # Look for the right JVM to use for jdir in $JDK_DIRS; do diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index 70afee8737..052936b962 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -6,14 +6,14 @@ tsd.network.port = 4242 # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Disable Nagel's algorithm. Default is True +# Disable Nagel's algorithm, default is True #tsd.network.tcp_no_delay = true -# Determines whether or not to send keepalive packets to peers, default +# Determines whether or not to send keepalive packets to peers, default # is True #tsd.network.keep_alive = true -# Determines if the same socket should be used for new connections, default +# Determines if the same socket should be used for new connections, default # is True #tsd.network.reuse_address = true @@ -44,7 +44,7 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true -# How often, in milliseconds, to flush the data point queue to storage, +# How often, in milliseconds, to flush the data point queue to storage, # default is 1,000 # tsd.storage.flush_interval = 1000 @@ -57,6 +57,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A comma separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost From 9947e9508abd0a5b52235b15dcc5605ff2a4dd0d Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Tue, 28 Nov 2017 21:46:20 -0600 Subject: [PATCH 681/826] Include #1109 and #1103 in Next Branch (#1116) * Support for downsample fill policies (#1109) * Update scripts + config in build-aux/deb (#1103) * build-aux/deb/init.d/opentsdb: fix JDK_DIRS list for JDK8 (oracle ain't the only JDK) * build-aux/deb/init.d/opentsdb: tidy up JDK_DIRS list * Remove JDK 6 and 7 if they're now deprecated (see 8feeb581bd80521dabb87d4be5ace7c785921d3b) * build-aux/deb/opentsdb.conf: remove trailing spaces * Harmonize build-aux/{rpm,deb}/opentsdb.conf (there is no reason for them to diverge) * build-aux/deb/init.d/opentsdb: put back JDK 7 (thanks, @IDerr) --- build-aux/deb/init.d/opentsdb | 14 ++++++++------ build-aux/deb/opentsdb.conf | 10 +++++----- tools/check_tsd | 10 ++++++++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/build-aux/deb/init.d/opentsdb b/build-aux/deb/init.d/opentsdb index 9e46e9037e..f0e69b2d25 100644 --- a/build-aux/deb/init.d/opentsdb +++ b/build-aux/deb/init.d/opentsdb @@ -29,12 +29,14 @@ MAX_OPEN_FILES=65535 # The first existing directory is used for JAVA_HOME # (if JAVA_HOME is not defined in $DEFAULT) -JDK_DIRS="/usr/lib/jvm/java-8-oracle \ - /usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ - /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ - /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-6-openjdk \ - /usr/lib/jvm/java-6-openjdk-amd64 /usr/lib/jvm/java-6-openjdk-i386 \ - /usr/lib/jvm/default-java" +JDK_DIRS="\ + /usr/lib/jvm/java-8-oracle /usr/lib/jvm/java-8-openjdk \ + /usr/lib/jvm/java-8-openjdk-amd64/ /usr/lib/jvm/java-8-openjdk-i386/ \ + \ + /usr/lib/jvm/java-7-oracle /usr/lib/jvm/java-7-openjdk \ + /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-i386/ \ + \ + /usr/lib/jvm/default-java" # Look for the right JVM to use for jdir in $JDK_DIRS; do diff --git a/build-aux/deb/opentsdb.conf b/build-aux/deb/opentsdb.conf index 70afee8737..052936b962 100644 --- a/build-aux/deb/opentsdb.conf +++ b/build-aux/deb/opentsdb.conf @@ -6,14 +6,14 @@ tsd.network.port = 4242 # The IPv4 network address to bind to, defaults to all addresses # tsd.network.bind = 0.0.0.0 -# Disable Nagel's algorithm. Default is True +# Disable Nagel's algorithm, default is True #tsd.network.tcp_no_delay = true -# Determines whether or not to send keepalive packets to peers, default +# Determines whether or not to send keepalive packets to peers, default # is True #tsd.network.keep_alive = true -# Determines if the same socket should be used for new connections, default +# Determines if the same socket should be used for new connections, default # is True #tsd.network.reuse_address = true @@ -44,7 +44,7 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Whether or not to enable data compaction in HBase, default is True #tsd.storage.enable_compaction = true -# How often, in milliseconds, to flush the data point queue to storage, +# How often, in milliseconds, to flush the data point queue to storage, # default is 1,000 # tsd.storage.flush_interval = 1000 @@ -57,6 +57,6 @@ tsd.core.plugin_path = /usr/share/opentsdb/plugins # Path under which the znode for the -ROOT- region is located, default is "/hbase" #tsd.storage.hbase.zk_basedir = /hbase -# A comma separated list of Zookeeper hosts to connect to, with or without +# A comma separated list of Zookeeper hosts to connect to, with or without # port specifiers, default is "localhost" #tsd.storage.hbase.zk_quorum = localhost diff --git a/tools/check_tsd b/tools/check_tsd index 0159db4d82..db888b82fa 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -36,6 +36,8 @@ AGGREGATORS = ('avg', 'count', 'dev', 'p50', 'p75', 'p90', 'p95', 'p99', 'p999', 'sum', 'zimsum') +FILL_POLICIES = ('none','nan','null','zero') + def main(argv): """Pulls data out of the TSDB and do very simple alerting from Nagios.""" @@ -54,6 +56,8 @@ def main(argv): help='Downsample function, e.g. one of avg, min, sum, or max.') parser.add_option('-W', '--downsample-window', type='int', default=60, metavar='SECONDS', help='Window size over which to downsample.') + parser.add_option('-F', '--downsample-fill-policy', default='none', metavar='POLICY', + help='Fill Policies, e.g. one of none, nan, null, or zero.') parser.add_option('-a', '--aggregator', default='sum', metavar='METHOD', help='Aggregation method: avg, min, sum (default), max.') parser.add_option('-x', '--method', dest='comparator', default='gt', @@ -93,6 +97,8 @@ def main(argv): parser.error("Comparator '%s' not valid." % options.comparator) elif options.downsample not in ('none',)+AGGREGATORS: parser.error("Downsample '%s' not valid." % options.downsample) + elif options.downsample_fill_policy not in FILL_POLICIES: + parser.error("Downsample Fill policy '%s' not valid." % options.downsample_fill_policy) elif options.aggregator not in AGGREGATORS: parser.error("Aggregator '%s' not valid." % options.aggregator) elif not options.metric: @@ -125,8 +131,8 @@ def main(argv): if options.downsample == 'none': downsampling = '' else: - downsampling = '%ds-%s:' % (options.downsample_window, - options.downsample) + downsampling = '%ds-%s-%s:' % (options.downsample_window, + options.downsample, options.downsample_fill_policy) if options.rate: rate = 'rate:' else: From 97102ba52cb96e3373890a22caa3485ada5bf065 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 30 Jan 2018 17:48:48 -0800 Subject: [PATCH 682/826] Add a logback-test.xml file to supress logback during tests, hopefully this will help speed things up a bit. Also mock out the timer and threads in TestFsck as it wasn't before! Also remove the precise limiter. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- .travis.yml | 2 +- test/resources/logback-test.xml | 1 + test/tools/TestFsck.java | 35 +++++++++++++++++++++++++++------ test/tools/TestFsckSalted.java | 25 +++++++++++++++++++++++ test/tools/TestUID.java | 19 +++++++++++++++--- 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 test/resources/logback-test.xml diff --git a/.travis.yml b/.travis.yml index 9354fb4ea2..d85ebeef4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: java before_script: ./build.sh pom.xml -script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet +script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet -Dlogback.configurationFile=test/resources/logback-test.xml addons: hostname: short-hostname jdk: diff --git a/test/resources/logback-test.xml b/test/resources/logback-test.xml new file mode 100644 index 0000000000..9afb833523 --- /dev/null +++ b/test/resources/logback-test.xml @@ -0,0 +1 @@ +<configuration /> diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index f99a343cf7..30ba044509 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -27,12 +29,14 @@ import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.meta.Annotation; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; import org.hbase.async.Bytes; import org.hbase.async.GetRequest; @@ -40,6 +44,7 @@ import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,7 +61,8 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, GetRequest.class, PutRequest.class, KeyValue.class, Fsck.class, - FsckOptions.class, Scanner.class, Annotation.class, Tags.class }) + FsckOptions.class, Scanner.class, Annotation.class, Tags.class, + HashedWheelTimer.class, Threads.class }) public class TestFsck { protected byte[] GLOBAL_ROW = new byte[] {0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}; @@ -66,12 +72,13 @@ public class TestFsck { protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; protected Config config; protected TSDB tsdb = null; - protected HBaseClient client = mock(HBaseClient.class); - protected UniqueId metrics = mock(UniqueId.class); - protected UniqueId tag_names = mock(UniqueId.class); - protected UniqueId tag_values = mock(UniqueId.class); + protected HBaseClient client; + protected UniqueId metrics; + protected UniqueId tag_names; + protected UniqueId tag_values; protected MockBase storage; - protected FsckOptions options = mock(FsckOptions.class); + protected FsckOptions options; + protected FakeTaskTimer timer; protected final static List<byte[]> tags = new ArrayList<byte[]>(1); static { tags.add(new byte[] { 0, 0, 1, 0, 0, 1}); @@ -80,6 +87,22 @@ public class TestFsck { @SuppressWarnings("unchecked") @Before public void before() throws Exception { + client = mock(HBaseClient.class); + metrics = mock(UniqueId.class); + tag_names = mock(UniqueId.class); + tag_values = mock(UniqueId.class); + options = mock(FsckOptions.class); + timer = new FakeTaskTimer(); + + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + config = new Config(false); tsdb = new TSDB(client, config); when(client.flush()).thenReturn(Deferred.fromResult(null)); diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java index 4ccd07ef60..192e8dfd71 100644 --- a/test/tools/TestFsckSalted.java +++ b/test/tools/TestFsckSalted.java @@ -1,11 +1,16 @@ package net.opentsdb.tools; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Field; import java.util.ArrayList; +import org.hbase.async.HBaseClient; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -15,15 +20,35 @@ import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; @PrepareForTest({ Const.class }) public class TestFsckSalted extends TestFsck { + @SuppressWarnings("unchecked") @Before public void before() throws Exception { + client = mock(HBaseClient.class); + metrics = mock(UniqueId.class); + tag_names = mock(UniqueId.class); + tag_values = mock(UniqueId.class); + options = mock(FsckOptions.class); + timer = new FakeTaskTimer(); + + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index e7a0bc939c..ef52bebd2d 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -15,19 +15,24 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Method; import net.opentsdb.core.TSDB; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.storage.MockBase; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.junit.After; import org.junit.Test; @@ -41,13 +46,14 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, HBaseClient.class, - KeyValue.class, UidManager.class, + KeyValue.class, UidManager.class, HashedWheelTimer.class, Threads.class, Scanner.class, DeleteRequest.class }) public class TestUID { private Config config; private TSDB tsdb = null; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; + private FakeTaskTimer timer = new FakeTaskTimer(); // names used for testing private byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); @@ -69,8 +75,15 @@ public class TestUID { @Before public void before() throws Exception { - - PowerMockito.whenNew(HBaseClient.class).withAnyArguments().thenReturn(client); + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + config = new Config(false); tsdb = new TSDB(client, config); PowerMockito.spy(System.class); From d7b786f6d8009ed8299b82946a3cb8424ba553da Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 30 Jan 2018 17:48:48 -0800 Subject: [PATCH 683/826] Add a logback-test.xml file to supress logback during tests, hopefully this will help speed things up a bit. Also mock out the timer and threads in TestFsck as it wasn't before! Also remove the precise limiter. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- .travis.yml | 2 +- test/resources/logback-test.xml | 1 + test/tools/TestFsck.java | 35 +++++++++++++++++++++++++++------ test/tools/TestFsckSalted.java | 25 +++++++++++++++++++++++ test/tools/TestUID.java | 19 +++++++++++++++--- 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 test/resources/logback-test.xml diff --git a/.travis.yml b/.travis.yml index 0c51b3a4f1..8ab8798005 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: java dist: trusty before_script: ./build.sh pom.xml -script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet +script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet -Dlogback.configurationFile=test/resources/logback-test.xml addons: hostname: short-hostname jdk: diff --git a/test/resources/logback-test.xml b/test/resources/logback-test.xml new file mode 100644 index 0000000000..9afb833523 --- /dev/null +++ b/test/resources/logback-test.xml @@ -0,0 +1 @@ +<configuration /> diff --git a/test/tools/TestFsck.java b/test/tools/TestFsck.java index f99a343cf7..30ba044509 100644 --- a/test/tools/TestFsck.java +++ b/test/tools/TestFsck.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -27,12 +29,14 @@ import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.meta.Annotation; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.NoSuchUniqueName; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; import org.hbase.async.Bytes; import org.hbase.async.GetRequest; @@ -40,6 +44,7 @@ import org.hbase.async.KeyValue; import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,7 +61,8 @@ "com.sum.*", "org.xml.*"}) @PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, GetRequest.class, PutRequest.class, KeyValue.class, Fsck.class, - FsckOptions.class, Scanner.class, Annotation.class, Tags.class }) + FsckOptions.class, Scanner.class, Annotation.class, Tags.class, + HashedWheelTimer.class, Threads.class }) public class TestFsck { protected byte[] GLOBAL_ROW = new byte[] {0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}; @@ -66,12 +72,13 @@ public class TestFsck { protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; protected Config config; protected TSDB tsdb = null; - protected HBaseClient client = mock(HBaseClient.class); - protected UniqueId metrics = mock(UniqueId.class); - protected UniqueId tag_names = mock(UniqueId.class); - protected UniqueId tag_values = mock(UniqueId.class); + protected HBaseClient client; + protected UniqueId metrics; + protected UniqueId tag_names; + protected UniqueId tag_values; protected MockBase storage; - protected FsckOptions options = mock(FsckOptions.class); + protected FsckOptions options; + protected FakeTaskTimer timer; protected final static List<byte[]> tags = new ArrayList<byte[]>(1); static { tags.add(new byte[] { 0, 0, 1, 0, 0, 1}); @@ -80,6 +87,22 @@ public class TestFsck { @SuppressWarnings("unchecked") @Before public void before() throws Exception { + client = mock(HBaseClient.class); + metrics = mock(UniqueId.class); + tag_names = mock(UniqueId.class); + tag_values = mock(UniqueId.class); + options = mock(FsckOptions.class); + timer = new FakeTaskTimer(); + + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + config = new Config(false); tsdb = new TSDB(client, config); when(client.flush()).thenReturn(Deferred.fromResult(null)); diff --git a/test/tools/TestFsckSalted.java b/test/tools/TestFsckSalted.java index 4ccd07ef60..192e8dfd71 100644 --- a/test/tools/TestFsckSalted.java +++ b/test/tools/TestFsckSalted.java @@ -1,11 +1,16 @@ package net.opentsdb.tools; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Field; import java.util.ArrayList; +import org.hbase.async.HBaseClient; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -15,15 +20,35 @@ import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; @PrepareForTest({ Const.class }) public class TestFsckSalted extends TestFsck { + @SuppressWarnings("unchecked") @Before public void before() throws Exception { + client = mock(HBaseClient.class); + metrics = mock(UniqueId.class); + tag_names = mock(UniqueId.class); + tag_values = mock(UniqueId.class); + options = mock(FsckOptions.class); + timer = new FakeTaskTimer(); + + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + PowerMockito.mockStatic(Const.class); PowerMockito.when(Const.SALT_BUCKETS()).thenReturn(2); PowerMockito.when(Const.SALT_WIDTH()).thenReturn(1); diff --git a/test/tools/TestUID.java b/test/tools/TestUID.java index e7a0bc939c..ef52bebd2d 100644 --- a/test/tools/TestUID.java +++ b/test/tools/TestUID.java @@ -15,19 +15,24 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.Method; import net.opentsdb.core.TSDB; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; import net.opentsdb.storage.MockBase; import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; import org.hbase.async.Bytes; import org.hbase.async.DeleteRequest; import org.hbase.async.HBaseClient; import org.hbase.async.KeyValue; import org.hbase.async.Scanner; +import org.jboss.netty.util.HashedWheelTimer; import org.junit.Before; import org.junit.After; import org.junit.Test; @@ -41,13 +46,14 @@ @PowerMockIgnore({"javax.management.*", "javax.xml.*", "ch.qos.*", "org.slf4j.*", "com.sum.*", "org.xml.*"}) @PrepareForTest({TSDB.class, Config.class, HBaseClient.class, - KeyValue.class, UidManager.class, + KeyValue.class, UidManager.class, HashedWheelTimer.class, Threads.class, Scanner.class, DeleteRequest.class }) public class TestUID { private Config config; private TSDB tsdb = null; private HBaseClient client = mock(HBaseClient.class); private MockBase storage; + private FakeTaskTimer timer = new FakeTaskTimer(); // names used for testing private byte[] NAME_FAMILY = "name".getBytes(MockBase.ASCII()); @@ -69,8 +75,15 @@ public class TestUID { @Before public void before() throws Exception { - - PowerMockito.whenNew(HBaseClient.class).withAnyArguments().thenReturn(client); + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + config = new Config(false); tsdb = new TSDB(client, config); PowerMockito.spy(System.class); From c1fe5f347a34e7bfd34d075e4bd3c3281059d746 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Fri, 30 Mar 2018 17:40:05 -0700 Subject: [PATCH 684/826] Fix #1180 by allowing the creation of the test JAR so that MockBase will be exported. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- pom.xml.in | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pom.xml.in b/pom.xml.in index 71b7647cb8..18f46f539a 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -335,6 +335,19 @@ </execution> </executions> </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.0.2</version> + <executions> + <execution> + <goals> + <goal>test-jar</goal> + </goals> + </execution> + </executions> + </plugin> </plugins> </build> From 7ce7ecc7c349371ca1dc71c250966fc924f776d4 Mon Sep 17 00:00:00 2001 From: bhourlier <bhourlier@acipia.fr> Date: Tue, 8 May 2018 18:08:54 +0200 Subject: [PATCH 685/826] Tags via custom HTTP header with Unit Test - provide some authentication (#1003) * Allow tags to be added via custom HTTP header * Add unit test for custom http header * Correction of an error caused by a bad file name in makefile * Suppression of modifications for test travis compilation --- src/core/IncomingDataPoint.java | 5 +++++ src/tsd/AbstractHttpQuery.java | 10 ++++++++++ src/tsd/PutDataPointRpc.java | 18 ++++++++++++++++++ src/utils/Config.java | 17 +++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/src/core/IncomingDataPoint.java b/src/core/IncomingDataPoint.java index dced8077ef..2d82ea2985 100644 --- a/src/core/IncomingDataPoint.java +++ b/src/core/IncomingDataPoint.java @@ -128,6 +128,11 @@ public final String getTSUID() { return tsuid; } + /** @param moretags the hashmap of kv pair to add */ + public final void addTags(HashMap<String, String> moretags) { + this.tags.putAll(moretags); + } + /** @param metric the metric to set */ public final void setMetric(String metric) { this.metric = metric; diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index b09bc8b8d7..3c67d6d8f0 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -165,6 +165,16 @@ public Map<String, String> getHeaders() { return headers; } + /** + * Return the value of the given HTTP Header + * first match wins + * @return Header value as string + */ + public String getHeaderValue(final String headerName) { + if (headerName == null) { return null; } + return request.headers().get(headerName); + } + /** @param stats The stats object to mark after writing is complete */ public void setStats(final QueryStats stats) { this.stats = stats; diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 71cd8ecc5d..e4c7c602d1 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -124,6 +124,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) throw new BadRequestException("No datapoints found in content"); } + final HashMap<String, String> query_tags = new HashMap<String, String>(); final boolean show_details = query.hasQueryStringParam("details"); final boolean show_summary = query.hasQueryStringParam("summary"); final boolean synchronous = query.hasQueryStringParam("sync"); @@ -138,6 +139,18 @@ public void execute(final TSDB tsdb, final HttpQuery query) int queued = 0; final List<Deferred<Boolean>> deferreds = synchronous ? new ArrayList<Deferred<Boolean>>(dps.size()) : null; + + if (tsdb.getConfig().enable_header_tag()) { + LOG.debug("Looking for tag header " + tsdb.getConfig().get_name_header_tag()); + final String header_tag_value = query.getHeaderValue(tsdb.getConfig().get_name_header_tag()) ; + if (header_tag_value != null) { + LOG.debug(" header found with value:" + header_tag_value); + Tags.parse(query_tags, header_tag_value); + } else { + LOG.debug(" no such header in request"); + } + } + for (final IncomingDataPoint dp : dps) { /** Handles passing a data point to the storage exception handler if @@ -170,6 +183,11 @@ public String toString() { } try { + /** Add additionnal tags from HTTP header */ + if ( (query_tags != null) && (query_tags.size() > 0) ) { + dp.addTags(query_tags); + } + if (dp.getMetric() == null || dp.getMetric().isEmpty()) { if (show_details) { details.add(this.getHttpDetails("Metric name was empty", dp)); diff --git a/src/utils/Config.java b/src/utils/Config.java index 782bf36430..cfcf278107 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -97,6 +97,9 @@ public class Config { /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; + /** tsd.http.header_tag */ + private String http_header_tag = null; + /** tsd.http.request.max_chunk */ private int max_chunked_requests = 4096; @@ -228,6 +231,16 @@ public int scanner_maxNumRows() { return scanner_max_num_rows; } + /** @return whether or not additional http header tag is allowed */ + public boolean enable_header_tag() { + return http_header_tag != null ; + } + + /** @return the lookup value for additional http header tag */ + public String get_name_header_tag() { + return http_header_tag ; + } + /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { return enable_chunked_requests; @@ -535,6 +548,7 @@ protected void setDefaults() { default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.query.allow_delete", "false"); + default_map.put("tsd.http.header_tag", ""); default_map.put("tsd.http.request.enable_chunked", "false"); default_map.put("tsd.http.request.max_chunk", "4096"); default_map.put("tsd.http.request.cors_domains", ""); @@ -652,6 +666,9 @@ protected void loadStaticVariables() { if (this.hasProperty("tsd.http.request.max_chunk")) { max_chunked_requests = this.getInt("tsd.http.request.max_chunk"); } + if (this.hasProperty("tsd.http.header_tag")) { + http_header_tag = this.getString("tsd.http.header_tag"); + } enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); From 849632355f9293ad0dc5abe63e35464c46b9e797 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Thu, 10 May 2018 00:01:20 -0700 Subject: [PATCH 686/826] Added Tests for TimeShift, removed extra /1000 (#1199) Fixes #1153 --- src/query/expression/TimeShift.java | 63 ++++++++++-------- test/query/expression/TestTimeShift.java | 81 ++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 test/query/expression/TestTimeShift.java diff --git a/src/query/expression/TimeShift.java b/src/query/expression/TimeShift.java index 876175d64a..c157f0c5d3 100644 --- a/src/query/expression/TimeShift.java +++ b/src/query/expression/TimeShift.java @@ -24,6 +24,7 @@ public class TimeShift implements Expression { /** * in place modify of TsdbResult array to increase timestamps by timeshift + * * @param data_query * @param results * @param params @@ -32,10 +33,10 @@ public class TimeShift implements Expression { @Override public DataPoints[] evaluate(TSQuery data_query, List<DataPoints[]> results, List<String> params) { //not 100% sure what to do here -> do I need to think of the case where I have no data points - if(results == null || results.isEmpty()) { + if (results == null || results.isEmpty()) { return new DataPoints[]{}; } - if(params == null || results.isEmpty()) { + if (params == null || params.isEmpty()) { throw new IllegalArgumentException("Need amount of timeshift to perform timeshift"); } @@ -48,7 +49,7 @@ public DataPoints[] evaluate(TSQuery data_query, List<DataPoints[]> results, Lis long timeshift = -1; if (param.startsWith("'") && param.endsWith("'")) { - timeshift = parseParam(param) / 1000; + timeshift = parseParam(param); } else { throw new RuntimeException("Invalid timeshift parameter: eg '10min'"); } @@ -57,15 +58,23 @@ public DataPoints[] evaluate(TSQuery data_query, List<DataPoints[]> results, Lis throw new RuntimeException("timeshift <= 0"); } - DataPoints[] inputPoints = results.get(0); + return performShift(results.get(0), timeshift); + } + + private static Boolean timeshiftIsInt(final long timeshift) { + return (timeshift == Math.floor(timeshift)) && + !Double.isInfinite(timeshift); + } + + DataPoints[] performShift(DataPoints[] inputPoints, long timeshift) { DataPoints[] outputPoints = new DataPoints[inputPoints.length]; - for(int n = 0; n < inputPoints.length; n++) { + for (int n = 0; n < inputPoints.length; n++) { outputPoints[n] = shift(inputPoints[n], timeshift); } return outputPoints; } - public static long parseParam(String param) { + long parseParam(String param) { char[] chars = param.toCharArray(); int tuIndex = 0; for (int c = 1; c < chars.length; c++) { @@ -81,7 +90,7 @@ public static long parseParam(String param) { } int time = Integer.parseInt(param.substring(1, tuIndex + 1)); - String unit = param.substring(tuIndex + 1, param.length() - 1); + String unit = param.substring(tuIndex + 1, param.length()).trim(); if ("sec".equals(unit)) { return TimeUnit.MILLISECONDS.convert(time, TimeUnit.SECONDS); } else if ("min".equals(unit)) { @@ -92,44 +101,44 @@ public static long parseParam(String param) { return TimeUnit.MILLISECONDS.convert(time, TimeUnit.DAYS); } else if ("week".equals(unit) || "weeks".equals(unit)) { //didn't have week so small cheat here - return TimeUnit.MILLISECONDS.convert(time*7, TimeUnit.DAYS); - } - else { + return TimeUnit.MILLISECONDS.convert(time * 7, TimeUnit.DAYS); + } else { throw new RuntimeException("unknown time unit=" + unit); } } /** * Adjusts the timestamp of each datapoint by timeshift - * @param points The data points to factor + * + * @param points The data points to factor * @param timeshift The factor to multiply by * @return The resulting data points */ - private DataPoints shift(final DataPoints points, final long timeshift) { + DataPoints shift(final DataPoints points, final long timeshift) { // TODO(cl) - Using an array as the size function may not return the exact // results and we should figure a way to avoid copying data anyway. final List<DataPoint> dps = new ArrayList<DataPoint>(); - final boolean shift_is_int = (timeshift == Math.floor(timeshift)) && - !Double.isInfinite(timeshift); - final SeekableView view = points.iterator(); - while (view.hasNext()) { - DataPoint pt = view.next(); - if (shift_is_int) { - dps.add(MutableDataPoint.ofLongValue(pt.timestamp() + timeshift, - pt.longValue())); - } else { - // NaNs are fine here, they'll just be re-computed as NaN - dps.add(MutableDataPoint.ofDoubleValue(pt.timestamp() + timeshift, - timeshift * pt.toDouble())); - } + + for (DataPoint pt : points) { + dps.add(shift(pt, timeshift)); } + final DataPoint[] results = new DataPoint[dps.size()]; dps.toArray(results); return new PostAggregatedDataPoints(points, results); } - @Override + DataPoint shift(final DataPoint pt, final long timeshift) { + if (timeshiftIsInt(timeshift)) { + return MutableDataPoint.ofLongValue(pt.timestamp() + timeshift, pt.longValue()); + } else { + // NaNs are fine here, they'll just be re-computed as NaN + return MutableDataPoint.ofDoubleValue(pt.timestamp() + timeshift, timeshift * pt.toDouble()); + } + } + + @Override public String writeStringField(List<String> params, String inner_expression) { - return "timeshift(" + inner_expression + ")"; + return "timeshift(" + inner_expression + ")"; } } diff --git a/test/query/expression/TestTimeShift.java b/test/query/expression/TestTimeShift.java new file mode 100644 index 0000000000..c6546d34f8 --- /dev/null +++ b/test/query/expression/TestTimeShift.java @@ -0,0 +1,81 @@ +package net.opentsdb.query.expression; + +import com.stumbleupon.async.Deferred; +import net.opentsdb.core.DataPoint; +import net.opentsdb.core.DataPoints; +import net.opentsdb.core.MutableDataPoint; +import net.opentsdb.core.SeekableView; +import net.opentsdb.meta.Annotation; +import org.hbase.async.Bytes; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class TestTimeShift { + private TimeShift timeshift; + private static final long BASE_TIME = 1356998400000L; + private static final DataPoint[] DATA_POINTS = new DataPoint[] { + // timestamp = 1,356,998,400,000 ms + MutableDataPoint.ofLongValue(BASE_TIME, 40), + // timestamp = 1,357,000,400,000 ms + MutableDataPoint.ofLongValue(BASE_TIME + 2000000, 50), + // timestamp = 1,357,002,000,000 ms + MutableDataPoint.ofLongValue(BASE_TIME + 3600000, 40), + // timestamp = 1,357,002,005,000 ms + MutableDataPoint.ofLongValue(BASE_TIME + 3605000, 50), + // timestamp = 1,357,005,600,000 ms + MutableDataPoint.ofLongValue(BASE_TIME + 7200000, 40), + // timestamp = 1,357,007,600,000 ms + MutableDataPoint.ofLongValue(BASE_TIME + 9200000, 50) + }; + + @Before + public void setUp() throws Exception { + this.timeshift = new TimeShift(); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void parseParam() throws Exception { + assertEquals(TimeUnit.DAYS.toMillis(7), this.timeshift.parseParam("+1week ")); + assertEquals(TimeUnit.DAYS.toMillis(1), this.timeshift.parseParam("+1days ")); + assertEquals(TimeUnit.HOURS.toMillis(1), this.timeshift.parseParam("+1hr ")); + assertEquals(TimeUnit.MINUTES.toMillis(1), this.timeshift.parseParam("+1min ")); + assertEquals(TimeUnit.SECONDS.toMillis(1), this.timeshift.parseParam("+1sec ")); + assertEquals(TimeUnit.DAYS.toMillis(7), this.timeshift.parseParam("+1 week ")); + assertEquals(TimeUnit.DAYS.toMillis(1), this.timeshift.parseParam("+1 days ")); + assertEquals(TimeUnit.HOURS.toMillis(1), this.timeshift.parseParam("+1 hr ")); + assertEquals(TimeUnit.MINUTES.toMillis(1), this.timeshift.parseParam("+1 min ")); + assertEquals(TimeUnit.SECONDS.toMillis(1), this.timeshift.parseParam("+1 sec ")); + assertEquals(TimeUnit.DAYS.toMillis(7), this.timeshift.parseParam("+1week")); + assertEquals(TimeUnit.DAYS.toMillis(1), this.timeshift.parseParam("+1days")); + assertEquals(TimeUnit.HOURS.toMillis(1), this.timeshift.parseParam("+1hr")); + assertEquals(TimeUnit.MINUTES.toMillis(1), this.timeshift.parseParam("+1min")); + assertEquals(TimeUnit.SECONDS.toMillis(1), this.timeshift.parseParam("+1sec")); + assertEquals(TimeUnit.DAYS.toMillis(7), this.timeshift.parseParam("+1 week")); + assertEquals(TimeUnit.DAYS.toMillis(1), this.timeshift.parseParam("+1 days")); + assertEquals(TimeUnit.HOURS.toMillis(1), this.timeshift.parseParam("+1 hr")); + assertEquals(TimeUnit.MINUTES.toMillis(1), this.timeshift.parseParam("+1 min")); + assertEquals(TimeUnit.SECONDS.toMillis(1), this.timeshift.parseParam("+1 sec")); + assertEquals(60000L, this.timeshift.parseParam("+1min")); + } + + @Test + public void shiftDataPoint() throws Exception { + DataPoint actualDp = timeshift.shift(DATA_POINTS[0], 60000L); + assertEquals(1356998460000L, actualDp.timestamp()); + DataPoint actualDp1 = timeshift.shift(DATA_POINTS[1], this.timeshift.parseParam("+1week")); + assertEquals(1357605200000L, actualDp1.timestamp()); + DataPoint actualDp2 = timeshift.shift(DATA_POINTS[1], this.timeshift.parseParam("+130days")); + assertEquals(1368232400000L, actualDp2.timestamp()); + } +} \ No newline at end of file From 4379e2438e8892eeb7517f8a3fb56340679e508d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 19 May 2018 13:57:24 -0700 Subject: [PATCH 687/826] Swap out Screwdriver for Travis to build and test as it would take too much work to make the tests faster to fit under the 50m time limit impossed by travis and SD will let us run much longer. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- .travis.yml | 10 ---------- screwdriver.yaml | 7 +++++++ 2 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .travis.yml create mode 100644 screwdriver.yaml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8ab8798005..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: java -dist: trusty -before_script: ./build.sh pom.xml -script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet -Dlogback.configurationFile=test/resources/logback-test.xml -addons: - hostname: short-hostname -jdk: - - oraclejdk8 -notifications: - email: false diff --git a/screwdriver.yaml b/screwdriver.yaml new file mode 100644 index 0000000000..11a4352285 --- /dev/null +++ b/screwdriver.yaml @@ -0,0 +1,7 @@ +shared: + image: maven + +jobs: + main: + steps: + - run_arbitrary_script: apt-get update && apt-get install autoconf make -y && ./build.sh pom.xml && mvn clean test --quiet From b95172b5e00893fd30d60920ee8ad93228a3130f Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 19 May 2018 13:55:31 -0700 Subject: [PATCH 688/826] Modify the Rollup query code so it uses only the downsampler to pick the proper data from storage. It's ugly but it's the least corrupt way to do it for now. Fix an issue with rollup queries not fetching data due to the date tiered timestamps not alinging on rollup boundaries. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/Downsampler.java | 8 ++++---- src/core/FillingDownsampler.java | 6 +++--- src/core/SaltScanner.java | 4 ++-- src/core/TsdbQuery.java | 16 +++++++++++++--- test/core/TestDownsampler.java | 12 ++++++------ test/core/TestFillingDownsampler.java | 8 ++++---- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/core/Downsampler.java b/src/core/Downsampler.java index 945f929312..173ef7e65b 100644 --- a/src/core/Downsampler.java +++ b/src/core/Downsampler.java @@ -163,9 +163,9 @@ public boolean hasNext() { public DataPoint next() { if (hasNext()) { if (rollup_query != null && - (rollup_query.getGroupBy() == Aggregators.AVG || - rollup_query.getGroupBy() == Aggregators.DEV)) { - if (rollup_query.getGroupBy() == Aggregators.AVG) { + (rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV)) { + if (rollup_query.getRollupAgg() == Aggregators.AVG) { if (specification.getFunction() == Aggregators.AVG) { double sum = 0; long count = 0; @@ -205,7 +205,7 @@ public double nextDoubleValue() { accumulator.iterator = accumulator.values.iterator(); value = specification.getFunction().runDouble(accumulator); } - } else if (rollup_query.getGroupBy() == Aggregators.DEV) { + } else if (rollup_query.getRollupAgg() == Aggregators.DEV) { throw new UnsupportedOperationException("Standard deviation over " + "rolled up data is not supported at this time"); } diff --git a/src/core/FillingDownsampler.java b/src/core/FillingDownsampler.java index 0ba721dd51..273f0ed18b 100644 --- a/src/core/FillingDownsampler.java +++ b/src/core/FillingDownsampler.java @@ -194,9 +194,9 @@ public DataPoint next() { // The calculated interval timestamp matches what we expect, so we can // do normal processing. if (rollup_query != null && - (rollup_query.getGroupBy() == Aggregators.AVG || - rollup_query.getGroupBy() == Aggregators.DEV)) { - if (rollup_query.getGroupBy() == Aggregators.AVG) { + (rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV)) { + if (rollup_query.getRollupAgg() == Aggregators.AVG) { if (specification.getFunction() == Aggregators.AVG) { double sum = 0; long count = 0; diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index a09a5d1d74..aaac0eb0b2 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -775,8 +775,8 @@ void processRow(final byte[] key, final ArrayList<KeyValue> row) { LOG.error("Failed to decode histogram data point", t); } } else { - if (rollup_query.getGroupBy() == Aggregators.AVG || - rollup_query.getGroupBy() == Aggregators.DEV) { + if (rollup_query.getRollupAgg() == Aggregators.AVG || + rollup_query.getRollupAgg() == Aggregators.DEV) { if (qual[0] == (byte) rollup_agg_id || qual[0] == (byte) rollup_count_id || Bytes.memcmp(RollupQuery.SUM, qual, 0, RollupQuery.SUM.length) == 0 || diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 0df64585f0..530bc53b6b 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1400,7 +1400,10 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { if(tsdb.getConfig().use_otsdb_timestamp()) { long stTime = (getScanStartTimeSeconds() * 1000); long endTime = end_time == UNSET ? -1 : (getScanEndTimeSeconds() * 1000); - if (tsdb.getConfig().get_date_tiered_compaction_start() <= stTime) { + if (tsdb.getConfig().get_date_tiered_compaction_start() <= stTime && + rollup_query == null) { + // TODO - we could set this for rollups but we also need to write + // the rollup columns at the proper time. scanner.setTimeRange(stTime, endTime); } } @@ -1416,12 +1419,12 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { // Set the Scanners column qualifier pattern with rollup aggregator // HBase allows only a single filter so if we have a row key filter, keep // it. If not, then we can do this - if (!rollup_query.getGroupBy().toString().equals("avg")) { + if (!rollup_query.getRollupAgg().toString().equals("avg")) { if (existing != null) { final List<ScanFilter> filters = new ArrayList<ScanFilter>(3); filters.add(existing); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(rollup_query.getGroupBy().toString() + new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() .getBytes(Const.ASCII_CHARSET)))); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator(new byte[] { @@ -1552,6 +1555,13 @@ private long getScanEndTimeSeconds() { end++; } } + + if (rollup_query != null) { + return RollupUtils.getRollupBasetime(end + + (rollup_query.getRollupInterval().getIntervalSeconds() * + rollup_query.getRollupInterval().getIntervals()), + rollup_query.getRollupInterval()); + } // The calculation depends on whether we're downsampling. if (downsampler != null && downsampler.getInterval() > 0) { diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 0a338f3b97..d0024716bf 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -1234,8 +1234,8 @@ public void testDownsampler_rollupAvg() { .setInterval("1h") .setRowSpan("1d") .build(); - final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, - 3600000, Aggregators.AVG); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.AVG, + 3600000, Aggregators.SUM); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), @@ -1269,8 +1269,8 @@ public void testDownsampler_rollupCount() { .setInterval("1h") .setRowSpan("1d") .build(); - final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, - 3600000, Aggregators.COUNT); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.COUNT, + 3600000, Aggregators.SUM); source = spy(SeekableViewsForTest.fromArray(new DataPoint[] { MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 0, 1), MutableDataPoint.ofDoubleValue(BASE_TIME + 5000L * 1, 2), @@ -1304,8 +1304,8 @@ public void testDownsampler_rollupDev() { .setInterval("1h") .setRowSpan("1d") .build(); - final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, - 3600000, Aggregators.DEV); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.DEV, + 3600000, Aggregators.SUM); specification = new DownsamplingSpecification("10s-dev"); downsampler = new Downsampler(source, specification, 0, 0, rollup_query); while (downsampler.hasNext()) { diff --git a/test/core/TestFillingDownsampler.java b/test/core/TestFillingDownsampler.java index 934fc9d178..8f38e41bed 100644 --- a/test/core/TestFillingDownsampler.java +++ b/test/core/TestFillingDownsampler.java @@ -903,7 +903,7 @@ public void testDownsampler_rollupAvg() { .setInterval("1h") .setRowSpan("1d") .build(); - final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.AVG, 3600000, Aggregators.SUM); final long baseTime = 1000L; final SeekableView source = @@ -984,7 +984,7 @@ public void testDownsampler_rollupCount() { .setInterval("1h") .setRowSpan("1d") .build(); - final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.COUNT, 3600000, Aggregators.SUM); final long baseTime = 1000L; final SeekableView source = @@ -1065,8 +1065,8 @@ public void testDownsampler_rollupDev() { .setInterval("1h") .setRowSpan("1d") .build(); - final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.SUM, - 3600000, Aggregators.DEV); + final RollupQuery rollup_query = new RollupQuery(interval, Aggregators.DEV, + 3600000, Aggregators.SUM); final long baseTime = 1000L; final SeekableView source = SeekableViewsForTest.fromArray(new DataPoint[] { From 05631e8ae50d800dfa441a375a2e45f95e4c223e Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 19 May 2018 13:57:24 -0700 Subject: [PATCH 689/826] Swap out Screwdriver for Travis to build and test as it would take too much work to make the tests faster to fit under the 50m time limit impossed by travis and SD will let us run much longer. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- .travis.yml | 9 --------- screwdriver.yaml | 7 +++++++ 2 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 .travis.yml create mode 100644 screwdriver.yaml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d85ebeef4f..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: java -before_script: ./build.sh pom.xml -script: export MAVEN_OPTS="-Xmx1024m" && mvn test --quiet -Dlogback.configurationFile=test/resources/logback-test.xml -addons: - hostname: short-hostname -jdk: - - oraclejdk8 -notifications: - email: false diff --git a/screwdriver.yaml b/screwdriver.yaml new file mode 100644 index 0000000000..11a4352285 --- /dev/null +++ b/screwdriver.yaml @@ -0,0 +1,7 @@ +shared: + image: maven + +jobs: + main: + steps: + - run_arbitrary_script: apt-get update && apt-get install autoconf make -y && ./build.sh pom.xml && mvn clean test --quiet From 96dd00153ede8317f1b25e5d12365ca848390761 Mon Sep 17 00:00:00 2001 From: liam humphreys <liam.humphreys1989@gmail.com> Date: Fri, 23 Feb 2018 11:07:52 +0000 Subject: [PATCH 690/826] fix asyncbigtable base url Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- third_party/asyncbigtable/include.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index 1b90ba5d39..d1b3c84d45 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/releases/com/pythian/opentsdb/asyncbigtable/0.3.0/ +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) From 742f4a28ee47ce4a6f90b41b55d4f501025c792b Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Fri, 4 May 2018 19:22:06 +0100 Subject: [PATCH 691/826] Allow TagVNotLiteralOrFilter to filter single char tag values Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/query/filter/TagVNotLiteralOrFilter.java | 5 ++++- test/query/filter/TestTagVNotLiteralOrFilter.java | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/query/filter/TagVNotLiteralOrFilter.java b/src/query/filter/TagVNotLiteralOrFilter.java index 78fae685ea..1e6500c5e7 100644 --- a/src/query/filter/TagVNotLiteralOrFilter.java +++ b/src/query/filter/TagVNotLiteralOrFilter.java @@ -60,9 +60,12 @@ public TagVNotLiteralOrFilter(final String tagk, final String filter, this.case_insensitive = case_insensitive; // we have to have at least one character. - if (filter == null || filter.length() < 2) { + if (filter == null || filter.isEmpty()) { throw new IllegalArgumentException("Filter cannot be null or empty"); } + if (filter.length() == 1 && filter.charAt(0) == '|') { + throw new IllegalArgumentException("Filter must contain more than just a pipe"); + } final String[] split = filter.split("\\|"); if (case_insensitive) { for (int i = 0; i < split.length; i++) { diff --git a/test/query/filter/TestTagVNotLiteralOrFilter.java b/test/query/filter/TestTagVNotLiteralOrFilter.java index 44df79466a..1cb1c5ba29 100644 --- a/test/query/filter/TestTagVNotLiteralOrFilter.java +++ b/test/query/filter/TestTagVNotLiteralOrFilter.java @@ -122,6 +122,13 @@ public void matchSingleCaseInsensitive() throws Exception { assertFalse(filter.match(tags).join()); assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } + + @Test + public void matchSingleCharacterTagValue() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "c"); + tags.put(TAGK, "c"); + assertFalse(filter.match(tags).join()); + } @Test (expected = IllegalArgumentException.class) public void ctorNullTagk() throws Exception { From 0c307b0a603dbbe0dd74e9412343e7faf3cc1a5b Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Fri, 4 May 2018 19:22:06 +0100 Subject: [PATCH 692/826] Allow TagVNotLiteralOrFilter to filter single char tag values Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/query/filter/TagVNotLiteralOrFilter.java | 5 ++++- test/query/filter/TestTagVNotLiteralOrFilter.java | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/query/filter/TagVNotLiteralOrFilter.java b/src/query/filter/TagVNotLiteralOrFilter.java index 78fae685ea..1e6500c5e7 100644 --- a/src/query/filter/TagVNotLiteralOrFilter.java +++ b/src/query/filter/TagVNotLiteralOrFilter.java @@ -60,9 +60,12 @@ public TagVNotLiteralOrFilter(final String tagk, final String filter, this.case_insensitive = case_insensitive; // we have to have at least one character. - if (filter == null || filter.length() < 2) { + if (filter == null || filter.isEmpty()) { throw new IllegalArgumentException("Filter cannot be null or empty"); } + if (filter.length() == 1 && filter.charAt(0) == '|') { + throw new IllegalArgumentException("Filter must contain more than just a pipe"); + } final String[] split = filter.split("\\|"); if (case_insensitive) { for (int i = 0; i < split.length; i++) { diff --git a/test/query/filter/TestTagVNotLiteralOrFilter.java b/test/query/filter/TestTagVNotLiteralOrFilter.java index 44df79466a..1cb1c5ba29 100644 --- a/test/query/filter/TestTagVNotLiteralOrFilter.java +++ b/test/query/filter/TestTagVNotLiteralOrFilter.java @@ -122,6 +122,13 @@ public void matchSingleCaseInsensitive() throws Exception { assertFalse(filter.match(tags).join()); assertTrue(((TagVNotLiteralOrFilter)filter).isCaseInsensitive()); } + + @Test + public void matchSingleCharacterTagValue() throws Exception { + TagVFilter filter = new TagVNotLiteralOrFilter(TAGK, "c"); + tags.put(TAGK, "c"); + assertFalse(filter.match(tags).join()); + } @Test (expected = IllegalArgumentException.class) public void ctorNullTagk() throws Exception { From a900ac8ddd54e1d3aff797e541ecc5b477567d57 Mon Sep 17 00:00:00 2001 From: Rory <wrk961@gmail.com> Date: Tue, 2 Jan 2018 15:12:42 -0500 Subject: [PATCH 693/826] Fix running query stats queryStart time Report queryStart time as epoch time in ms. query_start_ns is not an epoch time. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/stats/QueryStats.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index 1bf23c5d4e..4029c726b3 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -411,7 +411,7 @@ public static Map<String, Object> getRunningAndCompleteStats() { obj.put("remote", stats.remote_address); obj.put("user", stats.user); obj.put("headers", stats.headers);; - obj.put("queryStart", DateTime.msFromNano(stats.query_start_ns)); + obj.put("queryStart", stats.query_start_ms); obj.put("elapsed", DateTime.msFromNanoDiff(DateTime.nanoTime(), stats.query_start_ns)); running.add(obj); From 49d3161d084de1f422d9075d75e2572fd14ccd44 Mon Sep 17 00:00:00 2001 From: Rory <wrk961@gmail.com> Date: Tue, 2 Jan 2018 15:12:42 -0500 Subject: [PATCH 694/826] Fix running query stats queryStart time Report queryStart time as epoch time in ms. query_start_ns is not an epoch time. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/stats/QueryStats.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index c8db96bfed..fa39c17bb1 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -411,7 +411,7 @@ public static Map<String, Object> getRunningAndCompleteStats() { obj.put("remote", stats.remote_address); obj.put("user", stats.user); obj.put("headers", stats.headers);; - obj.put("queryStart", DateTime.msFromNano(stats.query_start_ns)); + obj.put("queryStart", stats.query_start_ms); obj.put("elapsed", DateTime.msFromNanoDiff(DateTime.nanoTime(), stats.query_start_ns)); running.add(obj); From 610858428ea86fc03343b7db5bdda89e9a188e6c Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 19 May 2018 15:18:18 -0700 Subject: [PATCH 695/826] Bump Jackson to 2.9.5 for security. Bump Netty to 3.10.6 also for security. (Last before switching to 4). Thanks @venkat1m. Fixes #1196 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tsd/AbstractHttpQuery.java | 15 ++++++++------- third_party/jackson/include.mk | 2 +- .../jackson/jackson-annotations-2.1.5.jar.md5 | 1 - .../jackson/jackson-annotations-2.9.5.jar.md5 | 1 + third_party/jackson/jackson-core-2.1.5.jar.md5 | 1 - third_party/jackson/jackson-core-2.9.5.jar.md5 | 1 + .../jackson/jackson-databind-2.1.5.jar.md5 | 1 - .../jackson/jackson-databind-2.9.5.jar.md5 | 1 + third_party/netty/include.mk | 4 ++-- third_party/netty/netty-3.10.6.Final.jar.md5 | 1 + third_party/netty/netty-3.9.1.Final.jar.md5 | 1 - 11 files changed, 15 insertions(+), 14 deletions(-) delete mode 100644 third_party/jackson/jackson-annotations-2.1.5.jar.md5 create mode 100644 third_party/jackson/jackson-annotations-2.9.5.jar.md5 delete mode 100644 third_party/jackson/jackson-core-2.1.5.jar.md5 create mode 100644 third_party/jackson/jackson-core-2.9.5.jar.md5 delete mode 100644 third_party/jackson/jackson-databind-2.1.5.jar.md5 create mode 100644 third_party/jackson/jackson-databind-2.9.5.jar.md5 create mode 100644 third_party/netty/netty-3.10.6.Final.jar.md5 delete mode 100644 third_party/netty/netty-3.9.1.Final.jar.md5 diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index f83d7f0682..d7ccc54775 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -128,8 +128,8 @@ public String getRemoteAddress() { */ public Map<String, String> getPrintableHeaders() { final Map<String, String> headers = new HashMap<String, String>( - request.getHeaders().size()); - for (final Entry<String, String> header : request.getHeaders()) { + request.headers().entries().size()); + for (final Entry<String, String> header : request.headers().entries()) { if (header.getKey().toLowerCase().equals("cookie")) { // null out the cookies headers.put(header.getKey(), "*******"); @@ -154,8 +154,8 @@ public Map<String, String> getPrintableHeaders() { */ public Map<String, String> getHeaders() { final Map<String, String> headers = new HashMap<String, String>( - request.getHeaders().size()); - for (final Entry<String, String> header : request.getHeaders()) { + request.headers().entries().size()); + for (final Entry<String, String> header : request.headers().entries()) { // http://tools.ietf.org/html/rfc2616#section-4.2 if (headers.containsKey(header.getKey())) { headers.put(header.getKey(), @@ -485,16 +485,17 @@ protected Logger logger() { } protected final String logChannel() { - if (request.containsHeader("X-Forwarded-For")) { + if (request.headers().contains("X-Forwarded-For")) { String inetAddress; - String proxyChain = request.getHeader("X-Forwarded-For"); + String proxyChain = request.headers().get("X-Forwarded-For"); int firstComma = proxyChain.indexOf(','); if (firstComma != -1) { inetAddress = proxyChain.substring(0, proxyChain.indexOf(',')); } else { inetAddress = proxyChain; } - return "[id: 0x" + Integer.toHexString(chan.hashCode()) + ", /" + inetAddress + " => " + chan.getLocalAddress() + ']'; + return "[id: 0x" + Integer.toHexString(chan.hashCode()) + + ", /" + inetAddress + " => " + chan.getLocalAddress() + ']'; } else { return chan.toString(); } diff --git a/third_party/jackson/include.mk b/third_party/jackson/include.mk index c74fb7fa39..d6e77b5957 100644 --- a/third_party/jackson/include.mk +++ b/third_party/jackson/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -JACKSON_VERSION := 2.4.3 +JACKSON_VERSION := 2.9.5 JACKSON_ANNOTATIONS_VERSION = $(JACKSON_VERSION) JACKSON_ANNOTATIONS := third_party/jackson/jackson-annotations-$(JACKSON_ANNOTATIONS_VERSION).jar diff --git a/third_party/jackson/jackson-annotations-2.1.5.jar.md5 b/third_party/jackson/jackson-annotations-2.1.5.jar.md5 deleted file mode 100644 index 5facae61a2..0000000000 --- a/third_party/jackson/jackson-annotations-2.1.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -bfe728a2d5f507e143ec41702a3dfc52 diff --git a/third_party/jackson/jackson-annotations-2.9.5.jar.md5 b/third_party/jackson/jackson-annotations-2.9.5.jar.md5 new file mode 100644 index 0000000000..7f344feb82 --- /dev/null +++ b/third_party/jackson/jackson-annotations-2.9.5.jar.md5 @@ -0,0 +1 @@ +93ff99082f89beba3dd7b594a478df10 diff --git a/third_party/jackson/jackson-core-2.1.5.jar.md5 b/third_party/jackson/jackson-core-2.1.5.jar.md5 deleted file mode 100644 index 356d9b7a84..0000000000 --- a/third_party/jackson/jackson-core-2.1.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -25f14871629c6ed2408438f8285ad26d diff --git a/third_party/jackson/jackson-core-2.9.5.jar.md5 b/third_party/jackson/jackson-core-2.9.5.jar.md5 new file mode 100644 index 0000000000..74729af0ab --- /dev/null +++ b/third_party/jackson/jackson-core-2.9.5.jar.md5 @@ -0,0 +1 @@ +ec59f24f7f8d9acf53301c562722adf2 diff --git a/third_party/jackson/jackson-databind-2.1.5.jar.md5 b/third_party/jackson/jackson-databind-2.1.5.jar.md5 deleted file mode 100644 index 3e9e342bb5..0000000000 --- a/third_party/jackson/jackson-databind-2.1.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -18603628104fa90698bfd713ffc03beb diff --git a/third_party/jackson/jackson-databind-2.9.5.jar.md5 b/third_party/jackson/jackson-databind-2.9.5.jar.md5 new file mode 100644 index 0000000000..273471bcf0 --- /dev/null +++ b/third_party/jackson/jackson-databind-2.9.5.jar.md5 @@ -0,0 +1 @@ +34b37affbf74f5d199be10622ddc83cd diff --git a/third_party/netty/include.mk b/third_party/netty/include.mk index 638da86057..a0ea78407a 100644 --- a/third_party/netty/include.mk +++ b/third_party/netty/include.mk @@ -23,8 +23,8 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -NETTY_MAJOR_VERSION = 3.9 -NETTY_VERSION := 3.9.4.Final +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) diff --git a/third_party/netty/netty-3.10.6.Final.jar.md5 b/third_party/netty/netty-3.10.6.Final.jar.md5 new file mode 100644 index 0000000000..bc161edbc0 --- /dev/null +++ b/third_party/netty/netty-3.10.6.Final.jar.md5 @@ -0,0 +1 @@ +e9cdf01138257f48d796fb2cf67af53e diff --git a/third_party/netty/netty-3.9.1.Final.jar.md5 b/third_party/netty/netty-3.9.1.Final.jar.md5 deleted file mode 100644 index 0005a0f5fc..0000000000 --- a/third_party/netty/netty-3.9.1.Final.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -c1a35f5f1dbc6d8f693b836a66070d45 From 61e18fbdaee207f8e78c1dedd29c017f86ac0696 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 19 May 2018 15:18:18 -0700 Subject: [PATCH 696/826] Bump Jackson to 2.9.5 for security. Bump Netty to 3.10.6 also for security. (Last before switching to 4). Thanks @venkat1m. Fixes #1196 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tsd/AbstractHttpQuery.java | 15 ++++++++------- third_party/jackson/include.mk | 2 +- .../jackson/jackson-annotations-2.1.5.jar.md5 | 1 - .../jackson/jackson-annotations-2.9.5.jar.md5 | 1 + third_party/jackson/jackson-core-2.1.5.jar.md5 | 1 - third_party/jackson/jackson-core-2.9.5.jar.md5 | 1 + .../jackson/jackson-databind-2.1.5.jar.md5 | 1 - .../jackson/jackson-databind-2.9.5.jar.md5 | 1 + third_party/netty/include.mk | 4 ++-- third_party/netty/netty-3.10.6.Final.jar.md5 | 1 + third_party/netty/netty-3.9.1.Final.jar.md5 | 1 - 11 files changed, 15 insertions(+), 14 deletions(-) delete mode 100644 third_party/jackson/jackson-annotations-2.1.5.jar.md5 create mode 100644 third_party/jackson/jackson-annotations-2.9.5.jar.md5 delete mode 100644 third_party/jackson/jackson-core-2.1.5.jar.md5 create mode 100644 third_party/jackson/jackson-core-2.9.5.jar.md5 delete mode 100644 third_party/jackson/jackson-databind-2.1.5.jar.md5 create mode 100644 third_party/jackson/jackson-databind-2.9.5.jar.md5 create mode 100644 third_party/netty/netty-3.10.6.Final.jar.md5 delete mode 100644 third_party/netty/netty-3.9.1.Final.jar.md5 diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 3c67d6d8f0..8669e5ee14 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -126,8 +126,8 @@ public String getRemoteAddress() { */ public Map<String, String> getPrintableHeaders() { final Map<String, String> headers = new HashMap<String, String>( - request.getHeaders().size()); - for (final Entry<String, String> header : request.getHeaders()) { + request.headers().entries().size()); + for (final Entry<String, String> header : request.headers().entries()) { if (header.getKey().toLowerCase().equals("cookie")) { // null out the cookies headers.put(header.getKey(), "*******"); @@ -152,8 +152,8 @@ public Map<String, String> getPrintableHeaders() { */ public Map<String, String> getHeaders() { final Map<String, String> headers = new HashMap<String, String>( - request.getHeaders().size()); - for (final Entry<String, String> header : request.getHeaders()) { + request.headers().entries().size()); + for (final Entry<String, String> header : request.headers().entries()) { // http://tools.ietf.org/html/rfc2616#section-4.2 if (headers.containsKey(header.getKey())) { headers.put(header.getKey(), @@ -487,16 +487,17 @@ protected Logger logger() { } protected final String logChannel() { - if (request.containsHeader("X-Forwarded-For")) { + if (request.headers().contains("X-Forwarded-For")) { String inetAddress; - String proxyChain = request.getHeader("X-Forwarded-For"); + String proxyChain = request.headers().get("X-Forwarded-For"); int firstComma = proxyChain.indexOf(','); if (firstComma != -1) { inetAddress = proxyChain.substring(0, proxyChain.indexOf(',')); } else { inetAddress = proxyChain; } - return "[id: 0x" + Integer.toHexString(chan.hashCode()) + ", /" + inetAddress + " => " + chan.getLocalAddress() + ']'; + return "[id: 0x" + Integer.toHexString(chan.hashCode()) + + ", /" + inetAddress + " => " + chan.getLocalAddress() + ']'; } else { return chan.toString(); } diff --git a/third_party/jackson/include.mk b/third_party/jackson/include.mk index c74fb7fa39..d6e77b5957 100644 --- a/third_party/jackson/include.mk +++ b/third_party/jackson/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -JACKSON_VERSION := 2.4.3 +JACKSON_VERSION := 2.9.5 JACKSON_ANNOTATIONS_VERSION = $(JACKSON_VERSION) JACKSON_ANNOTATIONS := third_party/jackson/jackson-annotations-$(JACKSON_ANNOTATIONS_VERSION).jar diff --git a/third_party/jackson/jackson-annotations-2.1.5.jar.md5 b/third_party/jackson/jackson-annotations-2.1.5.jar.md5 deleted file mode 100644 index 5facae61a2..0000000000 --- a/third_party/jackson/jackson-annotations-2.1.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -bfe728a2d5f507e143ec41702a3dfc52 diff --git a/third_party/jackson/jackson-annotations-2.9.5.jar.md5 b/third_party/jackson/jackson-annotations-2.9.5.jar.md5 new file mode 100644 index 0000000000..7f344feb82 --- /dev/null +++ b/third_party/jackson/jackson-annotations-2.9.5.jar.md5 @@ -0,0 +1 @@ +93ff99082f89beba3dd7b594a478df10 diff --git a/third_party/jackson/jackson-core-2.1.5.jar.md5 b/third_party/jackson/jackson-core-2.1.5.jar.md5 deleted file mode 100644 index 356d9b7a84..0000000000 --- a/third_party/jackson/jackson-core-2.1.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -25f14871629c6ed2408438f8285ad26d diff --git a/third_party/jackson/jackson-core-2.9.5.jar.md5 b/third_party/jackson/jackson-core-2.9.5.jar.md5 new file mode 100644 index 0000000000..74729af0ab --- /dev/null +++ b/third_party/jackson/jackson-core-2.9.5.jar.md5 @@ -0,0 +1 @@ +ec59f24f7f8d9acf53301c562722adf2 diff --git a/third_party/jackson/jackson-databind-2.1.5.jar.md5 b/third_party/jackson/jackson-databind-2.1.5.jar.md5 deleted file mode 100644 index 3e9e342bb5..0000000000 --- a/third_party/jackson/jackson-databind-2.1.5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -18603628104fa90698bfd713ffc03beb diff --git a/third_party/jackson/jackson-databind-2.9.5.jar.md5 b/third_party/jackson/jackson-databind-2.9.5.jar.md5 new file mode 100644 index 0000000000..273471bcf0 --- /dev/null +++ b/third_party/jackson/jackson-databind-2.9.5.jar.md5 @@ -0,0 +1 @@ +34b37affbf74f5d199be10622ddc83cd diff --git a/third_party/netty/include.mk b/third_party/netty/include.mk index 638da86057..a0ea78407a 100644 --- a/third_party/netty/include.mk +++ b/third_party/netty/include.mk @@ -23,8 +23,8 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -NETTY_MAJOR_VERSION = 3.9 -NETTY_VERSION := 3.9.4.Final +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) diff --git a/third_party/netty/netty-3.10.6.Final.jar.md5 b/third_party/netty/netty-3.10.6.Final.jar.md5 new file mode 100644 index 0000000000..bc161edbc0 --- /dev/null +++ b/third_party/netty/netty-3.10.6.Final.jar.md5 @@ -0,0 +1 @@ +e9cdf01138257f48d796fb2cf67af53e diff --git a/third_party/netty/netty-3.9.1.Final.jar.md5 b/third_party/netty/netty-3.9.1.Final.jar.md5 deleted file mode 100644 index 0005a0f5fc..0000000000 --- a/third_party/netty/netty-3.9.1.Final.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -c1a35f5f1dbc6d8f693b836a66070d45 From 411261e38725f5eac5e422655bba8f75e589dd1d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sun, 20 May 2018 11:03:56 -0700 Subject: [PATCH 697/826] Set tsd.storage.use_otsdb_timestamp to false by default. Will document it. Remove a duplicate class in the Makefile. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- Makefile.am | 1 - src/utils/Config.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 61b0ca42ad..bb87d585cc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -63,7 +63,6 @@ tsdb_SRC := \ src/core/HistogramSeekableView.java \ src/core/HistogramSpan.java \ src/core/HistogramSpanGroup.java \ - src/core/HistogramRowSeq.java \ src/core/iHistogramRowSeq.java \ src/core/IncomingDataPoint.java \ src/core/IncomingDataPoints.java \ diff --git a/src/utils/Config.java b/src/utils/Config.java index db4780708b..57ba8ad114 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -605,7 +605,7 @@ protected void setDefaults() { + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, " + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since"); default_map.put("tsd.query.timeout", "0"); - default_map.put("tsd.storage.use_otsdb_timestamp", "true"); + default_map.put("tsd.storage.use_otsdb_timestamp", "false"); default_map.put("tsd.storage.use_max_value", "true"); default_map.put("tsd.storage.get_date_tiered_compaction_start", "0"); From 09015baf569ffab5b90602953c38ae81138060b8 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 10:27:17 -0700 Subject: [PATCH 698/826] Remove the 'tsd.core.hist_decoder' config key as it's not used Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/utils/Config.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/Config.java b/src/utils/Config.java index 57ba8ad114..0e791e8968 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -530,7 +530,6 @@ protected void setDefaults() { default_map.put("tsd.core.connections.limit", "0"); default_map.put("tsd.core.enable_api", "true"); default_map.put("tsd.core.enable_ui", "true"); - default_map.put("tsd.core.hist_decoder", "net.opentsdb.core.SimpleHistogramDecoder"); default_map.put("tsd.core.meta.enable_realtime_ts", "false"); default_map.put("tsd.core.meta.enable_realtime_uid", "false"); default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false"); From 72a7f3717e14fc88935821dbde71593b4f7b48c2 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 11:12:07 -0700 Subject: [PATCH 699/826] Bump Asynchbase to 1.8.2 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 | 1 - third_party/hbase/asynchbase-1.8.2.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 create mode 100644 third_party/hbase/asynchbase-1.8.2.jar.md5 diff --git a/third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 b/third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 deleted file mode 100644 index cc3290d7d0..0000000000 --- a/third_party/hbase/asynchbase-1.8.0-20161127.193259-5.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -857f63ba713c1ba88706ee32085652b0 \ No newline at end of file diff --git a/third_party/hbase/asynchbase-1.8.2.jar.md5 b/third_party/hbase/asynchbase-1.8.2.jar.md5 new file mode 100644 index 0000000000..73b20d5c91 --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.2.jar.md5 @@ -0,0 +1 @@ +aa7df5b8b3c77d3671eff8bfa12a87c4 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index cb2b4f58c9..cbda6eec6d 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -ASYNCHBASE_VERSION := 1.8.0 +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) From a733c22e215f71f34b9b212c47db6d4b0711d958 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 11:12:07 -0700 Subject: [PATCH 700/826] Bump Asynchbase to 1.8.2 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- third_party/hbase/asynchbase-1.8.2.jar.md5 | 1 + third_party/hbase/include.mk | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 third_party/hbase/asynchbase-1.8.2.jar.md5 diff --git a/third_party/hbase/asynchbase-1.8.2.jar.md5 b/third_party/hbase/asynchbase-1.8.2.jar.md5 new file mode 100644 index 0000000000..73b20d5c91 --- /dev/null +++ b/third_party/hbase/asynchbase-1.8.2.jar.md5 @@ -0,0 +1 @@ +aa7df5b8b3c77d3671eff8bfa12a87c4 \ No newline at end of file diff --git a/third_party/hbase/include.mk b/third_party/hbase/include.mk index cb2b4f58c9..cbda6eec6d 100644 --- a/third_party/hbase/include.mk +++ b/third_party/hbase/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -ASYNCHBASE_VERSION := 1.8.0 +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) From fc15f9b2145db550912b5195d9b7a489c6e826a8 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Tue, 28 Nov 2017 15:27:39 -0600 Subject: [PATCH 701/826] Added example Authorization and Authentication Plugin Cleaned up checks in QueryRpc, removed duplicate code Added standardized permissions list Implemented check for HTTP_QUERY, HTTP_PUT, TELNET_PUT permissions Added suggested roles Remaining Items: Implement checks for remaining permissions: CREATE_TAGK,CREATE_TAGV, CREATE_METRIC Added Auth tests, fixed existing tests (maybe?) Need to catch the BadRequestException as we well for the current counter behavior. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- .../AllowAllAuthenticatingAuthorizer.java | 221 +++++++++++++ src/auth/Authentication.java | 165 +++++----- src/auth/AuthenticationChannelHandler.java | 1 + src/auth/Authorization.java | 14 +- src/auth/Permissions.java | 28 ++ src/auth/Roles.java | 56 ++++ src/tsd/PutDataPointRpc.java | 78 ++++- src/tsd/QueryRpc.java | 103 +++--- .../AllowAllAuthenticatingAuthorizerTest.java | 107 ++++++ test/core/BaseTsdbTest.java | 23 +- test/tsd/NettyMocks.java | 45 ++- test/tsd/TestQueryRpc.java | 307 ++++++++++-------- 12 files changed, 871 insertions(+), 277 deletions(-) create mode 100644 src/auth/AllowAllAuthenticatingAuthorizer.java create mode 100644 src/auth/Permissions.java create mode 100644 src/auth/Roles.java create mode 100644 test/auth/AllowAllAuthenticatingAuthorizerTest.java diff --git a/src/auth/AllowAllAuthenticatingAuthorizer.java b/src/auth/AllowAllAuthenticatingAuthorizer.java new file mode 100644 index 0000000000..e47d6d33fd --- /dev/null +++ b/src/auth/AllowAllAuthenticatingAuthorizer.java @@ -0,0 +1,221 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 <http://www.gnu.org/licenses/>. + +package net.opentsdb.auth; + +import com.stumbleupon.async.Deferred; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.query.pojo.Query; +import net.opentsdb.stats.StatsCollector; +import net.opentsdb.tsd.BadRequestException; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import java.util.concurrent.atomic.AtomicLong; + +/** + * The default authentication and authorization plugin. This plugin allows all users with no real authentication + * or authorization. By default, users are considered in the ADMINISTRATOR role and have all of the + * net.opentsdb.auth.Permissions + * + * @author jonathan.creasy + * @since 2.4.0 + */ +@SuppressWarnings("unused") +public class AllowAllAuthenticatingAuthorizer implements Authentication, Authorization { + private Roles roles; + + static AuthState accessDenied = new AuthState() { + Channel channel; + + @Override + public String getUser() { + return "guest"; + } + + @Override + public AuthStatus getStatus() { + return AuthStatus.FORBIDDEN; + } + + @Override + public String getMessage() { + return "Guest User forbidden by AllowAllAuthenticatingAuthorizer"; + } + + @Override + public Throwable getException() { + return null; + } + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + + @Override + public byte[] getToken() { + return new byte[0]; + } + }; + static AuthState accessGranted = new AuthState() { + Channel channel; + + @Override + public String getUser() { + return "guest"; + } + + @Override + public AuthStatus getStatus() { + return AuthStatus.SUCCESS; + } + + @Override + public String getMessage() { + return "Guest User allowed by AllowAllAuthenticatingAuthorizer"; + } + + @Override + public Throwable getException() { + return null; + } + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + + @Override + public byte[] getToken() { + return new byte[0]; + } + }; + + private static final AtomicLong queries_allowed = new AtomicLong(); + private static final AtomicLong queries_denied = new AtomicLong(); + private static final AtomicLong authentication_http_allowed = new AtomicLong(); + private static final AtomicLong authentication_http_denied = new AtomicLong(); + private static final AtomicLong authentication_telnet_allowed = new AtomicLong(); + private static final AtomicLong authentication_telnet_denied = new AtomicLong(); + private static final AtomicLong authorization_role_allowed = new AtomicLong(); + private static final AtomicLong authorization_permission_allowed = new AtomicLong(); + private static final AtomicLong authorization_role_denied = new AtomicLong(); + private static final AtomicLong authorization_permission_denied = new AtomicLong(); + + public Roles getRoles() { + return roles; + } + + public void setRoles(Roles roles) { + this.roles = roles; + } + + public AuthState getAccessDenied() { + return accessDenied; + } + + public AuthState getAccessGranted() { + return accessGranted; + } + + @Override + public void initialize(final TSDB tsdb) { + } + + @Override + public Deferred<Object> shutdown() { + return null; + } + + @Override + public String version() { + return "2.4.0"; + } + + @Override + public void collectStats(final StatsCollector collector) { + collector.record("authorization.queries.allowed", queries_allowed); + collector.record("authorization.queries.denied", queries_denied); + collector.record("authorization.allowed", authorization_role_allowed, "type=role"); + collector.record("authorization.denied", authorization_role_denied, "type=role"); + collector.record("authorization.allowed", authorization_permission_allowed, "type=permission"); + collector.record("authorization.denied", authorization_permission_denied, "type=permission"); + collector.record("authentication.succeeded", authentication_http_allowed, "type=http"); + collector.record("authentication.denied", authentication_http_denied, "type=http"); + collector.record("authentication.succeeded", authentication_telnet_allowed, "type=telnet"); + collector.record("authentication.denied", authentication_telnet_denied, "type=telnet"); + } + + @Override + public AuthState authenticateTelnet(final Channel channel, final String[] command) { + authentication_telnet_allowed.getAndIncrement(); + return accessGranted; + } + + @Override + public AuthState authenticateHTTP(final Channel channel, final HttpRequest req) { + authentication_http_allowed.getAndIncrement(); + return accessGranted; + } + + @Override + public Authorization authorization() { + return null; + } + + @Override + public boolean isReady(final TSDB tsdb, final Channel chan) { + if (tsdb.getAuth() != null && tsdb.getAuth().authorization() != null) { + if (chan.getAttachment() == null || !(chan.getAttachment() instanceof AuthState)) { + throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + "Authentication was enabled but the authentication state for " + + "this channel was not set properly"); + } + return true; + } else { + return false; + } + } + + @Override + public AuthState hasPermission(final AuthState state, final Permissions permission) { + if (this.roles != null && this.roles.hasPermission(permission)) { + authorization_permission_allowed.getAndIncrement(); + return accessGranted; + } + authorization_permission_denied.getAndIncrement(); + return accessDenied; + } + + @Override + public AuthState allowQuery(final AuthState state, final TSQuery query) { + if (this.roles != null && this.roles.hasPermission(Permissions.HTTP_QUERY)) { + queries_allowed.getAndIncrement(); + return accessGranted; + } + queries_denied.getAndIncrement(); + return accessDenied; + } + + @Override + public AuthState allowQuery(final AuthState state, final Query query) { + if (this.roles != null && this.roles.hasPermission(Permissions.HTTP_QUERY)) { + queries_allowed.getAndIncrement(); + return accessGranted; + } + queries_denied.getAndIncrement(); + return accessDenied; + } +} \ No newline at end of file diff --git a/src/auth/Authentication.java b/src/auth/Authentication.java index 38e0530b9a..acf16fe050 100644 --- a/src/auth/Authentication.java +++ b/src/auth/Authentication.java @@ -28,88 +28,101 @@ * An AuthState object is attached to the channel for evaluation later in the * pipeline. This state cannot be changed but cane be replaced. * <p> - * The plugin also includes an acessor to an Authorization plugin to allow or - * disallow operations per user. - * + * The plugin also includes an acessor to an Authorization plugin to allow or + * disallow operations per user. + * * @since 2.4 */ -public abstract class Authentication { +public interface Authentication { - /** - * Called by TSDB to initialize the plugin - * Implementations are responsible for setting up any IO they need as well - * as starting any required background threads. - * <b>Note:</b> Implementations should throw exceptions if they can't start - * up properly. The TSD will then shutdown so the operator can fix the - * problem. Please use IllegalArgumentException for configuration issues. - * @param tsdb The parent TSDB object - * @throws IllegalArgumentException if required configuration parameters are - * missing - * @throws RuntimeException if something else goes wrong - */ - public abstract void initialize(final TSDB tsdb); + /** + * Called by TSDB to initialize the plugin + * Implementations are responsible for setting up any IO they need as well + * as starting any required background threads. + * <b>Note:</b> Implementations should throw exceptions if they can't start + * up properly. The TSD will then shutdown so the operator can fix the + * problem. Please use IllegalArgumentException for configuration issues. + * + * @param tsdb The parent TSDB object + * @throws IllegalArgumentException if required configuration parameters are + * missing + * @throws RuntimeException if something else goes wrong + */ + public abstract void initialize(final TSDB tsdb); - /** - * Called to gracefully shutdown the plugin. Implementations should close - * any IO they have open - * @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<Void>}). - */ - public abstract Deferred<Object> shutdown(); + /** + * Called to gracefully shutdown the plugin. Implementations should close + * any IO they have open + * + * @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<Void>}). + */ + public abstract Deferred<Object> shutdown(); - /** - * Should return the version of this plugin in the format: - * MAJOR.MINOR.MAINT, e.g. 2.0.1. The MAJOR version should match the major - * version of OpenTSDB the plugin is meant to work with. - * @return A version string used to log the loaded version - */ - public abstract String version(); + /** + * Should return the version of this plugin in the format: + * MAJOR.MINOR.MAINT, e.g. 2.0.1. The MAJOR version should match the major + * version of OpenTSDB the plugin is meant to work with. + * + * @return A version string used to log the loaded version + */ + public abstract String version(); - /** - * Called by the TSD when a request for statistics collection has come in. The - * implementation may provide one or more statistics. If no statistics are - * available for the implementation, simply stub the method. - * @param collector The collector used for emitting statistics - */ - public abstract void collectStats(final StatsCollector collector); + /** + * Called by the TSD when a request for statistics collection has come in. The + * implementation may provide one or more statistics. If no statistics are + * available for the implementation, simply stub the method. + * + * @param collector The collector used for emitting statistics + */ + public abstract void collectStats(final StatsCollector collector); - /** - * Authenticate Telnet connections, provides the first line of the incoming - * connection. - * <p> - * NOTE: This method should not throw exceptions, rather return a state object - * with the AuthStatus.ERROR status. - * - * @param channel A non-null Netty channel to associate with the request. - * @param command A non-null list of "words" from a Telnet style command - * (strings or numbers separated by spaces) - * @return A non-null AuthState object with a valid AuthStatus to evaluate for - * a successful or unsuccessful authentication. - */ - public abstract AuthState authenticateTelnet(final Channel channel, - final String[] command); + /** + * Authenticate Telnet connections, provides the first line of the incoming + * connection. + * <p> + * NOTE: This method should not throw exceptions, rather return a state object + * with the AuthStatus.ERROR status. + * + * @param channel A non-null Netty channel to associate with the request. + * @param command A non-null list of "words" from a Telnet style command + * (strings or numbers separated by spaces) + * @return A non-null AuthState object with a valid AuthStatus to evaluate for + * a successful or unsuccessful authentication. + */ + public abstract AuthState authenticateTelnet(final Channel channel, + final String[] command); - /** - * Authenticate HTTP connections, provides the HTTPRequest object for the - * incoming connection. - * <p> - * NOTE: This method should not throw exceptions, rather return a state object - * with the AuthStatus.ERROR status. - * - * @param channel A non-null Netty channel to associate with the request. - * @param req A non-null HTTP request. - * @return A non-null AuthState object with a valid AuthStatus to evaluate for - * a successful or unsuccessful authentication. - */ - public abstract AuthState authenticateHTTP(final Channel channel, - final HttpRequest req); - - /** - * An optional authorization object. If authorization is not enabled, this - * call may return null. - * @return An authorization object or null; - */ - public abstract Authorization authorization(); - + /** + * Authenticate HTTP connections, provides the HTTPRequest object for the + * incoming connection. + * <p> + * NOTE: This method should not throw exceptions, rather return a state object + * with the AuthStatus.ERROR status. + * + * @param channel A non-null Netty channel to associate with the request. + * @param req A non-null HTTP request. + * @return A non-null AuthState object with a valid AuthStatus to evaluate for + * a successful or unsuccessful authentication. + */ + public abstract AuthState authenticateHTTP(final Channel channel, + final HttpRequest req); + + /** + * An optional authorization object. If authorization is not enabled, this + * call may return null. + * + * @return An authorization object or null; + */ + public abstract Authorization authorization(); + + /** + * Function to determine if the authorization and authentication system is valid for this channel. + * + * @param tsdb the current tsdb + * @param chan the current channel + * @return returns true if the authc/authz state is valid for the provided channel + */ + public boolean isReady(final TSDB tsdb, final Channel chan); } \ No newline at end of file diff --git a/src/auth/AuthenticationChannelHandler.java b/src/auth/AuthenticationChannelHandler.java index ec81968fe1..1ce19772d5 100644 --- a/src/auth/AuthenticationChannelHandler.java +++ b/src/auth/AuthenticationChannelHandler.java @@ -98,6 +98,7 @@ public void messageReceived(final ChannelHandlerContext ctx, String auth_response = TELNET_AUTH_FAILURE; final AuthState state = authentication.authenticateTelnet( authEvent.getChannel(), (String[]) authCommand); + if (state.getStatus() == AuthStatus.SUCCESS) { auth_response = TELNET_AUTH_SUCCESS; ctx.getPipeline().remove(this); diff --git a/src/auth/Authorization.java b/src/auth/Authorization.java index 97be53f9b2..b6ad5e2067 100644 --- a/src/auth/Authorization.java +++ b/src/auth/Authorization.java @@ -19,13 +19,15 @@ import net.opentsdb.query.pojo.Query; import net.opentsdb.stats.StatsCollector; +import java.util.EnumSet; + /** * A plugin interface for authorization calls, allowing or disallowing operations * in OpenTSDB. * * @since 2.4 */ -public abstract class Authorization { +public interface Authorization { /** * Called by TSDB to initialize the plugin @@ -65,7 +67,15 @@ public abstract class Authorization { * @param collector The collector used for emitting statistics */ public abstract void collectStats(final StatsCollector collector); - + + /** + * Determines if the user has a specified permission + * @param state + * @param permission + * @return + */ + public abstract AuthState hasPermission(final AuthState state, final Permissions permission); + /** * Determines if the user is allowed to execute the given query. * The returned state contains a status code regarding whether or not the query diff --git a/src/auth/Permissions.java b/src/auth/Permissions.java new file mode 100644 index 0000000000..edee4acce0 --- /dev/null +++ b/src/auth/Permissions.java @@ -0,0 +1,28 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 <http://www.gnu.org/licenses/>. + +package net.opentsdb.auth; + +/** + * The Permissions used within the OpenTSDB Code. + * Any authorization plugins need to be able to respond to inquiries on these permissions. Plugins and other + * third-party code may implement additional permissions. + * + * @author jonathan.creasy + * @since 2.4.0 + * + */ +public enum Permissions { + TELNET_PUT, HTTP_PUT, HTTP_QUERY, + CREATE_TAGK,CREATE_TAGV, CREATE_METRIC; +} diff --git a/src/auth/Roles.java b/src/auth/Roles.java new file mode 100644 index 0000000000..22e35cfbcd --- /dev/null +++ b/src/auth/Roles.java @@ -0,0 +1,56 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2016-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 <http://www.gnu.org/licenses/>. + +package net.opentsdb.auth; + +import java.util.*; + +/** + * Suggested standard Roles for OpenTSDB Users + * + * @author jonathan.creasy + * @since 2.4.0 + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class Roles { + final static EnumSet<Permissions> ADMINISTRATOR = EnumSet.allOf(Permissions.class); + final static EnumSet<Permissions> PUTONLY = EnumSet.of(Permissions.HTTP_PUT, Permissions.TELNET_PUT); + final static EnumSet<Permissions> WRITER = EnumSet.of(Permissions.HTTP_PUT, Permissions.TELNET_PUT, Permissions.CREATE_TAGV); + final static EnumSet<Permissions> READER = EnumSet.of(Permissions.HTTP_QUERY); + final static EnumSet<Permissions> CREATOR = EnumSet.of(Permissions.CREATE_METRIC, Permissions.CREATE_TAGK, Permissions.CREATE_TAGV); + final static EnumSet<Permissions> GUEST = EnumSet.noneOf(Permissions.class); + + @SuppressWarnings("Convert2Diamond") + private final Set<EnumSet<Permissions>> grantedPermissions = new HashSet<EnumSet<Permissions>>(); + + public Roles() { + this.grantedPermissions.add(GUEST); + } + + public Roles(final EnumSet<Permissions> permissions) { + this.grantedPermissions.add(permissions); + } + + public void grantPermissions(final EnumSet<Permissions> permissions) { + grantedPermissions.add(permissions); + } + + public Boolean hasPermission(final Permissions permission) { + for (EnumSet<Permissions> permissions : this.grantedPermissions) { + if (permissions.contains(permission)) { + return true; + } + } + return false; + } +} diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index cc79cbfc28..29e77f34e6 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -26,6 +26,11 @@ import com.stumbleupon.async.Deferred; import com.stumbleupon.async.TimeoutException; +import net.opentsdb.auth.AuthState; +import net.opentsdb.auth.Authentication; +import net.opentsdb.auth.Authorization; +import net.opentsdb.auth.Permissions; +import org.apache.zookeeper.KeeperException; import org.hbase.async.HBaseException; import org.hbase.async.PleaseThrottleException; import org.jboss.netty.channel.Channel; @@ -70,7 +75,11 @@ class PutDataPointRpc implements TelnetRpc, HttpRpc { protected static final ArrayList<Boolean> EMPTY_DEFERREDS = new ArrayList<Boolean>(0); protected static final AtomicLong telnet_requests = new AtomicLong(); + protected static final AtomicLong telnet_requests_unauthorized = new AtomicLong(); + protected static final AtomicLong telnet_requests_forbidden = new AtomicLong(); protected static final AtomicLong http_requests = new AtomicLong(); + protected static final AtomicLong http_requests_unauthorized = new AtomicLong(); + protected static final AtomicLong http_requests_forbidden = new AtomicLong(); protected static final AtomicLong raw_dps = new AtomicLong(); protected static final AtomicLong raw_histograms = new AtomicLong(); protected static final AtomicLong rollup_dps = new AtomicLong(); @@ -122,6 +131,7 @@ public Deferred<Object> execute(final TSDB tsdb, final Channel chan, telnet_requests.incrementAndGet(); final DataPointType type; final String command = cmd[0].toLowerCase(); + if (command.equals("put")) { type = DataPointType.PUT; raw_dps.incrementAndGet(); @@ -137,7 +147,9 @@ public Deferred<Object> execute(final TSDB tsdb, final Channel chan, String errmsg = null; try { - + + checkAuthorization(tsdb, chan, command); + /** * Error callback that handles passing a data point to the storage * exception handler as well as responding to the client when HBase @@ -267,13 +279,19 @@ public void execute(final TSDB tsdb, final HttpQuery query) "Method not allowed", "The HTTP method [" + query.method().getName() + "] is not permitted for this endpoint"); } + final List<IncomingDataPoint> dps; + //noinspection TryWithIdenticalCatches try { + checkAuthorization(tsdb, query); dps = query.serializer() - .parsePutV1(IncomingDataPoint.class, HttpJsonSerializer.TR_INCOMING); + .parsePutV1(IncomingDataPoint.class, HttpJsonSerializer.TR_INCOMING); } catch (BadRequestException e) { illegal_arguments.incrementAndGet(); throw e; + } catch (IllegalArgumentException e) { + illegal_arguments.incrementAndGet(); + throw e; } processDataPoint(tsdb, query, dps); } @@ -322,8 +340,8 @@ public <T extends IncomingDataPoint> void processDataPoint(final TSDB tsdb, raw_dps.incrementAndGet(); } - /** - * Error back callback to handle storage failures + /* + Error back callback to handle storage failures */ final class PutErrback implements Callback<Boolean, Exception> { public Boolean call(final Exception arg) { @@ -670,6 +688,10 @@ public String toString() { * @param collector The collector to use. */ public static void collectStats(final StatsCollector collector) { + collector.record("rpc.forbidden", telnet_requests_forbidden, "type=telnet"); + collector.record("rpc.unauthorized", telnet_requests_unauthorized, "type=telnet"); + collector.record("rpc.forbidden", http_requests_forbidden, "type=http"); + collector.record("rpc.unauthorized", http_requests_unauthorized, "type=http"); collector.record("rpc.received", http_requests, "type=put"); collector.record("rpc.errors", hbase_errors, "type=hbase_errors"); collector.record("rpc.errors", invalid_values, "type=invalid_values"); @@ -790,4 +812,52 @@ void handleStorageException(final TSDB tsdb, final IncomingDataPoint dp, handler.handleError(dp, e); } } + + private void checkAuthorization(final TSDB tsdb, final HttpQuery query) { + if (tsdb.getConfig().getBoolean("tsd.core.authentication.enable")) { + Authentication authentication = tsdb.getAuth(); + try { + if (authentication.isReady(tsdb, query.channel())) { + AuthState authState = (AuthState) query.channel().getAttachment(); + Authorization authorization = authentication.authorization(); + if ((authorization.hasPermission(authState, Permissions.TELNET_PUT).getStatus() != AuthState.AuthStatus.SUCCESS)) { + http_requests_forbidden.incrementAndGet(); + throw new BadRequestException(HttpResponseStatus.FORBIDDEN, "Forbidden for " + query.getQueryPath()); + } + } else { + http_requests_unauthorized.incrementAndGet(); + throw new BadRequestException(HttpResponseStatus.UNAUTHORIZED, "Unauthorized for " + query.getQueryPath()); + } + } catch (BadRequestException e) { + http_requests_unauthorized.incrementAndGet(); + throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, "Unable to check Authentication for" + query.getQueryPath()); + } + } + // No exceptions thrown, everything is fine. + } + + private void checkAuthorization(final TSDB tsdb, final Channel chan, final String command) { + if (tsdb.getConfig().getBoolean("tsd.core.authentication.enable")) { + Authentication authentication = tsdb.getAuth(); + try { + if (authentication == null) { + throw new IllegalStateException("Authentication is enabled but the Authentication class is NULL"); + } else if (authentication.isReady(tsdb, chan)) { + AuthState authState = (AuthState) chan.getAttachment(); + Authorization authorization = authentication.authorization(); + if ((authorization.hasPermission(authState, Permissions.TELNET_PUT).getStatus() != AuthState.AuthStatus.SUCCESS)) { + telnet_requests_forbidden.incrementAndGet(); + throw new IllegalArgumentException("Unauthorized command: " + command); + } + } else { + telnet_requests_unauthorized.incrementAndGet(); + throw new IllegalArgumentException("Unauthenticated command: " + command); + } + } catch (BadRequestException e) { + telnet_requests_unauthorized.incrementAndGet(); + throw new IllegalArgumentException("Unable to check Authentication for command: " + command); + } + } + // No exceptions thrown, everything is fine. + } } diff --git a/src/tsd/QueryRpc.java b/src/tsd/QueryRpc.java index e94d28642e..fb1e1fedc2 100644 --- a/src/tsd/QueryRpc.java +++ b/src/tsd/QueryRpc.java @@ -27,6 +27,7 @@ import org.hbase.async.RpcTimedOutException; import org.hbase.async.Bytes.ByteMap; import org.jboss.netty.buffer.ChannelBuffer; +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; @@ -73,6 +74,8 @@ final class QueryRpc implements HttpRpc { private static final Logger LOG = LoggerFactory.getLogger(QueryRpc.class); /** Various counters and metrics for reporting query stats */ + static final AtomicLong query_forbidden = new AtomicLong(); + static final AtomicLong query_unauthorized = new AtomicLong(); static final AtomicLong query_invalid = new AtomicLong(); static final AtomicLong query_exceptions = new AtomicLong(); static final AtomicLong query_success = new AtomicLong(); @@ -158,32 +161,9 @@ private void handleQuery(final TSDB tsdb, final HttpQuery query, throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, e.getMessage(), data_query.toString(), e); } - - if (tsdb.getAuth() != null && tsdb.getAuth().authorization() != null) { - if (query.channel().getAttachment() == null || - !(query.channel().getAttachment() instanceof AuthState)) { - throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, - "Authentication was enabled but the authentication state for " - + "this channel was not set properly"); - } - final AuthState state = tsdb.getAuth().authorization().allowQuery( - (AuthState) query.channel().getAttachment(), data_query); - switch (state.getStatus()) { - case SUCCESS: - // cary on :) - break; - case UNAUTHORIZED: - throw new BadRequestException(HttpResponseStatus.UNAUTHORIZED, - state.getMessage()); - case FORBIDDEN: - throw new BadRequestException(HttpResponseStatus.FORBIDDEN, - state.getMessage()); - default: - throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, - state.getMessage()); - } - } - + + checkAuthorization(tsdb, query.channel(), data_query); + // if the user tried this query multiple times from the same IP and src port // they'll be rejected on subsequent calls final QueryStats query_stats = @@ -351,30 +331,9 @@ private void handleExpressionQuery(final TSDB tsdb, final HttpQuery query) { final net.opentsdb.query.pojo.Query v2_query = JSON.parseToObject(query.getContent(), net.opentsdb.query.pojo.Query.class); v2_query.validate(); - if (tsdb.getAuth() != null && tsdb.getAuth().authorization() != null) { - if (query.channel().getAttachment() == null || - !(query.channel().getAttachment() instanceof AuthState)) { - throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, - "Authentication was enabled but the authentication state for " - + "this channel was not set properly"); - } - final AuthState state = tsdb.getAuth().authorization().allowQuery( - (AuthState) query.channel().getAttachment(), v2_query); - switch (state.getStatus()) { - case SUCCESS: - // cary on :) - break; - case UNAUTHORIZED: - throw new BadRequestException(HttpResponseStatus.UNAUTHORIZED, - state.getMessage()); - case FORBIDDEN: - throw new BadRequestException(HttpResponseStatus.FORBIDDEN, - state.getMessage()); - default: - throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, - state.getMessage()); - } - } + + checkAuthorization(tsdb, query.channel(), v2_query); + final QueryExecutor executor = new QueryExecutor(tsdb, v2_query); executor.execute(query); } @@ -883,7 +842,47 @@ private LastPointQuery parseLastPointQuery(final TSDB tsdb, query.setQueries(sub_queries); return query; } - + + private void checkAuthorization(final TSDB tsdb, final Channel chan, final net.opentsdb.query.pojo.Query data_query) { + if (tsdb.getConfig().getBoolean("tsd.core.authentication.enable")) { + if (tsdb.getAuth().isReady(tsdb, chan)) { + final AuthState state = tsdb.getAuth().authorization().allowQuery( + (AuthState) chan.getAttachment(), data_query); + handleAuthorization(state); + } + } + } + + private void checkAuthorization(final TSDB tsdb, final Channel chan, final TSQuery data_query) { + if (tsdb.getConfig().getBoolean("tsd.core.authentication.enable")) { + if (tsdb.getAuth().isReady(tsdb, chan)) { + final AuthState state = tsdb.getAuth().authorization().allowQuery( + (AuthState) chan.getAttachment(), data_query); + handleAuthorization(state); + } + } + } + + private void handleAuthorization(AuthState state) { + switch (state.getStatus()) { + case SUCCESS: + // cary on :) + break; + case UNAUTHORIZED: + query_unauthorized.incrementAndGet(); + throw new BadRequestException(HttpResponseStatus.UNAUTHORIZED, + state.getMessage()); + case FORBIDDEN: + query_forbidden.incrementAndGet(); + throw new BadRequestException(HttpResponseStatus.FORBIDDEN, + state.getMessage()); + default: + query_exceptions.incrementAndGet(); + throw new BadRequestException(HttpResponseStatus.INTERNAL_SERVER_ERROR, + state.getMessage()); + } + } + /** * Parse the "percentile" section of the query string and returns an list of * float that contains the percentile calculation paramters @@ -914,6 +913,8 @@ public static final List<Float> parsePercentiles(final String spec) { /** @param collector Populates the collector with statistics */ public static void collectStats(final StatsCollector collector) { + collector.record("http.query.unauthorized", query_unauthorized); + collector.record("http.query.forbidden", query_forbidden); collector.record("http.query.invalid_requests", query_invalid); collector.record("http.query.exceptions", query_exceptions); collector.record("http.query.success", query_success); diff --git a/test/auth/AllowAllAuthenticatingAuthorizerTest.java b/test/auth/AllowAllAuthenticatingAuthorizerTest.java new file mode 100644 index 0000000000..64e2ce213b --- /dev/null +++ b/test/auth/AllowAllAuthenticatingAuthorizerTest.java @@ -0,0 +1,107 @@ +package net.opentsdb.auth; + +import net.opentsdb.query.pojo.Query; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; +import net.opentsdb.utils.Config; +import org.jboss.netty.channel.Channel; +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; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({TSDB.class, TSQuery.class}) +public class AllowAllAuthenticatingAuthorizerTest { + private TSDB tsdb = null; + private AllowAllAuthenticatingAuthorizer authenticatingAuthorizer; + + private Channel getMockedChannel() { + Channel channel = mock(Channel.class); + when(channel.getAttachment()).thenReturn(AllowAllAuthenticatingAuthorizer.accessGranted); + return channel; + } + + @Before + public void setUp() throws Exception { + this.authenticatingAuthorizer = new AllowAllAuthenticatingAuthorizer(); + this.tsdb = mock(TSDB.class); + + final Config config = new Config(false); + config.overrideConfig("tsd.core.authentication.enable", "true"); + when(tsdb.getConfig()).thenReturn(config); + + final AuthState state = mock(AuthState.class); + when(state.getStatus()).thenReturn(AllowAllAuthenticatingAuthorizer.accessGranted.getStatus()); + + final Authorization authorization = mock(Authorization.class); + when(authorization.allowQuery(any(AuthState.class), any(TSQuery.class))).thenReturn(state); + + final Authentication authentication = mock(Authentication.class); + when(authentication.authorization()).thenReturn(authorization); + when(authentication.isReady(any(TSDB.class), any(Channel.class))).thenReturn(true); + when(tsdb.getAuth()).thenReturn(authentication); + } + + @Test + public void isReady() throws Exception { + Channel channel = getMockedChannel(); + assertTrue(authenticatingAuthorizer.isReady(tsdb, channel)); + } + + @Test + public void hasPermissionAdministrator() throws Exception { + AuthState authState = mock(AuthState.class); + this.authenticatingAuthorizer.setRoles(new Roles(Roles.ADMINISTRATOR)); + assertEquals(AllowAllAuthenticatingAuthorizer.accessGranted, this.authenticatingAuthorizer.hasPermission(authState, Permissions.TELNET_PUT)); + } + + @Test + public void hasPermissionGuest() throws Exception { + AuthState authState = mock(AuthState.class); + this.authenticatingAuthorizer.setRoles(new Roles(Roles.GUEST)); + assertEquals(AllowAllAuthenticatingAuthorizer.accessDenied, this.authenticatingAuthorizer.hasPermission(authState, Permissions.TELNET_PUT)); + } + + @Test + public void allowTSQueryAdministrator() throws Exception { + AuthState authState = mock(AuthState.class); + TSQuery tsQuery = mock(TSQuery.class); + Roles roles = new Roles(Roles.ADMINISTRATOR); + this.authenticatingAuthorizer.setRoles(roles); + assertEquals(AllowAllAuthenticatingAuthorizer.accessGranted, this.authenticatingAuthorizer.allowQuery(authState, tsQuery)); + } + + @Test + public void allowTSQueryGuest() throws Exception { + AuthState authState = mock(AuthState.class); + TSQuery tsQuery = mock(TSQuery.class); + Roles roles = new Roles(Roles.GUEST); + this.authenticatingAuthorizer.setRoles(roles); + assertEquals(AllowAllAuthenticatingAuthorizer.accessDenied, this.authenticatingAuthorizer.allowQuery(authState, tsQuery)); + } + + @Test + public void allowQueryAdministrator() throws Exception { + AuthState authState = mock(AuthState.class); + Query query = mock(Query.class); + Roles roles = new Roles(Roles.ADMINISTRATOR); + this.authenticatingAuthorizer.setRoles(roles); + assertEquals(AllowAllAuthenticatingAuthorizer.accessGranted, this.authenticatingAuthorizer.allowQuery(authState, query)); + } + + @Test + public void allowQueryGuest() throws Exception { + AuthState authState = mock(AuthState.class); + Query query = mock(Query.class); + Roles roles = new Roles(Roles.GUEST); + this.authenticatingAuthorizer.setRoles(roles); + assertEquals(AllowAllAuthenticatingAuthorizer.accessDenied, this.authenticatingAuthorizer.allowQuery(authState, query)); + } +} \ No newline at end of file diff --git a/test/core/BaseTsdbTest.java b/test/core/BaseTsdbTest.java index 75ed4c3a36..9dbc6c31e3 100644 --- a/test/core/BaseTsdbTest.java +++ b/test/core/BaseTsdbTest.java @@ -16,9 +16,8 @@ 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.Matchers.anyInt; -import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.*; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; @@ -28,6 +27,9 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import net.opentsdb.auth.AuthState; +import net.opentsdb.auth.Authentication; +import net.opentsdb.auth.Authorization; import net.opentsdb.meta.Annotation; import net.opentsdb.storage.MockBase; import net.opentsdb.uid.NoSuchUniqueId; @@ -40,6 +42,7 @@ import org.hbase.async.Bytes; import org.hbase.async.HBaseClient; import org.hbase.async.Scanner; +import org.jboss.netty.channel.Channel; import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.Timeout; import org.jboss.netty.util.TimerTask; @@ -129,10 +132,22 @@ public void before() throws Exception { .thenReturn(timer); PowerMockito.whenNew(HBaseClient.class).withAnyArguments() .thenReturn(client); - + + final AuthState state = mock(AuthState.class); + when(state.getStatus()).thenReturn(AuthState.AuthStatus.SUCCESS); + + final Authorization authorization = mock(Authorization.class); + when(authorization.allowQuery(any(AuthState.class), any(TSQuery.class))).thenReturn(state); + + final Authentication authentication = mock(Authentication.class); + when(authentication.authorization()).thenReturn(authorization); + when(authentication.isReady(any(TSDB.class), any(Channel.class))).thenReturn(true); + config = new Config(false); config.overrideConfig("tsd.storage.enable_compaction", "false"); + config.overrideConfig("tsd.core.authentication.enable", "false"); tsdb = PowerMockito.spy(new TSDB(config)); + when(tsdb.getAuth()).thenReturn(authentication); config.setAutoMetric(true); diff --git a/test/tsd/NettyMocks.java b/test/tsd/NettyMocks.java index 3c4fe35342..0c384af64b 100644 --- a/test/tsd/NettyMocks.java +++ b/test/tsd/NettyMocks.java @@ -19,7 +19,14 @@ import java.net.SocketAddress; import java.nio.charset.Charset; +import com.stumbleupon.async.Deferred; +import net.opentsdb.auth.AllowAllAuthenticatingAuthorizer; +import net.opentsdb.auth.AuthState; +import net.opentsdb.auth.Authentication; +import net.opentsdb.auth.Authorization; +import net.opentsdb.core.DataPoints; import net.opentsdb.core.TSDB; +import net.opentsdb.core.TSQuery; import net.opentsdb.utils.Config; import org.jboss.netty.buffer.ChannelBuffer; @@ -49,12 +56,44 @@ public final class NettyMocks { */ public static TSDB getMockedHTTPTSDB() throws Exception { final TSDB tsdb = mock(TSDB.class); + final Config config = new Config(false); config.overrideConfig("tsd.http.show_stack_trace", "true"); + config.overrideConfig("tsd.core.authentication.enable", "false"); when(tsdb.getConfig()).thenReturn(config); + + final Authentication authentication = mock(Authentication.class); + when(authentication.isReady(any(TSDB.class), any(Channel.class))).thenReturn(false); + when(tsdb.getAuth()).thenReturn(authentication); + return tsdb; } - + + /** + * Sets up a TSDB object for HTTP RPC tests that has a Config object + * @return A TSDB mock + */ + public static TSDB getMockedHTTPTSDBWithAuthEnabled(final AuthState.AuthStatus authStatus) throws Exception { + final TSDB tsdb = mock(TSDB.class); + + final Config config = new Config(false); + config.overrideConfig("tsd.http.show_stack_trace", "true"); + config.overrideConfig("tsd.core.authentication.enable", "true"); + when(tsdb.getConfig()).thenReturn(config); + + final AuthState state = mock(AuthState.class); + when(state.getStatus()).thenReturn(authStatus); + + final Authorization authorization = mock(Authorization.class); + when(authorization.allowQuery(any(AuthState.class), any(TSQuery.class))).thenReturn(state); + + final Authentication authentication = mock(Authentication.class); + when(authentication.authorization()).thenReturn(authorization); + when(authentication.isReady(any(TSDB.class), any(Channel.class))).thenReturn(true); + when(tsdb.getAuth()).thenReturn(authentication); + return tsdb; + } + /** * Returns a mocked Channel object that simply sets the name to * [fake channel] @@ -199,7 +238,9 @@ public static HttpQuery contentQuery(final TSDB tsdb, final String uri, return new HttpQuery(tsdb, req, channelMock); } - /** @param the query to mock a future callback for */ + /** + * @param query the query to mock a future callback for + */ public static void mockChannelFuture(final HttpQuery query) { final ChannelFuture future = new DefaultChannelFuture(query.channel(), false); when(query.channel().write(any(ChannelBuffer.class))).thenReturn(future); diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index 3b4c5e71f9..f9f855a32a 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -59,11 +59,11 @@ /** * Unit tests for the Query RPC class that handles parsing user queries for * timeseries data and returning that data - * <b>Note:</b> Testing query validation and such should be done in the + * <b>Note:</b> Testing query validation and such should be done in the * core.TestTSQuery and TestTSSubQuery classes */ @RunWith(PowerMockRunner.class) -@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, Query.class, +@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, Query.class, Deferred.class, TSQuery.class, DateTime.class, DeferredGroupException.class }) public final class TestQueryRpc { private TSDB tsdb = null; @@ -71,18 +71,18 @@ public final class TestQueryRpc { private Query empty_query = mock(Query.class); private Query query_result; private List<ExpressionTree> expressions; - + private static final Method parseQuery; static { try { - parseQuery = QueryRpc.class.getDeclaredMethod("parseQuery", + parseQuery = QueryRpc.class.getDeclaredMethod("parseQuery", TSDB.class, HttpQuery.class, List.class); parseQuery.setAccessible(true); } catch (Exception e) { throw new RuntimeException("Failed in static initializer", e); } } - + @Before public void before() throws Exception { tsdb = NettyMocks.getMockedHTTPTSDB(); @@ -90,7 +90,7 @@ public void before() throws Exception { query_result = mock(Query.class); rpc = new QueryRpc(); expressions = null; - + when(tsdb.newQuery()).thenReturn(query_result); when(empty_query.run()).thenReturn(new DataPoints[0]); when(query_result.configureFromQuery((TSQuery)any(), anyInt())) @@ -98,10 +98,10 @@ public void before() throws Exception { when(query_result.runAsync()) .thenReturn(Deferred.fromResult(new DataPoints[0])); } - + @Test public void parseQueryMType() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -112,18 +112,18 @@ public void parseQueryMType() throws Exception { assertEquals("sum", sub.getAggregator()); assertEquals("sys.cpu.0", sub.getMetric()); } - + @Test public void parseQueryMTypeWEnd() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&end=5m-ago&m=sum:sys.cpu.0"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertEquals("5m-ago", tsq.getEnd()); } - + @Test public void parseQuery2MType() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0&m=avg:sys.cpu.1"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq.getQueries()); @@ -137,28 +137,28 @@ public void parseQuery2MType() throws Exception { assertEquals("avg", sub2.getAggregator()); assertEquals("sys.cpu.1", sub2.getMetric()); } - + @Test public void parseQueryMTypeWRate() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:rate:sys.cpu.0"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertTrue(sub.getRate()); } - + @Test public void parseQueryMTypeWDS() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:1h-avg:sys.cpu.0"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertEquals("1h-avg", sub.getDownsample()); } - + @Test public void parseQueryMTypeWDSAndFill() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:1h-avg-lerp:sys.cpu.0"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -167,28 +167,28 @@ public void parseQueryMTypeWDSAndFill() throws Exception { @Test public void parseQueryMTypeWRateAndDS() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:1h-avg:rate:sys.cpu.0"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertTrue(sub.getRate()); assertEquals("1h-avg", sub.getDownsample()); } - + @Test public void parseQueryMTypeWTag() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=web01}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); assertNotNull(sub.getTags()); assertEquals("literal_or(web01)", sub.getTags().get("host")); } - + @Test public void parseQueryMTypeWGroupByRegex() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, - "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + TagVRegexFilter.FILTER_NAME + "(something(foo|bar))}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -196,11 +196,11 @@ public void parseQueryMTypeWGroupByRegex() throws Exception { assertEquals(1, sub.getFilters().size()); assertTrue(sub.getFilters().get(0) instanceof TagVRegexFilter); } - + @Test public void parseQueryMTypeWGroupByWildcardExplicit() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, - "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + + HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=" + TagVWildcardFilter.FILTER_NAME + "(*quirm)}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -208,10 +208,10 @@ public void parseQueryMTypeWGroupByWildcardExplicit() throws Exception { assertEquals(1, sub.getFilters().size()); assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); } - + @Test public void parseQueryMTypeWGroupByWildcardImplicit() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=*quirm}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -219,10 +219,10 @@ public void parseQueryMTypeWGroupByWildcardImplicit() throws Exception { assertEquals(1, sub.getFilters().size()); assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); } - + @Test public void parseQueryMTypeWWildcardFilterExplicit() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{host=wildcard(*quirm)}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -230,10 +230,10 @@ public void parseQueryMTypeWWildcardFilterExplicit() throws Exception { assertEquals(1, sub.getFilters().size()); assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); } - + @Test public void parseQueryMTypeWWildcardFilterImplicit() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{host=*quirm}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -241,10 +241,10 @@ public void parseQueryMTypeWWildcardFilterImplicit() throws Exception { assertEquals(1, sub.getFilters().size()); assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); } - + @Test public void parseQueryMTypeWGroupByAndWildcardFilterExplicit() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{colo=lga}{host=wildcard(*quirm)}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -252,10 +252,10 @@ public void parseQueryMTypeWGroupByAndWildcardFilterExplicit() throws Exception assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); assertTrue(sub.getFilters().get(1) instanceof TagVLiteralOrFilter); } - + @Test public void parseQueryMTypeWGroupByAndWildcardFilterSameTagK() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=quirm|tsort}" + "{host=wildcard(*quirm)}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); @@ -264,11 +264,11 @@ public void parseQueryMTypeWGroupByAndWildcardFilterSameTagK() throws Exception assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); assertTrue(sub.getFilters().get(1) instanceof TagVLiteralOrFilter); } - + @Test - public void parseQueryMTypeWGroupByFilterAndWildcardFilterSameTagK() + public void parseQueryMTypeWGroupByFilterAndWildcardFilterSameTagK() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + "{host=wildcard(*quirm)}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); @@ -278,44 +278,44 @@ public void parseQueryMTypeWGroupByFilterAndWildcardFilterSameTagK() assertTrue(sub.getFilters().get(0) instanceof TagVWildcardFilter); assertTrue(sub.getFilters().get(1) instanceof TagVWildcardFilter); } - + @Test (expected = IllegalArgumentException.class) public void parseQueryMTypeWGroupByFilterMissingClose() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + "{host=wildcard(*quirm)"); parseQuery.invoke(rpc, tsdb, query, expressions); } - + @Test (expected = IllegalArgumentException.class) public void parseQueryMTypeWGroupByFilterMissingEquals() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=wildcard(*tsort)}" + "{hostwildcard(*quirm)}"); parseQuery.invoke(rpc, tsdb, query, expressions); } - + @Test (expected = IllegalArgumentException.class) public void parseQueryMTypeWGroupByNoSuchFilter() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{host=nosuchfilter(*tsort)}" + "{host=dummyfilter(*quirm)}"); parseQuery.invoke(rpc, tsdb, query, expressions); } - + @Test public void parseQueryMTypeWEmptyFilterBrackets() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0{}{}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); sub.validateAndSetQuery(); assertEquals(0, sub.getFilters().size()); } - + @Test public void parseQueryMTypeWExplicit() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:explicit_tags:sys.cpu.0{host=web01}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -323,10 +323,10 @@ public void parseQueryMTypeWExplicit() throws Exception { assertEquals("literal_or(web01)", sub.getTags().get("host")); assertTrue(sub.getExplicitTags()); } - + @Test public void parseQueryMTypeWExplicitAndRate() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:explicit_tags:rate:sys.cpu.0{host=web01}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -335,10 +335,10 @@ public void parseQueryMTypeWExplicitAndRate() throws Exception { assertTrue(sub.getRate()); assertTrue(sub.getExplicitTags()); } - + @Test public void parseQueryMTypeWExplicitAndRateAndDS() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:explicit_tags:rate:1m-sum:sys.cpu.0{host=web01}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -348,10 +348,10 @@ public void parseQueryMTypeWExplicitAndRateAndDS() throws Exception { assertTrue(sub.getExplicitTags()); assertEquals("1m-sum", sub.getDownsample()); } - + @Test public void parseQueryMTypeWExplicitAndDSAndRate() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:explicit_tags:1m-sum:rate:sys.cpu.0{host=web01}"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); TSSubQuery sub = tsq.getQueries().get(0); @@ -361,10 +361,10 @@ public void parseQueryMTypeWExplicitAndDSAndRate() throws Exception { assertTrue(sub.getExplicitTags()); assertEquals("1m-sum", sub.getDownsample()); } - + @Test public void parseQueryTSUIDType() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:010101"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -376,10 +376,10 @@ public void parseQueryTSUIDType() throws Exception { assertEquals(1, sub.getTsuids().size()); assertEquals("010101", sub.getTsuids().get(0)); } - + @Test public void parseQueryTSUIDTypeMulti() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:010101,020202"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -392,10 +392,10 @@ public void parseQueryTSUIDTypeMulti() throws Exception { assertEquals("010101", sub.getTsuids().get(0)); assertEquals("020202", sub.getTsuids().get(1)); } - + @Test public void parseQuery2TSUIDType() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:010101&tsuid=avg:020202"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -413,10 +413,10 @@ public void parseQuery2TSUIDType() throws Exception { assertEquals(1, sub.getTsuids().size()); assertEquals("020202", sub.getTsuids().get(0)); } - + @Test public void parseQueryTSUIDTypeWRate() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:rate:010101"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -429,10 +429,10 @@ public void parseQueryTSUIDTypeWRate() throws Exception { assertEquals("010101", sub.getTsuids().get(0)); assertTrue(sub.getRate()); } - + @Test public void parseQueryTSUIDTypeWDS() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:1m-sum:010101"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -445,10 +445,10 @@ public void parseQueryTSUIDTypeWDS() throws Exception { assertEquals("010101", sub.getTsuids().get(0)); assertEquals("1m-sum", sub.getDownsample()); } - + @Test public void parseQueryTSUIDTypeWRateAndDS() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:1m-sum:rate:010101"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -462,37 +462,37 @@ public void parseQueryTSUIDTypeWRateAndDS() throws Exception { assertEquals("1m-sum", sub.getDownsample()); assertTrue(sub.getRate()); } - + @Test public void parseQueryWPadding() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.0&padding"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertTrue(tsq.getPadding()); } - + @Test (expected = BadRequestException.class) public void parseQueryStartMissing() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?end=1h-ago&m=sum:sys.cpu.0"); parseQuery.invoke(rpc, tsdb, query, expressions); } - + @Test (expected = BadRequestException.class) public void parseQueryNoSubQuery() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago"); parseQuery.invoke(rpc, tsdb, query, expressions); } - + @Test public void postQuerySimplePass() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - + HttpQuery query = NettyMocks.postQuery(tsdb, "/api/query", "{\"start\":1425440315306,\"queries\":" + "[{\"metric\":\"somemetric\",\"aggregator\":\"sum\",\"rate\":true," + @@ -516,55 +516,55 @@ public void postQueryNoMetricBadRequest() throws Exception { "\"rateOptions\":{\"counter\":false}}]}"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("No such name for 'foo': 'metrics'")); } @Test public void executeEmpty() throws Exception { - final HttpQuery query = NettyMocks.getQuery(tsdb, + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertEquals("[]", json); } - + @Test public void executeURI() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - - final HttpQuery query = NettyMocks.getQuery(tsdb, + + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } - + @Test public void executeURIDuplicates() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - - final HttpQuery query = NettyMocks.getQuery(tsdb, + + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user&m=sum:sys.cpu.user" + "&m=sum:sys.cpu.user"); NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } - + @Test public void executeNSU() throws Exception { final DeferredGroupException dge = mock(DeferredGroupException.class); @@ -572,16 +572,16 @@ public void executeNSU() throws Exception { when(query_result.configureFromQuery((TSQuery)any(), anyInt())) .thenReturn(Deferred.fromError(dge)); - - final HttpQuery query = NettyMocks.getQuery(tsdb, + + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:sys.cpu.user"); rpc.execute(tsdb, query); assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("No such name for 'foo': 'metrics'")); } - + @Test public void executeWithBadDSFill() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; @@ -589,8 +589,8 @@ public void executeWithBadDSFill() throws Exception { when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - try { - final HttpQuery query = NettyMocks.getQuery(tsdb, + try { + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:10m-avg-badbadbad:sys.cpu.user"); rpc.execute(tsdb, query); fail("expected BadRequestException"); @@ -600,42 +600,42 @@ public void executeWithBadDSFill() throws Exception { "Unrecognized fill policy: badbadbad")); } } - + @Test public void executePOST() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - + final HttpQuery query = NettyMocks.postQuery(tsdb,"/api/query", "{\"start\":\"1h-ago\",\"queries\":" + "[{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}]}"); NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } - + @Test public void executePOSTDuplicates() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - + final HttpQuery query = NettyMocks.postQuery(tsdb,"/api/query", "{\"start\":\"1h-ago\",\"queries\":" + "[{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}," + "{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}]}"); NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } - + @Test (expected = BadRequestException.class) public void deleteDatapointsBadRequest() throws Exception { HttpQuery query = NettyMocks.deleteQuery(tsdb, @@ -646,40 +646,40 @@ public void deleteDatapointsBadRequest() throws Exception { query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("Deleting data is not enabled")); } - + @Test public void gexp() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - - final HttpQuery query = NettyMocks.getQuery(tsdb, + + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query/gexp?start=1h-ago&exp=scale(sum:sys.cpu.user,1)"); NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); assertEquals(query.response().getStatus(), HttpResponseStatus.OK); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); } - + @Test public void gexpBadExpression() throws Exception { final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); when(query_result.runAsync()).thenReturn( Deferred.fromResult(datapoints)); - - final HttpQuery query = NettyMocks.getQuery(tsdb, + + final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query/gexp?start=1h-ago&exp=scale(sum:sys.cpu.user,notanumber)"); rpc.execute(tsdb, query); assertEquals(query.response().getStatus(), HttpResponseStatus.BAD_REQUEST); - final String json = + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("factor")); } - + @Test public void testParsePercentile() { final String s = "percentile[0.98,0.95,0.99]"; @@ -693,7 +693,7 @@ public void testParsePercentile() { strs.add(sss); strs.add(ss); strs.add(s); - + for (String str : strs) { List<Float> fs = QueryRpc.parsePercentiles(str); assertEquals(3, fs.size()); @@ -702,26 +702,26 @@ public void testParsePercentile() { assertEquals(0.99, fs.get(2), 0.0001); } } - + @Test public void parseHistogramQueryMType() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&m=sum:percentiles[0.98]:msg.end2end.latency"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); assertEquals("1h-ago", tsq.getStart()); assertNotNull(tsq.getQueries()); TSSubQuery sub = tsq.getQueries().get(0); - + assertNotNull(sub); assertEquals("sum", sub.getAggregator()); assertEquals("msg.end2end.latency", sub.getMetric()); assertEquals(0.98f, sub.getPercentiles().get(0).floatValue(), 0.0001); } - + @Test public void parseHistogramQueryTSUIDType() throws Exception { - HttpQuery query = NettyMocks.getQuery(tsdb, + HttpQuery query = NettyMocks.getQuery(tsdb, "/api/query?start=1h-ago&tsuid=sum:percentiles[0.98]:010101"); TSQuery tsq = (TSQuery) parseQuery.invoke(rpc, tsdb, query, expressions); assertNotNull(tsq); @@ -734,38 +734,69 @@ public void parseHistogramQueryTSUIDType() throws Exception { assertEquals("010101", sub.getTsuids().get(0)); assertEquals(0.98f, sub.getPercentiles().get(0).floatValue(), 0.0001); } - + @Test - public void v1Auth() throws Exception { + public void v1AuthAllowed() throws Exception { + final TSDB tsdb = NettyMocks.getMockedHTTPTSDBWithAuthEnabled(AuthStatus.SUCCESS); + when(tsdb.newQuery()).thenReturn(query_result); + when(query_result.configureFromQuery((TSQuery)any(), anyInt())) + .thenReturn(Deferred.fromResult(null)); + when(query_result.runAsync()) + .thenReturn(Deferred.fromResult(new DataPoints[0])); + final DataPoints[] datapoints = new DataPoints[1]; datapoints[0] = new MockDataPoints().getMock(); - when(query_result.runAsync()).thenReturn( - Deferred.fromResult(datapoints)); - - final Authorization authorization = mock(Authorization.class); - final Authentication authentication = mock(Authentication.class); - final AuthState state = mock(AuthState.class); - final HttpQuery query = NettyMocks.getQuery(tsdb, - "/api/query?start=1h-ago&m=sum:sys.cpu.user"); - when(tsdb.getAuth()).thenReturn(authentication); - when(query.channel().getAttachment()).thenReturn(state); - when(state.getStatus()).thenReturn(AuthStatus.SUCCESS); - when(authentication.authorization()).thenReturn(authorization); - when(authorization.allowQuery(eq(state), any(TSQuery.class))).thenReturn(state); - TestHttpQuery.mockChannelFuture(query); + when(query_result.runAsync()).thenReturn(Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.postQuery(tsdb,"/api/query", + "{\"start\":\"1h-ago\",\"queries\":" + + "[{\"metric\":\"sys.cpu.user\",\"aggregator\":\"sum\"}]}"); + + NettyMocks.mockChannelFuture(query); rpc.execute(tsdb, query); - String json = - query.response().getContent().toString(Charset.forName("UTF-8")); + final String json = query.response().getContent().toString(Charset.forName("UTF-8")); assertTrue(json.contains("\"metric\":\"system.cpu.user\"")); - - when(state.getStatus()).thenReturn(AuthStatus.UNAUTHORIZED); - + assertEquals(HttpResponseStatus.OK, query.response().getStatus()); + } + + @Test + public void v1AuthUnauthorized() throws Exception { + final TSDB tsdb = NettyMocks.getMockedHTTPTSDBWithAuthEnabled(AuthStatus.UNAUTHORIZED); + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.getQuery(tsdb,"/api/query?start=1h-ago&m=sum:sys.cpu.user"); + + try { + TestHttpQuery.mockChannelFuture(query); + rpc.execute(tsdb, query); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { + assertEquals(HttpResponseStatus.UNAUTHORIZED, e.getStatus()); + } + } + + @Test + public void v1AuthForbidden() throws Exception { + final TSDB tsdb = NettyMocks.getMockedHTTPTSDBWithAuthEnabled(AuthStatus.FORBIDDEN); + final DataPoints[] datapoints = new DataPoints[1]; + datapoints[0] = new MockDataPoints().getMock(); + when(query_result.runAsync()).thenReturn( + Deferred.fromResult(datapoints)); + + final HttpQuery query = NettyMocks.getQuery(tsdb, + "/api/query?start=1h-ago&m=sum:sys.cpu.user"); + try { + TestHttpQuery.mockChannelFuture(query); rpc.execute(tsdb, query); fail("Expected BadRequestException"); } catch (BadRequestException e) { - assertEquals(e.getStatus(), HttpResponseStatus.UNAUTHORIZED); + assertEquals(HttpResponseStatus.FORBIDDEN, e.getStatus()); } } + //TODO(cl) add unit tests for the rate options parsing } \ No newline at end of file From 07d00cc6ae20d2d8572d23f83d22087bacdf3ea4 Mon Sep 17 00:00:00 2001 From: opsun <cerz@qq.com> Date: Wed, 23 Aug 2017 17:46:59 +0800 Subject: [PATCH 702/826] HighestCurrent may return wrong result Because of DataPoints maybe empty and AggregationIterator.hasNextValue() will skip empty item. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/query/expression/HighestCurrent.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/query/expression/HighestCurrent.java b/src/query/expression/HighestCurrent.java index 11d9f04f82..e6295206a3 100644 --- a/src/query/expression/HighestCurrent.java +++ b/src/query/expression/HighestCurrent.java @@ -92,10 +92,14 @@ public DataPoints[] evaluate(final TSQuery data_query, MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); } - post_agg_results[ix++] = new PostAggregatedDataPoints(dps, - mutable_points.toArray(new DataPoint[mutable_points.size()])); + // Because of AggregationIterator.hasNextValue() will skip empty item. + if (mutable_points.size() > 0) { + post_agg_results[ix++] = new PostAggregatedDataPoints(dps, + mutable_points.toArray(new DataPoint[mutable_points.size()])); + } } } + num_results = ix; final SeekableView[] views = new SeekableView[num_results]; for (int i = 0; i < num_results; i++) { From 58ab43571c1ed823102f6344a4a1429fdaef43d8 Mon Sep 17 00:00:00 2001 From: opsun <cerz@qq.com> Date: Wed, 23 Aug 2017 17:46:59 +0800 Subject: [PATCH 703/826] HighestCurrent may return wrong result Because of DataPoints maybe empty and AggregationIterator.hasNextValue() will skip empty item. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/query/expression/HighestCurrent.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/query/expression/HighestCurrent.java b/src/query/expression/HighestCurrent.java index 11d9f04f82..e6295206a3 100644 --- a/src/query/expression/HighestCurrent.java +++ b/src/query/expression/HighestCurrent.java @@ -92,10 +92,14 @@ public DataPoints[] evaluate(final TSQuery data_query, MutableDataPoint.ofLongValue(point.timestamp(), point.longValue()) : MutableDataPoint.ofDoubleValue(point.timestamp(), point.doubleValue())); } - post_agg_results[ix++] = new PostAggregatedDataPoints(dps, - mutable_points.toArray(new DataPoint[mutable_points.size()])); + // Because of AggregationIterator.hasNextValue() will skip empty item. + if (mutable_points.size() > 0) { + post_agg_results[ix++] = new PostAggregatedDataPoints(dps, + mutable_points.toArray(new DataPoint[mutable_points.size()])); + } } } + num_results = ix; final SeekableView[] views = new SeekableView[num_results]; for (int i = 0; i < num_results; i++) { From 7551372728c25e0cd77510e98a2a8c4c76aec0bc Mon Sep 17 00:00:00 2001 From: xiayang <xiayang@jd.com> Date: Mon, 21 May 2018 16:01:53 -0700 Subject: [PATCH 704/826] Change cache-hits data type from int to long and fix spilling. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/TSDB.java | 6 +++--- src/uid/UniqueId.java | 30 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 1a5683386f..4a01e9606b 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -594,19 +594,19 @@ public Deferred<ArrayList<Object>> checkNecessaryTablesExist() { } /** Number of cache hits during lookups involving UIDs. */ - public int uidCacheHits() { + public long uidCacheHits() { return (metrics.cacheHits() + tag_names.cacheHits() + tag_values.cacheHits()); } /** Number of cache misses during lookups involving UIDs. */ - public int uidCacheMisses() { + public long uidCacheMisses() { return (metrics.cacheMisses() + tag_names.cacheMisses() + tag_values.cacheMisses()); } /** Number of cache entries currently in RAM for lookups involving UIDs. */ - public int uidCacheSize() { + public long uidCacheSize() { return (metrics.cacheSize() + tag_names.cacheSize() + tag_values.cacheSize()); } diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 22a384fc82..18bb30b3e5 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -83,6 +83,8 @@ public enum UniqueIdType { private static final short INITIAL_EXP_BACKOFF_DELAY = 800; /** Maximum number of results to return in suggest(). */ private static final short MAX_SUGGESTIONS = 25; + /** Maximum number of cache_hits. */ + private static final long MAX_CACHE_SIZE = 2000000000L; /** HBase client to use. */ private final HBaseClient client; @@ -112,9 +114,9 @@ public enum UniqueIdType { Collections.synchronizedSet(new HashSet<String>()); /** Number of times we avoided reading from HBase thanks to the cache. */ - private volatile int cache_hits; + private volatile long cache_hits; /** Number of times we had to read from HBase and populate the cache. */ - private volatile int cache_misses; + private volatile long cache_misses; /** How many times we collided with an existing ID when attempting to * generate a new UID */ private volatile int random_id_collisions; @@ -194,20 +196,32 @@ public UniqueId(final TSDB tsdb, final byte[] table, final String kind, } /** The number of times we avoided reading from HBase thanks to the cache. */ - public int cacheHits() { + public long cacheHits() { return cache_hits; } /** The number of times we had to read from HBase and populate the cache. */ - public int cacheMisses() { + public long cacheMisses() { return cache_misses; } /** Returns the number of elements stored in the internal cache. */ - public int cacheSize() { + public long cacheSize() { return name_cache.size() + id_cache.size(); } + /** + * Due to the var cache_hits type is int, but the max of int is + * 2147483648, and int happen spilling + */ + private void reNumCache() { + if (cache_hits > MAX_CACHE_SIZE) { + cache_hits = 1; + } else { + cache_hits++; + } + } + /** Returns the number of random UID collisions */ public int randomIdCollisions() { return random_id_collisions; @@ -291,7 +305,7 @@ public Deferred<String> getNameAsync(final byte[] id) { } final String name = getNameFromCache(id); if (name != null) { - cache_hits++; + reNumCache(); return Deferred.fromResult(name); } cache_misses++; @@ -346,7 +360,7 @@ public byte[] getId(final String name) throws NoSuchUniqueName, HBaseException { public Deferred<byte[]> getIdAsync(final String name) { final byte[] id = getIdFromCache(name); if (id != null) { - cache_hits++; + reNumCache(); return Deferred.fromResult(id); } cache_misses++; @@ -773,7 +787,7 @@ public Deferred<byte[]> getOrCreateIdAsync(final String name, // Look in the cache first. final byte[] id = getIdFromCache(name); if (id != null) { - cache_hits++; + reNumCache(); return Deferred.fromResult(id); } // Not found in our cache, so look in HBase instead. From d38f4455785d177d47c2b711ebb8881b5ea850ec Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 16:04:37 -0700 Subject: [PATCH 705/826] Tweak Xiayang's UID cache hit count by adding a miss counter and resetting when we hit Long.MAX_VALUE. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/uid/UniqueId.java | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 18bb30b3e5..0d74cdfa1d 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -83,8 +83,6 @@ public enum UniqueIdType { private static final short INITIAL_EXP_BACKOFF_DELAY = 800; /** Maximum number of results to return in suggest(). */ private static final short MAX_SUGGESTIONS = 25; - /** Maximum number of cache_hits. */ - private static final long MAX_CACHE_SIZE = 2000000000L; /** HBase client to use. */ private final HBaseClient client; @@ -211,17 +209,29 @@ public long cacheSize() { } /** - * Due to the var cache_hits type is int, but the max of int is - * 2147483648, and int happen spilling + * Resets the cache hits counter before rollover. Note that a few updates + * may be dropped due to race conditions at rollover. */ - private void reNumCache() { - if (cache_hits > MAX_CACHE_SIZE) { + private void incrementCacheHits() { + if (cache_hits >= Long.MAX_VALUE) { cache_hits = 1; } else { cache_hits++; } } + /** + * Resets the cache miss counter before rollover. Note that a few updates + * may be dropped due to race conditions at rollover. + */ + private void incrementCacheMiss() { + if (cache_misses >= Long.MAX_VALUE) { + cache_misses = 1; + } else { + cache_misses++; + } + } + /** Returns the number of random UID collisions */ public int randomIdCollisions() { return random_id_collisions; @@ -305,10 +315,10 @@ public Deferred<String> getNameAsync(final byte[] id) { } final String name = getNameFromCache(id); if (name != null) { - reNumCache(); + incrementCacheHits(); return Deferred.fromResult(name); } - cache_misses++; + incrementCacheMiss(); class GetNameCB implements Callback<String, String> { public String call(final String name) { if (name == null) { @@ -360,10 +370,10 @@ public byte[] getId(final String name) throws NoSuchUniqueName, HBaseException { public Deferred<byte[]> getIdAsync(final String name) { final byte[] id = getIdFromCache(name); if (id != null) { - reNumCache(); + incrementCacheHits(); return Deferred.fromResult(id); } - cache_misses++; + incrementCacheMiss(); class GetIdCB implements Callback<byte[], byte[]> { public byte[] call(final byte[] id) { if (id == null) { @@ -787,7 +797,7 @@ public Deferred<byte[]> getOrCreateIdAsync(final String name, // Look in the cache first. final byte[] id = getIdFromCache(name); if (id != null) { - reNumCache(); + incrementCacheHits(); return Deferred.fromResult(id); } // Not found in our cache, so look in HBase instead. From c2a2d9e40f29954f5d2011da7d16bb3b69509b44 Mon Sep 17 00:00:00 2001 From: xiayang <xiayang@jd.com> Date: Mon, 21 May 2018 16:01:53 -0700 Subject: [PATCH 706/826] Change cache-hits data type from int to long and fix spilling. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/TSDB.java | 6 +++--- src/uid/UniqueId.java | 30 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index e2ed27380a..119c207a24 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -729,19 +729,19 @@ public Deferred<ArrayList<Object>> checkNecessaryTablesExist() { } /** Number of cache hits during lookups involving UIDs. */ - public int uidCacheHits() { + public long uidCacheHits() { return (metrics.cacheHits() + tag_names.cacheHits() + tag_values.cacheHits()); } /** Number of cache misses during lookups involving UIDs. */ - public int uidCacheMisses() { + public long uidCacheMisses() { return (metrics.cacheMisses() + tag_names.cacheMisses() + tag_values.cacheMisses()); } /** Number of cache entries currently in RAM for lookups involving UIDs. */ - public int uidCacheSize() { + public long uidCacheSize() { return (metrics.cacheSize() + tag_names.cacheSize() + tag_values.cacheSize()); } diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index 35b9c6ec0a..f4119ef763 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -87,6 +87,8 @@ public enum UniqueIdType { private static final short INITIAL_EXP_BACKOFF_DELAY = 800; /** Maximum number of results to return in suggest(). */ private static final short MAX_SUGGESTIONS = 25; + /** Maximum number of cache_hits. */ + private static final long MAX_CACHE_SIZE = 2000000000L; /** HBase client to use. */ private final HBaseClient client; @@ -121,9 +123,9 @@ public enum UniqueIdType { Collections.synchronizedSet(new HashSet<String>()); /** Number of times we avoided reading from HBase thanks to the cache. */ - private volatile int cache_hits; + private volatile long cache_hits; /** Number of times we had to read from HBase and populate the cache. */ - private volatile int cache_misses; + private volatile long cache_misses; /** How many times we collided with an existing ID when attempting to * generate a new UID */ private volatile int random_id_collisions; @@ -236,23 +238,35 @@ public UniqueId(final TSDB tsdb, final byte[] table, final String kind, } /** The number of times we avoided reading from HBase thanks to the cache. */ - public int cacheHits() { + public long cacheHits() { return cache_hits; } /** The number of times we had to read from HBase and populate the cache. */ - public int cacheMisses() { + public long cacheMisses() { return cache_misses; } /** Returns the number of elements stored in the internal cache. */ - public int cacheSize() { + public long cacheSize() { if (use_lru) { return (int) (lru_name_cache.size() + lru_id_cache.size()); } return name_cache.size() + id_cache.size(); } + /** + * Due to the var cache_hits type is int, but the max of int is + * 2147483648, and int happen spilling + */ + private void reNumCache() { + if (cache_hits > MAX_CACHE_SIZE) { + cache_hits = 1; + } else { + cache_hits++; + } + } + /** Returns the number of random UID collisions */ public int randomIdCollisions() { return random_id_collisions; @@ -343,7 +357,7 @@ public Deferred<String> getNameAsync(final byte[] id) { } final String name = getNameFromCache(id); if (name != null) { - cache_hits++; + reNumCache(); return Deferred.fromResult(name); } cache_misses++; @@ -416,7 +430,7 @@ public byte[] getId(final String name) throws NoSuchUniqueName, HBaseException { public Deferred<byte[]> getIdAsync(final String name) { final byte[] id = getIdFromCache(name); if (id != null) { - cache_hits++; + reNumCache(); return Deferred.fromResult(id); } cache_misses++; @@ -861,7 +875,7 @@ public Deferred<byte[]> getOrCreateIdAsync(final String name, // Look in the cache first. final byte[] id = getIdFromCache(name); if (id != null) { - cache_hits++; + reNumCache(); return Deferred.fromResult(id); } // Not found in our cache, so look in HBase instead. From 627620b73a6c3d81d4a7121f3011f61b54ff1750 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 16:04:37 -0700 Subject: [PATCH 707/826] Tweak Xiayang's UID cache hit count by adding a miss counter and resetting when we hit Long.MAX_VALUE. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/uid/UniqueId.java | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/uid/UniqueId.java b/src/uid/UniqueId.java index f4119ef763..04eceb324b 100644 --- a/src/uid/UniqueId.java +++ b/src/uid/UniqueId.java @@ -87,8 +87,6 @@ public enum UniqueIdType { private static final short INITIAL_EXP_BACKOFF_DELAY = 800; /** Maximum number of results to return in suggest(). */ private static final short MAX_SUGGESTIONS = 25; - /** Maximum number of cache_hits. */ - private static final long MAX_CACHE_SIZE = 2000000000L; /** HBase client to use. */ private final HBaseClient client; @@ -256,17 +254,29 @@ public long cacheSize() { } /** - * Due to the var cache_hits type is int, but the max of int is - * 2147483648, and int happen spilling + * Resets the cache hits counter before rollover. Note that a few updates + * may be dropped due to race conditions at rollover. */ - private void reNumCache() { - if (cache_hits > MAX_CACHE_SIZE) { + private void incrementCacheHits() { + if (cache_hits >= Long.MAX_VALUE) { cache_hits = 1; } else { cache_hits++; } } + /** + * Resets the cache miss counter before rollover. Note that a few updates + * may be dropped due to race conditions at rollover. + */ + private void incrementCacheMiss() { + if (cache_misses >= Long.MAX_VALUE) { + cache_misses = 1; + } else { + cache_misses++; + } + } + /** Returns the number of random UID collisions */ public int randomIdCollisions() { return random_id_collisions; @@ -357,10 +367,10 @@ public Deferred<String> getNameAsync(final byte[] id) { } final String name = getNameFromCache(id); if (name != null) { - reNumCache(); + incrementCacheHits(); return Deferred.fromResult(name); } - cache_misses++; + incrementCacheMiss(); class GetNameCB implements Callback<String, String> { public String call(final String name) { if (name == null) { @@ -430,10 +440,10 @@ public byte[] getId(final String name) throws NoSuchUniqueName, HBaseException { public Deferred<byte[]> getIdAsync(final String name) { final byte[] id = getIdFromCache(name); if (id != null) { - reNumCache(); + incrementCacheHits(); return Deferred.fromResult(id); } - cache_misses++; + incrementCacheMiss(); class GetIdCB implements Callback<byte[], byte[]> { public byte[] call(final byte[] id) { if (id == null) { @@ -875,7 +885,7 @@ public Deferred<byte[]> getOrCreateIdAsync(final String name, // Look in the cache first. final byte[] id = getIdFromCache(name); if (id != null) { - reNumCache(); + incrementCacheHits(); return Deferred.fromResult(id); } // Not found in our cache, so look in HBase instead. From 26355ac18a2982f17edf773ab7ba711f085ac024 Mon Sep 17 00:00:00 2001 From: xiayang <xiayang@jd.com> Date: Mon, 21 May 2018 16:17:17 -0700 Subject: [PATCH 708/826] Add difference of the first value and the last value for aggregators Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/Aggregators.java | 53 +++++++++++++++++++++++++++++++++++++++ tools/check_tsd | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 9c2992d775..0475490f63 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -82,6 +82,11 @@ public enum Interpolation { public static final Aggregator DEV = new StdDev( Interpolation.LERP, "dev"); + /** Aggregator that returns the difference of the first value and the + * last value in the data points */ + public static final Aggregator DIFF = new Diff( + Interpolation.LERP, "diff"); + /** Sums data points but will cause the SpanGroup to return a 0 if timestamps * don't line up instead of interpolating. */ public static final Aggregator ZIMSUM = new Sum( @@ -174,6 +179,7 @@ public enum Interpolation { aggregators.put("median", MEDIAN); aggregators.put("mult", MULTIPLY); aggregators.put("dev", DEV); + aggregators.put("diff", DIFF); aggregators.put("count", COUNT); aggregators.put("zimsum", ZIMSUM); aggregators.put("mimmin", MIMMIN); @@ -526,6 +532,53 @@ public double runDouble(final Doubles values) { } + /** + * Difference of the first value and the last value in multi values aggregator. + */ + private static final class Diff extends net.opentsdb.core.Aggregator { + public Diff(final Interpolation method, final String name) { + super(method, name); + } + + @Override + public long runLong(final Longs values) { + long first_mean = values.nextLongValue(); + + if (!values.hasNextValue()) { + return 0; + } + + long last_mean = 0; + do { + last_mean = values.nextLongValue(); + } while (values.hasNextValue()); + + return last_mean - first_mean; + } + + @Override + public double runDouble(final Doubles values) { + double first_mean = values.nextDoubleValue(); + while (Double.isNaN(first_mean) && values.hasNextValue()) { + first_mean = values.nextDoubleValue(); + } + + if (Double.isNaN(first_mean)) { + return Double.NaN; + } + if (!values.hasNextValue()) { + return 0.; + } + + double last_mean = 0.; + do { + last_mean = values.nextDoubleValue(); + } while (values.hasNextValue()); + + return last_mean - first_mean; + } + } + private static final class Count extends Aggregator { public Count(final Interpolation method, final String name) { super(method, name); diff --git a/tools/check_tsd b/tools/check_tsd index db888b82fa..585e56827d 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -29,7 +29,7 @@ import sys import time from optparse import OptionParser -AGGREGATORS = ('avg', 'count', 'dev', +AGGREGATORS = ('avg', 'count', 'dev', 'diff', 'ep50r3', 'ep50r7', 'ep75r3', 'ep75r7', 'ep90r3', 'ep90r7', 'ep95r3', 'ep95r7', 'ep99r3', 'ep99r7', 'ep999r3', 'ep999r7', 'mimmin', 'mimmax', 'min', 'max', 'none', From 522d52aefa91294c242f46a66b369dab861dbfee Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 16:59:45 -0700 Subject: [PATCH 709/826] Revert "Tags via custom HTTP header with Unit Test - provide some authentication (#1003)" We'll put this in 2.4, not 2.3 This reverts commit 7ce7ecc7c349371ca1dc71c250966fc924f776d4. --- src/core/IncomingDataPoint.java | 5 ----- src/tsd/AbstractHttpQuery.java | 10 ---------- src/tsd/PutDataPointRpc.java | 18 ------------------ src/utils/Config.java | 17 ----------------- 4 files changed, 50 deletions(-) diff --git a/src/core/IncomingDataPoint.java b/src/core/IncomingDataPoint.java index 2d82ea2985..dced8077ef 100644 --- a/src/core/IncomingDataPoint.java +++ b/src/core/IncomingDataPoint.java @@ -128,11 +128,6 @@ public final String getTSUID() { return tsuid; } - /** @param moretags the hashmap of kv pair to add */ - public final void addTags(HashMap<String, String> moretags) { - this.tags.putAll(moretags); - } - /** @param metric the metric to set */ public final void setMetric(String metric) { this.metric = metric; diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index 8669e5ee14..01a36c4856 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -165,16 +165,6 @@ public Map<String, String> getHeaders() { return headers; } - /** - * Return the value of the given HTTP Header - * first match wins - * @return Header value as string - */ - public String getHeaderValue(final String headerName) { - if (headerName == null) { return null; } - return request.headers().get(headerName); - } - /** @param stats The stats object to mark after writing is complete */ public void setStats(final QueryStats stats) { this.stats = stats; diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index e4c7c602d1..71cd8ecc5d 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -124,7 +124,6 @@ public void execute(final TSDB tsdb, final HttpQuery query) throw new BadRequestException("No datapoints found in content"); } - final HashMap<String, String> query_tags = new HashMap<String, String>(); final boolean show_details = query.hasQueryStringParam("details"); final boolean show_summary = query.hasQueryStringParam("summary"); final boolean synchronous = query.hasQueryStringParam("sync"); @@ -139,18 +138,6 @@ public void execute(final TSDB tsdb, final HttpQuery query) int queued = 0; final List<Deferred<Boolean>> deferreds = synchronous ? new ArrayList<Deferred<Boolean>>(dps.size()) : null; - - if (tsdb.getConfig().enable_header_tag()) { - LOG.debug("Looking for tag header " + tsdb.getConfig().get_name_header_tag()); - final String header_tag_value = query.getHeaderValue(tsdb.getConfig().get_name_header_tag()) ; - if (header_tag_value != null) { - LOG.debug(" header found with value:" + header_tag_value); - Tags.parse(query_tags, header_tag_value); - } else { - LOG.debug(" no such header in request"); - } - } - for (final IncomingDataPoint dp : dps) { /** Handles passing a data point to the storage exception handler if @@ -183,11 +170,6 @@ public String toString() { } try { - /** Add additionnal tags from HTTP header */ - if ( (query_tags != null) && (query_tags.size() > 0) ) { - dp.addTags(query_tags); - } - if (dp.getMetric() == null || dp.getMetric().isEmpty()) { if (show_details) { details.add(this.getHttpDetails("Metric name was empty", dp)); diff --git a/src/utils/Config.java b/src/utils/Config.java index cfcf278107..782bf36430 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -97,9 +97,6 @@ public class Config { /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; - /** tsd.http.header_tag */ - private String http_header_tag = null; - /** tsd.http.request.max_chunk */ private int max_chunked_requests = 4096; @@ -231,16 +228,6 @@ public int scanner_maxNumRows() { return scanner_max_num_rows; } - /** @return whether or not additional http header tag is allowed */ - public boolean enable_header_tag() { - return http_header_tag != null ; - } - - /** @return the lookup value for additional http header tag */ - public String get_name_header_tag() { - return http_header_tag ; - } - /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { return enable_chunked_requests; @@ -548,7 +535,6 @@ protected void setDefaults() { default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.query.allow_delete", "false"); - default_map.put("tsd.http.header_tag", ""); default_map.put("tsd.http.request.enable_chunked", "false"); default_map.put("tsd.http.request.max_chunk", "4096"); default_map.put("tsd.http.request.cors_domains", ""); @@ -666,9 +652,6 @@ protected void loadStaticVariables() { if (this.hasProperty("tsd.http.request.max_chunk")) { max_chunked_requests = this.getInt("tsd.http.request.max_chunk"); } - if (this.hasProperty("tsd.http.header_tag")) { - http_header_tag = this.getString("tsd.http.header_tag"); - } enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); From e1937d3b0f52c7c1607974810990ef6ac18d6550 Mon Sep 17 00:00:00 2001 From: bhourlier <bhourlier@acipia.fr> Date: Tue, 8 May 2018 18:08:54 +0200 Subject: [PATCH 710/826] Tags via custom HTTP header with Unit Test - provide some authentication (#1003) * Allow tags to be added via custom HTTP header * Add unit test for custom http header * Correction of an error caused by a bad file name in makefile * Suppression of modifications for test travis compilation Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/IncomingDataPoint.java | 5 +++++ src/tsd/AbstractHttpQuery.java | 10 ++++++++++ src/tsd/PutDataPointRpc.java | 20 +++++++++++++++++++- src/utils/Config.java | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/core/IncomingDataPoint.java b/src/core/IncomingDataPoint.java index ca17f01721..df25306b99 100644 --- a/src/core/IncomingDataPoint.java +++ b/src/core/IncomingDataPoint.java @@ -136,6 +136,11 @@ public final String getTSUID() { return tsuid; } + /** @param moretags the hashmap of kv pair to add */ + public final void addTags(HashMap<String, String> moretags) { + this.tags.putAll(moretags); + } + /** @param metric the metric to set */ public final void setMetric(String metric) { this.metric = metric; diff --git a/src/tsd/AbstractHttpQuery.java b/src/tsd/AbstractHttpQuery.java index d7ccc54775..388127e9d6 100644 --- a/src/tsd/AbstractHttpQuery.java +++ b/src/tsd/AbstractHttpQuery.java @@ -167,6 +167,16 @@ public Map<String, String> getHeaders() { return headers; } + /** + * Return the value of the given HTTP Header + * first match wins + * @return Header value as string + */ + public String getHeaderValue(final String headerName) { + if (headerName == null) { return null; } + return request.headers().get(headerName); + } + /** @param stats The stats object to mark after writing is complete */ public void setStats(final QueryStats stats) { this.stats = stats; diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 29e77f34e6..ec2a8093cf 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -312,6 +312,7 @@ public <T extends IncomingDataPoint> void processDataPoint(final TSDB tsdb, throw new BadRequestException("No datapoints found in content"); } + final HashMap<String, String> query_tags = new HashMap<String, String>(); final boolean show_details = query.hasQueryStringParam("details"); final boolean show_summary = query.hasQueryStringParam("summary"); final boolean synchronous = query.hasQueryStringParam("sync"); @@ -326,7 +327,18 @@ public <T extends IncomingDataPoint> void processDataPoint(final TSDB tsdb, int queued = 0; final List<Deferred<Boolean>> deferreds = synchronous ? new ArrayList<Deferred<Boolean>>(dps.size()) : null; - + + if (tsdb.getConfig().enable_header_tag()) { + LOG.debug("Looking for tag header " + tsdb.getConfig().get_name_header_tag()); + final String header_tag_value = query.getHeaderValue(tsdb.getConfig().get_name_header_tag()) ; + if (header_tag_value != null) { + LOG.debug(" header found with value:" + header_tag_value); + Tags.parse(query_tags, header_tag_value); + } else { + LOG.debug(" no such header in request"); + } + } + for (final IncomingDataPoint dp : dps) { final DataPointType type; if (dp instanceof RollUpDataPoint) { @@ -387,10 +399,16 @@ public Boolean call(final Object obj) { } try { + /** Add additionnal tags from HTTP header */ + if ( (query_tags != null) && (query_tags.size() > 0) ) { + dp.addTags(query_tags); + } + if (!dp.validate(details)) { illegal_arguments.incrementAndGet(); continue; } + // TODO - refactor the add calls someday or move some of this into the // actual data point class. final Deferred<Boolean> deferred; diff --git a/src/utils/Config.java b/src/utils/Config.java index 0e791e8968..f92f2f5b56 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -97,6 +97,9 @@ public class Config { /** tsd.storage.fix_duplicates */ private boolean fix_duplicates = false; + /** tsd.http.header_tag */ + private String http_header_tag = null; + /** tsd.http.request.max_chunk */ private int max_chunked_requests = 4096; @@ -253,6 +256,16 @@ public int scanner_maxNumRows() { return scanner_max_num_rows; } + /** @return whether or not additional http header tag is allowed */ + public boolean enable_header_tag() { + return http_header_tag != null ; + } + + /** @return the lookup value for additional http header tag */ + public String get_name_header_tag() { + return http_header_tag ; + } + /** @return whether or not chunked requests are supported */ public boolean enable_chunked_requests() { return enable_chunked_requests; @@ -597,6 +610,7 @@ protected void setDefaults() { default_map.put("tsd.core.stats_with_port", "false"); default_map.put("tsd.http.show_stack_trace", "true"); default_map.put("tsd.http.query.allow_delete", "false"); + default_map.put("tsd.http.header_tag", ""); default_map.put("tsd.http.request.enable_chunked", "false"); default_map.put("tsd.http.request.max_chunk", "4096"); default_map.put("tsd.http.request.cors_domains", ""); @@ -717,6 +731,9 @@ public void loadStaticVariables() { if (this.hasProperty("tsd.http.request.max_chunk")) { max_chunked_requests = this.getInt("tsd.http.request.max_chunk"); } + if (this.hasProperty("tsd.http.header_tag")) { + http_header_tag = this.getString("tsd.http.header_tag"); + } enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing"); fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates"); scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows"); From 14783808b91b38586140da151f8421b62f3c436d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 17:21:39 -0700 Subject: [PATCH 711/826] Cut 2.3.1 Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- THANKS | 11 ++++++++++- configure.ac | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/THANKS b/THANKS index e7262c951e..716add80e9 100644 --- a/THANKS +++ b/THANKS @@ -7,7 +7,7 @@ complete and free from errors. Also see the AUTHORS file for the list of people and organizations with contributions significant enough to warrant copyright assignment. - +Adrian Goll Adrian Muraru <amuraru@adobe.com> Adrien Mogenet <adrien.mogenet@gmail.com> Alex Ioffe <deusaquilus@gmail.com> @@ -39,6 +39,7 @@ Hari Krishna Dara Hong Dai Thanh Hugo M Fernandes Hugo Trippaers <opensource@strocamp.net> +Ioanszilgyi Isaiah Choe Ivan Babrou Jacek Masiulaniec <jacek.masiulaniec@gmail.com> @@ -47,12 +48,14 @@ James Royalty Jan Mangs <jmangs@gmail.com> Jason Harvey <alienth@gmail.com> Jim Scott <jscott@mapr.com> +Jeffery Lim Jesse Chang <jesse.chang.2@gmail.com> Jim Westfall Johan Zeeck <johan.zeeck@tre.se> Johannes Meixner Jonathan Works <jonathan.works@threattrack.com> Josh Thomas <josh@kickbackpoints.com> +Jsbali Kevin Bowling Kevin Landreth Kieren Hynd <kieren.hynd@ticketmaster.co.uk> @@ -65,6 +68,7 @@ Liangliang He <heliangliang@xiaomi.com> Liu Yubao Loïs Burg <burg.lois@gmail.com> Lou Yunlong <lou.0211@gmail.com> +Marcin Januszkiewicz Matt Jibson <matt.jibson@gmail.com> Matt Schallert <mattschallert@gmail.com> Marc Tamsky @@ -76,8 +80,10 @@ Mike Bryant <mike@mikebryant.me.uk> Mike Kobyakov <mkobyakov@cyngn.com> Nathan Owens Nicole Nagele <nicole.nagele@uni-ak.ac.at> +Neil Fordyce Nikhil Benesch <me@designbynikhil.com> Nitin Aggarwal +Opsun Paula Keezer <paula.keezer@gmail.com> Peter Edwards Peter Gotz <peter.s.goetz@googlemail.com> @@ -90,6 +96,7 @@ Sean Miller Siddartha Guthikonda <siddartha.gu@gmail.com> Simon Matic Langford <simon@exemel.co.uk> Slawek Ligus <root@ooz.ie> +Suman Newton Sy Le <synle@synle.com> Tay Ray Chuan <raychuan@iweb.nus.edu.sg> Thomas Krajca <t.l.krajca@gmail.com> @@ -100,5 +107,7 @@ Tony Landells <tony.landells@gmail.com> Utkarsh Bhatnagar Vasiliy Kiryanov <vasiliy.kiryanov@gmail.com> Vitaliy Fuks +William Kronmiller +Xiayang Yulai Fu Zachary Kurey \ No newline at end of file diff --git a/configure.ac b/configure.ac index 5efc9c1da5..b5b3a3f435 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.3.0], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.3.1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From eca1ef413bfde7cde5450e093d544118e0877ef0 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 17:36:34 -0700 Subject: [PATCH 712/826] Missed the 2.3.1 release notes... damn. --- NEWS | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/NEWS b/NEWS index e2e4426532..beb38949ec 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,33 @@ OpenTSDB - User visible changes. +* Version 2.3.1 (2018-04-21) + +Noteworthy Changes: + - When setting up aggregators, advance to the first data point equal to or greater + than the query start timestamp. This helps with calendar downsampling intervals. + - Add support to the Nagios check script for downsampling fill policies. + +Bug Fixes: + - Fix expression calculation by avoiding double execution and checking both + output types for boolean values. + - Fixing missing tools scripts in builds. + - Default HBase 1.2.5 in the OSX install script + - Upgrade AsyncBigtable to 0.3.1 + - Log query stats when a channel is closed unexpectedly. + - Add the Java 8 path in the debian init script and remove Java 6. + - Pass the column family name to the get requests in the compaction scheduler. + - Fix a comparison issue in the UI on group by tags. + - Filter annotation queries by the starting timestamp, excluding those in a row that + began before the query start time. + - Tiny stap at purging backticks from Gnuplot scripts. + - Remove the `final` annotation from the meta classes so they can be extended. + - Fix the javacc maven plugin version. + - Fix the literal or filter to allow single character filters. + - Fix query start stats logging to use the ms instead of nano time. + - Move Jackson and Netty to newer versions for security reasons. + - Upgrade to AsyncHBase 1.8.2 for compatibility with HBase 1.3 and 2.0 + - Fix the Highest Current calculation to handle empty time series. + - Change the cache hits counters to longs. * Version 2.3.0 (2016-12-31) From d8658f0e7c20b9d5d593bac9a05aa8b3036350f1 Mon Sep 17 00:00:00 2001 From: John Ewing <j.ewing@talk21.com> Date: Mon, 10 Sep 2018 23:22:53 +0100 Subject: [PATCH 713/826] Add missing source files for authentication features to Makefile.am to repair build (#1315) --- Makefile.am | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile.am b/Makefile.am index bb87d585cc..d48d0a0134 100644 --- a/Makefile.am +++ b/Makefile.am @@ -93,10 +93,13 @@ tsdb_SRC := \ src/core/WritableDataPoints.java \ src/core/WriteableDataPointFilterPlugin.java \ src/graph/Plot.java \ - src/auth/AuthenticationChannelHandler.java \ - src/auth/Authentication.java \ - src/auth/Authorization.java \ - src/auth/AuthState.java \ + src/auth/AllowAllAuthenticatingAuthorizer.java \ + src/auth/AuthenticationChannelHandler.java \ + src/auth/Authentication.java \ + src/auth/Authorization.java \ + src/auth/AuthState.java \ + src/auth/Permissions.java \ + src/auth/Roles.java \ src/meta/Annotation.java \ src/meta/MetaDataCache.java \ src/meta/TSMeta.java \ From e4da2c04846b752961d79ab520bbe027e0fdaea1 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Tue, 30 Oct 2018 09:27:08 -0600 Subject: [PATCH 714/826] OpenTSDB repair wrapper (#1391) Initial commit of metric-by-metric fsck repair tool --- tools/repair-tsd | 206 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100755 tools/repair-tsd diff --git a/tools/repair-tsd b/tools/repair-tsd new file mode 100755 index 0000000000..1084b009fb --- /dev/null +++ b/tools/repair-tsd @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +from subprocess import Popen, PIPE, TimeoutExpired, check_output +from random import shuffle +import time +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +import logging +import pprint + +log = logging.getLogger("repair-tsd") +log.setLevel(logging.INFO) +ch = logging.StreamHandler() +logformat = '%(asctime)s %(name)s %(levelname)s %(message)s' +formatter = logging.Formatter(logformat) +ch.setFormatter(formatter) +log.addHandler(ch) + + +class TSDRepair(object): + def __init__(self, args): + self.time_chunk = args.get("time_chunk", 15) + self.timeout = int(self.time_chunk * 60) + self.retries = args.get("retries", 1) + self.multiplier = int(60 / self.time_chunk) + self.time_range = args.get("time_range", 48) + self.chunk_count = self.time_range * self.multiplier + self.tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") + self.cfg_path = args.get("cfg_path", "/etc/opentsdb/opentsdb.conf") + self.use_sudo = args.get("use_sudo", False) + self.sudo_user = args.get("sudo_user", "opentsdb") + self.log = logging.getLogger("repair-tsd") + self.base = "{} fsck --config={}".format(self.tsd_path, self.cfg_path) + self.check_cmd = "{} uid --config={} metrics".format(self.tsd_path, self.cfg_path) + if self.use_sudo: + self.base = "sudo -u {} {}".format(self.sudo_user, self.base) + self.check_cmd = "sudo -u {} {}".format(self.sudo_user, self.check_cmd) + + def _get_metrics(self): + """ + Collect all metrics from OpenTSDB + + :returns: all metrics + :rtype: list + """ + try: + self.store_path = args.get('store_path', '/tmp/opentsdb.list') + with open(self.store_path, 'r') as f_in: + finished_metrics = [m for m in f_in.read().split('\n') if m] + except Exception: + finished_metrics = [] + cmd = '{} uid --config={} grep metrics ".*"'.format(self.tsd_path, + self.cfg_path) + proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + results = proc.communicate() + metrics = [m.split(" ")[1].strip(":") + for m in results[0].decode().split("\n") if m] + metrics = [m for m in metrics if m and m != "\x00" and + m not in finished_metrics] + shuffle(metrics) + self.log.info("There are {} metrics to process".format(len(metrics))) + return metrics + + def _repair_metric_chunk(self, metric, chunk): + """ + Repair one 'chunk' of data for a metric + """ + self.log.debug("Running chunk {} for {}".format(chunk, metric)) + if chunk < 2: + timestr = "{}m-ago".format(self.time_chunk) + else: + timestr = "{}m-ago {}m-ago".format((chunk + 1) * self.time_chunk, + chunk * self.time_chunk) + cmd = "{} {} sum".format(self.base, timestr) + """ + Even though we're chunking, it's worth trying things more than once + """ + for x in range(1, self.retries + 2): + self.log.debug("Repair try {} for {}".format(x, timestr)) + fullcmd = "{} {} --fix-all --compact".format(cmd, metric) + self.log.debug("Full command: {}".format(fullcmd)) + metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) + try: + results, err = metricproc.communicate(timeout=self.timeout) + except TimeoutExpired: + self.log.debug("{} failed to complete in window (run {})".format(metric, x)) + continue + except Exception as e: + self.log.error("{} general exception :: {}".format(metric, + e)) + else: + results = [r for r in results.decode().split("\n") if r][-26:] + final_results = [] + """ + We'll only collect results that are non-0 + since we're not super interested in stuff that didn't change. + """ + for r in results: + # Strip the timestamp from the log line + line = r.split(" ")[6:] + try: + if int(line[-1]) != 0: + final_results.append(" ".join(line)) + except Exception: + final_results.append(" ".join(line)) + result_str = "\n".join(final_results) + self.log.debug("{} results:\n{}".format(metric, result_str)) + if chunk % 20 == 0: + self.log.info("Chunk {} of {} finished".format(chunk, self.chunk_count)) + else: + self.log.debug("Chunk {} of {} finished".format(chunk, self.chunk_count)) + try: + with open(self.store_path, 'a') as f_out: + f_out.write("{}\n".format(metric)) + except Exception: + pass + return None + else: + self.log.error("Failed to completely repair {}".format(metric)) + return metric + + def process_metrics(self): + """ + Run fsck on a list of metrics over a time range + """ + failed_metrics = [] + metrics = self._get_metrics() + for index, metric in enumerate(metrics): + try: + check_output("{} {}".format(self.check_cmd, metric), + shell=True) + except Exception: + log.warning("{} doesn't exist! Skipping...".format(metric)) + continue + logline = "{} ({} of {})".format(metric, index + 1, len(metrics)) + logline += " ({} failed) in {} chunks".format(len(failed_metrics), + self.chunk_count) + self.log.info(logline) + start_time = time.time() + start_time_min = int(start_time//60 * 60) + failed_metrics = [self._repair_metric_chunk(metric, x) + for x in range(1, self.chunk_count + 1)] + failed_metrics = [m for m in failed_metrics if m] + runtime = time.time() - start_time + self.log.info("{} repair took {} seconds".format(metric, + int(runtime))) + self.log.info("Failed metrics: {}".format(failed_metrics)) + return failed_metrics + + +def cli_opts(): + parser = ArgumentParser(description="Repair all OpenTSDB metrics", + formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument("--debug", action="store_true", default=False, + help="Show debug information") + parser.add_argument("--time-range", default="48", + help="How many hours of time we collect to repair") + parser.add_argument("--time-chunk", default="15", + help="How many minutes of data to scan per chunk") + parser.add_argument("--retries", default="1", + help="How many times we should try failed metrics") + parser.add_argument("--tsd-path", default="/usr/share/opentsdb/bin/tsdb", + 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="/opentsdb-fsck.list", + help="Path to OpenTSDB config") + parser.add_argument("--use-sudo", action="store_true", + default=False, + help="switch user when running repairs?") + parser.add_argument("--sudo-user", default="opentsdb", + help="User to switch to...") + return parser.parse_args() + + +def main(): + args = cli_opts() + if args.debug: + log.setLevel(logging.DEBUG) + try: + time_range = int(args.time_range) + except Exception as e: + log.error("Invalid time range {} :: {}".format(args.time_range, e)) + try: + retries = int(args.retries) + except Exception as e: + log.error("Invalid retry number {} :: {}".format(args.retries, e)) + try: + time_chunk = int(args.time_chunk) + if 60 % time_chunk != 0: + raise ArithmeticError + except Exception as e: + log.error("Invalid time chunk {} :: {}".format(args.retries, e)) + + repair_tool = TSDRepair({"time_range": time_range, + "use_sudo": args.use_sudo, + "sudo_user": args.sudo_user, + "time_chunk": time_chunk, + "tsd_path": args.tsd_path, + "cfg_path": args.cfg_path, + "store_path": args.store_path, + "retries": retries}) + repair_tool.process_metrics() + + +if __name__ == "__main__": + main() From 481282b8d32d644b34c9f4b4d1424c05167c7177 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 21 May 2018 17:11:40 -0700 Subject: [PATCH 715/826] Log the http header to tag extraction only if debug is enabled. --- src/tsd/PutDataPointRpc.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index ec2a8093cf..fbb4ccdb7b 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -329,12 +329,18 @@ public <T extends IncomingDataPoint> void processDataPoint(final TSDB tsdb, new ArrayList<Deferred<Boolean>>(dps.size()) : null; if (tsdb.getConfig().enable_header_tag()) { - LOG.debug("Looking for tag header " + tsdb.getConfig().get_name_header_tag()); - final String header_tag_value = query.getHeaderValue(tsdb.getConfig().get_name_header_tag()) ; + if (LOG.isDebugEnabled()) { + LOG.debug("Looking for tag header " + + tsdb.getConfig().get_name_header_tag()); + } + final String header_tag_value = query.getHeaderValue( + tsdb.getConfig().get_name_header_tag()) ; if (header_tag_value != null) { - LOG.debug(" header found with value:" + header_tag_value); + if (LOG.isDebugEnabled()) { + LOG.debug(" header found with value:" + header_tag_value); + } Tags.parse(query_tags, header_tag_value); - } else { + } else if (LOG.isDebugEnabled()) { LOG.debug(" no such header in request"); } } From 5fda8f32810056ee1400562dbb38f8cc8a39c1dc Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Tue, 30 Oct 2018 09:27:08 -0600 Subject: [PATCH 716/826] OpenTSDB repair wrapper (#1391) Initial commit of metric-by-metric fsck repair tool --- tools/repair-tsd | 206 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100755 tools/repair-tsd diff --git a/tools/repair-tsd b/tools/repair-tsd new file mode 100755 index 0000000000..1084b009fb --- /dev/null +++ b/tools/repair-tsd @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +from subprocess import Popen, PIPE, TimeoutExpired, check_output +from random import shuffle +import time +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +import logging +import pprint + +log = logging.getLogger("repair-tsd") +log.setLevel(logging.INFO) +ch = logging.StreamHandler() +logformat = '%(asctime)s %(name)s %(levelname)s %(message)s' +formatter = logging.Formatter(logformat) +ch.setFormatter(formatter) +log.addHandler(ch) + + +class TSDRepair(object): + def __init__(self, args): + self.time_chunk = args.get("time_chunk", 15) + self.timeout = int(self.time_chunk * 60) + self.retries = args.get("retries", 1) + self.multiplier = int(60 / self.time_chunk) + self.time_range = args.get("time_range", 48) + self.chunk_count = self.time_range * self.multiplier + self.tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") + self.cfg_path = args.get("cfg_path", "/etc/opentsdb/opentsdb.conf") + self.use_sudo = args.get("use_sudo", False) + self.sudo_user = args.get("sudo_user", "opentsdb") + self.log = logging.getLogger("repair-tsd") + self.base = "{} fsck --config={}".format(self.tsd_path, self.cfg_path) + self.check_cmd = "{} uid --config={} metrics".format(self.tsd_path, self.cfg_path) + if self.use_sudo: + self.base = "sudo -u {} {}".format(self.sudo_user, self.base) + self.check_cmd = "sudo -u {} {}".format(self.sudo_user, self.check_cmd) + + def _get_metrics(self): + """ + Collect all metrics from OpenTSDB + + :returns: all metrics + :rtype: list + """ + try: + self.store_path = args.get('store_path', '/tmp/opentsdb.list') + with open(self.store_path, 'r') as f_in: + finished_metrics = [m for m in f_in.read().split('\n') if m] + except Exception: + finished_metrics = [] + cmd = '{} uid --config={} grep metrics ".*"'.format(self.tsd_path, + self.cfg_path) + proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + results = proc.communicate() + metrics = [m.split(" ")[1].strip(":") + for m in results[0].decode().split("\n") if m] + metrics = [m for m in metrics if m and m != "\x00" and + m not in finished_metrics] + shuffle(metrics) + self.log.info("There are {} metrics to process".format(len(metrics))) + return metrics + + def _repair_metric_chunk(self, metric, chunk): + """ + Repair one 'chunk' of data for a metric + """ + self.log.debug("Running chunk {} for {}".format(chunk, metric)) + if chunk < 2: + timestr = "{}m-ago".format(self.time_chunk) + else: + timestr = "{}m-ago {}m-ago".format((chunk + 1) * self.time_chunk, + chunk * self.time_chunk) + cmd = "{} {} sum".format(self.base, timestr) + """ + Even though we're chunking, it's worth trying things more than once + """ + for x in range(1, self.retries + 2): + self.log.debug("Repair try {} for {}".format(x, timestr)) + fullcmd = "{} {} --fix-all --compact".format(cmd, metric) + self.log.debug("Full command: {}".format(fullcmd)) + metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) + try: + results, err = metricproc.communicate(timeout=self.timeout) + except TimeoutExpired: + self.log.debug("{} failed to complete in window (run {})".format(metric, x)) + continue + except Exception as e: + self.log.error("{} general exception :: {}".format(metric, + e)) + else: + results = [r for r in results.decode().split("\n") if r][-26:] + final_results = [] + """ + We'll only collect results that are non-0 + since we're not super interested in stuff that didn't change. + """ + for r in results: + # Strip the timestamp from the log line + line = r.split(" ")[6:] + try: + if int(line[-1]) != 0: + final_results.append(" ".join(line)) + except Exception: + final_results.append(" ".join(line)) + result_str = "\n".join(final_results) + self.log.debug("{} results:\n{}".format(metric, result_str)) + if chunk % 20 == 0: + self.log.info("Chunk {} of {} finished".format(chunk, self.chunk_count)) + else: + self.log.debug("Chunk {} of {} finished".format(chunk, self.chunk_count)) + try: + with open(self.store_path, 'a') as f_out: + f_out.write("{}\n".format(metric)) + except Exception: + pass + return None + else: + self.log.error("Failed to completely repair {}".format(metric)) + return metric + + def process_metrics(self): + """ + Run fsck on a list of metrics over a time range + """ + failed_metrics = [] + metrics = self._get_metrics() + for index, metric in enumerate(metrics): + try: + check_output("{} {}".format(self.check_cmd, metric), + shell=True) + except Exception: + log.warning("{} doesn't exist! Skipping...".format(metric)) + continue + logline = "{} ({} of {})".format(metric, index + 1, len(metrics)) + logline += " ({} failed) in {} chunks".format(len(failed_metrics), + self.chunk_count) + self.log.info(logline) + start_time = time.time() + start_time_min = int(start_time//60 * 60) + failed_metrics = [self._repair_metric_chunk(metric, x) + for x in range(1, self.chunk_count + 1)] + failed_metrics = [m for m in failed_metrics if m] + runtime = time.time() - start_time + self.log.info("{} repair took {} seconds".format(metric, + int(runtime))) + self.log.info("Failed metrics: {}".format(failed_metrics)) + return failed_metrics + + +def cli_opts(): + parser = ArgumentParser(description="Repair all OpenTSDB metrics", + formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument("--debug", action="store_true", default=False, + help="Show debug information") + parser.add_argument("--time-range", default="48", + help="How many hours of time we collect to repair") + parser.add_argument("--time-chunk", default="15", + help="How many minutes of data to scan per chunk") + parser.add_argument("--retries", default="1", + help="How many times we should try failed metrics") + parser.add_argument("--tsd-path", default="/usr/share/opentsdb/bin/tsdb", + 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="/opentsdb-fsck.list", + help="Path to OpenTSDB config") + parser.add_argument("--use-sudo", action="store_true", + default=False, + help="switch user when running repairs?") + parser.add_argument("--sudo-user", default="opentsdb", + help="User to switch to...") + return parser.parse_args() + + +def main(): + args = cli_opts() + if args.debug: + log.setLevel(logging.DEBUG) + try: + time_range = int(args.time_range) + except Exception as e: + log.error("Invalid time range {} :: {}".format(args.time_range, e)) + try: + retries = int(args.retries) + except Exception as e: + log.error("Invalid retry number {} :: {}".format(args.retries, e)) + try: + time_chunk = int(args.time_chunk) + if 60 % time_chunk != 0: + raise ArithmeticError + except Exception as e: + log.error("Invalid time chunk {} :: {}".format(args.retries, e)) + + repair_tool = TSDRepair({"time_range": time_range, + "use_sudo": args.use_sudo, + "sudo_user": args.sudo_user, + "time_chunk": time_chunk, + "tsd_path": args.tsd_path, + "cfg_path": args.cfg_path, + "store_path": args.store_path, + "retries": retries}) + repair_tool.process_metrics() + + +if __name__ == "__main__": + main() From 26cc90503c8f2755e8fef1e02f7ac59517c1599e Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Thu, 22 Nov 2018 03:31:46 +0800 Subject: [PATCH 717/826] Bugfix of NoSuchUniqueId exception occurred in DeferredGroup when resolving tags by ids. (#1410) Signed-off-by: clarsen <clarsen@yahoo-inc.com> --- src/core/Tags.java | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index ff1a4b2125..e884e25e58 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.Map; +import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.utils.Exceptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -353,7 +355,7 @@ static String getValue(final TSDB tsdb, final byte[] row, * Extracts the value ID of the given tag UD name from the given row key. * @param tsdb The TSDB instance to use for UniqueId lookups. * @param row The row key in which to search the tag name. - * @param name The name of the tag to search in the row key. + * @param tag_id The name of the tag to search in the row key. * @return The value ID associated with the given tag ID, or null if this * tag ID isn't present in this row key. */ @@ -404,7 +406,14 @@ static Map<String, String> getTags(final TSDB tsdb, final byte[] row) throws NoSuchUniqueId { try { return getTagsAsync(tsdb, row).joinUninterruptibly(); - } catch (RuntimeException e) { + } catch (DeferredGroupException e) { + final Throwable ex = Exceptions.getCause(e); + if (ex instanceof NoSuchUniqueId) { + throw (NoSuchUniqueId)ex; + } + + throw new RuntimeException("Should never be here", e); + } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("Should never be here", e); @@ -734,6 +743,14 @@ public static HashMap<String, String> resolveIds(final TSDB tsdb, return resolveIdsAsync(tsdb, tags).joinUninterruptibly(); } catch (NoSuchUniqueId e) { throw e; + } catch (DeferredGroupException e) { + final Throwable ex = Exceptions.getCause(e); + if (ex instanceof NoSuchUniqueId) { + throw (NoSuchUniqueId)ex; + } + // TODO process e.results() + + throw new RuntimeException("Shouldn't be here", e); } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } From f8ffbdd71ce4b316e9fad4e9ecb0e9ec77f5be2e Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Thu, 22 Nov 2018 03:31:46 +0800 Subject: [PATCH 718/826] Bugfix of NoSuchUniqueId exception occurred in DeferredGroup when resolving tags by ids. (#1410) Signed-off-by: clarsen <clarsen@yahoo-inc.com> --- src/core/Tags.java | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/core/Tags.java b/src/core/Tags.java index b83cf2cfe2..2e3167b16a 100644 --- a/src/core/Tags.java +++ b/src/core/Tags.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.Map; +import com.stumbleupon.async.DeferredGroupException; +import net.opentsdb.utils.Exceptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -353,7 +355,7 @@ static String getValue(final TSDB tsdb, final byte[] row, * Extracts the value ID of the given tag UD name from the given row key. * @param tsdb The TSDB instance to use for UniqueId lookups. * @param row The row key in which to search the tag name. - * @param name The name of the tag to search in the row key. + * @param tag_id The name of the tag to search in the row key. * @return The value ID associated with the given tag ID, or null if this * tag ID isn't present in this row key. */ @@ -404,7 +406,14 @@ static Map<String, String> getTags(final TSDB tsdb, final byte[] row) throws NoSuchUniqueId { try { return getTagsAsync(tsdb, row).joinUninterruptibly(); - } catch (RuntimeException e) { + } catch (DeferredGroupException e) { + final Throwable ex = Exceptions.getCause(e); + if (ex instanceof NoSuchUniqueId) { + throw (NoSuchUniqueId)ex; + } + + throw new RuntimeException("Should never be here", e); + } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("Should never be here", e); @@ -734,6 +743,14 @@ public static HashMap<String, String> resolveIds(final TSDB tsdb, return resolveIdsAsync(tsdb, tags).joinUninterruptibly(); } catch (NoSuchUniqueId e) { throw e; + } catch (DeferredGroupException e) { + final Throwable ex = Exceptions.getCause(e); + if (ex instanceof NoSuchUniqueId) { + throw (NoSuchUniqueId)ex; + } + // TODO process e.results() + + throw new RuntimeException("Shouldn't be here", e); } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } From 14e74b792313cd637434642496f56701e2a9f463 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Wed, 21 Nov 2018 12:38:33 -0700 Subject: [PATCH 719/826] A few updates to the repair wrapper (#1393) * Add repair wrapper * store as object * Move log message * Fix the rest of the logging * Don't try to repair deleted metrics and better logging * Try again immediately, rather than after a full run * Actually track failed metrics * Chunk repairs into 1 hour increments to reduce 'timeout' condition * Better chunking and some comments on weird code blocks * Return failed metrics correctly * Only log chunks when they take a long time * Spit repairs even further (30 minutes by default) * Improve logging * More updates to repair tool * Track finished metrics between runs... * Only write success when success happens * Better warnings around slow chunks * Multiprocessing of metrics (speeds things up) * flake8 fixes * Global counter to track progress * Configurable threads and track failed metrics * Don't require compaction --- tools/repair-tsd | 305 ++++++++++++++++++++++++++--------------------- 1 file changed, 169 insertions(+), 136 deletions(-) diff --git a/tools/repair-tsd b/tools/repair-tsd index 1084b009fb..43573dcdbf 100755 --- a/tools/repair-tsd +++ b/tools/repair-tsd @@ -2,10 +2,11 @@ from subprocess import Popen, PIPE, TimeoutExpired, check_output from random import shuffle +from multiprocessing import Pool, Value +import copy import time from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter import logging -import pprint log = logging.getLogger("repair-tsd") log.setLevel(logging.INFO) @@ -14,137 +15,155 @@ logformat = '%(asctime)s %(name)s %(levelname)s %(message)s' formatter = logging.Formatter(logformat) ch.setFormatter(formatter) log.addHandler(ch) +metric_count = Value('i', 0) +failed_count = Value('i', 0) -class TSDRepair(object): - def __init__(self, args): - self.time_chunk = args.get("time_chunk", 15) - self.timeout = int(self.time_chunk * 60) - self.retries = args.get("retries", 1) - self.multiplier = int(60 / self.time_chunk) - self.time_range = args.get("time_range", 48) - self.chunk_count = self.time_range * self.multiplier - self.tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") - self.cfg_path = args.get("cfg_path", "/etc/opentsdb/opentsdb.conf") - self.use_sudo = args.get("use_sudo", False) - self.sudo_user = args.get("sudo_user", "opentsdb") - self.log = logging.getLogger("repair-tsd") - self.base = "{} fsck --config={}".format(self.tsd_path, self.cfg_path) - self.check_cmd = "{} uid --config={} metrics".format(self.tsd_path, self.cfg_path) - if self.use_sudo: - self.base = "sudo -u {} {}".format(self.sudo_user, self.base) - self.check_cmd = "sudo -u {} {}".format(self.sudo_user, self.check_cmd) - - def _get_metrics(self): - """ - Collect all metrics from OpenTSDB - - :returns: all metrics - :rtype: list - """ - try: - self.store_path = args.get('store_path', '/tmp/opentsdb.list') - with open(self.store_path, 'r') as f_in: - finished_metrics = [m for m in f_in.read().split('\n') if m] - except Exception: - finished_metrics = [] - cmd = '{} uid --config={} grep metrics ".*"'.format(self.tsd_path, - self.cfg_path) - proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) - results = proc.communicate() - metrics = [m.split(" ")[1].strip(":") - for m in results[0].decode().split("\n") if m] - metrics = [m for m in metrics if m and m != "\x00" and - m not in finished_metrics] +def get_metrics(args): + """ + Collect all metrics from OpenTSDB + + :returns: all metrics + :rtype: list + """ + time_chunk = args.get("time_chunk", 15) + multiplier = int(60 / time_chunk) + time_range = args.get("time_range", 48) + tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") + cfg_path = args.get("cfg_path", "/etc/opentsdb/opentsdb.conf") + use_sudo = args.get("use_sudo", False) + sudo_user = args.get("sudo_user", "opentsdb") + base = "{} fsck --config={}".format(tsd_path, cfg_path) + check_cmd = "{} uid --config={} metrics".format(tsd_path, cfg_path) + if use_sudo: + base = "sudo -u {} {}".format(sudo_user, base) + check_cmd = "sudo -u {} {}".format(sudo_user, check_cmd) + cmd = '{} uid --config={} grep metrics ".*"'.format(tsd_path, + cfg_path) + proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + results = proc.communicate() + metriclist = [m.split(" ")[1].strip(":") + for m in results[0].decode().split("\n") if m] + metriclist = [m for m in metriclist if m and m != "\x00"] + metricobj = {"time_chunk": time_chunk, + "timeout": int(time_chunk * 60), + "retries": args.get("retries", 1), + "compact": args.get("compact", 1), + "multiplier": multiplier, + "metriccount": len(metriclist), + "chunk_count": time_range * multiplier, + "base": base, "check_cmd": check_cmd} + metrics = [] + for m in metriclist: + metric = copy.deepcopy(metricobj) + metric["metric"] = m + metrics.append(metric) + if args.get("shuffle", False): shuffle(metrics) - self.log.info("There are {} metrics to process".format(len(metrics))) - return metrics - - def _repair_metric_chunk(self, metric, chunk): - """ - Repair one 'chunk' of data for a metric - """ - self.log.debug("Running chunk {} for {}".format(chunk, metric)) - if chunk < 2: - timestr = "{}m-ago".format(self.time_chunk) - else: - timestr = "{}m-ago {}m-ago".format((chunk + 1) * self.time_chunk, - chunk * self.time_chunk) - cmd = "{} {} sum".format(self.base, timestr) - """ - Even though we're chunking, it's worth trying things more than once - """ - for x in range(1, self.retries + 2): - self.log.debug("Repair try {} for {}".format(x, timestr)) + log.info("There are {} metrics to process".format(len(metrics))) + return metrics + + +def repair_metric_chunk(metricobj, chunk): + """ + Repair one 'chunk' of data for a metric + """ + metric = metricobj["metric"] + time_chunk = metricobj["time_chunk"] + base = metricobj["base"] + timeout = metricobj["timeout"] + chunk_count = metricobj["chunk_count"] + compact = metricobj["compact"] + log.debug("Running chunk {} for {}".format(chunk, metric)) + if chunk < 2: + timestr = "{}m-ago".format(time_chunk) + else: + timestr = "{}m-ago {}m-ago".format((chunk + 1) * time_chunk, + chunk * time_chunk) + cmd = "{} {} sum".format(base, timestr) + """ + Even though we're chunking, it's worth trying things more than once + """ + for x in range(1, metricobj["retries"] + 2): + log.debug("Repair try {} for {}".format(x, timestr)) + if compact: fullcmd = "{} {} --fix-all --compact".format(cmd, metric) - self.log.debug("Full command: {}".format(fullcmd)) - metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) - try: - results, err = metricproc.communicate(timeout=self.timeout) - except TimeoutExpired: - self.log.debug("{} failed to complete in window (run {})".format(metric, x)) - continue - except Exception as e: - self.log.error("{} general exception :: {}".format(metric, - e)) - else: - results = [r for r in results.decode().split("\n") if r][-26:] - final_results = [] - """ - We'll only collect results that are non-0 - since we're not super interested in stuff that didn't change. - """ - for r in results: - # Strip the timestamp from the log line - line = r.split(" ")[6:] - try: - if int(line[-1]) != 0: - final_results.append(" ".join(line)) - except Exception: - final_results.append(" ".join(line)) - result_str = "\n".join(final_results) - self.log.debug("{} results:\n{}".format(metric, result_str)) - if chunk % 20 == 0: - self.log.info("Chunk {} of {} finished".format(chunk, self.chunk_count)) - else: - self.log.debug("Chunk {} of {} finished".format(chunk, self.chunk_count)) + else: + fullcmd = "{} {} --fix-all".format(cmd, metric) + log.debug("Full command: {}".format(fullcmd)) + metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) + try: + results, err = metricproc.communicate(timeout=timeout) + except TimeoutExpired: + log.warning("{}: chunk {} failed in window (run {})".format(metric, + chunk, + x)) + continue + except Exception as e: + log.error("{} general exception :: {}".format(metric, + e)) + else: + results = [r for r in results.decode().split("\n") if r][-26:] + final_results = [] + """ + We'll only collect results that are non-0 + since we're not super interested in stuff that didn't change. + """ + for r in results: + # Strip the timestamp from the log line + line = r.split(" ")[6:] try: - with open(self.store_path, 'a') as f_out: - f_out.write("{}\n".format(metric)) + if int(line[-1]) != 0: + final_results.append(" ".join(line)) except Exception: - pass - return None - else: - self.log.error("Failed to completely repair {}".format(metric)) + final_results.append(" ".join(line)) + result_str = "\n".join(final_results) + log.debug("{} results:\n{}".format(metric, result_str)) + if chunk % 20 == 0: + log.debug("Chunk {} of {} finished".format(chunk, chunk_count)) + return None + else: + log.error("Failed to completely repair {}".format(metric)) + return metric + + +def process_metric(metricobj): + """ + Run fsck on a list of metrics over a time range + """ + metric = metricobj["metric"] + chunk_count = metricobj["chunk_count"] + try: + check_output("{} {}".format(metricobj["check_cmd"], metric), + shell=True) + except Exception: + log.warning("{} doesn't exist! Skipping...".format(metric)) + return None + log.info("Repairing {} in {} chunks".format(metric, chunk_count)) + start_time = time.time() + for x in range(1, chunk_count + 1): + failed = repair_metric_chunk(metricobj, x) + if failed: + with failed_count.get_lock(): + failed_count.value += 1 return metric + runtime = time.time() - start_time + with metric_count.get_lock(): + metric_count.value += 1 + line = "{} repair took {} seconds".format(metric, + int(runtime)) + line += " ({} of {} metrics complete)".format(metric_count.value, + metricobj["metriccount"]) + line += " ({} failed)".format(failed_count.value) + log.info(line) + - def process_metrics(self): - """ - Run fsck on a list of metrics over a time range - """ - failed_metrics = [] - metrics = self._get_metrics() - for index, metric in enumerate(metrics): - try: - check_output("{} {}".format(self.check_cmd, metric), - shell=True) - except Exception: - log.warning("{} doesn't exist! Skipping...".format(metric)) - continue - logline = "{} ({} of {})".format(metric, index + 1, len(metrics)) - logline += " ({} failed) in {} chunks".format(len(failed_metrics), - self.chunk_count) - self.log.info(logline) - start_time = time.time() - start_time_min = int(start_time//60 * 60) - failed_metrics = [self._repair_metric_chunk(metric, x) - for x in range(1, self.chunk_count + 1)] - failed_metrics = [m for m in failed_metrics if m] - runtime = time.time() - start_time - self.log.info("{} repair took {} seconds".format(metric, - int(runtime))) - self.log.info("Failed metrics: {}".format(failed_metrics)) - return failed_metrics +def process_metrics(metric_list, threads): + threads = Pool(threads) + failed_metrics = threads.map(process_metric, metric_list) + failed_metrics = [m for m in failed_metrics if m] + log.warning("Failed metrics: {}".format(failed_metrics)) + return failed_metrics def cli_opts(): @@ -162,11 +181,19 @@ 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="/opentsdb-fsck.list", + 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?") + parser.add_argument("--compact", action="store_true", + default=False, + help="Run compaction with repairs") + parser.add_argument("--shuffle", action="store_true", + default=False, + help="Mix up incoming metric order") + parser.add_argument("--threads", default="4", + help="Total number of metrics to process at once") parser.add_argument("--sudo-user", default="opentsdb", help="User to switch to...") return parser.parse_args() @@ -184,22 +211,28 @@ def main(): retries = int(args.retries) except Exception as e: log.error("Invalid retry number {} :: {}".format(args.retries, e)) + try: + threads = int(args.threads) + except Exception as e: + log.error("Invalid thread count {} :: {}".format(args.threads, e)) try: time_chunk = int(args.time_chunk) if 60 % time_chunk != 0: raise ArithmeticError except Exception as e: - log.error("Invalid time chunk {} :: {}".format(args.retries, e)) - - repair_tool = TSDRepair({"time_range": time_range, - "use_sudo": args.use_sudo, - "sudo_user": args.sudo_user, - "time_chunk": time_chunk, - "tsd_path": args.tsd_path, - "cfg_path": args.cfg_path, - "store_path": args.store_path, - "retries": retries}) - repair_tool.process_metrics() + log.error("Invalid time chunk {} :: {}".format(args.time_chunk, e)) + + metric_list = get_metrics({"time_range": time_range, + "use_sudo": args.use_sudo, + "sudo_user": args.sudo_user, + "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}) + process_metrics(metric_list, threads) if __name__ == "__main__": From cb72b89789f2a53d317bc08414d03136735af3f0 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Wed, 21 Nov 2018 12:38:33 -0700 Subject: [PATCH 720/826] A few updates to the repair wrapper (#1393) * Add repair wrapper * store as object * Move log message * Fix the rest of the logging * Don't try to repair deleted metrics and better logging * Try again immediately, rather than after a full run * Actually track failed metrics * Chunk repairs into 1 hour increments to reduce 'timeout' condition * Better chunking and some comments on weird code blocks * Return failed metrics correctly * Only log chunks when they take a long time * Spit repairs even further (30 minutes by default) * Improve logging * More updates to repair tool * Track finished metrics between runs... * Only write success when success happens * Better warnings around slow chunks * Multiprocessing of metrics (speeds things up) * flake8 fixes * Global counter to track progress * Configurable threads and track failed metrics * Don't require compaction --- tools/repair-tsd | 305 ++++++++++++++++++++++++++--------------------- 1 file changed, 169 insertions(+), 136 deletions(-) diff --git a/tools/repair-tsd b/tools/repair-tsd index 1084b009fb..43573dcdbf 100755 --- a/tools/repair-tsd +++ b/tools/repair-tsd @@ -2,10 +2,11 @@ from subprocess import Popen, PIPE, TimeoutExpired, check_output from random import shuffle +from multiprocessing import Pool, Value +import copy import time from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter import logging -import pprint log = logging.getLogger("repair-tsd") log.setLevel(logging.INFO) @@ -14,137 +15,155 @@ logformat = '%(asctime)s %(name)s %(levelname)s %(message)s' formatter = logging.Formatter(logformat) ch.setFormatter(formatter) log.addHandler(ch) +metric_count = Value('i', 0) +failed_count = Value('i', 0) -class TSDRepair(object): - def __init__(self, args): - self.time_chunk = args.get("time_chunk", 15) - self.timeout = int(self.time_chunk * 60) - self.retries = args.get("retries", 1) - self.multiplier = int(60 / self.time_chunk) - self.time_range = args.get("time_range", 48) - self.chunk_count = self.time_range * self.multiplier - self.tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") - self.cfg_path = args.get("cfg_path", "/etc/opentsdb/opentsdb.conf") - self.use_sudo = args.get("use_sudo", False) - self.sudo_user = args.get("sudo_user", "opentsdb") - self.log = logging.getLogger("repair-tsd") - self.base = "{} fsck --config={}".format(self.tsd_path, self.cfg_path) - self.check_cmd = "{} uid --config={} metrics".format(self.tsd_path, self.cfg_path) - if self.use_sudo: - self.base = "sudo -u {} {}".format(self.sudo_user, self.base) - self.check_cmd = "sudo -u {} {}".format(self.sudo_user, self.check_cmd) - - def _get_metrics(self): - """ - Collect all metrics from OpenTSDB - - :returns: all metrics - :rtype: list - """ - try: - self.store_path = args.get('store_path', '/tmp/opentsdb.list') - with open(self.store_path, 'r') as f_in: - finished_metrics = [m for m in f_in.read().split('\n') if m] - except Exception: - finished_metrics = [] - cmd = '{} uid --config={} grep metrics ".*"'.format(self.tsd_path, - self.cfg_path) - proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) - results = proc.communicate() - metrics = [m.split(" ")[1].strip(":") - for m in results[0].decode().split("\n") if m] - metrics = [m for m in metrics if m and m != "\x00" and - m not in finished_metrics] +def get_metrics(args): + """ + Collect all metrics from OpenTSDB + + :returns: all metrics + :rtype: list + """ + time_chunk = args.get("time_chunk", 15) + multiplier = int(60 / time_chunk) + time_range = args.get("time_range", 48) + tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") + cfg_path = args.get("cfg_path", "/etc/opentsdb/opentsdb.conf") + use_sudo = args.get("use_sudo", False) + sudo_user = args.get("sudo_user", "opentsdb") + base = "{} fsck --config={}".format(tsd_path, cfg_path) + check_cmd = "{} uid --config={} metrics".format(tsd_path, cfg_path) + if use_sudo: + base = "sudo -u {} {}".format(sudo_user, base) + check_cmd = "sudo -u {} {}".format(sudo_user, check_cmd) + cmd = '{} uid --config={} grep metrics ".*"'.format(tsd_path, + cfg_path) + proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + results = proc.communicate() + metriclist = [m.split(" ")[1].strip(":") + for m in results[0].decode().split("\n") if m] + metriclist = [m for m in metriclist if m and m != "\x00"] + metricobj = {"time_chunk": time_chunk, + "timeout": int(time_chunk * 60), + "retries": args.get("retries", 1), + "compact": args.get("compact", 1), + "multiplier": multiplier, + "metriccount": len(metriclist), + "chunk_count": time_range * multiplier, + "base": base, "check_cmd": check_cmd} + metrics = [] + for m in metriclist: + metric = copy.deepcopy(metricobj) + metric["metric"] = m + metrics.append(metric) + if args.get("shuffle", False): shuffle(metrics) - self.log.info("There are {} metrics to process".format(len(metrics))) - return metrics - - def _repair_metric_chunk(self, metric, chunk): - """ - Repair one 'chunk' of data for a metric - """ - self.log.debug("Running chunk {} for {}".format(chunk, metric)) - if chunk < 2: - timestr = "{}m-ago".format(self.time_chunk) - else: - timestr = "{}m-ago {}m-ago".format((chunk + 1) * self.time_chunk, - chunk * self.time_chunk) - cmd = "{} {} sum".format(self.base, timestr) - """ - Even though we're chunking, it's worth trying things more than once - """ - for x in range(1, self.retries + 2): - self.log.debug("Repair try {} for {}".format(x, timestr)) + log.info("There are {} metrics to process".format(len(metrics))) + return metrics + + +def repair_metric_chunk(metricobj, chunk): + """ + Repair one 'chunk' of data for a metric + """ + metric = metricobj["metric"] + time_chunk = metricobj["time_chunk"] + base = metricobj["base"] + timeout = metricobj["timeout"] + chunk_count = metricobj["chunk_count"] + compact = metricobj["compact"] + log.debug("Running chunk {} for {}".format(chunk, metric)) + if chunk < 2: + timestr = "{}m-ago".format(time_chunk) + else: + timestr = "{}m-ago {}m-ago".format((chunk + 1) * time_chunk, + chunk * time_chunk) + cmd = "{} {} sum".format(base, timestr) + """ + Even though we're chunking, it's worth trying things more than once + """ + for x in range(1, metricobj["retries"] + 2): + log.debug("Repair try {} for {}".format(x, timestr)) + if compact: fullcmd = "{} {} --fix-all --compact".format(cmd, metric) - self.log.debug("Full command: {}".format(fullcmd)) - metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) - try: - results, err = metricproc.communicate(timeout=self.timeout) - except TimeoutExpired: - self.log.debug("{} failed to complete in window (run {})".format(metric, x)) - continue - except Exception as e: - self.log.error("{} general exception :: {}".format(metric, - e)) - else: - results = [r for r in results.decode().split("\n") if r][-26:] - final_results = [] - """ - We'll only collect results that are non-0 - since we're not super interested in stuff that didn't change. - """ - for r in results: - # Strip the timestamp from the log line - line = r.split(" ")[6:] - try: - if int(line[-1]) != 0: - final_results.append(" ".join(line)) - except Exception: - final_results.append(" ".join(line)) - result_str = "\n".join(final_results) - self.log.debug("{} results:\n{}".format(metric, result_str)) - if chunk % 20 == 0: - self.log.info("Chunk {} of {} finished".format(chunk, self.chunk_count)) - else: - self.log.debug("Chunk {} of {} finished".format(chunk, self.chunk_count)) + else: + fullcmd = "{} {} --fix-all".format(cmd, metric) + log.debug("Full command: {}".format(fullcmd)) + metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) + try: + results, err = metricproc.communicate(timeout=timeout) + except TimeoutExpired: + log.warning("{}: chunk {} failed in window (run {})".format(metric, + chunk, + x)) + continue + except Exception as e: + log.error("{} general exception :: {}".format(metric, + e)) + else: + results = [r for r in results.decode().split("\n") if r][-26:] + final_results = [] + """ + We'll only collect results that are non-0 + since we're not super interested in stuff that didn't change. + """ + for r in results: + # Strip the timestamp from the log line + line = r.split(" ")[6:] try: - with open(self.store_path, 'a') as f_out: - f_out.write("{}\n".format(metric)) + if int(line[-1]) != 0: + final_results.append(" ".join(line)) except Exception: - pass - return None - else: - self.log.error("Failed to completely repair {}".format(metric)) + final_results.append(" ".join(line)) + result_str = "\n".join(final_results) + log.debug("{} results:\n{}".format(metric, result_str)) + if chunk % 20 == 0: + log.debug("Chunk {} of {} finished".format(chunk, chunk_count)) + return None + else: + log.error("Failed to completely repair {}".format(metric)) + return metric + + +def process_metric(metricobj): + """ + Run fsck on a list of metrics over a time range + """ + metric = metricobj["metric"] + chunk_count = metricobj["chunk_count"] + try: + check_output("{} {}".format(metricobj["check_cmd"], metric), + shell=True) + except Exception: + log.warning("{} doesn't exist! Skipping...".format(metric)) + return None + log.info("Repairing {} in {} chunks".format(metric, chunk_count)) + start_time = time.time() + for x in range(1, chunk_count + 1): + failed = repair_metric_chunk(metricobj, x) + if failed: + with failed_count.get_lock(): + failed_count.value += 1 return metric + runtime = time.time() - start_time + with metric_count.get_lock(): + metric_count.value += 1 + line = "{} repair took {} seconds".format(metric, + int(runtime)) + line += " ({} of {} metrics complete)".format(metric_count.value, + metricobj["metriccount"]) + line += " ({} failed)".format(failed_count.value) + log.info(line) + - def process_metrics(self): - """ - Run fsck on a list of metrics over a time range - """ - failed_metrics = [] - metrics = self._get_metrics() - for index, metric in enumerate(metrics): - try: - check_output("{} {}".format(self.check_cmd, metric), - shell=True) - except Exception: - log.warning("{} doesn't exist! Skipping...".format(metric)) - continue - logline = "{} ({} of {})".format(metric, index + 1, len(metrics)) - logline += " ({} failed) in {} chunks".format(len(failed_metrics), - self.chunk_count) - self.log.info(logline) - start_time = time.time() - start_time_min = int(start_time//60 * 60) - failed_metrics = [self._repair_metric_chunk(metric, x) - for x in range(1, self.chunk_count + 1)] - failed_metrics = [m for m in failed_metrics if m] - runtime = time.time() - start_time - self.log.info("{} repair took {} seconds".format(metric, - int(runtime))) - self.log.info("Failed metrics: {}".format(failed_metrics)) - return failed_metrics +def process_metrics(metric_list, threads): + threads = Pool(threads) + failed_metrics = threads.map(process_metric, metric_list) + failed_metrics = [m for m in failed_metrics if m] + log.warning("Failed metrics: {}".format(failed_metrics)) + return failed_metrics def cli_opts(): @@ -162,11 +181,19 @@ 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="/opentsdb-fsck.list", + 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?") + parser.add_argument("--compact", action="store_true", + default=False, + help="Run compaction with repairs") + parser.add_argument("--shuffle", action="store_true", + default=False, + help="Mix up incoming metric order") + parser.add_argument("--threads", default="4", + help="Total number of metrics to process at once") parser.add_argument("--sudo-user", default="opentsdb", help="User to switch to...") return parser.parse_args() @@ -184,22 +211,28 @@ def main(): retries = int(args.retries) except Exception as e: log.error("Invalid retry number {} :: {}".format(args.retries, e)) + try: + threads = int(args.threads) + except Exception as e: + log.error("Invalid thread count {} :: {}".format(args.threads, e)) try: time_chunk = int(args.time_chunk) if 60 % time_chunk != 0: raise ArithmeticError except Exception as e: - log.error("Invalid time chunk {} :: {}".format(args.retries, e)) - - repair_tool = TSDRepair({"time_range": time_range, - "use_sudo": args.use_sudo, - "sudo_user": args.sudo_user, - "time_chunk": time_chunk, - "tsd_path": args.tsd_path, - "cfg_path": args.cfg_path, - "store_path": args.store_path, - "retries": retries}) - repair_tool.process_metrics() + log.error("Invalid time chunk {} :: {}".format(args.time_chunk, e)) + + metric_list = get_metrics({"time_range": time_range, + "use_sudo": args.use_sudo, + "sudo_user": args.sudo_user, + "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}) + process_metrics(metric_list, threads) if __name__ == "__main__": From f6f20d9123bc5fa1f5c5ecce3816c4e766f1714b Mon Sep 17 00:00:00 2001 From: Great Snoopy <GreatSnoopy@gmail.com> Date: Wed, 21 Nov 2018 21:39:45 +0200 Subject: [PATCH 721/826] ISSUE-1344 Fix NPE when null is given as datapoint in a set (#1347) Signed-off-by: clarsen <clarsen@yahoo-inc.com> --- src/tsd/PutDataPointRpc.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 71cd8ecc5d..cd189eb2af 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -170,6 +170,14 @@ public String toString() { } try { + if (dp == null) { + if (show_details) { + details.add(this.getHttpDetails("Unexpected null datapoint encountered in set.", dp)); + } + LOG.warn("Datapoint null was encountered in set."); + illegal_arguments.incrementAndGet(); + continue; + } if (dp.getMetric() == null || dp.getMetric().isEmpty()) { if (show_details) { details.add(this.getHttpDetails("Metric name was empty", dp)); From 1030459269d000ffe78275cfb7d0f919184c1855 Mon Sep 17 00:00:00 2001 From: Eric Price <eric2025@gmail.com> Date: Tue, 4 Dec 2018 10:54:33 -0800 Subject: [PATCH 722/826] Added performace data for Nagios/Icinga to check_tsd (#1226) --- tools/check_tsd | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index db888b82fa..16f08a295f 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -248,7 +248,7 @@ def main(argv): rv = 1 nbad = nwarn else: - rv=0 + rv = 0 if options.verbose and len(datapoints) != npoints: print ('ignored %d/%d data points for being more than %ds old' % (len(datapoints) - npoints, len(datapoints), options.duration)) @@ -264,9 +264,16 @@ def main(argv): # in nrpe, pipe character is something special, but it's used in tag # searches. Translate it to something else for the purposes of output. ttags = tags.replace("|",":") + + # Retrieve metric name for performance data label. + perf_label = options.metric.split(".")[-1] + if not rv: - print ('OK: %s%s: %d values OK, last=%r' - % (options.metric, ttags, npoints, val)) + status = 'OK: %s%s: %d values OK, last=%r' \ + % (options.metric, ttags, npoints, val) + status += ' | {0}={1};{2};{3};0;{3}'.format( + perf_label, npoints, options.warning, options.critical) + print(status) else: if rv == 1: level = 'WARNING' @@ -274,9 +281,12 @@ def main(argv): elif rv == 2: level = 'CRITICAL' threshold = options.critical - print ('%s: %s%s %s %s: %d/%d bad values (%.1f%%) worst: %r @ %s' - % (level, options.metric, ttags, options.comparator, threshold, - nbad, npoints, bad_pct, badval, badts)) + status = '%s: %s%s %s %s: %d/%d bad values (%.1f%%) worst: %r @ %s' \ + % (level, options.metric, ttags, options.comparator, threshold, + nbad, npoints, bad_pct, badval, badts) + status += ' | {0}={1};{2};{3};0;{3}'.format( + perf_label, npoints, options.warning, options.critical) + print(status) return rv From 87529ce5f66cda1c5de28f08a24ad6b514d4ee57 Mon Sep 17 00:00:00 2001 From: Eric Price <eric.price@upsight.com> Date: Mon, 4 Jun 2018 15:50:12 -0700 Subject: [PATCH 723/826] Added performace data for Nagios/Icinga to check_tsd --- tools/check_tsd | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tools/check_tsd b/tools/check_tsd index 585e56827d..d6cd8b0f62 100755 --- a/tools/check_tsd +++ b/tools/check_tsd @@ -248,7 +248,7 @@ def main(argv): rv = 1 nbad = nwarn else: - rv=0 + rv = 0 if options.verbose and len(datapoints) != npoints: print ('ignored %d/%d data points for being more than %ds old' % (len(datapoints) - npoints, len(datapoints), options.duration)) @@ -264,9 +264,16 @@ def main(argv): # in nrpe, pipe character is something special, but it's used in tag # searches. Translate it to something else for the purposes of output. ttags = tags.replace("|",":") + + # Retrieve metric name for performance data label. + perf_label = options.metric.split(".")[-1] + if not rv: - print ('OK: %s%s: %d values OK, last=%r' - % (options.metric, ttags, npoints, val)) + status = 'OK: %s%s: %d values OK, last=%r' \ + % (options.metric, ttags, npoints, val) + status += ' | {0}={1};{2};{3};0;{3}'.format( + perf_label, npoints, options.warning, options.critical) + print(status) else: if rv == 1: level = 'WARNING' @@ -274,9 +281,12 @@ def main(argv): elif rv == 2: level = 'CRITICAL' threshold = options.critical - print ('%s: %s%s %s %s: %d/%d bad values (%.1f%%) worst: %r @ %s' - % (level, options.metric, ttags, options.comparator, threshold, - nbad, npoints, bad_pct, badval, badts)) + status = '%s: %s%s %s %s: %d/%d bad values (%.1f%%) worst: %r @ %s' \ + % (level, options.metric, ttags, options.comparator, threshold, + nbad, npoints, bad_pct, badval, badts) + status += ' | {0}={1};{2};{3};0;{3}'.format( + perf_label, npoints, options.warning, options.critical) + print(status) return rv From 7c1b66f2b0f5c527001c836e6481b6b092e60317 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Wed, 5 Dec 2018 02:55:46 +0800 Subject: [PATCH 724/826] Add contribution guidelines. (#1428) Signed-off-by: qudongfang <qudongfang@gmail.com> --- CONTRIBUTING.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..0043b493eb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contribution Guidelines + +There are a number of talented developers creating tools for OpenTSDB or contributing code directly to the project. +If you are interested in helping, by adding new features, fixing bugs, adding tools or simply updating documentation, please read the guidelines below. Then sign the contributors agreement and send us a pull request! + +## Guidelines + + +- Please file [issues on GitHub](https://github.com/OpenTSDB/opentsdb/issues) after checking to see if anyone has posted a bug already. Make sure your bug reports contain enough details so they can be easily understood by others and quickly fixed. +- Read the Development page for tips +- The best way to contribute code is to fork the main repo and [send a pull request](https://help.github.com/articles/using-pull-requests) on GitHub. + - Bug fixes should be done in the `master` branch + - New features or major changes should be done in the `next` branch +- Alternatively, you can send a plain-text patch to the [mailing list](https://groups.google.com/forum/#!forum/opentsdb). +- Before your code changes can be included, please file the [Contribution License Agreement](https://docs.google.com/spreadsheet/embeddedform?formkey=dFNiOFROLXJBbFBmMkQtb1hNMWhUUnc6MQ). +- Unlike, say, the Apache Software Foundation, we do not require every single code change to be attached to an issue. Feel free to send as many small fixes as you want. +- Please break down your changes into as many small commits as possible. +- Please respect the coding style of the code you're changing. + - Indent code with 2 spaces, no tabs + - Keep code to 80 columns + - Curly brace on the same line as if, for, while, etc + - Variables need descriptive names `like_this` (instead of the typical Java style of `likeThis`) + - Methods named `likeThis()` starting with lower case letters + - Classes named `LikeThis`, starting with upper case letters + - Use the `final` keyword as much as you can, particularly in method parameters and returns statements + - Avoid checked exceptions as much as possible + - Always provide the most restrictive visibility to classes and members + - Javadoc all of your classes and methods. Some folks make use the Java API directly and we'll build docs for the site, so the more the merrier + - Don't add dependencies to the core OpenTSDB library unless absolutely necessary + - Add unit tests for any classes/methods you create and verify that your change doesn't break existing unit tests. We know UTs aren't fun, but they are useful + +## Git Repository + +OpenTSDB is maintained in [GitHub](https://github.com/OpenTSDB/opentsdb/). There are a limited number of branches and the purpose of each branch is as follows: + +- `maintenance` - This was the previously released version of OpenTSDB, usually the last minor version. E.g. if 2.3.0 or 2.3.1 is the current release, `maintenance` will have the last 2.2.x version. This branch +should rarely have PRs pointed at it. Rather patches against `master` that would apply to previous releases can be cherry-picked. +- `master` - The current release version of OpenTSDB. Only pull requests with bug fixes should be given against `master`. When enough PRs have been merged, we'll cut another PATCH version, e.g. 2.2.0 to 2.2.1. Patches with new features or behavior modifications should point to `next`. Patches against `master` should be cherry-picked to the downstream branches. +- `next` - This is the next minor release version of OpenTSDB and contains code in development or, when the version is marked as RC, then release candidate code. If the version is marked as a SNAPSHOT then new features can be applied to `next`. Once it moves to RC, then new features should be issued against the `put` branch. Otherwise only bug fixes should be given for RC code. +- `put` - When the next branch is in an RC state, new features should be applied against `put` which will be the next minor version. +- `X.0` - The next major version of OpenTSDB that may include breaking API changes. When the code is in a fairly stable state, it will be promoted up to `next` as an RC, then `master` for an official `release`. + +Any other branches are likely to be pruned at some point as they may contain stale code or temporary hacks. + +## Details + +- [General Development](http://opentsdb.net/docs/build/html/development/development.html) +- [Plugins](http://opentsdb.net/docs/build/html/development/plugins.html) +- [HTTP API](http://opentsdb.net/docs/build/html/development/http_api.html) + +Thank you for your contributions! From fd3025935a0aa8cd27fabca2e611157eb7866443 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@gmail.com> Date: Fri, 23 Nov 2018 13:27:23 +0800 Subject: [PATCH 725/826] Add contribution guidelines. Signed-off-by: qudongfang <qudongfang@gmail.com> --- CONTRIBUTING.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..0043b493eb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contribution Guidelines + +There are a number of talented developers creating tools for OpenTSDB or contributing code directly to the project. +If you are interested in helping, by adding new features, fixing bugs, adding tools or simply updating documentation, please read the guidelines below. Then sign the contributors agreement and send us a pull request! + +## Guidelines + + +- Please file [issues on GitHub](https://github.com/OpenTSDB/opentsdb/issues) after checking to see if anyone has posted a bug already. Make sure your bug reports contain enough details so they can be easily understood by others and quickly fixed. +- Read the Development page for tips +- The best way to contribute code is to fork the main repo and [send a pull request](https://help.github.com/articles/using-pull-requests) on GitHub. + - Bug fixes should be done in the `master` branch + - New features or major changes should be done in the `next` branch +- Alternatively, you can send a plain-text patch to the [mailing list](https://groups.google.com/forum/#!forum/opentsdb). +- Before your code changes can be included, please file the [Contribution License Agreement](https://docs.google.com/spreadsheet/embeddedform?formkey=dFNiOFROLXJBbFBmMkQtb1hNMWhUUnc6MQ). +- Unlike, say, the Apache Software Foundation, we do not require every single code change to be attached to an issue. Feel free to send as many small fixes as you want. +- Please break down your changes into as many small commits as possible. +- Please respect the coding style of the code you're changing. + - Indent code with 2 spaces, no tabs + - Keep code to 80 columns + - Curly brace on the same line as if, for, while, etc + - Variables need descriptive names `like_this` (instead of the typical Java style of `likeThis`) + - Methods named `likeThis()` starting with lower case letters + - Classes named `LikeThis`, starting with upper case letters + - Use the `final` keyword as much as you can, particularly in method parameters and returns statements + - Avoid checked exceptions as much as possible + - Always provide the most restrictive visibility to classes and members + - Javadoc all of your classes and methods. Some folks make use the Java API directly and we'll build docs for the site, so the more the merrier + - Don't add dependencies to the core OpenTSDB library unless absolutely necessary + - Add unit tests for any classes/methods you create and verify that your change doesn't break existing unit tests. We know UTs aren't fun, but they are useful + +## Git Repository + +OpenTSDB is maintained in [GitHub](https://github.com/OpenTSDB/opentsdb/). There are a limited number of branches and the purpose of each branch is as follows: + +- `maintenance` - This was the previously released version of OpenTSDB, usually the last minor version. E.g. if 2.3.0 or 2.3.1 is the current release, `maintenance` will have the last 2.2.x version. This branch +should rarely have PRs pointed at it. Rather patches against `master` that would apply to previous releases can be cherry-picked. +- `master` - The current release version of OpenTSDB. Only pull requests with bug fixes should be given against `master`. When enough PRs have been merged, we'll cut another PATCH version, e.g. 2.2.0 to 2.2.1. Patches with new features or behavior modifications should point to `next`. Patches against `master` should be cherry-picked to the downstream branches. +- `next` - This is the next minor release version of OpenTSDB and contains code in development or, when the version is marked as RC, then release candidate code. If the version is marked as a SNAPSHOT then new features can be applied to `next`. Once it moves to RC, then new features should be issued against the `put` branch. Otherwise only bug fixes should be given for RC code. +- `put` - When the next branch is in an RC state, new features should be applied against `put` which will be the next minor version. +- `X.0` - The next major version of OpenTSDB that may include breaking API changes. When the code is in a fairly stable state, it will be promoted up to `next` as an RC, then `master` for an official `release`. + +Any other branches are likely to be pruned at some point as they may contain stale code or temporary hacks. + +## Details + +- [General Development](http://opentsdb.net/docs/build/html/development/development.html) +- [Plugins](http://opentsdb.net/docs/build/html/development/plugins.html) +- [HTTP API](http://opentsdb.net/docs/build/html/development/http_api.html) + +Thank you for your contributions! From 6225b4f94c421449ac0af645d97f8b6b1fa0a8e7 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Wed, 5 Dec 2018 02:58:18 +0800 Subject: [PATCH 726/826] ISSUE-1430: Bugfix of 'make clean'. (#1431) Signed-off-by: qudongfang <qudongfang@gmail.com> --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 1d40e4fb32..d255445d6e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -763,7 +763,7 @@ mostlyclean-local: && find $(package_dir) -depth -type d -exec rmdir {} ';' \ && dir=$(package_dir) && dir=$${dir%/*} \ && while test x"$$dir" != x"$${dir%/*}"; do \ - rmdir "$$dir" && dir=$${dir%/*} || break; \ + rm -rf "$$dir" && dir=$${dir%/*} || break; \ done \ && rmdir "$$dir" From 8632f0cdafab9e51d5d8287c283d19c04706799f Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@gmail.com> Date: Mon, 26 Nov 2018 11:07:15 +0800 Subject: [PATCH 727/826] ISSUE-1430: Bugfix of 'make clean'. Signed-off-by: qudongfang <qudongfang@gmail.com> --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index d48d0a0134..4eaf8ba563 100644 --- a/Makefile.am +++ b/Makefile.am @@ -832,7 +832,7 @@ mostlyclean-local: && find $(package_dir) -depth -type d -exec rmdir {} ';' \ && dir=$(package_dir) && dir=$${dir%/*} \ && while test x"$$dir" != x"$${dir%/*}"; do \ - rmdir "$$dir" && dir=$${dir%/*} || break; \ + rm -rf "$$dir" && dir=$${dir%/*} || break; \ done \ && rmdir "$$dir" From 61ce1faaa29a060d9641089e0ce375d2b9f99500 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Wed, 5 Dec 2018 03:17:03 +0800 Subject: [PATCH 728/826] Bugfix of NPE in 'UidManager:printResult'. (#1432) Signed-off-by: qudongfang <qudongfang@gmail.com> --- src/tools/UidManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 5e4185d4f4..1eead8bf10 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -330,6 +330,9 @@ private static int grep(final HBaseClient client, private static boolean printResult(final ArrayList<KeyValue> row, final byte[] family, final boolean formard) { + if (null == row || row.isEmpty()) { + return false; + } final byte[] key = row.get(0).key(); String name = formard ? CliUtils.fromBytes(key) : null; String id = formard ? null : Arrays.toString(key); From 22090e99810b67a5d6ab5b7ffc10bb93911dc9a9 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@gmail.com> Date: Mon, 26 Nov 2018 19:55:05 +0800 Subject: [PATCH 729/826] Bugfix of NPE in 'UidManager:printResult'. Signed-off-by: qudongfang <qudongfang@gmail.com> --- src/tools/UidManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/UidManager.java b/src/tools/UidManager.java index 5e4185d4f4..1eead8bf10 100644 --- a/src/tools/UidManager.java +++ b/src/tools/UidManager.java @@ -330,6 +330,9 @@ private static int grep(final HBaseClient client, private static boolean printResult(final ArrayList<KeyValue> row, final byte[] family, final boolean formard) { + if (null == row || row.isEmpty()) { + return false; + } final byte[] key = row.get(0).key(); String name = formard ? CliUtils.fromBytes(key) : null; String id = formard ? null : Arrays.toString(key); From 3c68e38f85610825afd42e6569aadbd10a98f5a3 Mon Sep 17 00:00:00 2001 From: noharm <harm.kroon@tomtom.com> Date: Tue, 4 Dec 2018 23:16:55 +0100 Subject: [PATCH 730/826] Add systemd unit template to the RPM build (#1450) * Add systemd unit template * Update logback.xml to allow a log file per tsdb instance * Update init.d file to pass log file and query log options * Add systemd unit file Add log dir Create user and group on install Set owner for log and cache dir Softlink systemd unit file if systemd is detected. Otherwise softlink init.d script Delete softlinks on rpm removal * Add systemd unit template * Reload systemd after install or removal of unit file --- Makefile.am | 11 +++++++++-- build-aux/rpm/init.d/opentsdb | 3 ++- build-aux/rpm/logback.xml | 8 ++++---- build-aux/rpm/systemd/opentsdb@.service | 15 ++++++++++++++ opentsdb.spec.in | 26 ++++++++++++++++++------- 5 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 build-aux/rpm/systemd/opentsdb@.service diff --git a/Makefile.am b/Makefile.am index 4eaf8ba563..db7e89dd1b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -29,7 +29,8 @@ nodist_bin_SCRIPTS = tsdb dist_noinst_SCRIPTS = src/create_table.sh src/upgrade_1to2.sh src/mygnuplot.sh \ src/mygnuplot.bat src/opentsdb.conf tools/opentsdb_restart.py src/logback.xml 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/logback.xml build-aux/rpm/init.d/opentsdb \ + build-aux/rpm/systemd/opentsdb@.service tsdb_SRC := \ src/core/AggregationIterator.java \ src/core/Aggregator.java \ @@ -709,17 +710,23 @@ install-data-etc: destdataetcdir="$(DESTDIR)$(pkgdatadir)/etc" ; \ destdataconfdir="$$destdataetcdir/opentsdb" ; \ destdatainitdir="$$destdataetcdir/init.d" ; \ + destdatasystemddir="$$destdataetcdir/systemd/system" ; \ echo " $(mkdir_p) $$destdataconfdir"; \ $(mkdir_p) "$$destdataconfdir" || exit 1; \ echo " $(mkdir_p) $$destdatainitdir"; \ $(mkdir_p) "$$destdatainitdir" || exit 1; \ + echo " $(mkdir_p) $$destdatasystemddir"; \ + $(mkdir_p) "$$destdatasystemddir" || exit 1; \ conf_files="$$conf_files $(top_srcdir)/build-aux/rpm/opentsdb.conf" ; \ conf_files="$$conf_files $(top_srcdir)/build-aux/rpm/logback.xml" ; \ echo " $(INSTALL_SCRIPT)" $$conf_files "$$destdataconfdir" ; \ $(INSTALL_DATA) $$conf_files "$$destdataconfdir" || exit 1; \ init_file="$(top_srcdir)/build-aux/rpm/init.d/opentsdb" ; \ echo " $(INSTALL_SCRIPT)" $$init_file "$$destdatainitdir" ; \ - $(INSTALL_SCRIPT) $$init_file "$$destdatainitdir" || exit 1; + $(INSTALL_SCRIPT) $$init_file "$$destdatainitdir" || exit 1; \ + systemd_file="$(top_srcdir)/build-aux/rpm/systemd/opentsdb@.service" ; \ + echo " $(INSTALL_SCRIPT)" $$systemd_file "$$destdatasystemddir" ; \ + $(INSTALL_SCRIPT) $$systemd_file "$$destdatasystemddir" || exit 1; uninstall-data-etc: @$(NORMAL_UNINSTALL) diff --git a/build-aux/rpm/init.d/opentsdb b/build-aux/rpm/init.d/opentsdb index 5f4ee1d8d5..8d589f9b0a 100644 --- a/build-aux/rpm/init.d/opentsdb +++ b/build-aux/rpm/init.d/opentsdb @@ -75,7 +75,7 @@ start() { # Set a default value for JVMARGS : ${JVMXMX:=-Xmx6000m} - : ${JVMARGS:=-DLOG_FILE_PREFIX=${LOG_FILE} -enableassertions -enablesystemassertions $JVMXMX -XX:OnOutOfMemoryError=/usr/share/opentsdb/tools/opentsdb_restart.py} + : ${JVMARGS:=-DLOG_FILE=${LOG_FILE}opentsdb.log -DQUERY_LOG=${LOG_FILE}queries.log -enableassertions -enablesystemassertions $JVMXMX -XX:OnOutOfMemoryError=/usr/share/opentsdb/tools/opentsdb_restart.py} export JVMARGS if [ "`id -u -n`" == root ] ; then @@ -83,6 +83,7 @@ start() { # daemons to create and rename log files. chown $USER: $LOG_DIR > /dev/null 2>&1 chown $USER: ${LOG_FILE}*opentsdb.log > /dev/null 2>&1 + chown $USER: ${LOG_FILE}*queries.log > /dev/null 2>&1 chown $USER: ${LOG_FILE}opentsdb.out > /dev/null 2>&1 chown $USER: ${LOG_FILE}opentsdb.err > /dev/null 2>&1 diff --git a/build-aux/rpm/logback.xml b/build-aux/rpm/logback.xml index 9c32b2ecbe..4fae3c5655 100644 --- a/build-aux/rpm/logback.xml +++ b/build-aux/rpm/logback.xml @@ -19,11 +19,11 @@ <!-- Appender to write OpenTSDB data to a set of rotating log files --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> - <file>/var/log/opentsdb/opentsdb.log</file> + <file>${LOG_FILE}</file> <append>true</append> <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"> - <fileNamePattern>/var/log/opentsdb/opentsdb.log.%i</fileNamePattern> + <fileNamePattern>${LOG_FILE}.%i</fileNamePattern> <minIndex>1</minIndex> <maxIndex>3</maxIndex> </rollingPolicy> @@ -40,11 +40,11 @@ <!-- Appender for writing full and completed queries to a log file. To use it, make sure to set the "level" to "INFO" in QueryLog below. --> <appender name="QUERY_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> - <file>/var/log/opentsdb/queries.log</file> + <file>${QUERY_LOG}</file> <append>true</append> <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"> - <fileNamePattern>/var/log/opentsdb/queries.log.%i</fileNamePattern> + <fileNamePattern>${QUERY_LOG}.%i</fileNamePattern> <minIndex>1</minIndex> <maxIndex>4</maxIndex> </rollingPolicy> diff --git a/build-aux/rpm/systemd/opentsdb@.service b/build-aux/rpm/systemd/opentsdb@.service new file mode 100644 index 0000000000..847667a032 --- /dev/null +++ b/build-aux/rpm/systemd/opentsdb@.service @@ -0,0 +1,15 @@ +[Unit] +Description=OpenTSDB on port %i +After=network-online.target +Before=shutdown.target + +[Service] +Type=simple +User=opentsdb +Group=opentsdb +LimitNOFILE=65535 +Environment=JAVA_HOME=/usr/lib/jvm/jre-openjdk +Environment='JVMARGS=-Xmx6000m -DLOG_FILE=/var/log/opentsdb/%p_%i.log -DQUERY_LOG=/var/log/opentsdb/%p_%i_queries.log -XX:+ExitOnOutOfMemoryError -enableassertions -enablesystemassertions' +ExecStart=/usr/bin/tsdb tsd --config /etc/opentsdb/opentsdb.conf --port %i +Restart=always +StandardOutput=journal diff --git a/opentsdb.spec.in b/opentsdb.spec.in index 14376d4ee5..6e90bdd292 100644 --- a/opentsdb.spec.in +++ b/opentsdb.spec.in @@ -55,7 +55,8 @@ make %install rm -rf %{buildroot} make install DESTDIR=%{buildroot} -mkdir -p %{buildroot}/var/cache/opentsdb +mkdir -p %{buildroot}%{_localstatedir}/cache/opentsdb +mkdir -p %{buildroot}%{_localstatedir}/log/opentsdb mkdir -p %{buildroot}%{_datarootdir}/opentsdb/plugins # TODO: Use alternatives to manage the init script and configuration. @@ -70,29 +71,40 @@ rm -rf %{buildroot} %attr(0755,root,root) %{_datarootdir}/opentsdb/plugins %attr(0755,root,root) %{_datarootdir}/opentsdb/tools/* %attr(0755,root,root) %{_datarootdir}/opentsdb/etc/init.d/opentsdb +%attr(0644,root,root) %{_datarootdir}/opentsdb/etc/systemd/system/opentsdb@.service %config %{_datarootdir}/opentsdb/etc/opentsdb/opentsdb.conf %config %{_datarootdir}/opentsdb/etc/opentsdb/logback.xml %doc %{_datarootdir}/opentsdb %{_bindir}/tsdb -%dir %{_localstatedir}/cache/opentsdb +%dir %attr(0755,opentsdb,opentsdb) %{_localstatedir}/cache/opentsdb +%dir %attr(0755,opentsdb,opentsdb) %{_localstatedir}/log/opentsdb %changelog -%post +%pre +getent group opentsdb 2>/dev/null >/dev/null || /usr/sbin/groupadd -r opentsdb +getent passwd opentsbd 2>&1 > /dev/null || /usr/sbin/useradd -c "OpenTSDB" -s /sbin/nologin -g opentsdb -r -d %{_datarootdir}/opentsdb opentsdb 2> /dev/null || : +%post if [ $1 -eq 1 ]; then # we're installing the first version of this package ln -s %{_datarootdir}/opentsdb/etc/opentsdb /etc/opentsdb - ln -s %{_datarootdir}/opentsdb/etc/init.d/opentsdb /etc/init.d/opentsdb + if [ -d /run/systemd/system ]; then + ln -s %{_datarootdir}/opentsdb/etc/systemd/system/opentsdb@.service /lib/systemd/system + systemctl daemon-reload + else + ln -s %{_datarootdir}/opentsdb/etc/init.d/opentsdb /etc/init.d/opentsdb + fi fi exit 0 %postun - if [ $1 -eq 0 ]; then # we're removing last version of this package - rm -rf /etc/opentsdb - rm -rf /etc/init.d/opentsdb + [ -L /etc/opentsdb ] && rm -f /etc/opentsdb + [ -L /etc/init.d/opentsdb ] && rm -f /etc/init.d/opentsdb + [ -L /lib/systemd/system/opentsdb@.service ] && rm -f /lib/systemd/system/opentsdb@.service + [ -d /run/systemd/system ] && systemctl daemon-reload fi exit 0 From e2ea14f8830adffaf61b778d2ad1c2e34b4e0469 Mon Sep 17 00:00:00 2001 From: Zhong Chaoqiang <35595648+ZhongChaoqiang@users.noreply.github.com> Date: Mon, 10 Dec 2018 11:22:37 +0800 Subject: [PATCH 731/826] Parameter sync_timeout don't perform when details and summary are not set.(#1283) (#1288) --- src/tsd/PutDataPointRpc.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index fbb4ccdb7b..56318ba9f4 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -580,11 +580,11 @@ public void run(final Timeout timeout) throws Exception { writes_timedout.addAndGet(timeouts); final int failures = dps.size() - queued; if (!show_summary && !show_details) { - throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, - "The put call has timedout with " + good_writes - + " successful writes, " + failed_writes + " failed writes and " - + timeouts + " timed out writes.", - "Please see the TSD logs or append \"details\" to the put request"); + query.sendReply(HttpResponseStatus.BAD_REQUEST, query.serializer().formatErrorV1( + new BadRequestException(HttpResponseStatus.BAD_REQUEST, + "The put call has timedout with " + good_writes + " successful writes, " + + failed_writes + " failed writes and " + timeouts + " timed out writes.", + "Please see the TSD logs or append \"details\" to the put request"))); } else { final HashMap<String, Object> summary = new HashMap<String, Object>(); summary.put("success", good_writes); From c34cd6ea378ae8141bdb7c88ecfa8351db8d6a1a Mon Sep 17 00:00:00 2001 From: Great Snoopy <GreatSnoopy@gmail.com> Date: Wed, 21 Nov 2018 21:39:45 +0200 Subject: [PATCH 732/826] ISSUE-1344 Fix NPE when null is given as datapoint in a set (#1347) Signed-off-by: clarsen <clarsen@yahoo-inc.com> --- src/tsd/PutDataPointRpc.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 56318ba9f4..f5d82a7643 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -405,9 +405,13 @@ public Boolean call(final Object obj) { } try { - /** Add additionnal tags from HTTP header */ - if ( (query_tags != null) && (query_tags.size() > 0) ) { - dp.addTags(query_tags); + if (dp == null) { + if (show_details) { + details.add(this.getHttpDetails("Unexpected null datapoint encountered in set.", dp)); + } + LOG.warn("Datapoint null was encountered in set."); + illegal_arguments.incrementAndGet(); + continue; } if (!dp.validate(details)) { From dc315037db3015df767f6abf0e8ec9adc614ce00 Mon Sep 17 00:00:00 2001 From: whitelilis <whitelilis@gmail.com> Date: Tue, 11 Dec 2018 09:49:58 +0800 Subject: [PATCH 733/826] Bugfix: UI 'format' may contains "%", issue-1229 (#1304) * ui format may contains "%", so encode it in QueryUi.java and decode it in Plot.java * Add some comment for change. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/graph/Plot.java | 8 ++++++++ src/tsd/client/QueryUi.java | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/graph/Plot.java b/src/graph/Plot.java index 2d69db5b7b..db141ce497 100644 --- a/src/graph/Plot.java +++ b/src/graph/Plot.java @@ -15,6 +15,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -134,6 +135,13 @@ public Plot(final long start_time, final long end_time, TimeZone tz) { * </ul> */ public void setParams(final Map<String, String> params) { + // check "format y" and "format y2" + String[] y_format_keys = {"format y", "format y2"}; + for(String k : y_format_keys){ + if(params.containsKey(k)){ + params.put(k, URLDecoder.decode(params.get(k))); + } + } this.params = params; } diff --git a/src/tsd/client/QueryUi.java b/src/tsd/client/QueryUi.java index 23e4952c1c..440b1419bd 100644 --- a/src/tsd/client/QueryUi.java +++ b/src/tsd/client/QueryUi.java @@ -18,6 +18,7 @@ * virtually no exposure to the technology except through the tutorial. --tsuna */ +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -723,12 +724,12 @@ private void addLabels(final StringBuilder url) { private void addFormats(final StringBuilder url) { final String yformat = this.yformat.getText(); if (!yformat.isEmpty()) { - url.append("&yformat=").append(yformat); + url.append("&yformat=").append(URL.encode(yformat)); } if (y2format.isEnabled()) { final String y2format = this.y2format.getText(); if (!y2format.isEmpty()) { - url.append("&y2format=").append(y2format); + url.append("&y2format=").append(URL.encode(y2format)); } } } From 13128461d34833cfec04a68f664466d304ba3432 Mon Sep 17 00:00:00 2001 From: Eduardo95 <df14@software.nju.edu.cn> Date: Fri, 16 Nov 2018 10:30:08 +0800 Subject: [PATCH 734/826] Extend an aggregator of SquareSum, which returns the square sum of the data point. This might be useful in some scientific calculations add some tests for squareSum Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/Aggregators.java | 38 ++++++++++++++++++++++++++++++++++ test/core/TestAggregators.java | 25 ++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/core/Aggregators.java b/src/core/Aggregators.java index 0475490f63..531f0121c9 100644 --- a/src/core/Aggregators.java +++ b/src/core/Aggregators.java @@ -102,6 +102,9 @@ public enum Interpolation { public static final Aggregator MIMMAX = new Max( Interpolation.MIN, "mimmax"); + /** Aggregator that returns the square sum of the data point. */ + public static final Aggregator SQUARESUM = new SquareSum(Interpolation.ZIM, "squareSum"); + /** Aggregator that returns the number of data points. * WARNING: This currently interpolates with zero-if-missing. In this case * counts will be off when counting multiple time series. Only use this when @@ -187,6 +190,7 @@ public enum Interpolation { aggregators.put("first", FIRST); aggregators.put("last", LAST); aggregators.put("pfsum", PFSUM); + aggregators.put("squareSum", SQUARESUM); PercentileAgg[] percentiles = { p999, p99, p95, p90, p75, p50, @@ -256,6 +260,40 @@ public double runDouble(final Doubles values) { } + private static final class SquareSum extends Aggregator { + public SquareSum(final Interpolation method, final String name) { + super(method, name); + } + + @Override + public long runLong(final Longs values) { + long a = values.nextLongValue(); + long result = a * a; + while (values.hasNextValue()) { + a = values.nextLongValue(); + result += a * a; + } + return result; + } + + @Override + public double runDouble(final Doubles values) { + double result = 0.; + long n = 0L; + + while (values.hasNextValue()) { + final double val = values.nextDoubleValue(); + if (!Double.isNaN(val)) { + result += val * val; + ++n; + } + } + + return (0L == n) ? Double.NaN : result; + } + + } + private static final class Min extends Aggregator { public Min(final Interpolation method, final String name) { super(method, name); diff --git a/test/core/TestAggregators.java b/test/core/TestAggregators.java index bba62f2ec7..54273dea7c 100644 --- a/test/core/TestAggregators.java +++ b/test/core/TestAggregators.java @@ -257,4 +257,29 @@ private void assertAggregatorEquals(long value, Aggregator agg, Numbers numbers) } numbers.reset(); } + + @Test + public void testSquareSumFewDataInputs(){ + final long[] longValues = new long[2]; + for (int i = 0; i < longValues.length; i++) { + longValues[i] = i + 1; + } + + Numbers values = new Numbers(longValues); + assertAggregatorEquals(5, net.opentsdb.core.Aggregators.get("squareSum"), values); + } + + @Test + public void testSquareSumRandomInputs(){ + final long[] longValues = new long[100]; + long summ = 0; + for (int i = 0; i < longValues.length; i++) { + long temp = random.nextLong(); + longValues[i] = temp; + summ += temp * temp; + } + + Numbers values = new Numbers(longValues); + assertAggregatorEquals(summ, net.opentsdb.core.Aggregators.get("squareSum"), values); + } } From ed56c5a8251922364640efe7bca7568bf3626638 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Mon, 10 Dec 2018 19:05:51 -0700 Subject: [PATCH 735/826] Segment compact and repair commands to improve performance (#1433) * Add repair wrapper * store as object * Move log message * Fix the rest of the logging * Don't try to repair deleted metrics and better logging * Try again immediately, rather than after a full run * Actually track failed metrics * Chunk repairs into 1 hour increments to reduce 'timeout' condition * Better chunking and some comments on weird code blocks * Return failed metrics correctly * Only log chunks when they take a long time * Spit repairs even further (30 minutes by default) * Improve logging * More updates to repair tool * Track finished metrics between runs... * Only write success when success happens * Better warnings around slow chunks * Multiprocessing of metrics (speeds things up) * flake8 fixes * Global counter to track progress * Configurable threads and track failed metrics * Don't require compaction * 'uid metrics' command doesn't reliably exit, use 'uid grep metrics' instead * Split compact and repair commands, as they seem to cause scanner problems in HBase 2.0 * Slightly better logging and default to hour chunks (better compact?) * Actually kill subprocesses when timeout is reached * flake8 fixes * Allow for time chunks larger than 60, but still ensure alignment * Simplify to list comprehension * More threads, increasing timeouts for bigger metrics, and actually kill timed out subprocesses * Nicer log message and fix bad setting * Better logging Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- tools/repair-tsd | 166 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 117 insertions(+), 49 deletions(-) diff --git a/tools/repair-tsd b/tools/repair-tsd index 43573dcdbf..2da6c7653d 100755 --- a/tools/repair-tsd +++ b/tools/repair-tsd @@ -2,11 +2,15 @@ from subprocess import Popen, PIPE, TimeoutExpired, check_output from random import shuffle -from multiprocessing import Pool, Value +from pprint import pformat +import signal +import os +from multiprocessing import Pool, Value, cpu_count import copy import time from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter import logging +import math log = logging.getLogger("repair-tsd") log.setLevel(logging.INFO) @@ -19,6 +23,24 @@ metric_count = Value('i', 0) failed_count = Value('i', 0) +def get_large_divisors(num): + """ + Get "large" divisors of num + i.e. only numbers that, when multiplied, are > than num + + e.g. 60 -> 10, 12, 15, 20, 30, 60 + + * Because while 1, 2, 3, 4, 6 are all divisors, they are too small + when mutiplied together + + :param int num: The number to check + :returns: all large divisors + :rtype: list + """ + return [int(num / i) for i in range(1, int(math.sqrt(num)) + 1) + if num % i == 0 and i * i != num] + + def get_metrics(args): """ Collect all metrics from OpenTSDB @@ -26,7 +48,7 @@ def get_metrics(args): :returns: all metrics :rtype: list """ - time_chunk = args.get("time_chunk", 15) + time_chunk = args.get("time_chunk", 60) multiplier = int(60 / time_chunk) time_range = args.get("time_range", 48) tsd_path = args.get("tsd_path", "/usr/share/opentsdb/bin/tsdb") @@ -34,7 +56,7 @@ def get_metrics(args): use_sudo = args.get("use_sudo", False) sudo_user = args.get("sudo_user", "opentsdb") base = "{} fsck --config={}".format(tsd_path, cfg_path) - check_cmd = "{} uid --config={} metrics".format(tsd_path, cfg_path) + check_cmd = "{} uid --config={} grep metrics".format(tsd_path, cfg_path) if use_sudo: base = "sudo -u {} {}".format(sudo_user, base) check_cmd = "sudo -u {} {}".format(sudo_user, check_cmd) @@ -46,9 +68,9 @@ def get_metrics(args): for m in results[0].decode().split("\n") if m] metriclist = [m for m in metriclist if m and m != "\x00"] metricobj = {"time_chunk": time_chunk, - "timeout": int(time_chunk * 60), + "timeout": int((time_chunk * 60) / 2), "retries": args.get("retries", 1), - "compact": args.get("compact", 1), + "compact": args.get("compact", False), "multiplier": multiplier, "metriccount": len(metriclist), "chunk_count": time_range * multiplier, @@ -64,6 +86,62 @@ def get_metrics(args): return metrics +def _process_metric_chunk(metric, chunk, x, cmd, timeout): + """ + Actually run the cli command to repair this chunk. + we segment this so the calls to both the compact and + regular runs can be somewhat consistently managed + + :param str metric: name of the metric we're processing + :param int chunk: Which time segment we're processing + :param int x: which attempt for the time segment we're on + :param str cmd: The actual command we'll run + :param int timeout: How long the command is allowed to run + :returns: whether the command was successful + :rtype: bool + """ + log.debug("Running command: {}".format(cmd)) + metricproc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, + preexec_fn=os.setsid) + try: + results, err = metricproc.communicate(timeout=timeout) + except TimeoutExpired: + msg = "{}: chunk {} failed (timeout: {}) (run {})".format(metric, + chunk, + timeout, + x) + log.warning(msg) + try: + os.killpg(os.getpgid(metricproc.pid), signal.SIGTERM) + except Exception as e: + log.warning("Couldn't kill subprocess :: {}".format(e)) + return False + except Exception as e: + log.error("{} general exception :: {}".format(metric, e)) + try: + os.killpg(os.getpgid(metricproc.pid), signal.SIGTERM) + except Exception as e: + log.warning("Couldn't kill subprocess :: {}".format(e)) + return False + results = [r for r in results.decode().split("\n") if r][-26:] + final_results = [] + """ + We'll only collect results that are non-0 + since we're not super interested in stuff that didn't change. + """ + for r in results: + # Strip the timestamp from the log line + line = r.split(" ")[6:] + try: + if int(line[-1]) != 0: + final_results.append(" ".join(line)) + except Exception: + final_results.append(" ".join(line)) + result_str = "\n".join(final_results) + log.debug("{} results:\n{}".format(metric, result_str)) + return True + + def repair_metric_chunk(metricobj, chunk): """ Repair one 'chunk' of data for a metric @@ -81,47 +159,26 @@ def repair_metric_chunk(metricobj, chunk): timestr = "{}m-ago {}m-ago".format((chunk + 1) * time_chunk, chunk * time_chunk) cmd = "{} {} sum".format(base, timestr) + fullcmd = "{} {} --delete-bad-compacts --delete-bad-rows \ + --delete-bad-values --delete-unknown-columns \ + --delete-orphans --resolve-duplicates --fix".format(cmd, + metric) + ccmd = "{} {} --fix --compact".format(cmd, metric) """ Even though we're chunking, it's worth trying things more than once """ for x in range(1, metricobj["retries"] + 2): log.debug("Repair try {} for {}".format(x, timestr)) - if compact: - fullcmd = "{} {} --fix-all --compact".format(cmd, metric) - else: - fullcmd = "{} {} --fix-all".format(cmd, metric) - log.debug("Full command: {}".format(fullcmd)) - metricproc = Popen(fullcmd, shell=True, stdout=PIPE, stderr=PIPE) - try: - results, err = metricproc.communicate(timeout=timeout) - except TimeoutExpired: - log.warning("{}: chunk {} failed in window (run {})".format(metric, - chunk, - x)) + if not _process_metric_chunk(metric, chunk, x, fullcmd, timeout * x): continue - except Exception as e: - log.error("{} general exception :: {}".format(metric, - e)) - else: - results = [r for r in results.decode().split("\n") if r][-26:] - final_results = [] - """ - We'll only collect results that are non-0 - since we're not super interested in stuff that didn't change. - """ - for r in results: - # Strip the timestamp from the log line - line = r.split(" ")[6:] - try: - if int(line[-1]) != 0: - final_results.append(" ".join(line)) - except Exception: - final_results.append(" ".join(line)) - result_str = "\n".join(final_results) - log.debug("{} results:\n{}".format(metric, result_str)) - if chunk % 20 == 0: - log.debug("Chunk {} of {} finished".format(chunk, chunk_count)) - return None + if compact: + if not _process_metric_chunk(metric, chunk, x, ccmd, timeout * x): + continue + if chunk % 20 == 0: + log.info("{} -> Chunk {} of {} finished".format(metric, + chunk, + chunk_count)) + return None else: log.error("Failed to completely repair {}".format(metric)) return metric @@ -134,7 +191,7 @@ def process_metric(metricobj): metric = metricobj["metric"] chunk_count = metricobj["chunk_count"] try: - check_output("{} {}".format(metricobj["check_cmd"], metric), + check_output("{} \"^{}$\"".format(metricobj["check_cmd"], metric), shell=True) except Exception: log.warning("{} doesn't exist! Skipping...".format(metric)) @@ -150,8 +207,8 @@ def process_metric(metricobj): runtime = time.time() - start_time with metric_count.get_lock(): metric_count.value += 1 - line = "{} repair took {} seconds".format(metric, - int(runtime)) + line = "COMPLETE: {} repair took {} seconds".format(metric, + int(runtime)) line += " ({} of {} metrics complete)".format(metric_count.value, metricobj["metriccount"]) line += " ({} failed)".format(failed_count.value) @@ -162,7 +219,6 @@ def process_metrics(metric_list, threads): threads = Pool(threads) failed_metrics = threads.map(process_metric, metric_list) failed_metrics = [m for m in failed_metrics if m] - log.warning("Failed metrics: {}".format(failed_metrics)) return failed_metrics @@ -173,7 +229,7 @@ def cli_opts(): help="Show debug information") parser.add_argument("--time-range", default="48", help="How many hours of time we collect to repair") - parser.add_argument("--time-chunk", default="15", + parser.add_argument("--time-chunk", default="60", help="How many minutes of data to scan per chunk") parser.add_argument("--retries", default="1", help="How many times we should try failed metrics") @@ -192,7 +248,7 @@ def cli_opts(): parser.add_argument("--shuffle", action="store_true", default=False, help="Mix up incoming metric order") - parser.add_argument("--threads", default="4", + parser.add_argument("--threads", default="{}".format(int(cpu_count() / 2)), help="Total number of metrics to process at once") parser.add_argument("--sudo-user", default="opentsdb", help="User to switch to...") @@ -201,6 +257,7 @@ def cli_opts(): def main(): args = cli_opts() + chunks = get_large_divisors(60) if args.debug: log.setLevel(logging.DEBUG) try: @@ -217,8 +274,12 @@ def main(): log.error("Invalid thread count {} :: {}".format(args.threads, e)) try: time_chunk = int(args.time_chunk) - if 60 % time_chunk != 0: - raise ArithmeticError + if time_chunk < 60: + if time_chunk not in chunks: + raise ArithmeticError + if time_chunk > 60: + if not any(n for n in chunks if time_chunk % chunk == 0): + raise ArithmeticError except Exception as e: log.error("Invalid time chunk {} :: {}".format(args.time_chunk, e)) @@ -232,7 +293,14 @@ def main(): "shuffle": args.shuffle, "compact": args.compact, "retries": retries}) - process_metrics(metric_list, threads) + stime = time.time() + failed = process_metrics(metric_list, threads) + etime = time.time() + log.info("Processed {} metrics in [{}] seconds".format(len(metric_list), + etime - stime)) + if len(failed) > 0: + log.warning("{} failed metrics:\n{}".format(len(failed), + pformat(sorted(failed)))) if __name__ == "__main__": From 99b64ebb92870ebb69702a7ab25794ad67e09145 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Tue, 11 Dec 2018 10:11:12 +0800 Subject: [PATCH 736/826] ISSUE #1442 (#1452) 1. Add error callbacks to fsck fix operations to log out errors. 2. Add new options to FsckOptions. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tools/Fsck.java | 127 +++++++++++++++++++++++++++++-------- src/tools/FsckOptions.java | 34 ++++++++++ 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 6ed8f05871..8347000d69 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -32,6 +32,7 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import net.opentsdb.core.AppendDataPoints; @@ -154,7 +155,7 @@ public void runFullTable() throws Exception { final List<Thread> threads = new ArrayList<Thread>(scanners.size()); int i = 0; for (final Scanner scanner : scanners) { - final FsckWorker worker = new FsckWorker(scanner, i++); + final FsckWorker worker = new FsckWorker(scanner, i++, this.options); worker.setName("Fsck #" + i); worker.start(); threads.add(worker); @@ -193,7 +194,7 @@ public void runQueries(final List<Query> queries) throws Exception { final List<Thread> threads = new ArrayList<Thread>(scanners.size()); int i = 0; for (final Scanner scanner : scanners) { - final FsckWorker worker = new FsckWorker(scanner, i++); + final FsckWorker worker = new FsckWorker(scanner, i++, this.options); worker.setName("Fsck #" + i); worker.start(); threads.add(worker); @@ -232,6 +233,23 @@ long correctable() { fixable_compacted_columns.get() + value_encoding.get(); } + /** + * Log all Throwables + */ + final class GeneralErrCallBack implements Callback<Deferred<Object>, Exception> { + private Object[] parameters; + + GeneralErrCallBack(Object... parameters) { + this.parameters = parameters; + } + + @Override + public Deferred<Object> call(Exception arg) throws Exception { + LOG.error("when: %s, something went wrong: %s", parameters, arg); + throw arg; + } + } + /** * A worker thread that takes a query or a chunk of the main data table and * performs the actual FSCK process. @@ -247,6 +265,8 @@ final class FsckWorker extends Thread { * previously processed row keys */ final Set<String> tsuids = new HashSet<String>(); + final FsckOptions options; + /** Shared flags and values for compiling a compacted column */ byte[] compact_qualifier = null; int qualifier_index = 0; @@ -261,10 +281,11 @@ final class FsckWorker extends Thread { * @param scanner The scanner to use for iterationg * @param thread_id Id of the thread this worker is assigned for logging */ - FsckWorker(final Scanner scanner, final int thread_id) { + FsckWorker(final Scanner scanner, final int thread_id, final FsckOptions options) { this.scanner = scanner; this.thread_id = thread_id; query = null; + this.options = options; } /** @@ -354,7 +375,11 @@ private void fsckRow(final ArrayList<KeyValue> row, LOG.error("Invalid qualifier, must be on 2 bytes or more.\n\t" + kv); if (options.fix() && options.deleteUnknownColumns()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } unknown_fixed.getAndIncrement(); } continue; @@ -374,7 +399,11 @@ private void fsckRow(final ArrayList<KeyValue> row, "of bytes.\n\t" + kv); if (options.fix() && options.deleteUnknownColumns()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } unknown_fixed.getAndIncrement(); } continue; @@ -463,7 +492,11 @@ private void fsckRow(final ArrayList<KeyValue> row, LOG.error(e.getMessage()); if (options.fix() && options.deleteBadCompacts()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } bad_compacted_columns_deleted.getAndIncrement(); } } @@ -509,7 +542,11 @@ private boolean fsckKey(final byte[] key) throws Exception { if (options.fix() && options.deleteBadRows()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } bad_key_fixed.getAndIncrement(); } return false; @@ -529,7 +566,11 @@ private boolean fsckKey(final byte[] key) throws Exception { if (options.fix() && options.deleteOrphans()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } orphans_fixed.getAndIncrement(); } return false; @@ -545,7 +586,11 @@ private boolean fsckKey(final byte[] key) throws Exception { if (options.fix() && options.deleteOrphans()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), key); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } orphans_fixed.getAndIncrement(); } return false; @@ -670,11 +715,15 @@ private void fsckDataPoints(final Map<Long, ArrayList<DP>> datapoints) duplicates_fixed_comp.getAndIncrement(); } else if (!dp.compacted) { LOG.debug("Removing duplicate data point: " + dp.kv); - tsdb.getClient().delete( - new DeleteRequest( - tsdb.dataTable(), dp.kv.key(), dp.kv.family(), dp.qualifier() - ) - ); + DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), + dp.kv.key(), + dp.kv.family(), + dp.qualifier()); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } duplicates_fixed.getAndIncrement(); } } @@ -809,13 +858,17 @@ private boolean fsckFloat(final DP dp) throws Exception { final float value_as_float = Float.intBitsToFloat(Bytes.getInt(value, 4)); value = Bytes.fromInt( - Float.floatToRawIntBits((float)value_as_float)); + Float.floatToRawIntBits(value_as_float)); if (compact_row || options.compact()) { appendDP(qual, value, 4); } else if (!dp.compacted){ - final PutRequest put = RequestBuilder.buildPutRequest(tsdb.getConfig(), tsdb.dataTable(), - dp.kv.key(), dp.kv.family(), qual, value, dp.kv.timestamp()); - tsdb.getClient().put(put); + final PutRequest put = new PutRequest(tsdb.dataTable(), + dp.kv.key(), dp.kv.family(), qual, value); + Deferred<Object> operation_result = tsdb.getClient().put(put); + operation_result.addErrback(new GeneralErrCallBack(put)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } } else { LOG.error("SHOULDN'T be here as we didn't compact or fix a " + "single value"); @@ -834,7 +887,11 @@ private boolean fsckFloat(final DP dp) throws Exception { if (options.fix() && options.deleteBadValues() && !dp.compacted) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } bad_values_deleted.getAndIncrement(); } else if (dp.compacted) { LOG.error("The value was in a compacted column. This should " @@ -853,13 +910,17 @@ private boolean fsckFloat(final DP dp) throws Exception { final float value_as_float = Float.intBitsToFloat(Bytes.getInt(value, 4)); value = Bytes.fromInt( - Float.floatToRawIntBits((float)value_as_float)); + Float.floatToRawIntBits(value_as_float)); if (compact_row || options.compact()) { appendDP(qual, value, 4); } else if (!dp.compacted) { final PutRequest put = new PutRequest(tsdb.dataTable(), dp.kv.key(), dp.kv.family(), qual, value); - tsdb.getClient().put(put); + Deferred<Object> operation_result = tsdb.getClient().put(put); + operation_result.addErrback(new GeneralErrCallBack(put)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } } else { LOG.error("SHOULDN'T be here as we didn't compact or fix a single value"); } @@ -876,7 +937,11 @@ private boolean fsckFloat(final DP dp) throws Exception { + " was only " + value.length + " bytes.\n\t" + dp.kv); if (options.fix() && options.deleteBadValues() && !dp.compacted) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } bad_values_deleted.getAndIncrement(); } else if (dp.compacted) { LOG.error("The previous value was in a compacted column. This should " @@ -892,7 +957,11 @@ private boolean fsckFloat(final DP dp) throws Exception { + " bytes.\n\t" + dp.kv); if (options.fix() && options.deleteBadValues() && !dp.compacted) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } bad_values_deleted.getAndIncrement(); } else if (dp.compacted) { LOG.error("The previous value was in a compacted column. This should " @@ -937,7 +1006,11 @@ private boolean fsckInteger(final DP dp) throws Exception { + "should be " + length + " bytes.\n\t" + dp.kv); if (options.fix() && options.deleteBadValues()) { final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } bad_values_deleted.getAndIncrement(); } else if (dp.compacted) { LOG.error("The previous value was in a compacted column. This should " @@ -981,7 +1054,11 @@ private boolean fsckInteger(final DP dp) throws Exception { tsdb.getClient().put(put).joinUninterruptibly(); final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(), dp.kv.key(), dp.kv.family(), qual); - tsdb.getClient().delete(delete); + Deferred<Object> operation_result = tsdb.getClient().delete(delete); + operation_result.addErrback(new GeneralErrCallBack(delete)); + if (options.fixInSync()) { + operation_result.join(options.getFixTimeout()); + } } vle_fixed.getAndIncrement(); } // don't return true here as we don't consider a VLE an error. diff --git a/src/tools/FsckOptions.java b/src/tools/FsckOptions.java index 1f0a8b9cbb..9112b45564 100644 --- a/src/tools/FsckOptions.java +++ b/src/tools/FsckOptions.java @@ -28,6 +28,8 @@ final class FsckOptions { private boolean delete_bad_rows; private boolean delete_bad_compacts; private int threads; + private long fix_timeout; // fix timeout for each operation results, time unit: milliseconds + private boolean fix_in_sync_mode = false; // wait for each fix operation to finish to continue /** * Default Ctor that sets the options based on command line flags and config @@ -52,6 +54,8 @@ public FsckOptions(final ArgP argp, final Config config) { argp.has("--fix-all"); delete_bad_compacts = argp.has("--delete-bad-compacts") || argp.has("--fix-all"); + fix_in_sync_mode = argp.has("--sync"); + if (argp.has("--threads")) { threads = Integer.parseInt(argp.get("--threads")); if (threads < 1) { @@ -221,4 +225,34 @@ public void setThreads(final int threads) { } this.threads = threads; } + + /** + * @param timeout The maximum time to wait in milliseconds. A value of 0 + * means no timeout. + */ + public void setFixTimeout(long timeout) { + this.fix_timeout = timeout; + } + + /** + * @return timeout + */ + public long getFixTimeout() { + return this.fix_timeout; + } + + /** + * @param flag Wheather to wait for the result of the fix operation. + */ + public void setFixInSync(boolean flag) { + this.fix_in_sync_mode = flag; + } + + /** + * Wait for each fix operation to finish to continue. + * @return true if in sync mode + */ + public boolean fixInSync() { + return this.fix_in_sync_mode; + } } From 69e14d97a97ab449167a8570a43f27dde03f81e7 Mon Sep 17 00:00:00 2001 From: Hari Krishna Dara <haridara@gmail.com> Date: Tue, 11 Dec 2018 07:54:56 +0530 Subject: [PATCH 737/826] Support environment variables to specify a custom DATA_BLOCK_ENCODING and TTL (#587) --- src/create_table.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/create_table.sh b/src/create_table.sh index ad01f623c6..fdbe5e57a9 100755 --- a/src/create_table.sh +++ b/src/create_table.sh @@ -19,6 +19,8 @@ BLOOMFILTER=${BLOOMFILTER-'ROW'} COMPRESSION=${COMPRESSION-'LZO'} # All compression codec names are upper case (NONE, LZO, SNAPPY, etc). COMPRESSION=`echo "$COMPRESSION" | tr a-z A-Z` +DATA_BLOCK_ENCODING=${DATA_BLOCK_ENCODING-'NONE'} +TSDB_TTL=${TSDB_TTL-'FOREVER'} case $COMPRESSION in (NONE|LZO|GZIP|SNAPPY) :;; # Known good. @@ -34,15 +36,15 @@ hbh=$HBASE_HOME unset HBASE_HOME exec "$hbh/bin/hbase" shell <<EOF create '$UID_TABLE', - {NAME => 'id', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'}, - {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 'id', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'}, + {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'} create '$TSDB_TABLE', - {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING', TTL => '$TSDB_TTL'} create '$TREE_TABLE', - {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'} create '$META_TABLE', - {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'} EOF From 1b161684d59c9fd6c0e29fc5d368918b48ba067b Mon Sep 17 00:00:00 2001 From: Hari Krishna Dara <haridara@gmail.com> Date: Tue, 11 Dec 2018 07:54:56 +0530 Subject: [PATCH 738/826] Support environment variables to specify a custom DATA_BLOCK_ENCODING and TTL (#587) --- src/create_table.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/create_table.sh b/src/create_table.sh index ad01f623c6..fdbe5e57a9 100755 --- a/src/create_table.sh +++ b/src/create_table.sh @@ -19,6 +19,8 @@ BLOOMFILTER=${BLOOMFILTER-'ROW'} COMPRESSION=${COMPRESSION-'LZO'} # All compression codec names are upper case (NONE, LZO, SNAPPY, etc). COMPRESSION=`echo "$COMPRESSION" | tr a-z A-Z` +DATA_BLOCK_ENCODING=${DATA_BLOCK_ENCODING-'NONE'} +TSDB_TTL=${TSDB_TTL-'FOREVER'} case $COMPRESSION in (NONE|LZO|GZIP|SNAPPY) :;; # Known good. @@ -34,15 +36,15 @@ hbh=$HBASE_HOME unset HBASE_HOME exec "$hbh/bin/hbase" shell <<EOF create '$UID_TABLE', - {NAME => 'id', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'}, - {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 'id', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'}, + {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'} create '$TSDB_TABLE', - {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING', TTL => '$TSDB_TTL'} create '$TREE_TABLE', - {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 't', VERSIONS => 1, COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'} create '$META_TABLE', - {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER'} + {NAME => 'name', COMPRESSION => '$COMPRESSION', BLOOMFILTER => '$BLOOMFILTER', DATA_BLOCK_ENCODING => '$DATA_BLOCK_ENCODING'} EOF From dfe34f069849471b08cc976bc08c68187a0d669c Mon Sep 17 00:00:00 2001 From: chaotian <iver85@gmail.com> Date: Tue, 11 Dec 2018 10:36:10 +0800 Subject: [PATCH 739/826] The symbolic of static content should use relative path (#349) * Update Makefile.am The symbolic of static content should use relative path * Set the abs_srcdir and abs_builddir in runtime instead of compiling time Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- Makefile.am | 2 +- tsdb.in | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index db7e89dd1b..d3ce9287e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -618,7 +618,7 @@ gwttsd: staticroot $(mkdir_p) $(DEV_TSD_STATICROOT) cp $(dist_static_DATA:%=$(srcdir)/%) $(DEV_TSD_STATICROOT) find -L $(DEV_TSD_STATICROOT) -type l -exec rm {} \; - p=`pwd`/gwt/queryui && cd $(DEV_TSD_STATICROOT) \ + p=../gwt/queryui && cd $(DEV_TSD_STATICROOT) \ && for i in $$p/*; do ln -s -f "$$i" || break; done find -L $(DEV_TSD_STATICROOT)/gwt -type f | xargs touch @touch .staticroot-stamp diff --git a/tsdb.in b/tsdb.in index b68eaf2c89..346ebbde8a 100644 --- a/tsdb.in +++ b/tsdb.in @@ -6,8 +6,8 @@ mydir=`dirname "$0"` # Either: # abs_srcdir and abs_builddir are set: we're running in a dev tree # or pkgdatadir is set: we've been installed, we respect that. -abs_srcdir='@abs_srcdir@' -abs_builddir='@abs_builddir@' +abs_srcdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.." +abs_builddir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" pkgdatadir='@pkgdatadir@' configdir='@configdir@' # Either we've been installed and pkgdatadir exists, or we haven't been From 5b987f9c4fb67c9df12ec7679d282f1a759fc8fb Mon Sep 17 00:00:00 2001 From: chaotian <iver85@gmail.com> Date: Tue, 11 Dec 2018 10:36:10 +0800 Subject: [PATCH 740/826] The symbolic of static content should use relative path (#349) * Update Makefile.am The symbolic of static content should use relative path * Set the abs_srcdir and abs_builddir in runtime instead of compiling time Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- Makefile.am | 2 +- tsdb.in | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index d255445d6e..c311463adc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -548,7 +548,7 @@ gwttsd: staticroot $(mkdir_p) $(DEV_TSD_STATICROOT) cp $(dist_static_DATA:%=$(srcdir)/%) $(DEV_TSD_STATICROOT) find -L $(DEV_TSD_STATICROOT) -type l -exec rm {} \; - p=`pwd`/gwt/queryui && cd $(DEV_TSD_STATICROOT) \ + p=../gwt/queryui && cd $(DEV_TSD_STATICROOT) \ && for i in $$p/*; do ln -s -f "$$i" || break; done find -L $(DEV_TSD_STATICROOT)/gwt -type f | xargs touch @touch .staticroot-stamp diff --git a/tsdb.in b/tsdb.in index b68eaf2c89..346ebbde8a 100644 --- a/tsdb.in +++ b/tsdb.in @@ -6,8 +6,8 @@ mydir=`dirname "$0"` # Either: # abs_srcdir and abs_builddir are set: we're running in a dev tree # or pkgdatadir is set: we've been installed, we respect that. -abs_srcdir='@abs_srcdir@' -abs_builddir='@abs_builddir@' +abs_srcdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.." +abs_builddir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" pkgdatadir='@pkgdatadir@' configdir='@configdir@' # Either we've been installed and pkgdatadir exists, or we haven't been From a9c735b97241205aa8d62ff69895ba183d4e8a06 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 10 Dec 2018 22:00:55 -0800 Subject: [PATCH 741/826] Fix #1436 by passing the base_timestamp to the put request function. Thanks to @kennethleider Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/BatchedDataPoints.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/BatchedDataPoints.java b/src/core/BatchedDataPoints.java index 12d990204d..f21a70d23f 100644 --- a/src/core/BatchedDataPoints.java +++ b/src/core/BatchedDataPoints.java @@ -137,6 +137,8 @@ public Deferred<Object> persist() { final byte[] q = Arrays.copyOfRange(batched_qualifier, 0, qualifier_index); final byte[] v = Arrays.copyOfRange(batched_value, 0, value_index); final byte[] r = Arrays.copyOfRange(row_key, 0, row_key.length); + final long base_time = this.base_time; // shadow fixes issue #1436 + System.out.println(Arrays.toString(q) + " " + Arrays.toString(v) + " " + Arrays.toString(r)); reset(); return tsdb.put(r, q, v, base_time); } From 072a825d9105f4205043fec619fbde639818348a Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sat, 15 Oct 2016 14:46:47 -0700 Subject: [PATCH 742/826] Fix #572 and #877 by allowing for a config flag that allows for importing out of order timestamps. Any timestamp that was out of order will simply be redirected to the standard TSDB call. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/IncomingDataPoints.java | 25 ++++++++++++++++--------- src/core/TSDB.java | 2 +- src/utils/Config.java | 1 + 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 6f6245b780..4789662a7e 100644 --- a/src/core/IncomingDataPoints.java +++ b/src/core/IncomingDataPoints.java @@ -47,6 +47,9 @@ final class IncomingDataPoints implements WritableDataPoints { /** The {@code TSDB} instance we belong to. */ private final TSDB tsdb; + + /** Whether or not to allow out of order data. */ + private final boolean allow_out_of_order_data; /** * The row key. Optional salt + 3 bytes for the metric name, 4 bytes for @@ -88,11 +91,8 @@ final class IncomingDataPoints implements WritableDataPoints { */ IncomingDataPoints(final TSDB tsdb) { this.tsdb = tsdb; - // the qualifiers and values were meant for pre-compacting the rows. We - // could implement this later, but for now we don't need to track the values - // as they'll just consume space during an import - // this.qualifiers = new short[3]; - // this.values = new long[3]; + allow_out_of_order_data = tsdb.getConfig() + .getBoolean("tsd.core.bulk.allow_out_of_order_timestamps"); } /** @@ -284,10 +284,17 @@ private Deferred<Object> addPointInternal(final long timestamp, // always maintain last_ts in milliseconds if ((ms_timestamp ? timestamp : timestamp * 1000) <= last_ts) { - throw new IllegalArgumentException("New timestamp=" + timestamp - + " is less than or equal to previous=" + last_ts - + " when trying to add value=" + Arrays.toString(value) + " to " - + this); + if (allow_out_of_order_data) { + // as we don't want to perform any funky calculations to find out if + // we're still in the same time range, just pass it off to the regular + // TSDB add function. + return tsdb.addPointInternal(metric, timestamp, value, tags, flags); + } else { + throw new IllegalArgumentException("New timestamp=" + timestamp + + " is less than or equal to previous=" + last_ts + + " when trying to add value=" + Arrays.toString(value) + " to " + + this); + } } /** Callback executed for chaining filter calls to see if the value diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 4a01e9606b..25d11cf3d8 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -946,7 +946,7 @@ public Deferred<Object> addPoint(final String metric, tags, flags); } - private Deferred<Object> addPointInternal(final String metric, + Deferred<Object> addPointInternal(final String metric, final long timestamp, final byte[] value, final Map<String, String> tags, diff --git a/src/utils/Config.java b/src/utils/Config.java index 782bf36430..5929a3d3cd 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -502,6 +502,7 @@ protected void setDefaults() { default_map.put("tsd.core.preload_uid_cache.max_entries", "300000"); default_map.put("tsd.core.storage_exception_handler.enable", "false"); default_map.put("tsd.core.uid.random_metrics", "false"); + default_map.put("tsd.core.bulk.allow_out_of_order_timestamps", "false"); default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); From 52198cc0bc6e64e26e7f089df5c76feffd1898f7 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 10 Dec 2018 22:50:45 -0800 Subject: [PATCH 743/826] Fix #1326 by parsing out the `use_meta` query param for searches. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tsd/SearchRpc.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 64b3ee38f7..d4bc3c9ee8 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -152,6 +152,11 @@ private final SearchQuery parseQueryString(final HttpQuery query, } } + if (query.hasQueryStringParam("use_meta")) { + search_query.setUseMeta(Boolean.parseBoolean( + query.getQueryStringParam("use_meta"))); + } + return search_query; } From 49d6289879e89e90ec1526313af8a2c9bf82fa66 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Mon, 10 Dec 2018 22:50:45 -0800 Subject: [PATCH 744/826] Fix #1326 by parsing out the `use_meta` query param for searches. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tsd/SearchRpc.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsd/SearchRpc.java b/src/tsd/SearchRpc.java index 64b3ee38f7..d4bc3c9ee8 100644 --- a/src/tsd/SearchRpc.java +++ b/src/tsd/SearchRpc.java @@ -152,6 +152,11 @@ private final SearchQuery parseQueryString(final HttpQuery query, } } + if (query.hasQueryStringParam("use_meta")) { + search_query.setUseMeta(Boolean.parseBoolean( + query.getQueryStringParam("use_meta"))); + } + return search_query; } From 65cf06b76d4519abadf4d4b7cfb79c22824b0764 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 11 Dec 2018 15:06:36 -0800 Subject: [PATCH 745/826] Make the clean cache script a bit more OS agnostic fixing #1268. Thanks @myg821561935 --- tools/clean_cache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/clean_cache.sh b/tools/clean_cache.sh index 3b1e0fe7f7..1dad8046bf 100755 --- a/tools/clean_cache.sh +++ b/tools/clean_cache.sh @@ -8,5 +8,5 @@ diskSpaceIsShort() { } if diskSpaceIsShort; then - ( cd ${CACHE_DIR} && find . -x -exec rm {} \; ) + ( cd ${CACHE_DIR} && find . -type f -exec rm {} \; ) fi From e106175dcc7295b2b59fa310b1c48ab2ab793906 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 11 Dec 2018 15:06:36 -0800 Subject: [PATCH 746/826] Make the clean cache script a bit more OS agnostic fixing #1268. Thanks @myg821561935 --- tools/clean_cache.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/clean_cache.sh b/tools/clean_cache.sh index 3b1e0fe7f7..1dad8046bf 100755 --- a/tools/clean_cache.sh +++ b/tools/clean_cache.sh @@ -8,5 +8,5 @@ diskSpaceIsShort() { } if diskSpaceIsShort; then - ( cd ${CACHE_DIR} && find . -x -exec rm {} \; ) + ( cd ${CACHE_DIR} && find . -type f -exec rm {} \; ) fi From d43a3d95c1d3585217e30a5869f0413a5e35aabe Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 11 Dec 2018 17:32:13 -0800 Subject: [PATCH 747/826] Fix #1124 by setting the default timestamp to Long.MAX_VALUE instead of min value. --- src/tools/Fsck.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index 8347000d69..fe97bf2e1e 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -618,7 +618,7 @@ private void fsckDataPoints(final Map<Long, ArrayList<DP>> datapoints) boolean has_milliseconds = false; boolean has_duplicates = false; boolean has_uncorrected_value_error = false; - long timestamp = Long.MIN_VALUE; + long timestamp = Long.MAX_VALUE; for (final Map.Entry<Long, ArrayList<DP>> time_map : datapoints.entrySet()) { if (key == null) { @@ -676,7 +676,7 @@ private void fsckDataPoints(final Map<Long, ArrayList<DP>> datapoints) } unique_columns.put(dp_to_keep.kv.qualifier(), dp_to_keep.kv.value()); - timestamp = Math.max(timestamp, dp_to_keep.kv.timestamp()); + timestamp = Math.min(timestamp, dp_to_keep.kv.timestamp()); valid_datapoints.getAndIncrement(); has_uncorrected_value_error |= Internal.isFloat(dp_to_keep.qualifier()) ? fsckFloat(dp_to_keep) : fsckInteger(dp_to_keep); From d342301898ccc3edaee18d711a6d9684007d16b8 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Tue, 11 Dec 2018 18:01:33 -0800 Subject: [PATCH 748/826] Fix #1083 where the rollup filter wasn't set to MUST_PASS_ALL for a single aggregation. Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/core/TsdbQuery.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 530bc53b6b..a0dd841d6a 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1421,17 +1421,19 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { // it. If not, then we can do this if (!rollup_query.getRollupAgg().toString().equals("avg")) { if (existing != null) { - final List<ScanFilter> filters = new ArrayList<ScanFilter>(3); + final List<ScanFilter> filters = new ArrayList<ScanFilter>(2); filters.add(existing); - filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + final List<ScanFilter> rollup_filters = new ArrayList<ScanFilter>(2); + rollup_filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator(rollup_query.getRollupAgg().toString() .getBytes(Const.ASCII_CHARSET)))); - filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, + rollup_filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator(new byte[] { (byte) tsdb.getRollupConfig().getIdForAggregator( rollup_query.getRollupAgg().toString()) }))); - scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ONE)); + filters.add(new FilterList(rollup_filters, Operator.MUST_PASS_ONE)); + scanner.setFilter(new FilterList(filters, Operator.MUST_PASS_ALL)); } else { final List<ScanFilter> filters = new ArrayList<ScanFilter>(2); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, @@ -1451,7 +1453,7 @@ protected Scanner getScanner(final int salt_bucket) throws HBaseException { filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryPrefixComparator("count".getBytes()))); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, - new BinaryPrefixComparator(new byte[] { + new BinaryPrefixComparator(new byte[] { (byte) tsdb.getRollupConfig().getIdForAggregator("sum") }))); filters.add(new QualifierFilter(CompareFilter.CompareOp.EQUAL, From 93aaac45ea77f26022802dfeed223f12fa5a690e Mon Sep 17 00:00:00 2001 From: Zephyr Guo <gzh1992n@gmail.com> Date: Wed, 12 Dec 2018 10:25:16 +0800 Subject: [PATCH 749/826] COMMON: (#1453) - Use DIFF encoding by default - Verify DATA_BLOCK_ENCODING variable Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/create_table.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/create_table.sh b/src/create_table.sh index fdbe5e57a9..1cbe666319 100755 --- a/src/create_table.sh +++ b/src/create_table.sh @@ -19,7 +19,10 @@ BLOOMFILTER=${BLOOMFILTER-'ROW'} COMPRESSION=${COMPRESSION-'LZO'} # All compression codec names are upper case (NONE, LZO, SNAPPY, etc). COMPRESSION=`echo "$COMPRESSION" | tr a-z A-Z` -DATA_BLOCK_ENCODING=${DATA_BLOCK_ENCODING-'NONE'} +# DIFF encoding is very useful for OpenTSDB's case that many small KVs and common prefix. +# 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'} case $COMPRESSION in @@ -29,6 +32,13 @@ case $COMPRESSION in ;; esac +case $DATA_BLOCK_ENCODING in + (NONE|PREFIX|DIFF|FAST_DIFF|ROW_INDEX_V1) :;; # Know good + (*) + echo >&2 "warning: encoding '$DATA_BLOCK_ENCODING' might not be supported." + ;; +esac + # HBase scripts also use a variable named `HBASE_HOME', and having this # variable in the environment with a value somewhat different from what # they expect can confuse them in some cases. So rename the variable. From 54601d9df306ec3d58357f48ee760adc06281fd5 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sun, 16 Dec 2018 15:31:26 -0800 Subject: [PATCH 750/826] Cut 2.3.2. Thanks to everyone who helped with this one. --- NEWS | 25 +++++++++++++++++++++++++ THANKS | 9 ++++++++- configure.ac | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index beb38949ec..bf54e42d0d 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,30 @@ OpenTSDB - User visible changes. +* Version 2.3.2 (2018-12-16) + +Noteworthy Changes: + - A new Python wrapper script to make FSCK repair runs easier. + - Track performance in the Nagios/Icinga script + - Add a Contributions file. + - Add a config, 'tsd.core.bulk.allow_out_of_order_timestamps' to allow out of + order timestamps for bulk ingest. + - NOTE: This version also includes a JDK 8 compiled version of Jackson due to + security patches. If you need to run with an older JDK please replace the + Jackson JARs with older versions. + +Bug Fixes: + - Unwrap NoSuchUniqueIds when writing data points to make it easier to understand + exceptions. + - Fix an NPE in the PutDataPointRpc class if a data point in the list is null. + - Fix a Makefile error in the clean portion. + - Fix an NPOE in the UIDManager print result. + - Fix a bug in the UI where Y formats may contain a percent sign. + - Allow specifying the data block encoding and TTL in the HBase table creation + script. + - Change the make and TSDB scripts to use relative paths. + - Fix parsing of `use_meta` from the URI for the search endpoint. + - Fix the clean cache script to be a bit more OS agnostic. + * Version 2.3.1 (2018-04-21) Noteworthy Changes: diff --git a/THANKS b/THANKS index 716add80e9..fe9daee443 100644 --- a/THANKS +++ b/THANKS @@ -24,15 +24,18 @@ Bryan Zubrod <bzubrod@adknowledge.com> Camden Narzt Can Zhang Carlos Devoto +Chaotian Chris McClymont <chris@mcclymont.it> Cristian Sechel Christophe Furmaniak Dave Barr <dave.barr@gmail.com> Davide D Amico Dfsklar +Eric Price Ethan Wang Filippo Giunchedi <fgiunchedi@gmail.com> Gabriel Nicolas Avellaneda +GreatSnoopy Guenther Schmuelling <schmuell@pepperdata.com> Haiyang Jiang Hari Krishna Dara @@ -53,6 +56,7 @@ Jesse Chang <jesse.chang.2@gmail.com> Jim Westfall Johan Zeeck <johan.zeeck@tre.se> Johannes Meixner +John Seekins Jonathan Works <jonathan.works@threattrack.com> Josh Thomas <josh@kickbackpoints.com> Jsbali @@ -84,6 +88,7 @@ Neil Fordyce Nikhil Benesch <me@designbynikhil.com> Nitin Aggarwal Opsun +Qu Dong Fang Paula Keezer <paula.keezer@gmail.com> Peter Edwards Peter Gotz <peter.s.goetz@googlemail.com> @@ -108,6 +113,8 @@ Utkarsh Bhatnagar Vasiliy Kiryanov <vasiliy.kiryanov@gmail.com> Vitaliy Fuks William Kronmiller +White Lilis Xiayang Yulai Fu -Zachary Kurey \ No newline at end of file +Zachary Kurey +Zephyr Guo \ No newline at end of file diff --git a/configure.ac b/configure.ac index b5b3a3f435..4a137d3e18 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.3.1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.3.2], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 14ab3ef8a865816cf920aa69f2e019b7261a7847 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Sun, 16 Dec 2018 18:37:30 -0800 Subject: [PATCH 751/826] Cut the 2.4.0 release. --- NEWS | 55 ++++++++++++++++++++++++++++++++++++++++------------ THANKS | 6 +++++- configure.ac | 2 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/NEWS b/NEWS index 7454ccfa5f..be2f210687 100644 --- a/NEWS +++ b/NEWS @@ -1,19 +1,34 @@ OpenTSDB - Changelog -* Version 2.4.0 RC2 (2017-10-08) +* Version 2.4.0 (2018-12-16) Noteworthy Changes: - - Modify the RPC handler plugin system so that it parses only the first part of - the URI instead of the entire path. Now plugins can implement sub-paths. - - Return the HTML 5 doctype for built-in UI pages - - Add an optional byte and/or data point limit to the amount of data fetched - from storage. This allows admins to prevent OOMing TSDs due to massive queries. - - Allow a start time via config when enabling the date tiered compaction in HBase - - Provide the option of using an LRU for caching UIDs to avoid OOMing writers and - readers with too many strings - - Optionally avoid writing to the forward or reverse UID maps when a specific TSD - operational mode is enabled to avoid wasting memory on maps that will never be - used. + - Set default data block encoding to `DIFF` in the create table script. + - Add callbacks to log errors in the FSCK tool when a call was made to + fix something. + - Add a sum of squares aggregator "squareSum". + - Add the diff aggregator that computes the difference between the first + and last values. + - Add a SystemD template to the RPM package. + - Allow tags to be added via HTTP header. + - Add example implementations for the Authorization and Authentication + plugins. + - Change `tsd.storage.use_otsdb_timestamp` to default to false. + - Literal or filter now allows single character values. + - Rollup query code now only uses the downsampler value to pick an interval. + - Add jdk 8 in the debian script. + - Setup fill policies in the Nagios check + +Bug Fixes: + - Fix rollup scanner filter for single aggregate queries. + - Fix FSCK HBase timestamps when deduping. Sometimes they were negative. + - Fix exception handling when writing data over HTTP with the sync flag enabled. + - Fix missing source files in the Makefile. + - Change UID cache to longs from ints and add hit and miss counters. + - Fix HighestCurrent returning the wrong results. + - Fix running query stats queryStart timestamp to millis. + - Fix TimeShift millisecond bug. + - Fix post remove step in the debian package. * Version 2.3.2 (2018-12-16) @@ -40,6 +55,22 @@ Bug Fixes: - Fix parsing of `use_meta` from the URI for the search endpoint. - Fix the clean cache script to be a bit more OS agnostic. + +* Version 2.4.0 RC2 (2017-10-08) + +Noteworthy Changes: + - Modify the RPC handler plugin system so that it parses only the first part of + the URI instead of the entire path. Now plugins can implement sub-paths. + - Return the HTML 5 doctype for built-in UI pages + - Add an optional byte and/or data point limit to the amount of data fetched + from storage. This allows admins to prevent OOMing TSDs due to massive queries. + - Allow a start time via config when enabling the date tiered compaction in HBase + - Provide the option of using an LRU for caching UIDs to avoid OOMing writers and + readers with too many strings + - Optionally avoid writing to the forward or reverse UID maps when a specific TSD + operational mode is enabled to avoid wasting memory on maps that will never be + used. + * Version 2.3.1 (2018-04-21) Noteworthy Changes: diff --git a/THANKS b/THANKS index a732d4ce56..0d0a7fec73 100644 --- a/THANKS +++ b/THANKS @@ -21,6 +21,7 @@ Arvind Jayaprakash <work@anomalizer.net> Berk D. Demir <bdd@mindcast.org> Bikrant Neupane Bizhu Qiu +BHourlier Bryan Hernandez <bryan4887@gmail.com> Bryan Zubrod <bzubrod@adknowledge.com> Camden Narzt @@ -60,6 +61,7 @@ Jesse Chang <jesse.chang.2@gmail.com> Jim Westfall Johan Zeeck <johan.zeeck@tre.se> Johannes Meixner +John Ewing John Seekins Jonathan Works <jonathan.works@threattrack.com> Josh Thomas <josh@kickbackpoints.com> @@ -95,6 +97,7 @@ Nicole Nagele <nicole.nagele@uni-ak.ac.at> Neil Fordyce Nikhil Benesch <me@designbynikhil.com> Nitin Aggarwal +NoHarm Opsun Qu Dong Fang Paula Keezer <paula.keezer@gmail.com> @@ -126,4 +129,5 @@ White Lilis Xiayang Yulai Fu Zachary Kurey -Zephyr Guo \ No newline at end of file +Zephyr Guo +Zong Chaoqiang \ No newline at end of file diff --git a/configure.ac b/configure.ac index 659a5cd976..536f8e1115 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.4.0RC2], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.4.0], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 55f388c1c4e37438bb9261778da606e9e152f289 Mon Sep 17 00:00:00 2001 From: qudongfang <qudongfang@users.noreply.github.com> Date: Thu, 10 Jan 2019 13:33:58 +0800 Subject: [PATCH 752/826] Bugfix of FsckOptions. (#1464) Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- src/tools/FsckOptions.java | 1 + 1 file changed, 1 insertion(+) 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 From 3fc2d491c3c1ad397252c0a80203a69a3f9e3ef3 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Sun, 27 Jan 2019 13:32:06 -0700 Subject: [PATCH 753/826] always write cli tools to stdout (#1488) Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- build-aux/deb/logback.xml | 3 +++ build-aux/rpm/logback.xml | 3 +++ src/logback.xml | 3 +++ 3 files changed, 9 insertions(+) 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 @@ <logger name="org.apache.zookeeper" level="INFO"/> <logger name="org.hbase.async" level="INFO"/> <logger name="com.stumbleupon.async" level="INFO"/> + <logger name="net.opentsdb.tools" level="INFO"> + <appender-ref ref="STDOUT"/> + </logger> <!-- Fallthrough root logger and router --> <root level="INFO"> 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 @@ <logger name="org.apache.zookeeper" level="INFO"/> <logger name="org.hbase.async" level="INFO"/> <logger name="com.stumbleupon.async" level="INFO"/> + <logger name="net.opentsdb.tools" level="INFO"> + <appender-ref ref="STDOUT"/> + </logger> <!-- Fallthrough root logger and router --> <root level="INFO"> 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 @@ <logger name="org.apache.zookeeper" level="INFO"/> <logger name="org.hbase.async" level="INFO"/> <logger name="com.stumbleupon.async" level="INFO"/> + <logger name="net.opentsdb.tools" level="INFO"> + <appender-ref ref="STDOUT"/> + </logger> <!-- Fallthrough root logger and router --> <root level="INFO"> From 3de8134dfdd9cd5cefbeda45e2d5989175d3a07e Mon Sep 17 00:00:00 2001 From: ABC_CODER <designer.shaoyan@gmail.com> Date: Thu, 16 May 2019 00:49:01 +0800 Subject: [PATCH 754/826] Fix OpenTSDB#1632 (#1634) --- src/core/Internal.java | 65 +- ...ueryRpcLastDataPointWhenEnableAppends.java | 957 ++++++++++++++++++ 2 files changed, 1000 insertions(+), 22 deletions(-) create mode 100644 test/tsd/TestQueryRpcLastDataPointWhenEnableAppends.java 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<Cell> extractDataPoints(final ArrayList<KeyValue> 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/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 <http://www.gnu.org/licenses/>. +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 From 674a4ccebca511b63eaa6d5fbebdbdfda5e6a234 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Wed, 29 May 2019 07:43:19 -0600 Subject: [PATCH 755/826] Add "check_tsd_v2" script (#1567) Enhanced check_tsd script evaluates each individual metric group separately when given a filter --- tools/check_tsd_v2 | 304 +++++++++++++++++++++++++++++++++++++++++++++ tools/repair-tsd | 3 - 2 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 tools/check_tsd_v2 diff --git a/tools/check_tsd_v2 b/tools/check_tsd_v2 new file mode 100644 index 0000000000..2c558a814e --- /dev/null +++ b/tools/check_tsd_v2 @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 + +from urllib import request +import json +import operator +import time +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("repair-tsd") +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: + 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&noAnnotations=true&m={}:".format(args.duration, + 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, now): + """ + 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 + :param float now: the current time (generated by time.time()) + :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(): + # handle out-of-time metrics + if args.ignore_recent: + try: + ts = float(ts) + except ValueError: + log.error("Bad timestamp for {}: {}".format(",".join(mresult["tags"]), ts)) + mresult["crit_alarm"] = True + break + delta = now - ts + if delta >= args.ignore_recent: + log.debug("Timestamp outside evaluation range: {}".format(ts)) + continue + 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) + 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: + mresult["crit_alarm"] = True + else: + mresult["crit_alarm"] = True + if mresult["warn"] > 0: + if args.percent_over > 0 and mresult["warn_percent"] > args.percent_over: + mresult["warn_alarm"] = True + else: + 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) + """ + now = time.time() + for m in _get_metrics(query, args.timeout): + yield _process_metric(m, args, comparisons, now) + + +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 that are that >= seconds ago.") + 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) + 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 not r["crit_alarm"] and not r["warn_alarm"]: + continue + 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("{} => alarmed {} times in range. ({}% 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/repair-tsd b/tools/repair-tsd index 2da6c7653d..0d96742644 100755 --- a/tools/repair-tsd +++ b/tools/repair-tsd @@ -237,8 +237,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?") @@ -289,7 +287,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}) From 8ffc084593b0ee2482837b1cf4468c13c30155c5 Mon Sep 17 00:00:00 2001 From: Simon Matic Langford <simon@exemel.co.uk> Date: Wed, 29 May 2019 14:43:58 +0100 Subject: [PATCH 756/826] Collect stats from meta cache plugin if configured (#1649) --- src/core/TSDB.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 119c207a24..ab6b95d68b 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -906,6 +906,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. */ From b5133bd365a479cbc22f7f0cda888275575a04b6 Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Wed, 29 May 2019 14:46:47 +0100 Subject: [PATCH 757/826] Fix SaltScanner race condition on spans maps (#1651) * Fix SaltScanner race condition on spans maps * Fix 1.6 compatibility --- src/core/MultiGetQuery.java | 13 +++++++------ src/core/SaltScanner.java | 26 +++++++++++++------------ src/core/TsdbQuery.java | 17 ++++++++-------- test/core/TestMultiGetQuery.java | 13 +++++++------ test/core/TestSaltScanner.java | 13 +++++++------ test/core/TestSaltScannerHistogram.java | 10 ++++++---- 6 files changed, 50 insertions(+), 42 deletions(-) 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<Integer, List<SimpleEntry<byte[], List<HistogramDataPoint>>>> histMap = Maps.newConcurrentMap(); - private final Deferred<TreeMap<byte[], Span>> results = - new Deferred<TreeMap<byte[], Span>>(); + private final Deferred<SortedMap<byte[], Span>> results = + new Deferred<SortedMap<byte[], Span>>(); - private final Deferred<TreeMap<byte[], HistogramSpan>> histogramResults = - new Deferred<TreeMap<byte[], HistogramSpan>>(); + private final Deferred<SortedMap<byte[], HistogramSpan>> histogramResults = + new Deferred<SortedMap<byte[], HistogramSpan>>(); private final ArrayList<List<MultiGetTask>> multi_get_tasks; private final ArrayList<AtomicInteger> 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<TreeMap<byte[], Span>> fetch() { + public Deferred<SortedMap<byte[], Span>> fetch() { if(tags.isEmpty()) { return Deferred.fromResult(null); } @@ -620,7 +621,7 @@ public Deferred<TreeMap<byte[], Span>> 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<TreeMap<byte[], HistogramSpan>> fetchHistogram() { + public Deferred<SortedMap<byte[], HistogramSpan>> fetchHistogram() { startFetch(); return histogramResults; } diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index aaac0eb0b2..e4d42328a5 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -12,14 +12,16 @@ // see <http://www.gnu.org/licenses/>. 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; @@ -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<byte[], Span> spans; + private final SortedMap<byte[], Span> spans; - private final TreeMap<byte[], HistogramSpan> histSpans; + private final SortedMap<byte[], HistogramSpan> histSpans; /** The list of pre-configured scanners. One scanner should be created per * salt bucket. */ @@ -93,11 +95,11 @@ public class SaltScanner { List<HistogramDataPoint>>>>(); /** A deferred to call with the spans on completion */ - private final Deferred<TreeMap<byte[], Span>> results = - new Deferred<TreeMap<byte[], Span>>(); + private final Deferred<SortedMap<byte[], Span>> results = + new Deferred<SortedMap<byte[], Span>>(); - private final Deferred<TreeMap<byte[], HistogramSpan>> histogramResults = - new Deferred<TreeMap<byte[], HistogramSpan>>(); + private final Deferred<SortedMap<byte[], HistogramSpan>> histogramResults = + new Deferred<SortedMap<byte[], HistogramSpan>>(); /** 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. */ @@ -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; @@ -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<TreeMap<byte[], Span>> scan() { + public Deferred<SortedMap<byte[], Span>> scan() { start_time = System.currentTimeMillis(); int i = 0; for (final Scanner scanner: scanners) { @@ -273,7 +275,7 @@ public Deferred<TreeMap<byte[], Span>> scan() { return results; } - public Deferred<TreeMap<byte[], HistogramSpan>> scanHistogram() { + public Deferred<SortedMap<byte[], HistogramSpan>> scanHistogram() { start_time = DateTime.currentTimeMillis(); int index = 0; @@ -528,7 +530,7 @@ public Object scan() { /** * Iterate through each row of the scanner results, parses out data * points (and optional meta data). - * @return null if no rows were found, otherwise the TreeMap with spans + * @return null if no rows were found, otherwise the SortedMap with spans */ @Override public Object call(final ArrayList<ArrayList<KeyValue>> rows) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index a0dd841d6a..88c4d91cb0 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; @@ -791,7 +792,7 @@ public boolean isHistogramQuery() { * perform the search. * @throws IllegalArgumentException if bad data was retrieved from HBase. */ - private Deferred<TreeMap<byte[], Span>> findSpans() throws HBaseException { + private Deferred<SortedMap<byte[], Span>> findSpans() throws HBaseException { final short metric_width = tsdb.metrics.width(); final TreeMap<byte[], Span> spans = // The key is a row key from HBase. new TreeMap<byte[], Span>(new SpanCmp( @@ -831,7 +832,7 @@ private Deferred<TreeMap<byte[], Span>> findSpans() throws HBaseException { } } - private Deferred<TreeMap<byte[], Span>> findSpansWithMultiGetter() throws HBaseException { + private Deferred<SortedMap<byte[], Span>> findSpansWithMultiGetter() throws HBaseException { final short metric_width = tsdb.metrics.width(); final TreeMap<byte[], Span> spans = // The key is a row key from HBase. new TreeMap<byte[], Span>(new SpanCmp(metric_width)); @@ -857,7 +858,7 @@ private Deferred<TreeMap<byte[], Span>> findSpansWithMultiGetter() throws HBaseE * perform the search. * @throws IllegalArgumentException if bad data was retreived from HBase. */ - private Deferred<TreeMap<byte[], HistogramSpan>> findHistogramSpans() throws HBaseException { + private Deferred<SortedMap<byte[], HistogramSpan>> findHistogramSpans() throws HBaseException { final short metric_width = tsdb.metrics.width(); final TreeMap<byte[], HistogramSpan> histSpans = new TreeMap<byte[], HistogramSpan>(new SpanCmp(metric_width)); @@ -896,7 +897,7 @@ private Deferred<TreeMap<byte[], HistogramSpan>> findHistogramSpans() throws HBa } } - private Deferred<TreeMap<byte[], HistogramSpan>> findHistogramSpansWithMultiGetter() throws HBaseException { + private Deferred<SortedMap<byte[], HistogramSpan>> findHistogramSpansWithMultiGetter() throws HBaseException { final short metric_width = tsdb.metrics.width(); // The key is a row key from HBase final TreeMap<byte[], HistogramSpan> histSpans = new TreeMap<byte[], HistogramSpan>(new SpanCmp(metric_width)); @@ -913,7 +914,7 @@ private Deferred<TreeMap<byte[], HistogramSpan>> findHistogramSpansWithMultiGett * {@link TsdbQuery#findSpans} to group and sort the results. */ private class GroupByAndAggregateCB implements - Callback<DataPoints[], TreeMap<byte[], Span>>{ + Callback<DataPoints[], SortedMap<byte[], Span>>{ /** * Creates the {@link SpanGroup}s to form the final results of this query. @@ -923,7 +924,7 @@ private class GroupByAndAggregateCB implements * any 'GROUP BY' formulated in this query. */ @Override - public DataPoints[] call(final TreeMap<byte[], Span> spans) throws Exception { + public DataPoints[] call(final SortedMap<byte[], Span> spans) throws Exception { if (query_stats != null) { query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, (System.nanoTime() - TsdbQuery.this.scan_start_time)); @@ -1048,7 +1049,7 @@ public DataPoints[] call(final TreeMap<byte[], Span> spans) throws Exception { * {@link TsdbQuery#findHistogramSpans} to group and sort the results. */ private class HistogramGroupByAndAggregateCB implements - Callback<DataPoints[], TreeMap<byte[], HistogramSpan>>{ + Callback<DataPoints[], SortedMap<byte[], HistogramSpan>>{ /** * Creates the {@link HistogramSpanGroup}s to form the final results of this query. @@ -1057,7 +1058,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<byte[], HistogramSpan> spans) throws Exception { + public DataPoints[] call(final SortedMap<byte[], HistogramSpan> spans) throws Exception { if (query_stats != null) { query_stats.addStat(query_index, QueryStat.QUERY_SCAN_TIME, (System.nanoTime() - TsdbQuery.this.scan_start_time)); 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<byte[], Span> results = mgq.fetch().join(); + final SortedMap<byte[], Span> 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<byte[], Span> results = mgq.fetch().join(); + final SortedMap<byte[], Span> 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<byte[], Span> results = mgq.fetch().join(); + final SortedMap<byte[], Span> 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<byte[], Span> results = mgq.fetch().join(); + final SortedMap<byte[], Span> 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<byte[], Span> results = mgq.fetch().join(); + final SortedMap<byte[], Span> 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<byte[], Span> results = mgq.fetch().join(); + final SortedMap<byte[], Span> results = mgq.fetch().join(); assertSame(spans, results); verify(client, times(4)).get(anyList()); validateSpans(); 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..da0141a259 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.scan().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.scan().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.scan().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.scan().joinUninterruptibly()).areEqual()); assertEquals(0, spans.size()); } From 6742aca42c794c0c11799972da7b53989d394e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= <bjorn.marschollek@skyscanner.net> Date: Tue, 27 Aug 2019 16:12:09 +0100 Subject: [PATCH 758/826] Synchronise the KVs list for scanner results Synchronises the list that holds the KeyValues that have been produced by the scanner callbacks. The list is accessed from multiple threads at a time and wasn't thread-safe, causing inconsistent results and partial loss of data in the response. Relates to: #1753 Resolves: #1760 --- src/core/SaltScanner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index e4d42328a5..518fbed5e4 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -464,7 +464,7 @@ final class ScannerCB implements Callback<Object, ArrayList<ArrayList<KeyValue>>> { private final Scanner scanner; private final int index; - private final List<KeyValue> kvs = new ArrayList<KeyValue>(); + private final List<KeyValue> kvs = Collections.synchronizedList(new ArrayList<KeyValue>()); private final ByteMap<List<Annotation>> annotations = new ByteMap<List<Annotation>>(); private final Set<String> skips = Collections.newSetFromMap( From ad77fdc48eaa44639a47ea27ac591001da81b7f3 Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Thu, 20 Jun 2019 13:42:54 +0100 Subject: [PATCH 759/826] Allow rollup downsample and series aggregator to be different --- src/rollup/RollupSeq.java | 4 +-- test/rollup/TestRollupSeq.java | 50 ++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) 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/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, From 4cce43ae73f07209e1fa6484d0f7e18916b2b853 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Sun, 17 Nov 2019 22:10:54 -0800 Subject: [PATCH 760/826] Fix TestSaltScannerHistogram, looks like the method was renamed and the UTs were not adjusted. --- test/core/TestSaltScannerHistogram.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index da0141a259..76d779b0fd 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -199,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(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); HistogramSpan span = spans.get(key_a); @@ -232,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(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); HistogramSpan span = spans.get(key_a); @@ -267,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(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(3, spans.size()); HistogramSpan span = spans.get(key_a); @@ -301,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(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); + assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(0, spans.size()); } From 4fc7da509600c45b9ae44594fa39798eb2a8dba6 Mon Sep 17 00:00:00 2001 From: "Sean P. Miller" <spmiller@verizonmedia.com> Date: Wed, 20 Nov 2019 02:09:06 +0000 Subject: [PATCH 761/826] Snapshot version for 2.4.1 RC. --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 536f8e1115..20b3399356 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.4.0], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.4.1-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 81d21126e42067ea944d900c6c780fd54996af2e Mon Sep 17 00:00:00 2001 From: Ronan Harmegnies <ronan.harmegnies@3ds.com> Date: Thu, 26 Dec 2019 16:04:38 +0100 Subject: [PATCH 762/826] ExplicitTags filtering with FuzzyFilters --- src/query/QueryUtil.java | 300 +++++++++++++++++++++++----- test/core/TestTsdbQueryQueries.java | 7 +- test/query/TestQueryUtil.java | 2 +- 3 files changed, 258 insertions(+), 51 deletions(-) diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 203616bd22..4361b3a4d7 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,12 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.hbase.async.ScanFilter; import org.hbase.async.Scanner; /** @@ -175,9 +175,218 @@ 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<byte[][]> 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<Entry<byte[], byte[][]>> it = row_key_literals == null ? + new ByteMap<byte[][]>().iterator() : row_key_literals.iterator(); + + while(it.hasNext()) { + Entry<byte[], byte[][]> 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. + * @return A sorted, non-empty list of FuzzyFilterPair + */ + private static List<FuzzyFilterPair> buildFuzzyFilters( + final ByteMap<byte[][]> row_key_literals) { + 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<FuzzyFilterPair> fuzzy_filter_pairs = + new ArrayList<FuzzyFilterPair>(); + + // 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 = new byte[row_key_size]; + byte[] first_fuzzy_mask = new byte[row_key_size]; + int fuzzy_offset = 0; + // 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; + } + if (row_key_literals != null) { + final Iterator<Entry<byte[], byte[][]>> it = row_key_literals.iterator(); + while(it.hasNext()) { + Entry<byte[], byte[][]> 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<name_width; i++) { + first_fuzzy_mask[fuzzy_offset++] = 0; + } + + final byte[] tag_value; + if (entry.getValue()!=null && entry.getValue().length > 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<value_width; i++) { + first_fuzzy_mask[fuzzy_offset++] = 0; + } + } else { + // not filtered with fuzzy filter -> skip + for (int i=0; i<value_width; i++) { + first_fuzzy_key[fuzzy_offset] = 0; + first_fuzzy_mask[fuzzy_offset++] = 1; + } + } + } + } + } + fuzzy_filter_pairs.add(new FuzzyFilterPair(first_fuzzy_key, first_fuzzy_mask)); + + if (row_key_literals != null) { + // generate filters for all combinations of tag values + fuzzy_offset = prefix_width; + final Iterator<Entry<byte[], byte[][]>> it = row_key_literals.iterator(); + while(it.hasNext()) { + final Entry<byte[], byte[][]> 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) { + final List<FuzzyFilterPair> duplicate_fuzzy_filters = + new ArrayList<FuzzyFilterPair>(fuzzy_filter_pairs); + for (int i=1; i<entry.getValue().length; i++) { + final byte[] tag_value = entry.getValue()[i]; + + for (FuzzyFilterPair pair: duplicate_fuzzy_filters) { + byte[] fuzzy_key = + Arrays.copyOf(pair.getRowKey(), row_key_size); + System.arraycopy(tag_value, 0, + fuzzy_key, fuzzy_offset, value_width); + + fuzzy_filter_pairs.add( + new FuzzyFilterPair(fuzzy_key, first_fuzzy_mask)); + } + } + } + fuzzy_offset += value_width; + } + } + + // Sort filters list over rowkey + Collections.sort(fuzzy_filter_pairs, new Comparator<FuzzyFilterPair>() { + @Override + public int compare(FuzzyFilterPair pair1, FuzzyFilterPair pair2) { + return Bytes.memcmp(pair1.getRowKey(), pair2.getRowKey()); + } + }); + + return fuzzy_filter_pairs; + } + /** * 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 +414,53 @@ public static void setDataTableScanFilter( return; } - 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))]; - System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, - scanner.getCurrentKey().length); - } else { - fuzzy_key = fuzzy_mask = 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)); + if (group_bys != null) { + Collections.sort(group_bys, Bytes.MEMCMP); } - if (!(explicit_tags && enable_fuzzy_filter)) { - scanner.setFilter(regex_filter); - return; - } + final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + + Const.TIMESTAMP_BYTES; - 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; + if (explicit_tags && enable_fuzzy_filter) { + final List<FuzzyFilterPair> fuzzy_filter_pairs = + buildFuzzyFilters(row_key_literals); + + // The Fuzzy Filter list is sorted: the first and last filters row key + // can be used to build a start and stop keys for the scanner + final byte[] start_key = Arrays.copyOf( + fuzzy_filter_pairs.get(0).getRowKey(), + fuzzy_filter_pairs.get(0).getRowKey().length); + System.arraycopy(scanner.getCurrentKey(), 0, start_key, 0, prefix_width); + + final byte[] stop_key = Arrays.copyOf( + fuzzy_filter_pairs.get(fuzzy_filter_pairs.size()-1).getRowKey(), + start_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(); } - idx += TSDB.tagk_width(); + + scanner.setStartKey(start_key); + scanner.setStopKey(stop_key); + scanner.setFilter(new FuzzyRowFilter(fuzzy_filter_pairs)); + } else { + final String regex = getRowKeyUIDRegex(row_key_literals, explicit_tags); + final KeyRegexpFilter regex_filter = new KeyRegexpFilter( + regex.toString(), Const.ASCII_CHARSET); + if (LOG.isDebugEnabled()) { + LOG.debug("Regex for scanner: " + scanner + ": " + + byteRegexToString(regex)); + } + + scanner.setFilter(regex_filter); } - scanner.setStopKey(stop_key); - final List<ScanFilter> filters = new ArrayList<ScanFilter>(2); - filters.add( - new FuzzyRowFilter( - new FuzzyRowFilter.FuzzyFilterPair(fuzzy_key, fuzzy_mask))); - filters.add(regex_filter); - scanner.setFilter(new FilterList(filters)); } /** diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 8acbc61bc4..06e503a821 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -33,6 +33,7 @@ import net.opentsdb.rollup.RollupInterval; import org.hbase.async.Bytes; import org.hbase.async.FilterList; +import org.hbase.async.FuzzyRowFilter; import org.hbase.async.Scanner; import org.junit.Before; import org.junit.Test; @@ -1632,7 +1633,7 @@ public void filterExplicitTagsOK() throws Exception { assertEquals(300, dps[0].aggregatedSize()); // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { - assertTrue(scanner.getFilter() instanceof FilterList); + assertTrue(scanner.getFilter() instanceof FuzzyRowFilter); } } @@ -1663,7 +1664,7 @@ public void filterExplicitTagsGroupByOK() throws Exception { assertEquals(300, dps[0].aggregatedSize()); // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { - assertTrue(scanner.getFilter() instanceof FilterList); + assertTrue(scanner.getFilter() instanceof FuzzyRowFilter); } } @@ -1689,7 +1690,7 @@ public void filterExplicitTagsMissing() throws Exception { assertEquals(0, dps.length); // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { - assertTrue(scanner.getFilter() instanceof FilterList); + assertTrue(scanner.getFilter() instanceof FuzzyRowFilter); } } diff --git a/test/query/TestQueryUtil.java b/test/query/TestQueryUtil.java index 03760e8499..31e938b55b 100644 --- a/test/query/TestQueryUtil.java +++ b/test/query/TestQueryUtil.java @@ -129,7 +129,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<byte[][]> tags = new ByteMap<byte[][]>(); tags.put(new byte[] { 0, 0, 1 }, new byte[][] { new byte[] {0, 0, 1} }); QueryUtil.setDataTableScanFilter( From e8f49005c161b1cd12c88cbc1a043ad5621cd21a Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Wed, 6 May 2020 14:52:47 -0700 Subject: [PATCH 763/826] Fix PR 1896 with the fuzzy filter list so that it will honor the regex filter and properly ignore rows that don't match the explicit filter. Also sort the fuzzy filter list in ascending order and implement a static comparator instead of instantiating one on each call. --- src/query/QueryUtil.java | 204 ++++++++++++++++------------ test/core/TestTsdbQueryQueries.java | 19 ++- test/query/TestQueryUtil.java | 2 +- 3 files changed, 134 insertions(+), 91 deletions(-) diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index 4361b3a4d7..5324e0e822 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -31,8 +31,14 @@ 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 com.google.common.base.Strings; +import com.google.common.collect.Lists; + import org.hbase.async.Scanner; /** @@ -268,10 +274,12 @@ private static String getRowKeyUIDRegex( * 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<FuzzyFilterPair> buildFuzzyFilters( - final ByteMap<byte[][]> row_key_literals) { + final ByteMap<byte[][]> 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(); @@ -287,14 +295,16 @@ private static List<FuzzyFilterPair> buildFuzzyFilters( } } final List<FuzzyFilterPair> fuzzy_filter_pairs = - new ArrayList<FuzzyFilterPair>(); + new ArrayList<FuzzyFilterPair>(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 = new byte[row_key_size]; - byte[] first_fuzzy_mask = new byte[row_key_size]; + 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) { @@ -302,87 +312,88 @@ private static List<FuzzyFilterPair> buildFuzzyFilters( first_fuzzy_mask[fuzzy_offset++] = (row_key_literals != null) ? (byte)1 : (byte)0; } - if (row_key_literals != null) { - final Iterator<Entry<byte[], byte[][]>> it = row_key_literals.iterator(); - while(it.hasNext()) { - Entry<byte[], byte[][]> entry = it.next(); - final boolean not_key = - entry.getValue() != null && entry.getValue().length == 0; + + // first pass to build the key and mask + Iterator<Entry<byte[], byte[][]>> it = row_key_literals.iterator(); + while(it.hasNext()) { + Entry<byte[], byte[][]> 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<name_width; i++) { - first_fuzzy_mask[fuzzy_offset++] = 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<name_width; i++) { + first_fuzzy_mask[fuzzy_offset++] = 0; + } - final byte[] tag_value; - if (entry.getValue()!=null && entry.getValue().length > 0) { - tag_value = entry.getValue()[0]; - } else { - tag_value = null; + final byte[] tag_value; + if (entry.getValue()!=null && entry.getValue().length > 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<value_width; i++) { + first_fuzzy_mask[fuzzy_offset++] = 0; } - if (tag_value!=null) { - System.arraycopy(tag_value, 0, - first_fuzzy_key, fuzzy_offset, value_width); - for (int i=0; i<value_width; i++) { - first_fuzzy_mask[fuzzy_offset++] = 0; - } - } else { - // not filtered with fuzzy filter -> skip - for (int i=0; i<value_width; i++) { - first_fuzzy_key[fuzzy_offset] = 0; - first_fuzzy_mask[fuzzy_offset++] = 1; - } + } else { + // not filtered with fuzzy filter -> skip + for (int i=0; i<value_width; i++) { + first_fuzzy_key[fuzzy_offset] = 0; + first_fuzzy_mask[fuzzy_offset++] = 1; } } } } fuzzy_filter_pairs.add(new FuzzyFilterPair(first_fuzzy_key, first_fuzzy_mask)); - if (row_key_literals != null) { - // generate filters for all combinations of tag values - fuzzy_offset = prefix_width; - final Iterator<Entry<byte[], byte[][]>> it = row_key_literals.iterator(); - while(it.hasNext()) { - final Entry<byte[], byte[][]> entry = it.next(); - fuzzy_offset += name_width; + // generate filters for all combinations of tag values using the first key + // as the template. + fuzzy_offset = prefix_width; + it = row_key_literals.iterator(); + while (it.hasNext()) { + final Entry<byte[], byte[][]> 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) { - final List<FuzzyFilterPair> duplicate_fuzzy_filters = - new ArrayList<FuzzyFilterPair>(fuzzy_filter_pairs); - for (int i=1; i<entry.getValue().length; i++) { - final byte[] tag_value = entry.getValue()[i]; + // 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<entry.getValue().length; i++) { + final byte[] tag_value = entry.getValue()[i]; + byte[] local_fuzzy_key = + Arrays.copyOf(first_fuzzy_key, row_key_size); + System.arraycopy(tag_value, 0, + local_fuzzy_key, fuzzy_offset, value_width); - for (FuzzyFilterPair pair: duplicate_fuzzy_filters) { - byte[] fuzzy_key = - Arrays.copyOf(pair.getRowKey(), row_key_size); - System.arraycopy(tag_value, 0, - fuzzy_key, fuzzy_offset, value_width); - - fuzzy_filter_pairs.add( - new FuzzyFilterPair(fuzzy_key, first_fuzzy_mask)); - } - } + fuzzy_filter_pairs.add( + new FuzzyFilterPair(local_fuzzy_key, first_fuzzy_mask)); } - fuzzy_offset += value_width; } + fuzzy_offset += value_width; } - + // Sort filters list over rowkey - Collections.sort(fuzzy_filter_pairs, new Comparator<FuzzyFilterPair>() { - @Override - public int compare(FuzzyFilterPair pair1, FuzzyFilterPair pair2) { - return Bytes.memcmp(pair1.getRowKey(), pair2.getRowKey()); - } - }); - + Collections.sort(fuzzy_filter_pairs, FUZZY_FILTER_CMP); return fuzzy_filter_pairs; } + /** + * Comparator that sorts the fuzzy filter list ascending based on the row + * key. + */ + private static class FuzzyFilterComparator implements Comparator<FuzzyFilterPair> { + @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. @@ -421,22 +432,26 @@ public static void setDataTableScanFilter( final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; - if (explicit_tags && enable_fuzzy_filter) { + 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<FuzzyFilterPair> fuzzy_filter_pairs = - buildFuzzyFilters(row_key_literals); - + buildFuzzyFilters(row_key_literals, fuzzy_key); + // The Fuzzy Filter list is sorted: the first and last filters row key - // can be used to build a start and stop keys for the scanner - final byte[] start_key = Arrays.copyOf( - fuzzy_filter_pairs.get(0).getRowKey(), - fuzzy_filter_pairs.get(0).getRowKey().length); - System.arraycopy(scanner.getCurrentKey(), 0, start_key, 0, prefix_width); - + // 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(), - start_key.length); - System.arraycopy(scanner.getCurrentKey(), 0, - stop_key, 0, prefix_width); + 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 @@ -447,18 +462,33 @@ public static void setDataTableScanFilter( idx += TSDB.tagk_width(); } - scanner.setStartKey(start_key); + scanner.setStartKey(fuzzy_key); scanner.setStopKey(stop_key); - scanner.setFilter(new FuzzyRowFilter(fuzzy_filter_pairs)); - } else { - final String regex = getRowKeyUIDRegex(row_key_literals, explicit_tags); - final KeyRegexpFilter regex_filter = new KeyRegexpFilter( - regex.toString(), Const.ASCII_CHARSET); + fuzzy_filter = new FuzzyRowFilter(fuzzy_filter_pairs); + } else { + fuzzy_filter = null; + } + + 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 (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); } } diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 06e503a821..e07be2e852 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -34,6 +34,7 @@ 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; @@ -1633,7 +1634,11 @@ public void filterExplicitTagsOK() throws Exception { assertEquals(300, dps[0].aggregatedSize()); // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { - assertTrue(scanner.getFilter() instanceof FuzzyRowFilter); + 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,7 +1669,11 @@ public void filterExplicitTagsGroupByOK() throws Exception { assertEquals(300, dps[0].aggregatedSize()); // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { - assertTrue(scanner.getFilter() instanceof FuzzyRowFilter); + 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,7 +1699,11 @@ public void filterExplicitTagsMissing() throws Exception { assertEquals(0, dps.length); // assert fuzzy for (final MockScanner scanner : storage.getScanners()) { - assertTrue(scanner.getFilter() instanceof FuzzyRowFilter); + 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 31e938b55b..ab04c38568 100644 --- a/test/query/TestQueryUtil.java +++ b/test/query/TestQueryUtil.java @@ -139,7 +139,7 @@ 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)); From 1d3b180b75ea9e2f7cb5b18f211c40a02e57c744 Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Mon, 26 Aug 2019 15:30:29 +0100 Subject: [PATCH 764/826] Test rollup filter fix for #1083 --- test/core/TestTsdbQueryQueries.java | 71 +++++++++++++++++++++++++++++ test/storage/MockBase.java | 9 +++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index e07be2e852..cf316977c5 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -30,6 +30,8 @@ import java.util.List; import java.util.Map; +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; @@ -1554,6 +1556,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<byte[]> families = new ArrayList<byte[]>(); + 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<String, Integer>() {{ + 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<String, String>() {{ put("host", "web01");}}, false, "1h", "sum", null); + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap<String, String>() {{ put("host", "web02");}}, false, "1h", "sum", null); + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap<String, String>() {{ put("host", "web01");}}, false, "1h", "count", null); + this.tsdb.addAggregatePoint(METRIC_STRING, start_timestamp, 42L, new HashMap<String, String>() {{ 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(Arrays.asList(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 { 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<ArrayList<ArrayList<KeyValue>>> 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; + } } } From 3db2df3cac8394b2ce01122fffab99a6b3b9eeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= <bjorn.marschollek@skyscanner.net> Date: Thu, 22 Aug 2019 16:09:54 +0100 Subject: [PATCH 765/826] Fix concurrent result reporting from scanners Fixes a concurrency bug where scanners report their results into a map and would overwrite each other's results Resolves: #1753 --- src/core/SaltScanner.java | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 518fbed5e4..2e2de6afab 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -23,8 +23,8 @@ 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; @@ -126,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.*/ @@ -237,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) { @@ -911,7 +911,7 @@ void close(final boolean ok) { if (ok && exception == null) { validateAndTriggerCallback(kvs, annotations, histograms); } else { - countdown.countDown(); + countdown.decrementAndGet(); } } } @@ -926,10 +926,8 @@ private void validateAndTriggerCallback( final Map<byte[], List<Annotation>> annotations, final List<SimpleEntry<byte[], List<HistogramDataPoint>>> histograms) { - countdown.countDown(); - final long count = countdown.getCount(); if (kvs.size() > 0) { - kv_map.put((int) count, kvs); + kv_map.put(index, kvs); } for (final byte[] key : annotations.keySet()) { @@ -941,10 +939,11 @@ private void validateAndTriggerCallback( } if (histograms.size() > 0) { - histMap.put((int) count, histograms); + histMap.put(index, histograms); } - - if (countdown.getCount() <= 0) { + + int scannersRunning = countdown.decrementAndGet(); + if (scannersRunning <= 0) { try { mergeAndReturnResults(); } catch (final Exception ex) { @@ -962,7 +961,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) { From e87e68e7ea6fc9486d077121db5f664284b1b4a6 Mon Sep 17 00:00:00 2001 From: Selim Chergui <selim.chergui.ext@veolia.com> Date: Sat, 25 Jan 2020 11:36:38 +0100 Subject: [PATCH 766/826] Update Maven jars URLs with HTTPS access --- third_party/apache/include.mk | 2 +- third_party/guava/include.mk | 2 +- third_party/gwt/include.mk | 6 +++--- third_party/hamcrest/include.mk | 2 +- third_party/hbase/include.mk | 2 +- third_party/jackson/include.mk | 6 +++--- third_party/javacc/include.mk | 2 +- third_party/javassist/include.mk | 2 +- third_party/jexl/include.mk | 6 +++--- third_party/jgrapht/include.mk | 2 +- third_party/junit/include.mk | 2 +- third_party/kryo/include.mk | 10 +++++----- third_party/logback/include.mk | 6 +++--- third_party/mockito/include.mk | 2 +- third_party/netty/include.mk | 2 +- third_party/objenesis/include.mk | 2 +- third_party/powermock/include.mk | 2 +- third_party/protobuf/include.mk | 2 +- third_party/slf4j/include.mk | 4 ++-- third_party/suasync/include.mk | 2 +- third_party/validation-api/include.mk | 2 +- third_party/zookeeper/include.mk | 2 +- 22 files changed, 35 insertions(+), 35 deletions(-) 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/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..7a0a904ce5 100644 --- a/third_party/jackson/include.mk +++ b/third_party/jackson/include.mk @@ -17,21 +17,21 @@ JACKSON_VERSION := 2.9.5 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/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/include.mk b/third_party/kryo/include.mk index d9343cc4fc..9b5a1ca9fb 100644 --- a/third_party/kryo/include.mk +++ b/third_party/kryo/include.mk @@ -15,30 +15,30 @@ KRYO_VERSION := 2.21.1 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/$(KRYO_VERSION) $(KRYO): $(KRYO).md5 set dummy "$(KRYO_BASE_URL)" "$(KRYO)"; shift; $(FETCH_DEPENDENCY) REFLECTASM_VERSION := 1.07 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/$(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 := 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/$(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/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://www.gnu.org/licenses/>. -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) From 2d55130604040fbe26b29e71e1cf2c231fd4a715 Mon Sep 17 00:00:00 2001 From: pengmengqing <pengmengqing@xiaomi.com> Date: Fri, 17 Jan 2020 11:04:27 +0800 Subject: [PATCH 767/826] Remove excess param in javadoc for RpcHandler --- src/tsd/RpcHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tsd/RpcHandler.java b/src/tsd/RpcHandler.java index 0a89c99e0e..a841e49c2a 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 */ From 29916417abb1490c718660bdb66b59a0539588f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Zettergren?= <bjorn.zettergren@deltaprojects.com> Date: Thu, 21 May 2020 23:13:13 +0200 Subject: [PATCH 768/826] Fix check_tsd_v2 (#1937) * renamed instancename of logger The previous name was copied from another script, cosmetic change only * Change behaviour of --ignore-recent option Previous option would fetch data from opentsdb from --duration seconds ago to time.now(), and then try to remove timestamps that was inside the --ignore-recent seconds ago, however the logic was flawed and it actually only included these seconds. Furthermore opentsdb supports setting an "end" parameter, so we use this to only get the data we want. for example -d 180 -I 80, would render a query parameter that looks like `?start=180s-ago&end=80s-ago`. Keeps it simple. Also added debuglogging to output the actual query sent to OpenTSDB if --debug option is enabled. * fixed logic of --percent-over parameter Previous behaviour didn't work due to wrong logic, would set "crit" or "warn" to True regardless. This change fixes that. * better output from logging Add logmessages to be consistent across alerting-scenarios, and changed format of some floats. Fixed a log messaged that displayed "crit" value where it should have been "warn" value. * Fixed bug in logic that parses results Removed an if statement that `continue`:ed the for-loop if a result was neither a `crit` or `warn` already, however this check also made the logic skip the test to see if no values were returned by opentsdb and -A flag was specified to alert in such scenarios. * changed check for timestamps type Previous behaviour was to check if a timestamp could be cast as a float, which is a bit weird, because opentsdb will return integers. I do doubt that opentsdb would return a timestamp that is not an integer to begin with, so i suspect this check is redundant, but leaving it in for now regardless, as per discussion in PR. --- tools/check_tsd_v2 | 61 ++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 29 deletions(-) mode change 100644 => 100755 tools/check_tsd_v2 diff --git a/tools/check_tsd_v2 b/tools/check_tsd_v2 old mode 100644 new mode 100755 index 2c558a814e..8d92b48e90 --- a/tools/check_tsd_v2 +++ b/tools/check_tsd_v2 @@ -3,7 +3,6 @@ from urllib import request import json import operator -import time import logging from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter @@ -16,7 +15,7 @@ AGGREGATORS = ("avg", "count", "dev", "diff", FILL_POLICIES = ("none", "nan", "null", "zero") METHODS = ("gt", "ge", "lt", "le", "eq", "ne") ALARMS = ("warn", "crit") -log = logging.getLogger("repair-tsd") +log = logging.getLogger("check_tsd_v2") log.setLevel(logging.INFO) ch = logging.StreamHandler() logformat = "%(message)s" @@ -35,6 +34,7 @@ def _get_metrics(query, timeout): :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: @@ -57,8 +57,11 @@ def build_query(args): else: query = "http" query += "://{}:{}/api/query?".format(args.host, args.port) - query += "start={}s-ago&noAnnotations=true&m={}:".format(args.duration, - args.aggregator) + 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: @@ -123,7 +126,7 @@ def build_comparisons(expressions): return sorted_comp -def _process_metric(m, args, comparisons, now): +def _process_metric(m, args, comparisons): """ Evaluate a single metric from OpenTSDB. In this case, a metric is a object containing a list @@ -133,7 +136,6 @@ def _process_metric(m, args, comparisons, now): :param dict m: the actual metric data :param dict args: all arguments needed to perform evaluations :param list comparisons: all comparison tuples - :param float now: the current time (generated by time.time()) :returns: object describing the metric evaluated and its state :rtype: dict """ @@ -149,18 +151,13 @@ def _process_metric(m, args, comparisons, now): avglist = [] for ts, d in m["dps"].items(): - # handle out-of-time metrics - if args.ignore_recent: - try: - ts = float(ts) - except ValueError: - log.error("Bad timestamp for {}: {}".format(",".join(mresult["tags"]), ts)) - mresult["crit_alarm"] = True - break - delta = now - ts - if delta >= args.ignore_recent: - log.debug("Timestamp outside evaluation range: {}".format(ts)) - continue + 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 @@ -168,17 +165,23 @@ def _process_metric(m, args, comparisons, now): 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: - mresult["crit_alarm"] = True + 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: - mresult["warn_alarm"] = True + 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 @@ -189,16 +192,15 @@ def process_metrics(query, args, comparisons): 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) """ - now = time.time() for m in _get_metrics(query, args.timeout): - yield _process_metric(m, args, comparisons, now) + yield _process_metric(m, args, comparisons) def cli_opts(): @@ -232,7 +234,7 @@ def cli_opts(): 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 that are that >= seconds ago.") + 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.") @@ -260,6 +262,9 @@ def main(): 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) @@ -270,8 +275,6 @@ def main(): total = [] for r in process_metrics(query, args, comparisons): total.append(r["tags"]) - if not r["crit_alarm"] and not r["warn_alarm"]: - continue if args.alarm_empty and r["empty"]: log.info("{} => no data returned in range.".format(",".join(r["tags"]))) crits.append(r["tags"]) @@ -285,7 +288,7 @@ def main(): warn = True alerts = r["crit"] + r["warn"] perc = r["crit_percent"] + r["warn_percent"] - log.info("{} => alarmed {} times in range. ({}% alarms). Avg. Value: {}".format(",".join(r["tags"]), + 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) From 3bf09fa59fdd877bdd61fd538e14ec3a79119b1c Mon Sep 17 00:00:00 2001 From: Benedict Jin <asdf2014@apache.org> Date: Fri, 22 May 2020 05:14:22 +0800 Subject: [PATCH 769/826] Rename maxScannerUidtoStringTime into maxScannerUidToStringTime (#1875) --- src/stats/QueryStats.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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<String, Object> 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)); From 79821b4201032bac7208d0353b95074fb790f199 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Thu, 21 May 2020 14:36:00 -0700 Subject: [PATCH 770/826] Fix the missing index from #1754 in the salt scanner. --- src/core/SaltScanner.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 2e2de6afab..b5da4e2da7 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -926,8 +926,9 @@ private void validateAndTriggerCallback( final Map<byte[], List<Annotation>> annotations, final List<SimpleEntry<byte[], List<HistogramDataPoint>>> histograms) { + int scannersRunning = countdown.decrementAndGet(); if (kvs.size() > 0) { - kv_map.put(index, kvs); + kv_map.put(scannersRunning, kvs); } for (final byte[] key : annotations.keySet()) { @@ -939,10 +940,9 @@ private void validateAndTriggerCallback( } if (histograms.size() > 0) { - histMap.put(index, histograms); + histMap.put(scannersRunning, histograms); } - int scannersRunning = countdown.decrementAndGet(); if (scannersRunning <= 0) { try { mergeAndReturnResults(); From 206ae2a0a06819326fe1351122cd5af421c9584c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= <oyvind@wergeland.org> Date: Tue, 27 Aug 2019 15:45:38 +0200 Subject: [PATCH 771/826] Force Sunday as first day of week. --- test/core/TestDownsampler.java | 12 ++++++++++-- test/utils/TestDateTime.java | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index d0024716bf..9fda1ba7ec 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); diff --git a/test/utils/TestDateTime.java b/test/utils/TestDateTime.java index 1b54e602ca..0f42843507 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; @@ -782,6 +783,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()); From 12bc21ddcc3d559c8d1ca696906888a88d056876 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Thu, 21 May 2020 16:39:15 -0700 Subject: [PATCH 772/826] Tweak TestTsdbQueryQueries to pass in older java versions. --- test/core/TestTsdbQueryQueries.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index cf316977c5..11d6cc3c4e 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -45,6 +45,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; @@ -1605,7 +1606,7 @@ public void runRollupFiltering() throws Exception { sub.setMetric(METRIC_STRING); sub.setAggregator("sum"); sub.setDownsample("1h-sum"); - sub.setFilters(Arrays.asList(new TagVLiteralOrFilter("host", TAGV_STRING))); + sub.setFilters(Lists.newArrayList(new TagVLiteralOrFilter("host", TAGV_STRING))); ts_query.setQueries(Arrays.asList(sub)); ts_query.validateAndSetQuery(); From 5b49374d7991a60286019641b8ecb2424a0f6825 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Mon, 12 Oct 2020 10:09:10 -0700 Subject: [PATCH 773/826] Fix the min case for doubles in AggregationIterator. --- src/core/AggregationIterator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From f716d9eef5b4e4e9de914f90450cf443362653bb Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Mon, 12 Oct 2020 10:18:07 -0700 Subject: [PATCH 774/826] Fix the Screw Driver config. --- screwdriver.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/screwdriver.yaml b/screwdriver.yaml index 11a4352285..f8c3ceff7e 100644 --- a/screwdriver.yaml +++ b/screwdriver.yaml @@ -1,7 +1,9 @@ shared: - image: maven + image: maven:3-adoptopenjdk-8 jobs: main: + requires: [~pr, ~commit] + timeout: 180 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 From 01768f8f18d78e4fc3e565b52fa08a11e91db20e Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Mon, 12 Oct 2020 11:25:04 -0700 Subject: [PATCH 775/826] Fix UT for JDK8 --- test/core/TestTsdbQueryQueries.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index 11d6cc3c4e..b6c0359aeb 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -30,6 +30,7 @@ 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; @@ -1606,7 +1607,7 @@ public void runRollupFiltering() throws Exception { sub.setMetric(METRIC_STRING); sub.setAggregator("sum"); sub.setDownsample("1h-sum"); - sub.setFilters(Lists.newArrayList(new TagVLiteralOrFilter("host", TAGV_STRING))); + sub.setFilters(Lists.<TagVFilter>newArrayList(new TagVLiteralOrFilter("host", TAGV_STRING))); ts_query.setQueries(Arrays.asList(sub)); ts_query.validateAndSetQuery(); From 32b9d1b2274f951c88d34a754bd8f6c73cba9541 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Mon, 12 Oct 2020 14:29:40 -0700 Subject: [PATCH 776/826] Fix the count rollup table queries wherein we weren't returning count. --- src/rollup/RollupSeq.java | 10 ++++++++-- src/tsd/PutDataPointRpc.java | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/rollup/RollupSeq.java b/src/rollup/RollupSeq.java index 4ff0903d33..fb420ab4a3 100644 --- a/src/rollup/RollupSeq.java +++ b/src/rollup/RollupSeq.java @@ -134,7 +134,6 @@ public void setRow(final KeyValue column) { //Check whether the cell is generated by same rollup aggregator if (need_count) { - System.out.println("AGG ID: " + agg_id + " COUNT ID: " + count_id + " MASK: " + (column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK)); if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == agg_id) { append(column, false, false); } else if ((column.qualifier()[0] & RollupUtils.AGGREGATOR_MASK) == count_id) { @@ -659,10 +658,17 @@ public boolean isInteger() { @Override public long valueCount() { - if (count_values == null) { + if (count_values == null && rollup_query.getRollupAgg() != Aggregators.COUNT) { // real values (sum, max, min) so just return 1. return 1; } + if (rollup_query.getRollupAgg() == Aggregators.COUNT) { + if (isInteger()) { + return longValue(); + } else { + return (long) doubleValue(); + } + } final byte flags = (byte) count_qualifier; final byte vlen = (byte) ((flags & Const.LENGTH_MASK) + 1); if ((count_qualifier & Const.FLAG_FLOAT) == 0x0) { diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index f5d82a7643..a8dd330ea5 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -539,7 +539,7 @@ public Boolean call(final Object obj) { if (show_details) { details.add(getHttpDetails("Unexpected exception", dp)); } - LOG.warn("Unexpected exception: " + dp); + LOG.warn("Unexpected exception: " + dp, e); unknown_errors.incrementAndGet(); } } From c858257a14b93187512676d05a1e4b57e7e89fb6 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Mon, 12 Oct 2020 14:34:44 -0700 Subject: [PATCH 777/826] PR for SD config. --- screwdriver.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/screwdriver.yaml b/screwdriver.yaml index f8c3ceff7e..1357d02594 100644 --- a/screwdriver.yaml +++ b/screwdriver.yaml @@ -2,8 +2,10 @@ shared: 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] - timeout: 180 steps: - run_arbitrary_script: apt-get update && apt-get install autoconf make python -y && ./build.sh pom.xml && mvn clean test --quiet From f08dd760ca5071475d2ea59d5c9f33ea09a5fa28 Mon Sep 17 00:00:00 2001 From: Tony Di Nucci <35197667+tdinucci@users.noreply.github.com> Date: Mon, 26 Oct 2020 20:39:47 +0000 Subject: [PATCH 778/826] Fix: Rollup queries with count aggregator produce unexpected results (#1895) Co-authored-by: Tony Di Nucci <tony.dinucci@skyscanner.net> --- src/core/Downsampler.java | 5 +---- src/core/FillingDownsampler.java | 5 +---- src/core/TsdbQuery.java | 3 --- test/core/TestDownsampler.java | 19 +++++++++++++++++-- test/core/TestFillingDownsampler.java | 26 ++++++++++++++++++++------ 5 files changed, 39 insertions(+), 19 deletions(-) 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/TsdbQuery.java b/src/core/TsdbQuery.java index 88c4d91cb0..762e0cd272 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1678,9 +1678,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 " diff --git a/test/core/TestDownsampler.java b/test/core/TestDownsampler.java index 9fda1ba7ec..5432b4c48c 100644 --- a/test/core/TestDownsampler.java +++ b/test/core/TestDownsampler.java @@ -1277,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(); @@ -1297,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()); } From 1d35f45925994b079d1cf1f4d2a0df67aa2ea1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= <bjorn.marschollek@skyscanner.net> Date: Wed, 24 Feb 2021 19:58:34 +0100 Subject: [PATCH 779/826] Add support for splitting rollup queries (#1853) * Add an SLA config flag for rollup intervals Adds a configuration option for rollup intervals to specify their maximum acceptable delay. Queries that cover a time between now and that maximum delay will need to query other tables for that time interval. * Add global config flag to enable splitting queries Adds a global config flag to enable splitting queries that would hit the rollup table, but the rollup table has a delay SLA configured. In that case, this feature allows splitting a query into to; one that gets the data from the rollups table until the time where it's guaranteed to be available, and the rest from the raw table. * Add a new SplitRollupQuery Adds a SplitRollupQuery class that suports splitting a rollup query into two separate queries. This is useful for when a rollup table is filled by e.g. a batch job that processes the data from the previous day on a daily basis. Rollup data for yesterday will then only be available some time today. This delay SLA can be configured on a per-table basis. The delay would specify by how much time the table can be behind real time. If a query comes in that would query data from that blackout period where data is only available in the raw table, but not yet guaranteed to be in the rollup table, the incoming query can be split into two using the SplitRollupQuery class. It wraps a query that queries the rollup table until the last guaranteed to be available timestamp based on the SLA; and one that gets the remaining data from the raw table. * Extract an AbstractQuery Extracts an AbstractQuery from the TsdbQuery implementation since we'd like to reuse some parts of it in other Query classes (in this case SplitRollupQuery) * Extract an AbstractSpanGroup * Avoid NullPointerException when setting start time Avoids a NullPointerException that happened when we were trying to set the start time on a query that would be eligible to split, but due to the SLA config only hit the raw table anyway. * Scale timestamps to milliseconds for split queries Scales all timestamps for split queries to milliseconds. It's important to maintain consistent units between all the partial queries that make up the bigger one. * Fix starting time error for split queries Fixes a bug that would happen when the start time of a query aligns perfectly with the time configured in the SLA for the delay of a rollup table. For a defined SLA, e.g. 1 day, if the start time of the query was exactly 1 day ago, the end time of the rollups part of the query would be updated and then be equal to its start time. That isn't allowed and causes a query exception. --- Makefile.am | 10 + src/core/AbstractQuery.java | 66 +++ src/core/AbstractSpanGroup.java | 70 +++ src/core/GroupCallback.java | 30 ++ src/core/Query.java | 32 +- src/core/SeekableViewChain.java | 97 ++++ src/core/SpanGroup.java | 63 +-- src/core/SplitRollupQuery.java | 480 ++++++++++++++++++ src/core/SplitRollupSpanGroup.java | 417 +++++++++++++++ src/core/TSDB.java | 27 +- src/core/TSQuery.java | 11 +- src/core/TsdbQuery.java | 187 +++++-- src/query/QueryUtil.java | 20 + src/rollup/RollupInterval.java | 45 +- src/rollup/RollupQuery.java | 23 + src/utils/Config.java | 1 + src/utils/DateTime.java | 7 +- test/core/BaseTsdbTest.java | 21 +- test/core/TestSeekableViewChain.java | 99 ++++ test/core/TestSplitRollupQuery.java | 329 ++++++++++++ test/core/TestSplitRollupSpanGroup.java | 194 +++++++ test/core/TestTSDBAddAggregatePoint.java | 1 + .../core/TestTSDBAddAggregatePointSalted.java | 1 + test/core/TestTSQuery.java | 44 +- test/core/TestTsdbQuery.java | 210 +++++++- test/query/TestQueryUtil.java | 20 + test/rollup/TestRollupConfig.java | 45 +- test/rollup/TestRollupInterval.java | 11 +- test/rollup/TestRollupQuery.java | 75 +++ test/tsd/TestRollupRpc.java | 6 +- test/utils/TestDateTime.java | 5 + 31 files changed, 2508 insertions(+), 139 deletions(-) create mode 100644 src/core/AbstractQuery.java create mode 100644 src/core/AbstractSpanGroup.java create mode 100644 src/core/GroupCallback.java create mode 100644 src/core/SeekableViewChain.java create mode 100644 src/core/SplitRollupQuery.java create mode 100644 src/core/SplitRollupSpanGroup.java create mode 100644 test/core/TestSeekableViewChain.java create mode 100644 test/core/TestSplitRollupQuery.java create mode 100644 test/core/TestSplitRollupSpanGroup.java create mode 100644 test/rollup/TestRollupQuery.java diff --git a/Makefile.am b/Makefile.am index d3ce9287e7..9bf563d304 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 \ @@ -81,11 +84,14 @@ tsdb_SRC := \ 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 \ @@ -317,8 +323,11 @@ test_SRC := \ test/core/TestRowKey.java \ test/core/TestRowSeq.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 \ @@ -376,6 +385,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 \ 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 <http://www.gnu.org/licenses/>. +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. + * <p> + * 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 + * <p> + * 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 <http://www.gnu.org/licenses/>. +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. + * <p> + * 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. + * <p> + * This is one of the rare (if not the only) implementations of + * {@link DataPoints} for which {@link #getTags} can potentially return + * an empty map. + * <p> + * 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/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 <http://www.gnu.org/licenses/>. +package net.opentsdb.core; + +import com.stumbleupon.async.Callback; + +import java.util.ArrayList; + +class GroupCallback implements Callback<Object, ArrayList<Object>> { + /** + * 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<Object> ignored) { + return null; + } +} diff --git a/src/core/Query.java b/src/core/Query.java index 3d95834004..455f641a25 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -171,7 +171,23 @@ public void setTimeSeries(final List<String> tsuids, */ public Deferred<Object> 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<Object> configureFromQuery(final TSQuery query, final int index, boolean force_raw); + /** * Downsamples the results by specifying a fixed interval between points. * <p> @@ -262,7 +278,19 @@ public Deferred<Object> 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/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 <http://www.gnu.org/licenses/>. +package net.opentsdb.core; + +import java.util.List; +import java.util.NoSuchElementException; + +public class SeekableViewChain implements SeekableView { + + private final List<SeekableView> iterators; + private int currentIterator; + + SeekableViewChain(List<SeekableView> 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 <em>view</em> 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. + * <p> + * 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 <http://www.gnu.org/licenses/>. 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<Annotation> 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<Annotation>(); 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<Span> 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 <http://www.gnu.org/licenses/>. +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<Object> rollupResolution; + private Deferred<Object> rawResolution; + + SplitRollupQuery(final TSDB tsdb, TsdbQuery rollupQuery, Deferred<Object> 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. + * <p> + * 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<String, String> 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<String, String> 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. + * <b>Note:</b> 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<String> 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. + * <b>Note:</b> 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<String> 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<Object> configureFromQuery(TSQuery query, int index) { + return configureFromQuery(query, index, false); + } + + @Override + public Deferred<Object> 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. + * <p> + * 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. + * <p> + * 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<DataPoints[]> runAsync() throws HBaseException { + Deferred<DataPoints[]> rollupResults = Deferred.fromResult(new DataPoints[0]); + if (rollupQuery != null) { + rollupResults = rollupQuery.runAsync(); + } + Deferred<DataPoints[]> 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 + * <p> + * 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<DataPoints[]> runHistogramAsync() throws HBaseException { + Deferred<DataPoints[]> rollupResults = Deferred.fromResult(new DataPoints[0]); + if (rollupQuery != null) { + rollupResults = rollupQuery.runHistogramAsync(); + } Deferred<DataPoints[]> 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<Float> percentiles) { + if (rollupQuery != null) { + rollupQuery.setPercentiles(percentiles); + } + rawQuery.setPercentiles(percentiles); + } + + private class RunCB implements Callback<DataPoints[], ArrayList<DataPoints[]>> { + + private ByteMap<SpanGroup> makeSpanGroupMap(DataPoints[] dataPointsArray) { + ByteMap<SpanGroup> 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<SpanGroup> rollupResults = makeSpanGroupMap(rollup); + ByteMap<SpanGroup> rawResults = makeSpanGroupMap(raw); + + TreeSet<byte[]> allGroups = new TreeSet<>(Bytes.MEMCMP); + allGroups.addAll(rollupResults.keySet()); + allGroups.addAll(rawResults.keySet()); + + List<SplitRollupSpanGroup> 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<DataPoints[]> 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<SpanGroup> 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<String> 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<String, String> getTags() { + Map<String, String> 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<Map<String, String>> getTagsAsync() { + class GetTagsCB implements Callback<Map<String, String>, ArrayList<Map<String, String>>> { + @Override + public Map<String, String> call(ArrayList<Map<String, String>> resolvedTags) throws Exception { + Map<String, String> tags = new HashMap<>(); + for (Map<String, String> groupTags : resolvedTags) { + tags.putAll(groupTags); + } + return tags; + } + } + + List<Deferred<Map<String, String>>> 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<byte[]> getTagUids() { + Bytes.ByteMap<byte[]> 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. + * <p> + * 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). + * <p> + * 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<String> getAggregatedTags() { + List<String> 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. + * <p> + * 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). + * <p> + * 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<List<String>> getAggregatedTagsAsync() { + class GetAggregatedTagsCB implements Callback<List<String>, ArrayList<List<String>>> { + @Override + public List<String> call(ArrayList<List<String>> resolvedTags) throws Exception { + List<String> aggregatedTags = new ArrayList<>(); + for (List<String> groupTags : resolvedTags) { + aggregatedTags.addAll(groupTags); + } + return aggregatedTags; + } + } + + List<Deferred<List<String>>> 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<byte[]> getAggregatedTagUids() { + List<byte[]> 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<String> getTSUIDs() { + List<String> 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<Annotation> getAnnotations() { + List<Annotation> annotations = new ArrayList<>(); + + for (SpanGroup group : spanGroups) { + List<Annotation> groupAnnotations = group.getAnnotations(); + if (groupAnnotations != null) { + annotations.addAll(group.getAnnotations()); + } + } + + return annotations; + } + + /** + * Returns the number of data points. + * <p> + * This method must be implemented in {@code O(1)} or {@code O(n)} + * where <code>n = {@link #aggregatedSize} > 0</code>. + * + * @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. + * <p> + * 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. + * <p> + * 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 <em>zero-copy view</em> to go through {@code size()} data points. + * <p> + * The iterator returned must return each {@link DataPoint} in {@code O(1)}. + * <b>The {@link DataPoint} returned must not be stored</b> 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<SeekableView> 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. + * <p> + * This method must be implemented in + * <code>O({@link #aggregatedSize})</code> or better. + * <p> + * It is guaranteed that <pre>timestamp(i) < timestamp(i+1)</pre> + * + * @param i + * @return A strictly positive integer. + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * <code>[0, {@link #size} - 1]</code> + */ + @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. + * <p> + * This method must be implemented in + * <code>O({@link #aggregatedSize})</code> 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 + * <code>[0, {@link #size} - 1]</code> + */ + @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. + * <p> + * This method must be implemented in + * <code>O({@link #aggregatedSize})</code> or better. + * Use {@link #iterator} to get successive {@code O(1)} accesses. + * + * @param i + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * <code>[0, {@link #size} - 1]</code> + * @throws ClassCastException if the + * <code>{@link #isInteger isInteger(i)} == false</code>. + * @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. + * <p> + * This method must be implemented in + * <code>O({@link #aggregatedSize})</code> or better. + * Use {@link #iterator} to get successive {@code O(1)} accesses. + * + * @param i + * @throws IndexOutOfBoundsException if {@code i} is not in the range + * <code>[0, {@link #size} - 1]</code> + * @throws ClassCastException if the + * <code>{@link #isInteger isInteger(i)} == true</code>. + * @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. + * <p> + * 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 ab6b95d68b..b3e5633cad 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -183,7 +183,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 +318,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 +326,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( @@ -549,7 +557,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 +574,7 @@ public void initializePlugins(final boolean init_rpcs) { public final Authentication getAuth() { return this.authentication; } - + /** * Returns the configured HBase client * @return The HBase client @@ -2124,7 +2132,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; diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index e571004d66..1d525b6c9f 100644 --- a/src/core/TSQuery.java +++ b/src/core/TSQuery.java @@ -240,8 +240,15 @@ public Deferred<Query[]> buildQueriesAsync(final TSDB tsdb) { final List<Deferred<Object>> deferreds = new ArrayList<Deferred<Object>>(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<Object> 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 762e0cd272..de2a3779a2 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -61,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); @@ -229,7 +229,7 @@ public boolean fallback() { return this == ROLLUP_FALLBACK || this == ROLLUP_FALLBACK_RAW; } } - + /** Constructor. */ public TsdbQuery(final TSDB tsdb) { this.tsdb = tsdb; @@ -429,10 +429,72 @@ public void setTimeSeries(final List<String> 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<Object> split(final TSQuery query, final int index, final TsdbQuery rawQuery) { + if (!needsSplitting()) { + throw new IllegalStateException("Query is not eligible for splitting" + this.toString()); + } + + Deferred<Object> 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<Object> configureFromQuery(final TSQuery query, - final int index) { + public Deferred<Object> configureFromQuery(final TSQuery query, + final int index) { + return configureFromQuery(query, index, false); + } + + + public Deferred<Object> configureFromQuery(final TSQuery query, + final int index, boolean force_raw) { if (query.getQueries() == null || query.getQueries().isEmpty()) { throw new IllegalArgumentException("Missing sub queries"); } @@ -477,7 +539,7 @@ public Deferred<Object> 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()); } @@ -576,7 +638,7 @@ private List<Deferred<byte[]>> resolveTagFilters() { return deferreds; } } - + // fire off the callback chain by resolving the metric first return tsdb.metrics.getIdAsync(sub_query.getMetric()) .addCallbackDeferring(new MetricCB()); @@ -587,7 +649,7 @@ private List<Deferred<byte[]>> 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); } /** @@ -704,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<DataPoints[]> runAsync() throws HBaseException { Deferred<DataPoints[]> result = null; + if (use_multi_gets && override_multi_get) { result = this.findSpansWithMultiGetter().addCallback(new GroupByAndAggregateCB()); } else { @@ -777,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 @@ -838,8 +903,8 @@ private Deferred<SortedMap<byte[], Span>> findSpansWithMultiGetter() throws HBas new TreeMap<byte[], Span>(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(); @@ -954,7 +1019,8 @@ public DataPoints[] call(final SortedMap<byte[], Span> spans) throws Exception { getStartTime(), getEndTime(), query_index, - rollup_query); + rollup_query, + null); group.add(span); groups[i++] = group; } @@ -974,7 +1040,8 @@ public DataPoints[] call(final SortedMap<byte[], Span> 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); } @@ -1018,6 +1085,11 @@ public DataPoints[] call(final SortedMap<byte[], Span> 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, @@ -1025,11 +1097,8 @@ public DataPoints[] call(final SortedMap<byte[], Span> 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()); @@ -1503,7 +1572,7 @@ else if (pre_aggregate) { } /** 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(); @@ -1545,7 +1614,7 @@ private long getScanStartTimeSeconds() { } /** Returns the UNIX timestamp at which we must stop scanning. */ - private long getScanEndTimeSeconds() { + long getScanEndTimeSeconds() { // Begin with the raw query end time. long end = getEndTime(); @@ -1823,6 +1892,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/query/QueryUtil.java b/src/query/QueryUtil.java index 5324e0e822..bf19f2e46b 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -639,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/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/utils/Config.java b/src/utils/Config.java index f92f2f5b56..723df36c7a 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -577,6 +577,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"); 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/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 <http://www.gnu.org/licenses/>. +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<SeekableView> 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<SeekableView> 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<SeekableView> 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 <http://www.gnu.org/licenses/>. +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<String> 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<String> 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<Float> 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<String> 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 <http://www.gnu.org/licenses/>. +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<SpanGroup> 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<byte[]> uids1 = new Bytes.ByteMap<>(); + uids1.put(new byte[]{0, 0, 1}, new byte[]{0, 0, 2}); + final Bytes.ByteMap<byte[]> 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<byte[]> 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<byte[]> uids1 = new ArrayList<>(); + uids1.add(new byte[]{0, 0, 1}); + final List<byte[]> uids2 = new ArrayList<>(); + uids2.add(new byte[]{0, 0, 2}); + + when(rollupSpanGroup.getAggregatedTagUids()).thenReturn(uids1); + when(rawSpanGroup.getAggregatedTagUids()).thenReturn(uids2); + + List<byte[]> 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<SeekableView> { + 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/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..e564e62560 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -12,22 +12,17 @@ // see <http://www.gnu.org/licenses/>. 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; @@ -42,6 +37,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 +51,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 @@ -356,6 +361,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 +591,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<TSSubQuery> sub_queries = new ArrayList<TSSubQuery>(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<TSSubQuery> sub_queries = new ArrayList<TSSubQuery>(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/query/TestQueryUtil.java b/test/query/TestQueryUtil.java index ab04c38568..ca0d12f25d 100644 --- a/test/query/TestQueryUtil.java +++ b/test/query/TestQueryUtil.java @@ -12,6 +12,8 @@ // see <http://www.gnu.org/licenses/>. 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; @@ -145,4 +149,20 @@ public void setDataTableScanFilterEnableBoth() throws Exception { 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/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<Object> 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 <http://www.gnu.org/licenses/>. +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/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/utils/TestDateTime.java b/test/utils/TestDateTime.java index 0f42843507..cee9866487 100644 --- a/test/utils/TestDateTime.java +++ b/test/utils/TestDateTime.java @@ -414,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() { From 7924d4d3fff5fc07705aab3bbc5f2ce14482506b Mon Sep 17 00:00:00 2001 From: Hari Sekhon <harisekhon@gmail.com> Date: Wed, 24 Feb 2021 21:44:36 +0000 Subject: [PATCH 780/826] TSDB list running queries script (#1692) * added tsdb_list_running_queries.py * updated error handling * updated tsdb_list_running_queries.py * various cleanup, removal of old code etc * switched to custom ConnectionException * added comment for now_length Co-authored-by: Hari Sekhon <Hari.Sekhon@gresearch.co.uk> --- tools/tsdb_list_running_queries.py | 140 +++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 tools/tsdb_list_running_queries.py 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) From d4a598f6065226fe73136c69291906068a470e87 Mon Sep 17 00:00:00 2001 From: Brandon Dutra <brandondutra@google.com> Date: Wed, 24 Feb 2021 14:21:34 -0800 Subject: [PATCH 781/826] Update include.mk (#1550) The / at the end of url makes url in this form https://repo1.maven.org/maven2/com/pythian/opentsdb/asyncbigtable/0.3.0//asyncbigtable-0.3.0-jar-with-dependencies.jar but that "//" in the url makes a 404! --- third_party/asyncbigtable/include.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From ab2067ed7913c16a43107f98c938af285a68329d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Fri, 19 Mar 2021 14:26:06 -0700 Subject: [PATCH 782/826] Start supporting rollups in FSCK. --- src/tools/Fsck.java | 143 +++++++++++++++++++++- src/tools/FsckOptions.java | 6 - test/tools/TestFsckWRollups.java | 204 +++++++++++++++++++++++++++++++ 3 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 test/tools/TestFsckWRollups.java diff --git a/src/tools/Fsck.java b/src/tools/Fsck.java index fe97bf2e1e..1881290f4a 100644 --- a/src/tools/Fsck.java +++ b/src/tools/Fsck.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2014 The OpenTSDB Authors. +// Copyright (C) 2014-2021 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 @@ -32,6 +32,7 @@ import org.hbase.async.PutRequest; import org.hbase.async.Scanner; +import com.google.common.base.Strings; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; @@ -46,6 +47,8 @@ import net.opentsdb.core.TSDB; import net.opentsdb.core.Tags; import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.rollup.RollupUtils; import net.opentsdb.uid.NoSuchUniqueId; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.Config; @@ -363,6 +366,7 @@ private void fsckRow(final ArrayList<KeyValue> row, final long base_time = Bytes.getUnsignedInt(row.get(0).key(), Const.SALT_WIDTH() + TSDB.metrics_width()); + NextKV: for (final KeyValue kv : row) { kvs_processed.getAndIncrement(); // these are not final as they may be modified when fixing is enabled @@ -385,8 +389,8 @@ private void fsckRow(final ArrayList<KeyValue> row, continue; } - // All data point columns have an even number of bytes, so if we find - // one that has an odd length, it could be an OpenTSDB object or it + // Almost all data point columns have an even number of bytes, so if we + // find one that has an odd length, it could be an OpenTSDB object or it // could be junk that made it into the table. if (qual.length % 2 != 0) { // If this test fails, the column is not a TSDB object such as an @@ -413,6 +417,7 @@ private void fsckRow(final ArrayList<KeyValue> row, // or interface. // TODO - perform validation of the annotation if (qual[0] == Annotation.PREFIX()) { + // TODO - don't increment if we have a rollup instead! annotations.getAndIncrement(); continue; } else if (qual[0] == AppendDataPoints.APPEND_COLUMN_PREFIX) { @@ -427,6 +432,106 @@ private void fsckRow(final ArrayList<KeyValue> row, LOG.error("Unexpected exception processing append data point: " + kv, e); } continue; + } else if (tsdb.getRollupConfig() != null) { + // it could be a rollup with numeric prefix. Maybe possibly. + int agg_id = qual[0]; + String aggregation = tsdb.getRollupConfig().getAggregatorForId(agg_id); + if (!Strings.isNullOrEmpty(aggregation)) { + StringBuilder buffer = null; + boolean marked_value_as_bad = false; + boolean marked_value_as_good = false; + for (RollupInterval interval : tsdb.getRollupConfig().getIntervals()) { + long offset = RollupUtils.getOffsetFromRollupQualifier(qual, 1, interval); + if (offset < 0 || offset >= interval.getIntervals()) { + // definitely not a rollup for this interval. + continue; + } + + // TODO - need to set the table if we add full support for walking + // the rollup tables too. + boolean wrong_table = !Bytes.equals(tsdb.dataTable(), interval.getTemporalTable()); + long timestamp = RollupUtils.getTimestampFromRollupQualifier(qual, base_time, interval, 1); + short len = Internal.getValueLengthFromQualifier(qual, 1); + len++; + short flags = Internal.getFlagsFromQualifier(qual, 1); + String err = null; + try { + if (Internal.isFloat(qual, 1)) { + Internal.extractFloatingPointValue(value, 0, (byte) flags); + } else { + Internal.extractIntegerValue(value, 0, (byte) flags); + } + if (!marked_value_as_good) { + valid_datapoints.incrementAndGet(); + marked_value_as_good = true; + } + } catch (IllegalDataException e) { + if (!marked_value_as_bad) { + bad_values.incrementAndGet(); + marked_value_as_bad = true; + } + err = e.getMessage(); + } + + // now see if something was wrong + boolean early_ts = timestamp / 1000 < base_time; + boolean late_ts = (timestamp / 1000) >= + base_time + (interval.getIntervals() * interval.getIntervalSeconds()); + + if (err == null && !wrong_table && !early_ts && !late_ts) { + // good rollup + valid_datapoints.incrementAndGet(); + continue NextKV; + } + + if (buffer == null) { + buffer = new StringBuilder(); + } + if (buffer.length() > 0) { + buffer.append("\n"); + } + buffer.append("\tPossible rollup"); + if (wrong_table) { + buffer.append(" in the wrong table [") + .append(tsdb.dataTable()) + .append("]"); + } + buffer.append(" Agg=") + .append(aggregation) + .append(" Interval=") + .append(interval) + .append(" Offset=") + .append(offset); + if (early_ts) { + buffer.append(" Timestamp (") + .append(timestamp) + .append(") earlier than row timestamps (") + .append(base_time) + .append(")"); + } else if (late_ts) { + buffer.append(" Timestamp (") + .append(timestamp) + .append(") later than next row timestamps (") + .append(base_time + + (interval.getIntervals() * interval.getIntervalSeconds())) + .append(")"); + } else { + buffer.append(" Timestamp=") + .append(timestamp); + } + buffer.append(" "); + parseUnknownDP(value, 0, len, buffer); + buffer.append("\n\t\t") + .append(kv); + } + + if (buffer != null) { + LOG.warn(buffer.toString()); + continue; + } + } + // otherwise fall through, not a rollup that's configured on this + // system. } LOG.warn("Found an object possibly from a future version of OpenTSDB\n\t" + kv); @@ -1186,6 +1291,38 @@ public String toString() { } } } + + private static void parseUnknownDP(final byte[] values, + final int offset, + final int length, + final StringBuilder buffer) { + if (length == 1) { + buffer.append((int) values[offset]) + .append(" 1b integer"); + } else if (length == 2) { + buffer.append(Bytes.getShort(values, offset)) + .append(" 2b integer"); + } else if (length == 4) { + int val = Bytes.getInt(values, offset); + buffer.append(val) + .append(" 4b integer || ") + .append(Float.intBitsToFloat(val)) + .append(" 4b float"); + } else if (length == 8) { + long val = Bytes.getLong(values, offset); + buffer.append(val) + .append(" 8b long || ") + .append(Double.longBitsToDouble(val)) + .append(" 8b double"); + } else { + buffer.append("[") + // TODO - ugly copy creating garbage. Could walk and print. + .append(Arrays.toString(Arrays.copyOfRange(values, offset, length))) + .append("] ") + .append(length) + .append("b unknown"); + } + } /** * Silly little class to report the progress while fscking diff --git a/src/tools/FsckOptions.java b/src/tools/FsckOptions.java index 9fc005e71e..481f799ff0 100644 --- a/src/tools/FsckOptions.java +++ b/src/tools/FsckOptions.java @@ -139,7 +139,6 @@ public boolean deleteUnknownColumns() { public boolean deleteBadValues() { return delete_bad_values; } - /** @return Remove rows with invalid keys */ public boolean deleteBadRows() { @@ -162,7 +161,6 @@ public void setFix(final boolean fix) { this.fix = fix; } - /** @param compact Whether or not to compact rows while processing. Can cause * compaction without the --fix flag. Will skip rows with duplicate data * points unless --last-write-wins is also specified or set in the config @@ -176,27 +174,23 @@ public void setResolveDupes(final boolean fix_dupes) { this.resolve_dupes = fix_dupes; } - /** @param last_write_wins Accept data points with the most recent timestamp when duplicates * are found */ public void setLastWriteWins(final boolean last_write_wins) { this.last_write_wins = last_write_wins; } - /** @param delete_orphans Whether or not to delete rows where the UIDs failed to resolve * to a name */ public void setDeleteOrphans(final boolean delete_orphans) { this.delete_orphans = delete_orphans; } - /** @param delete_unknown_columns Delete columns that aren't recognized */ public void setDeleteUnknownColumns(final boolean delete_unknown_columns) { this.delete_unknown_columns = delete_unknown_columns; } - /** @param delete_bad_values Remove data points with bad values */ public void setDeleteBadValues(final boolean delete_bad_values) { this.delete_bad_values = delete_bad_values; diff --git a/test/tools/TestFsckWRollups.java b/test/tools/TestFsckWRollups.java new file mode 100644 index 0000000000..60656fcd0a --- /dev/null +++ b/test/tools/TestFsckWRollups.java @@ -0,0 +1,204 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2021 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see <http://www.gnu.org/licenses/>. +package net.opentsdb.tools; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import net.opentsdb.core.Query; +import net.opentsdb.core.TSDB; +import net.opentsdb.core.Tags; +import net.opentsdb.core.BaseTsdbTest.FakeTaskTimer; +import net.opentsdb.meta.Annotation; +import net.opentsdb.rollup.RollupConfig; +import net.opentsdb.rollup.RollupInterval; +import net.opentsdb.storage.MockBase; +import net.opentsdb.uid.NoSuchUniqueId; +import net.opentsdb.uid.NoSuchUniqueName; +import net.opentsdb.uid.UniqueId; +import net.opentsdb.utils.Config; +import net.opentsdb.utils.Threads; + +import org.hbase.async.Bytes; +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.jboss.netty.util.HashedWheelTimer; +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 org.powermock.reflect.Whitebox; + +import com.stumbleupon.async.Deferred; + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ TSDB.class, Config.class, UniqueId.class, HBaseClient.class, + GetRequest.class, PutRequest.class, KeyValue.class, Fsck.class, + FsckOptions.class, Scanner.class, Annotation.class, Tags.class, + HashedWheelTimer.class, Threads.class }) +public class TestFsckWRollups { + protected byte[] GLOBAL_ROW = + new byte[] {0, 0, 0, 0x52, (byte)0xC3, 0x5A, (byte)0x80}; + protected byte[] ROW = MockBase.stringToBytes("00000150E22700000001000001"); + protected byte[] ROW2 = MockBase.stringToBytes("00000150E23510000001000001"); + protected byte[] ROW3 = MockBase.stringToBytes("00000150E24320000001000001"); + protected byte[] BAD_KEY = { 0x00, 0x00, 0x01 }; + protected Config config; + protected TSDB tsdb = null; + protected HBaseClient client; + protected UniqueId metrics; + protected UniqueId tag_names; + protected UniqueId tag_values; + protected MockBase storage; + protected FsckOptions options; + protected FakeTaskTimer timer; + protected final static List<byte[]> tags = new ArrayList<byte[]>(1); + static { + tags.add(new byte[] { 0, 0, 1, 0, 0, 1}); + } + + @SuppressWarnings("unchecked") + @Before + public void before() throws Exception { + client = mock(HBaseClient.class); + metrics = mock(UniqueId.class); + tag_names = mock(UniqueId.class); + tag_values = mock(UniqueId.class); + options = mock(FsckOptions.class); + timer = new FakeTaskTimer(); + + PowerMockito.mockStatic(Threads.class); + PowerMockito.when(Threads.newTimer(anyString())).thenReturn(timer); + PowerMockito.when(Threads.newTimer(anyInt(), anyString())).thenReturn(timer); + + PowerMockito.whenNew(HashedWheelTimer.class).withNoArguments() + .thenReturn(timer); + PowerMockito.whenNew(HBaseClient.class).withAnyArguments() + .thenReturn(client); + + config = new Config(false); + tsdb = new TSDB(client, config); + when(client.flush()).thenReturn(Deferred.fromResult(null)); + + storage = new MockBase(tsdb, client, true, true, true, true); + storage.setFamily("t".getBytes(MockBase.ASCII())); + + when(options.fix()).thenReturn(false); + when(options.compact()).thenReturn(false); + when(options.resolveDupes()).thenReturn(false); + when(options.lastWriteWins()).thenReturn(false); + when(options.deleteOrphans()).thenReturn(false); + when(options.deleteUnknownColumns()).thenReturn(false); + when(options.deleteBadValues()).thenReturn(false); + when(options.deleteBadRows()).thenReturn(false); + when(options.deleteBadCompacts()).thenReturn(false); + when(options.threads()).thenReturn(1); + + // replace the "real" field objects with mocks + Field met = tsdb.getClass().getDeclaredField("metrics"); + met.setAccessible(true); + met.set(tsdb, metrics); + + Field tagk = tsdb.getClass().getDeclaredField("tag_names"); + tagk.setAccessible(true); + tagk.set(tsdb, tag_names); + + Field tagv = tsdb.getClass().getDeclaredField("tag_values"); + tagv.setAccessible(true); + tagv.set(tsdb, tag_values); + + // mock UniqueId + when(metrics.getId("sys.cpu.user")).thenReturn(new byte[] { 0, 0, 1 }); + when(metrics.getNameAsync(new byte[] { 0, 0, 1 })) + .thenReturn(Deferred.fromResult("sys.cpu.user")); + when(metrics.getId("sys.cpu.system")) + .thenThrow(new NoSuchUniqueName("sys.cpu.system", "metric")); + when(metrics.getId("sys.cpu.nice")).thenReturn(new byte[] { 0, 0, 2 }); + when(metrics.getName(new byte[] { 0, 0, 2 })).thenReturn("sys.cpu.nice"); + when(tag_names.getId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getName(new byte[] { 0, 0, 1 })).thenReturn("host"); + when(tag_names.getOrCreateId("host")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_names.getId("dc")).thenThrow(new NoSuchUniqueName("dc", "metric")); + when(tag_values.getId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getName(new byte[] { 0, 0, 1 })).thenReturn("web01"); + when(tag_values.getOrCreateId("web01")).thenReturn(new byte[] { 0, 0, 1 }); + when(tag_values.getId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getName(new byte[] { 0, 0, 2 })).thenReturn("web02"); + when(tag_values.getOrCreateId("web02")).thenReturn(new byte[] { 0, 0, 2 }); + when(tag_values.getId("web03")) + .thenThrow(new NoSuchUniqueName("web03", "metric")); + + PowerMockito.mockStatic(Tags.class); + when(Tags.resolveIds((TSDB)any(), (ArrayList<byte[]>)any())) + .thenReturn(null); // don't care + + when(metrics.width()).thenReturn((short)3); + when(tag_names.width()).thenReturn((short)3); + when(tag_values.width()).thenReturn((short)3); + + RollupConfig rollup_config = RollupConfig.builder() + .addAggregationId("sum", 0) + .addAggregationId("count", 1) + .addAggregationId("min", 2) + .addAggregationId("max", 3) + .addInterval(RollupInterval.builder() + .setInterval("1m") + .setRowSpan("24h") + .setTable("tsdb-1m") + .setPreAggregationTable("tsdb-preagg-1m")) + .addInterval(RollupInterval.builder() + .setInterval("1h") + .setRowSpan("1d") + .setTable("tsdb-1h") + .setPreAggregationTable("tsdb-preagg-1h")) + .build(); + Whitebox.setInternalState(tsdb, "rollup_config", rollup_config); + } + + @Test + public void oneBadOneGood() throws Exception { + final byte[] qual1 = { 0x01, 0x00, 0x00 }; + final byte[] val1 = { 4 }; + final byte[] qual2 = { (byte) 0x2, 0x00, 0x02 }; + final byte[] val2 = new byte[] { 0, 0, 0, 5 }; + storage.addColumn(ROW, qual1, val1); + storage.addColumn(ROW, qual2, val2); + + final Fsck fsck = new Fsck(tsdb, options); + fsck.runFullTable(); + assertEquals(2, fsck.kvs_processed.get()); + assertEquals(1, fsck.bad_values.get()); + assertEquals(1, fsck.totalErrors()); + } + +} From d33befed9085d7f23fb826e9d9c25f87a8c95857 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Sat, 8 May 2021 11:03:58 -0700 Subject: [PATCH 783/826] Fix building after adding the Split rollup query code. --- src/core/SplitRollupQuery.java | 8 +++--- src/core/SplitRollupSpanGroup.java | 37 +++++++++++++++++-------- test/core/TestSeekableViewChain.java | 8 +++--- test/core/TestSplitRollupQuery.java | 14 +++++----- test/core/TestSplitRollupSpanGroup.java | 8 +++--- test/core/TestTsdbQuery.java | 2 +- 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/core/SplitRollupQuery.java b/src/core/SplitRollupQuery.java index 7121c99762..b4c513a9bc 100644 --- a/src/core/SplitRollupQuery.java +++ b/src/core/SplitRollupQuery.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2021 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 @@ -431,7 +431,7 @@ public void setPercentiles(List<Float> percentiles) { private class RunCB implements Callback<DataPoints[], ArrayList<DataPoints[]>> { private ByteMap<SpanGroup> makeSpanGroupMap(DataPoints[] dataPointsArray) { - ByteMap<SpanGroup> map = new ByteMap<>(); + ByteMap<SpanGroup> map = new ByteMap<SpanGroup>(); for (DataPoints points : dataPointsArray) { if (!(points instanceof SpanGroup)) { @@ -448,11 +448,11 @@ private DataPoints[] merge(DataPoints[] rollup, DataPoints[] raw) { ByteMap<SpanGroup> rollupResults = makeSpanGroupMap(rollup); ByteMap<SpanGroup> rawResults = makeSpanGroupMap(raw); - TreeSet<byte[]> allGroups = new TreeSet<>(Bytes.MEMCMP); + TreeSet<byte[]> allGroups = new TreeSet<byte[]>(Bytes.MEMCMP); allGroups.addAll(rollupResults.keySet()); allGroups.addAll(rawResults.keySet()); - List<SplitRollupSpanGroup> results = new ArrayList<>(allGroups.size()); + List<SplitRollupSpanGroup> results = new ArrayList<SplitRollupSpanGroup>(allGroups.size()); for (byte[] group : allGroups) { SpanGroup rawGroup = rawResults.get(group); diff --git a/src/core/SplitRollupSpanGroup.java b/src/core/SplitRollupSpanGroup.java index dd6ab87830..bcbcd0684a 100644 --- a/src/core/SplitRollupSpanGroup.java +++ b/src/core/SplitRollupSpanGroup.java @@ -1,3 +1,15 @@ +// This file is part of OpenTSDB. +// Copyright (C) 2021 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see <http://www.gnu.org/licenses/>. package net.opentsdb.core; import com.stumbleupon.async.Callback; @@ -11,7 +23,7 @@ import java.util.Map; public class SplitRollupSpanGroup extends AbstractSpanGroup { - private final List<SpanGroup> spanGroups = new ArrayList<>(); + private final List<SpanGroup> spanGroups = new ArrayList<SpanGroup>(); public SplitRollupSpanGroup(SpanGroup... groups) { for (SpanGroup group : groups) { @@ -59,7 +71,7 @@ public byte[] metricUID() { */ @Override public Map<String, String> getTags() { - Map<String, String> tags = new HashMap<>(); + Map<String, String> tags = new HashMap<String, String>(); for (SpanGroup group : spanGroups) { tags.putAll(group.getTags()); @@ -79,7 +91,7 @@ public Deferred<Map<String, String>> getTagsAsync() { class GetTagsCB implements Callback<Map<String, String>, ArrayList<Map<String, String>>> { @Override public Map<String, String> call(ArrayList<Map<String, String>> resolvedTags) throws Exception { - Map<String, String> tags = new HashMap<>(); + Map<String, String> tags = new HashMap<String, String>(); for (Map<String, String> groupTags : resolvedTags) { tags.putAll(groupTags); } @@ -87,7 +99,7 @@ public Map<String, String> call(ArrayList<Map<String, String>> resolvedTags) thr } } - List<Deferred<Map<String, String>>> deferreds = new ArrayList<>(spanGroups.size()); + List<Deferred<Map<String, String>>> deferreds = new ArrayList<Deferred<Map<String, String>>>(spanGroups.size()); for (SpanGroup group : spanGroups) { deferreds.add(group.getTagsAsync()); @@ -107,7 +119,7 @@ public Map<String, String> call(ArrayList<Map<String, String>> resolvedTags) thr */ @Override public Bytes.ByteMap<byte[]> getTagUids() { - Bytes.ByteMap<byte[]> tagUids = new Bytes.ByteMap<>(); + Bytes.ByteMap<byte[]> tagUids = new Bytes.ByteMap<byte[]>(); for (SpanGroup group : spanGroups) { tagUids.putAll(group.getTagUids()); @@ -132,7 +144,7 @@ public Bytes.ByteMap<byte[]> getTagUids() { */ @Override public List<String> getAggregatedTags() { - List<String> aggregatedTags = new ArrayList<>(); + List<String> aggregatedTags = new ArrayList<String>(); for (SpanGroup group : spanGroups) { aggregatedTags.addAll(group.getAggregatedTags()); @@ -161,7 +173,7 @@ public Deferred<List<String>> getAggregatedTagsAsync() { class GetAggregatedTagsCB implements Callback<List<String>, ArrayList<List<String>>> { @Override public List<String> call(ArrayList<List<String>> resolvedTags) throws Exception { - List<String> aggregatedTags = new ArrayList<>(); + List<String> aggregatedTags = new ArrayList<String>(); for (List<String> groupTags : resolvedTags) { aggregatedTags.addAll(groupTags); } @@ -169,7 +181,8 @@ public List<String> call(ArrayList<List<String>> resolvedTags) throws Exception } } - List<Deferred<List<String>>> deferreds = new ArrayList<>(spanGroups.size()); + List<Deferred<List<String>>> deferreds = + new ArrayList<Deferred<List<String>>>(spanGroups.size()); for (SpanGroup group : spanGroups) { deferreds.add(group.getAggregatedTagsAsync()); } @@ -185,7 +198,7 @@ public List<String> call(ArrayList<List<String>> resolvedTags) throws Exception */ @Override public List<byte[]> getAggregatedTagUids() { - List<byte[]> aggTagUids = new ArrayList<>(); + List<byte[]> aggTagUids = new ArrayList<byte[]>(); for (SpanGroup group : spanGroups) { aggTagUids.addAll(group.getAggregatedTagUids()); @@ -201,7 +214,7 @@ public List<byte[]> getAggregatedTagUids() { */ @Override public List<String> getTSUIDs() { - List<String> tsuids = new ArrayList<>(); + List<String> tsuids = new ArrayList<String>(); for (SpanGroup group : spanGroups) { tsuids.addAll(group.getTSUIDs()); @@ -218,7 +231,7 @@ public List<String> getTSUIDs() { */ @Override public List<Annotation> getAnnotations() { - List<Annotation> annotations = new ArrayList<>(); + List<Annotation> annotations = new ArrayList<Annotation>(); for (SpanGroup group : spanGroups) { List<Annotation> groupAnnotations = group.getAnnotations(); @@ -280,7 +293,7 @@ public int aggregatedSize() { */ @Override public SeekableView iterator() { - List<SeekableView> iterators = new ArrayList<>(); + List<SeekableView> iterators = new ArrayList<SeekableView>(); for (SpanGroup group : spanGroups) { iterators.add(group.iterator()); } diff --git a/test/core/TestSeekableViewChain.java b/test/core/TestSeekableViewChain.java index 5da9efbc62..5350abd29f 100644 --- a/test/core/TestSeekableViewChain.java +++ b/test/core/TestSeekableViewChain.java @@ -40,7 +40,7 @@ public void before() throws Exception { @Test public void testIteratorChain() { - List<SeekableView> iterators = new ArrayList<>(); + List<SeekableView> iterators = new ArrayList<SeekableView>(); for (int i = 0; i < 3; i++) { iterators.add(SeekableViewsForTest.fromArray(DATA_POINTS_1)); } @@ -57,7 +57,7 @@ public void testIteratorChain() { @Test public void testSeek() { - List<SeekableView> iterators = new ArrayList<>(); + List<SeekableView> iterators = new ArrayList<SeekableView>(); iterators.add(SeekableViewsForTest.generator( BASE_TIME, 10000, 5, true )); @@ -80,7 +80,7 @@ public void testSeek() { @Test public void testEmptyChain() { - SeekableViewChain chain = new SeekableViewChain(new ArrayList<>()); + SeekableViewChain chain = new SeekableViewChain(new ArrayList<SeekableView>()); assertFalse(chain.hasNext()); } @@ -90,7 +90,7 @@ public void testRemoveUnsupported() { } private SeekableViewChain makeChain(int numIterators) { - List<SeekableView> iterators = new ArrayList<>(); + List<SeekableView> iterators = new ArrayList<SeekableView>(); for (int i = 0; i < numIterators; i++) { iterators.add(SeekableViewsForTest.fromArray(DATA_POINTS_1)); } diff --git a/test/core/TestSplitRollupQuery.java b/test/core/TestSplitRollupQuery.java index d2006ca9d2..9ce3082b3d 100644 --- a/test/core/TestSplitRollupQuery.java +++ b/test/core/TestSplitRollupQuery.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2015-2017 The OpenTSDB Authors. +// Copyright (C) 2021 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 @@ -188,14 +188,14 @@ public void configureFromQueryThrowsIfForcedRaw() { @Test public void configureFromQuerySplitsRollupQuery() { mockEnableRollupQuerySplitting(); - doReturn(Deferred.fromResult(null)).when(rollupQuery).split(any(), anyInt(), any()); + doReturn(Deferred.fromResult(null)).when(rollupQuery).split(any(TSQuery.class), anyInt(), any(TsdbQuery.class)); assertNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); rollupQuery.setStartTime(0); queryUnderTest.configureFromQuery(null, 0, false); - verify(rollupQuery).split(eq(null), eq(0), anyObject()); + verify(rollupQuery).split(eq((TSQuery) null), eq(0), any(TsdbQuery.class)); assertNotNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); } @@ -203,7 +203,7 @@ public void configureFromQuerySplitsRollupQuery() { public void configureFromQuerySplitsRollupQueryWithRawOnlyQuery() { mockEnableRollupQuerySplitting(); doReturn(true).when(rollupQuery).needsSplitting(); - doReturn(Deferred.fromResult(null)).when(rollupQuery).split(any(), anyInt(), any()); + doReturn(Deferred.fromResult(null)).when(rollupQuery).split(any(TSQuery.class), anyInt(), any(TsdbQuery.class)); rollupQuery.setStartTime(DateTime.currentTimeMillis()); @@ -211,7 +211,7 @@ public void configureFromQuerySplitsRollupQueryWithRawOnlyQuery() { queryUnderTest.configureFromQuery(null, 0, false); - verify(rollupQuery).split(eq(null), eq(0), anyObject()); + verify(rollupQuery).split(eq((TSQuery) null), eq(0), any(TsdbQuery.class)); assertNotNull(Whitebox.getInternalState(queryUnderTest, "rawQuery")); assertNull(Whitebox.getInternalState(queryUnderTest, "rollupQuery")); } @@ -294,7 +294,7 @@ public void runAsyncMergesResults() { verify(rollupQuery).runAsync(); verify(rawQuery).runAsync(); - List<String> actualGroups = new ArrayList<>(actualPoints.length); + List<String> actualGroups = new ArrayList<String>(actualPoints.length); for (DataPoints dataPoints : actualPoints) { actualGroups.add(new String(((SplitRollupSpanGroup) dataPoints).group())); } @@ -309,7 +309,7 @@ private SpanGroup makeSpanGroup(String group) { tsdb, 0, 42, - new ArrayList<>(), + new ArrayList<Span>(), false, new RateOptions(), Aggregators.SUM, diff --git a/test/core/TestSplitRollupSpanGroup.java b/test/core/TestSplitRollupSpanGroup.java index 30ee198a0c..48dbf41911 100644 --- a/test/core/TestSplitRollupSpanGroup.java +++ b/test/core/TestSplitRollupSpanGroup.java @@ -97,9 +97,9 @@ public void testAggregatedSize() { @Test public void testGetTagUids() { - final Bytes.ByteMap<byte[]> uids1 = new Bytes.ByteMap<>(); + final Bytes.ByteMap<byte[]> uids1 = new Bytes.ByteMap<byte[]>(); uids1.put(new byte[]{0, 0, 1}, new byte[]{0, 0, 2}); - final Bytes.ByteMap<byte[]> uids2 = new Bytes.ByteMap<>(); + final Bytes.ByteMap<byte[]> uids2 = new Bytes.ByteMap<byte[]>(); uids2.put(new byte[]{0, 0, 3}, new byte[]{0, 0, 4}); when(rollupSpanGroup.getTagUids()).thenReturn(uids1); @@ -116,9 +116,9 @@ public void testGetTagUids() { @Test public void testGetAggregatedTagUids() { - final List<byte[]> uids1 = new ArrayList<>(); + final List<byte[]> uids1 = new ArrayList<byte[]>(); uids1.add(new byte[]{0, 0, 1}); - final List<byte[]> uids2 = new ArrayList<>(); + final List<byte[]> uids2 = new ArrayList<byte[]>(); uids2.add(new byte[]{0, 0, 2}); when(rollupSpanGroup.getAggregatedTagUids()).thenReturn(uids1); diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index e564e62560..445685d732 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -372,7 +372,7 @@ public void configureFromQueryWithForceRaw() throws Exception { query.configureFromQuery(ts_query, 0, true).joinUninterruptibly(); assertFalse(query.isRollupQuery()); - verify(query, never()).transformDownSamplerToRollupQuery(any(), any()); + verify(query, never()).transformDownSamplerToRollupQuery(any(Aggregator.class), anyString()); assertArrayEquals(METRIC_BYTES, ForTesting.getMetric(query)); assertEquals(1, ForTesting.getFilters(query).size()); From b89fded4ee326dc064b9d7e471e9f29f7d1dede9 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Sat, 8 May 2021 14:59:57 -0700 Subject: [PATCH 784/826] Fix remote code execution #2051 by adding regex validators for the Gnuplot params and introducting the tsd.gnuplot.options.allowlist setting that is a strict matching allow list of o= values from the query string that will be allowed through. By default tihs is empty so if folks are using this query param, they'll different graphs until they add the options they need. --- src/graph/Plot.java | 5 +- src/tsd/GraphHandler.java | 124 ++++++++++--- test/tsd/TestGraphHandler.java | 323 ++++++++++++++++++--------------- 3 files changed, 280 insertions(+), 172 deletions(-) diff --git a/src/graph/Plot.java b/src/graph/Plot.java index db141ce497..9ca14a8a52 100644 --- a/src/graph/Plot.java +++ b/src/graph/Plot.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-2021 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 @@ -15,7 +15,6 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; -import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -139,7 +138,7 @@ public void setParams(final Map<String, String> params) { String[] y_format_keys = {"format y", "format y2"}; for(String k : y_format_keys){ if(params.containsKey(k)){ - params.put(k, URLDecoder.decode(params.get(k))); + params.put(k, params.get(k)); } } this.params = params; diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 229cff97af..8e065bfc81 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2010-2012 The OpenTSDB Authors. +// Copyright (C) 2010-2021 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 @@ -19,21 +19,26 @@ import java.io.IOException; import java.io.PrintWriter; import java.net.URL; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; +import com.google.common.base.Strings; +import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,7 +58,6 @@ import net.opentsdb.utils.JSON; import com.stumbleupon.async.Callback; -import com.stumbleupon.async.Deferred; /** * Stateless handler of HTTP graph requests (the {@code /q} endpoint). @@ -66,6 +70,17 @@ final class GraphHandler implements HttpRpc { private static final boolean IS_WINDOWS = System.getProperty("os.name", "").contains("Windows"); + private static Pattern RANGE_VALIDATOR = Pattern.compile( + "\\[\\\"?-?\\d+\\.?(\\d+)?([eE]-?\\d+)?\\\"?:\\\"?-?(\\d+\\.?\\d+?)?([eE]-?\\d+)?\\\"?\\]"); + private static Pattern LABEL_VALIDATOR = Pattern.compile("[a-zA-z0-9 \\-_]"); + private static Pattern KEY_VALIDATOR = Pattern.compile( + "(out|left|top|center|right|horiz|box|bottom)?\\s?"); + private static Pattern STYLE_VALIDATOR = Pattern.compile("(linespoint|points|circles|dots)"); + private static Pattern COLOR_VALIDATOR = Pattern.compile("(x|X)[a-fA-F0-9]{6}"); + private static Pattern SMOOTH_VALIDATOR = Pattern.compile("unique|frequency|fnormal|cumulative|cnormal|bins|csplines|acsplines|mcsplines|bezier|sbezier|unwrap|zsort"); + // NOTE: This one should be tightened for only time based formatters. + private static Pattern FORMAT_VALIDATOR = Pattern.compile("(%[a-zA-Z])+[:\\/]?\\s?"); + private static Pattern WXH_VALIDATOR = Pattern.compile("^\\d+x\\d+$"); /** Number of times we had to do all the work up to running Gnuplot. */ private static final AtomicInteger graphs_generated = new AtomicInteger(); @@ -171,7 +186,29 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) // Build the queries for the parsed TSQuery Query[] tsdbqueries = tsquery.buildQueries(tsdb); - List<String> options = query.getQueryStringParams("o"); + List<String> options = null; + final String options_allow_list = tsdb.getConfig().getString( + "tsd.gnuplot.options.allowlist"); + if (!Strings.isNullOrEmpty(options_allow_list)) { + String[] allow_list_strings = options_allow_list.split(";"); + Set<String> allow_list = Sets.newHashSet(); + for (int i = 0; i < allow_list_strings.length; i++) { + String allow = allow_list_strings[i]; + if (allow != null) { + allow = URLDecoder.decode(allow.trim()); + allow_list.add(allow); + } + } + + options = query.getQueryStringParams("o"); + for (int i = 0; i < options.size(); i++) { + if (!allow_list.contains(options.get(i))) { + throw new BadRequestException("Query option at index " + i + + " was not in the allow list."); + } + } + } + if (options == null) { options = new ArrayList<String>(tsdbqueries.length); for (int i = 0; i < tsdbqueries.length; i++) { @@ -180,15 +217,6 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) } else if (options.size() != tsdbqueries.length) { throw new BadRequestException(options.size() + " `o' parameters, but " + tsdbqueries.length + " `m' parameters."); - } else { - for (final String option : options) { - // TODO - far from perfect, should help a little. - if (option.contains("`") || option.contains("%60") || - option.contains("`")) { - throw new BadRequestException("Option contained a back-tick. " - + "That's a no-no."); - } - } } for (final Query tsdbquery : tsdbqueries) { try { @@ -635,13 +663,12 @@ private HashMap<String, Object> loadCachedJson(final HttpQuery query, /** Parses the {@code wxh} query parameter to set the graph dimension. */ static void setPlotDimensions(final HttpQuery query, final Plot plot) { - final String wxh = query.getQueryStringParam("wxh"); + String wxh = query.getQueryStringParam("wxh"); if (wxh != null && !wxh.isEmpty()) { - // TODO - far from perfect, should help a little. - if (wxh.contains("`") || wxh.contains("%60") || - wxh.contains("`")) { - throw new BadRequestException("WXH contained a back-tick. " - + "That's a no-no."); + wxh = URLDecoder.decode(wxh.trim()); + if (!WXH_VALIDATOR.matcher(wxh).find()) { + throw new IllegalArgumentException("'wxh' was invalid. " + + "Must satisfy the pattern " + WXH_VALIDATOR.toString()); } final int wxhlength = wxh.length(); if (wxhlength < 7) { // 100x100 minimum. @@ -687,13 +714,16 @@ private static String stringify(final String s) { * @return {@code null} if the parameter wasn't passed, otherwise the * value of the last occurrence of the parameter. */ - private static String popParam(final Map<String, List<String>> querystring, - final String param) { + public static String popParam(final Map<String, List<String>> querystring, + final String param) { final List<String> params = querystring.remove(param); if (params == null) { return null; } - final String given = params.get(params.size() - 1); + String given = params.get(params.size() - 1); + if (given != null) { + given = URLDecoder.decode(given.trim()); + } // TODO - far from perfect, should help a little. if (given.contains("`") || given.contains("%60") || given.contains("`")) { @@ -713,24 +743,52 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { final Map<String, List<String>> querystring = query.getQueryString(); String value; if ((value = popParam(querystring, "yrange")) != null) { + if (!RANGE_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'yrange' was invalid. " + + "Must be in the format [min:max]."); + } params.put("yrange", value); } if ((value = popParam(querystring, "y2range")) != null) { + if (!RANGE_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'y2range' was invalid. " + + "Must be in the format [min:max]."); + } params.put("y2range", value); } if ((value = popParam(querystring, "ylabel")) != null) { + if (!LABEL_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'ylabel' was invalid. Must " + + "satisfy the pattern " + LABEL_VALIDATOR.toString()); + } params.put("ylabel", stringify(value)); } if ((value = popParam(querystring, "y2label")) != null) { + if (!LABEL_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'y2label' was invalid. Must " + + "satisfy the pattern " + LABEL_VALIDATOR.toString()); + } params.put("y2label", stringify(value)); } if ((value = popParam(querystring, "yformat")) != null) { + if (!FORMAT_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'yformat' was invalid. Must " + + "satisfy the pattern " + FORMAT_VALIDATOR.toString()); + } params.put("format y", stringify(value)); } if ((value = popParam(querystring, "y2format")) != null) { + if (!FORMAT_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'y2format' was invalid. Must " + + "satisfy the pattern " + FORMAT_VALIDATOR.toString()); + } params.put("format y2", stringify(value)); } if ((value = popParam(querystring, "xformat")) != null) { + if (!FORMAT_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'xformat' was invalid. Must " + + "satisfy the pattern " + FORMAT_VALIDATOR.toString()); + } params.put("format x", stringify(value)); } if ((value = popParam(querystring, "ylog")) != null) { @@ -740,21 +798,45 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("logscale y2", ""); } if ((value = popParam(querystring, "key")) != null) { + if (!KEY_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'key' was invalid. Must " + + "satisfy the pattern " + KEY_VALIDATOR.toString()); + } params.put("key", value); } if ((value = popParam(querystring, "title")) != null) { + if (!LABEL_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'title' was invalid. Must " + + "satisfy the pattern " + LABEL_VALIDATOR.toString()); + } params.put("title", stringify(value)); } if ((value = popParam(querystring, "bgcolor")) != null) { + if (!COLOR_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'bgcolor' was invalid. Must " + + "be a hex value e.g. 'xFFFFFF'"); + } params.put("bgcolor", value); } if ((value = popParam(querystring, "fgcolor")) != null) { + if (!COLOR_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'fgcolor' was invalid. Must " + + "be a hex value e.g. 'xFFFFFF'"); + } params.put("fgcolor", value); } if ((value = popParam(querystring, "smooth")) != null) { + if (!SMOOTH_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'smooth' was invalid. Must " + + "satisfy the pattern " + SMOOTH_VALIDATOR.toString()); + } params.put("smooth", value); } if ((value = popParam(querystring, "style")) != null) { + if (!STYLE_VALIDATOR.matcher(value).find()) { + throw new BadRequestException("'style' was invalid. Must " + + "satisfy the pattern " + STYLE_VALIDATOR.toString()); + } params.put("style", value); } // This must remain after the previous `if' in order to properly override diff --git a/test/tsd/TestGraphHandler.java b/test/tsd/TestGraphHandler.java index 8e2fdb3401..71311856fa 100644 --- a/test/tsd/TestGraphHandler.java +++ b/test/tsd/TestGraphHandler.java @@ -1,91 +1,118 @@ -//// This file is part of OpenTSDB. -//// Copyright (C) 2011-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 <http://www.gnu.org/licenses/>. -//package net.opentsdb.tsd; -// -//import java.io.File; -//import java.lang.reflect.Method; -// -//import org.jboss.netty.channel.Channel; -//import org.junit.Test; -//import org.junit.runner.RunWith; -// -//import static org.junit.Assert.assertFalse; -//import static org.junit.Assert.assertTrue; -//import static org.mockito.Matchers.anyString; -//import static org.mockito.Mockito.times; -//import static org.mockito.Mockito.verify; -//import static org.mockito.Mockito.when; -// -//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 org.powermock.reflect.Whitebox; -// -//import static org.powermock.api.mockito.PowerMockito.mock; -// -//@RunWith(PowerMockRunner.class) -//// "Classloader hell"... It's real. Tell PowerMock to ignore these classes -//// because they fiddle with the class loader. We don't test them anyway. -//@PowerMockIgnore({"javax.management.*", "javax.xml.*", -// "ch.qos.*", "org.slf4j.*", -// "com.sum.*", "org.xml.*"}) -//@PrepareForTest({ GraphHandler.class, HttpQuery.class }) -//public final class TestGraphHandler { -// -// private final static Method sm; -// static { -// try { -// sm = GraphHandler.class.getDeclaredMethod("staleCacheFile", -// HttpQuery.class, long.class, long.class, File.class); -// sm.setAccessible(true); -// } catch (Exception e) { -// throw new RuntimeException("Failed in static initializer", e); -// } -// } -// -// @Test // If the file doesn't exist, we don't use it, obviously. -// public void staleCacheFileDoesntExist() throws Exception { -// final File cachedfile = fakeFile("/cache/fake-file"); -// // From the JDK manual: "returns 0L if the file does not exist -// // or if an I/O error occurs" -// when(cachedfile.lastModified()).thenReturn(0L); -// -// assertTrue("File is stale", staleCacheFile(null, 0, 10, cachedfile)); -// -// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. -// } -// -// @Test // If the mtime of a file is in the future, we don't use it. -// public void staleCacheFileInTheFuture() throws Exception { -// PowerMockito.mockStatic(System.class); -// -// final HttpQuery query = fakeHttpQuery(); -// final File cachedfile = fakeFile("/cache/fake-file"); -// -// final long now = 1000L; -// when(System.currentTimeMillis()).thenReturn(now); -// when(cachedfile.lastModified()).thenReturn(now + 1000L); -// final long end_time = now; -// -// assertTrue("File is stale", -// staleCacheFile(query, end_time, 10, cachedfile)); -// -// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. -// PowerMockito.verifyStatic(); // Verify that ... -// System.currentTimeMillis(); // ... this was called only once. -// } -// +// This file is part of OpenTSDB. +// Copyright (C) 2011-2021 The OpenTSDB Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 2.1 of the License, or (at your +// option) any later version. This program is distributed in the hope that it +// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. You should have received a copy +// of the GNU Lesser General Public License along with this program. If not, +// see <http://www.gnu.org/licenses/>. +package net.opentsdb.tsd; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.jboss.netty.channel.Channel; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 org.powermock.reflect.Whitebox; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import net.opentsdb.graph.Plot; + +import static org.powermock.api.mockito.PowerMockito.mock; + +@RunWith(PowerMockRunner.class) +// "Classloader hell"... It's real. Tell PowerMock to ignore these classes +// because they fiddle with the class loader. We don't test them anyway. +@PowerMockIgnore({"javax.management.*", "javax.xml.*", + "ch.qos.*", "org.slf4j.*", + "com.sum.*", "org.xml.*"}) +@PrepareForTest({ GraphHandler.class, HttpQuery.class, Plot.class }) +public final class TestGraphHandler { + + private final static Method sm; + static { + try { + sm = GraphHandler.class.getDeclaredMethod("staleCacheFile", + HttpQuery.class, long.class, long.class, File.class); + sm.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException("Failed in static initializer", e); + } + } + + @Test + public void setPlotParams() throws Exception { + Plot plot = mock(Plot.class); + HttpQuery query = mock(HttpQuery.class); + Map<String, List<String>> params = Maps.newHashMap(); + when(query.getQueryString()).thenReturn(params); + + params.put("yrange", Lists.newArrayList("[0:42]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[33:system('touch /tmp/poc.txt')]")); + try { + GraphHandler.setPlotParams(query, plot); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + } + + @Test // If the file doesn't exist, we don't use it, obviously. + public void staleCacheFileDoesntExist() throws Exception { + final File cachedfile = fakeFile("/cache/fake-file"); + // From the JDK manual: "returns 0L if the file does not exist + // or if an I/O error occurs" + when(cachedfile.lastModified()).thenReturn(0L); + + assertTrue("File is stale", staleCacheFile(null, 0, 10, cachedfile)); + + verify(cachedfile).lastModified(); // Ensure we do a single stat() call. + } + + @Test // If the mtime of a file is in the future, we don't use it. + public void staleCacheFileInTheFuture() throws Exception { + PowerMockito.mockStatic(System.class); + + final HttpQuery query = fakeHttpQuery(); + final File cachedfile = fakeFile("/cache/fake-file"); + + final long now = 1000L; + when(System.currentTimeMillis()).thenReturn(now); + when(cachedfile.lastModified()).thenReturn(now + 1000L); + final long end_time = now; + + assertTrue("File is stale", + staleCacheFile(query, end_time, 10, cachedfile)); + + verify(cachedfile).lastModified(); // Ensure we do a single stat() call. + PowerMockito.verifyStatic(); // Verify that ... + System.currentTimeMillis(); // ... this was called only once. + } + // @Test // End time in the future => OK to serve stale file up to max_age. // public void staleCacheFileEndTimeInFuture() throws Exception { // PowerMockito.mockStatic(System.class); @@ -110,7 +137,7 @@ // PowerMockito.verifyStatic(times(3)); // System.currentTimeMillis(); // } -// + // @Test // No end time = end time is now. // public void staleCacheFileEndTimeIsNow() throws Exception { // PowerMockito.mockStatic(System.class); @@ -136,27 +163,27 @@ // PowerMockito.verifyStatic(times(3)); // System.currentTimeMillis(); // } -// -// @Test // End time in the past, file's mtime predates it. -// public void staleCacheFileEndTimeInPastOlderFile() throws Exception { -// PowerMockito.mockStatic(System.class); -// -// final HttpQuery query = fakeHttpQuery(); -// final File cachedfile = fakeFile("/cache/fake-file"); -// -// final long end_time = 8000L; -// final long now = end_time + 2000L; -// when(System.currentTimeMillis()).thenReturn(now); -// when(cachedfile.lastModified()).thenReturn(5000L); -// -// assertTrue("File predates end-time and cannot be re-used", -// staleCacheFile(query, end_time, 4, cachedfile)); -// -// verify(cachedfile).lastModified(); // Ensure we do a single stat() call. -// PowerMockito.verifyStatic(); // Verify that ... -// System.currentTimeMillis(); // ... this was called only once. -// } -// + + @Test // End time in the past, file's mtime predates it. + public void staleCacheFileEndTimeInPastOlderFile() throws Exception { + PowerMockito.mockStatic(System.class); + + final HttpQuery query = fakeHttpQuery(); + final File cachedfile = fakeFile("/cache/fake-file"); + + final long end_time = 8000L; + final long now = end_time + 2000L; + when(System.currentTimeMillis()).thenReturn(now); + when(cachedfile.lastModified()).thenReturn(5000L); + + assertTrue("File predates end-time and cannot be re-used", + staleCacheFile(query, end_time, 4, cachedfile)); + + verify(cachedfile).lastModified(); // Ensure we do a single stat() call. + PowerMockito.verifyStatic(); // Verify that ... + System.currentTimeMillis(); // ... this was called only once. + } + // @Test // End time in the past, file's mtime is after it. // public void staleCacheFileEndTimeInPastCacheableFile() throws Exception { // PowerMockito.mockStatic(System.class); @@ -176,41 +203,41 @@ // PowerMockito.verifyStatic(); // Verify that ... // System.currentTimeMillis(); // ... this was called only once. // } -// -// /** -// * Helper to call private static method. -// * There's one slight difference: the {@code end_time} parameter is in -// * milliseconds here, instead of seconds. -// */ -// private static boolean staleCacheFile(final HttpQuery query, -// final long end_time, -// final long max_age, -// final File cachedfile) throws Exception { -// PowerMockito.mockStatic(System.class); -// PowerMockito.when(System.getProperty(anyString(), anyString())).thenReturn(""); -// PowerMockito.when(System.getProperty(anyString())).thenReturn(""); -// PowerMockito.spy(GraphHandler.class); -// PowerMockito.doReturn("").when(GraphHandler.class, "findGnuplotHelperScript"); -// -// return Whitebox.<Boolean>invokeMethod(GraphHandler.class, "staleCacheFile", -// query, end_time / 1000, max_age, -// cachedfile); -// -// //return (Boolean)sm.invoke(null, query, end_time / 1000, max_age, cachedfile); -// } -// -// private static HttpQuery fakeHttpQuery() { -// final HttpQuery query = mock(HttpQuery.class); -// final Channel chan = NettyMocks.fakeChannel(); -// when(query.channel()).thenReturn(chan); -// return query; -// } -// -// private static File fakeFile(final String path) { -// final File file = mock(File.class); -// when(file.getPath()).thenReturn(path); -// when(file.toString()).thenReturn(path); -// return file; -// } -// -//} + + /** + * Helper to call private static method. + * There's one slight difference: the {@code end_time} parameter is in + * milliseconds here, instead of seconds. + */ + private static boolean staleCacheFile(final HttpQuery query, + final long end_time, + final long max_age, + final File cachedfile) throws Exception { + PowerMockito.mockStatic(System.class); + PowerMockito.when(System.getProperty(anyString(), anyString())).thenReturn(""); + PowerMockito.when(System.getProperty(anyString())).thenReturn(""); + PowerMockito.spy(GraphHandler.class); + PowerMockito.doReturn("").when(GraphHandler.class, "findGnuplotHelperScript"); + + return Whitebox.<Boolean>invokeMethod(GraphHandler.class, "staleCacheFile", + query, end_time / 1000, max_age, + cachedfile); + + //return (Boolean)sm.invoke(null, query, end_time / 1000, max_age, cachedfile); + } + + private static HttpQuery fakeHttpQuery() { + final HttpQuery query = mock(HttpQuery.class); + final Channel chan = NettyMocks.fakeChannel(); + when(query.channel()).thenReturn(chan); + return query; + } + + private static File fakeFile(final String path) { + final File file = mock(File.class); + when(file.getPath()).thenReturn(path); + when(file.toString()).thenReturn(path); + return file; + } + +} From fc34c0e62903db2d4bf010e378fe4f13f57a9c4d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Sat, 24 Jul 2021 11:38:13 -0700 Subject: [PATCH 785/826] Add a systemd standard opentsdb service script to launch using the port defined in the config file. Fixes #2033 --- Makefile.am | 4 +++- build-aux/rpm/opentsdb.conf | 2 +- build-aux/rpm/systemd/opentsdb.service | 14 ++++++++++++++ opentsdb.spec.in | 5 +++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 build-aux/rpm/systemd/opentsdb.service diff --git a/Makefile.am b/Makefile.am index 9bf563d304..da55b8b30b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2012 The OpenTSDB Authors. +# Copyright (C) 2011-2021 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -30,6 +30,7 @@ dist_noinst_SCRIPTS = src/create_table.sh src/upgrade_1to2.sh src/mygnuplot.sh \ src/mygnuplot.bat src/opentsdb.conf tools/opentsdb_restart.py src/logback.xml 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 \ build-aux/rpm/systemd/opentsdb@.service tsdb_SRC := \ src/core/AbstractSpanGroup.java \ @@ -734,6 +735,7 @@ install-data-etc: init_file="$(top_srcdir)/build-aux/rpm/init.d/opentsdb" ; \ echo " $(INSTALL_SCRIPT)" $$init_file "$$destdatainitdir" ; \ $(INSTALL_SCRIPT) $$init_file "$$destdatainitdir" || exit 1; \ + systemd_file="$(top_srcdir)/build-aux/rpm/systemd/opentsdb.service" ; \ systemd_file="$(top_srcdir)/build-aux/rpm/systemd/opentsdb@.service" ; \ echo " $(INSTALL_SCRIPT)" $$systemd_file "$$destdatasystemddir" ; \ $(INSTALL_SCRIPT) $$systemd_file "$$destdatasystemddir" || exit 1; diff --git a/build-aux/rpm/opentsdb.conf b/build-aux/rpm/opentsdb.conf index 052936b962..6c90f316c1 100644 --- a/build-aux/rpm/opentsdb.conf +++ b/build-aux/rpm/opentsdb.conf @@ -30,7 +30,7 @@ tsd.http.staticroot = /usr/share/opentsdb/static/ # Where TSD should write it's cache files to # *** REQUIRED *** -tsd.http.cachedir = /tmp/opentsdb +tsd.http.cachedir = /var/tmp/opentsdb # --------- CORE ---------- # Whether or not to automatically create UIDs for new metric types, default diff --git a/build-aux/rpm/systemd/opentsdb.service b/build-aux/rpm/systemd/opentsdb.service new file mode 100644 index 0000000000..55a56e74d7 --- /dev/null +++ b/build-aux/rpm/systemd/opentsdb.service @@ -0,0 +1,14 @@ +[Unit] +Description=OpenTSDB +After=network-online.target +Before=shutdown.target + +[Service] +Type=simple +User=opentsdb +Group=opentsdb +LimitNOFILE=65535 +Environment='JVMARGS=-DLOG_FILE=/var/log/opentsdb/opentsdb.log -DQUERY_LOG=/var/log/opentsdb/queries.log -XX:+ExitOnOutOfMemoryError -enableassertions -enablesystemassertions' +ExecStart=/usr/bin/tsdb tsd --config /usr/share/opentsdb/etc/opentsdb/opentsdb.conf +Restart=always +StandardOutput=journal diff --git a/opentsdb.spec.in b/opentsdb.spec.in index 6e90bdd292..5943b7f66e 100644 --- a/opentsdb.spec.in +++ b/opentsdb.spec.in @@ -57,6 +57,7 @@ rm -rf %{buildroot} make install DESTDIR=%{buildroot} mkdir -p %{buildroot}%{_localstatedir}/cache/opentsdb mkdir -p %{buildroot}%{_localstatedir}/log/opentsdb +mkdir -p %{buildroot}%{_localstatedir}/tmp/opentsdb mkdir -p %{buildroot}%{_datarootdir}/opentsdb/plugins # TODO: Use alternatives to manage the init script and configuration. @@ -77,8 +78,10 @@ rm -rf %{buildroot} %doc %{_datarootdir}/opentsdb %{_bindir}/tsdb +%dir %attr(0755,opentsdb,opentsdb) %{_tmppath}/opentsdb %dir %attr(0755,opentsdb,opentsdb) %{_localstatedir}/cache/opentsdb %dir %attr(0755,opentsdb,opentsdb) %{_localstatedir}/log/opentsdb +%dir %attr(0755,opentsdb,opentsdb) %{_localstatedir}/tmp/opentsdb %changelog @@ -91,6 +94,7 @@ if [ $1 -eq 1 ]; then # we're installing the first version of this package ln -s %{_datarootdir}/opentsdb/etc/opentsdb /etc/opentsdb if [ -d /run/systemd/system ]; then + ln -s %{_datarootdir}/opentsdb/etc/systemd/system/opentsdb.service /lib/systemd/system ln -s %{_datarootdir}/opentsdb/etc/systemd/system/opentsdb@.service /lib/systemd/system systemctl daemon-reload else @@ -104,6 +108,7 @@ if [ $1 -eq 0 ]; then # we're removing last version of this package [ -L /etc/opentsdb ] && rm -f /etc/opentsdb [ -L /etc/init.d/opentsdb ] && rm -f /etc/init.d/opentsdb + [ -L /lib/systemd/system/opentsdb.service ] && rm -f /lib/systemd/system/opentsdb.service [ -L /lib/systemd/system/opentsdb@.service ] && rm -f /lib/systemd/system/opentsdb@.service [ -d /run/systemd/system ] && systemctl daemon-reload fi From cf5999d4d2a5a0e35aef6d98d6ee8c06131510cc Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Tue, 27 Jul 2021 13:02:27 -0700 Subject: [PATCH 786/826] Fix for #1886 to allow metric only queries in the expressions and allow for no filters. --- src/query/pojo/Query.java | 30 +++--- src/tsd/QueryExecutor.java | 168 +++++++++++++++++---------------- test/query/pojo/TestQuery.java | 12 +++ 3 files changed, 118 insertions(+), 92 deletions(-) diff --git a/src/query/pojo/Query.java b/src/query/pojo/Query.java index 731c449950..d00201bacc 100644 --- a/src/query/pojo/Query.java +++ b/src/query/pojo/Query.java @@ -122,20 +122,24 @@ public void validate() { final Set<String> filter_ids = new HashSet<String>(); - for (Filter filter : filters) { - if (filter_ids.contains(filter.getId())) { - throw new IllegalArgumentException("duplicated filter id: " - + filter.getId()); + if (filters != null) { + for (Filter filter : filters) { + if (filter_ids.contains(filter.getId())) { + throw new IllegalArgumentException("duplicated filter id: " + + filter.getId()); + } + filter_ids.add(filter.getId()); } - filter_ids.add(filter.getId()); } - - for (Expression expression : expressions) { - if (variable_ids.contains(expression.getId())) { - throw new IllegalArgumentException("Duplicated variable or expression id: " - + expression.getId()); + + if (expressions != null) { + for (Expression expression : expressions) { + if (variable_ids.contains(expression.getId())) { + throw new IllegalArgumentException("Duplicated variable or expression id: " + + expression.getId()); + } + variable_ids.add(expression.getId()); } - variable_ids.add(expression.getId()); } validateCollection(metrics, "metric"); @@ -172,6 +176,10 @@ public void validate() { * @throws IllegalArgumentException if one or more parameters were invalid */ private void validateFilters() { + if (filters == null) { + return; + } + Set<String> ids = new HashSet<String>(); for (Filter filter : filters) { ids.add(filter.getId()); diff --git a/src/tsd/QueryExecutor.java b/src/tsd/QueryExecutor.java index b0598241e2..b465ba285f 100644 --- a/src/tsd/QueryExecutor.java +++ b/src/tsd/QueryExecutor.java @@ -194,22 +194,24 @@ public QueryExecutor(final TSDB tsdb, final Query query) { ts_query.setQueries(subs); // setup expressions - for (final Expression expression : query.getExpressions()) { - // TODO - flags - - // TODO - get a default from the configs - final SetOperator operator = expression.getJoin() != null ? - expression.getJoin().getOperator() : SetOperator.UNION; - final boolean qts = expression.getJoin() == null ? false : expression.getJoin().getUseQueryTags(); - final boolean ats = expression.getJoin() == null ? true : expression.getJoin().getIncludeAggTags(); - final ExpressionIterator iterator = - new ExpressionIterator(expression.getId(), expression.getExpr(), - operator, qts, ats); - if (expression.getFillPolicy() != null) { - iterator.setFillPolicy(expression.getFillPolicy()); + if (query.getExpressions() != null) { + for (final Expression expression : query.getExpressions()) { + // TODO - flags + + // TODO - get a default from the configs + final SetOperator operator = expression.getJoin() != null ? + expression.getJoin().getOperator() : SetOperator.UNION; + final boolean qts = expression.getJoin() == null ? false : expression.getJoin().getUseQueryTags(); + final boolean ats = expression.getJoin() == null ? true : expression.getJoin().getIncludeAggTags(); + final ExpressionIterator iterator = + new ExpressionIterator(expression.getId(), expression.getExpr(), + operator, qts, ats); + if (expression.getFillPolicy() != null) { + iterator.setFillPolicy(expression.getFillPolicy()); + } + expressions.put(expression.getId(), iterator); + } - expressions.put(expression.getId(), iterator); - } ts_query.validateAndSetQuery(); @@ -263,19 +265,21 @@ public Object call(final ArrayList<DataPoints[]> query_results) final Entry<String, TSSubQuery> entry = it.next(); if (entry.getValue().equals(sub)) { sub_query_results.put(entry.getKey(), query_results.get(i)); - for (final ExpressionIterator ei : expressions.values()) { - if (ei.getVariableNames().contains(entry.getKey())) { - final TimeSyncedIterator tsi = new TimeSyncedIterator( - entry.getKey(), sub.getFilterTagKs(), - query_results.get(i)); - final NumericFillPolicy fill = fills.get(entry.getKey()); - if (fill != null) { - tsi.setFillPolicy(fill); - } - ei.addResults(entry.getKey(), tsi); - if (LOG.isDebugEnabled()) { - LOG.debug("Added results for " + entry.getKey() + - " to " + ei.getId()); + if (expressions != null) { + for (final ExpressionIterator ei : expressions.values()) { + if (ei.getVariableNames().contains(entry.getKey())) { + final TimeSyncedIterator tsi = new TimeSyncedIterator( + entry.getKey(), sub.getFilterTagKs(), + query_results.get(i)); + final NumericFillPolicy fill = fills.get(entry.getKey()); + if (fill != null) { + tsi.setFillPolicy(fill); + } + ei.addResults(entry.getKey(), tsi); + if (LOG.isDebugEnabled()) { + LOG.debug("Added results for " + entry.getKey() + + " to " + ei.getId()); + } } } } @@ -287,74 +291,76 @@ public Object call(final ArrayList<DataPoints[]> query_results) final DirectedAcyclicGraph<String, DefaultEdge> graph = new DirectedAcyclicGraph<String, DefaultEdge>(DefaultEdge.class); - for (final Entry<String, ExpressionIterator> eii : expressions.entrySet()) { - if (LOG.isDebugEnabled()) { - LOG.debug(String.format("Expression entry key is %s, value is %s", - eii.getKey(), eii.getValue().toString())); - LOG.debug(String.format("Time to loop through the variable names " - + "for %s", eii.getKey())); - } - - if (!graph.containsVertex(eii.getKey())) { + if (expressions != null) { + for (final Entry<String, ExpressionIterator> eii : expressions.entrySet()) { if (LOG.isDebugEnabled()) { - LOG.debug("Adding vertex " + eii.getKey()); + LOG.debug(String.format("Expression entry key is %s, value is %s", + eii.getKey(), eii.getValue().toString())); + LOG.debug(String.format("Time to loop through the variable names " + + "for %s", eii.getKey())); } - graph.addVertex(eii.getKey()); - } - - for (final String var : eii.getValue().getVariableNames()) { - if (LOG.isDebugEnabled()) { - LOG.debug(String.format("var is %s", var)); - } - - final ExpressionIterator ei = expressions.get(var); - if (ei != null) { + if (!graph.containsVertex(eii.getKey())) { if (LOG.isDebugEnabled()) { - LOG.debug(String.format("The expression iterator for %s is %s", - var, ei.toString())); - } - - // TODO - really ought to calculate this earlier - if (eii.getKey().equals(var)) { - throw new IllegalArgumentException( - "Self referencing expression found: " + eii.getKey()); + LOG.debug("Adding vertex " + eii.getKey()); } + graph.addVertex(eii.getKey()); + } + for (final String var : eii.getValue().getVariableNames()) { if (LOG.isDebugEnabled()) { - LOG.debug("Nested expression detected. " + eii.getKey() + - " depends on " + var); + LOG.debug(String.format("var is %s", var)); } - if (!graph.containsVertex(eii.getKey())) { + final ExpressionIterator ei = expressions.get(var); + + if (ei != null) { if (LOG.isDebugEnabled()) { - LOG.debug("Added vertex " + eii.getKey()); + LOG.debug(String.format("The expression iterator for %s is %s", + var, ei.toString())); } - graph.addVertex(eii.getKey()); - } else if (LOG.isDebugEnabled()) { - LOG.debug("Already contains vertex " + eii.getKey()); - } - if (!graph.containsVertex(var)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Added vertex " + var); + // TODO - really ought to calculate this earlier + if (eii.getKey().equals(var)) { + throw new IllegalArgumentException( + "Self referencing expression found: " + eii.getKey()); } - graph.addVertex(var); - } else if (LOG.isDebugEnabled()) { - LOG.debug("Already contains vertex " + var); - } - try { if (LOG.isDebugEnabled()) { - LOG.debug("Added Edge " + eii.getKey() + " - " + var); + LOG.debug("Nested expression detected. " + eii.getKey() + + " depends on " + var); + } + + if (!graph.containsVertex(eii.getKey())) { + if (LOG.isDebugEnabled()) { + LOG.debug("Added vertex " + eii.getKey()); + } + graph.addVertex(eii.getKey()); + } else if (LOG.isDebugEnabled()) { + LOG.debug("Already contains vertex " + eii.getKey()); } - graph.addDagEdge(eii.getKey(), var); - } catch (CycleFoundException cfe) { - throw new IllegalArgumentException("Circular reference found: " + - eii.getKey(), cfe); + + if (!graph.containsVertex(var)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Added vertex " + var); + } + graph.addVertex(var); + } else if (LOG.isDebugEnabled()) { + LOG.debug("Already contains vertex " + var); + } + + try { + if (LOG.isDebugEnabled()) { + LOG.debug("Added Edge " + eii.getKey() + " - " + var); + } + graph.addDagEdge(eii.getKey(), var); + } catch (CycleFoundException cfe) { + throw new IllegalArgumentException("Circular reference found: " + + eii.getKey(), cfe); + } + } else if (LOG.isDebugEnabled()) { + LOG.debug(String.format("The expression iterator for %s is null", var)); } - } else if (LOG.isDebugEnabled()) { - LOG.debug(String.format("The expression iterator for %s is null", var)); } } } @@ -362,8 +368,8 @@ public Object call(final ArrayList<DataPoints[]> query_results) // compile all of the expressions final long intersect_start = DateTime.currentTimeMillis(); - final Integer expressionLength = expressions.size(); - final ExpressionIterator[] compile_stack = + final int expressionLength = expressions == null ? 0 : expressions.size(); + final ExpressionIterator[] compile_stack = new ExpressionIterator[expressionLength]; final TopologicalOrderIterator<String, DefaultEdge> it = new TopologicalOrderIterator<String, DefaultEdge>(graph); diff --git a/test/query/pojo/TestQuery.java b/test/query/pojo/TestQuery.java index 613ff787a8..fb00681a0a 100644 --- a/test/query/pojo/TestQuery.java +++ b/test/query/pojo/TestQuery.java @@ -215,6 +215,18 @@ public void serialize() throws Exception { // TODO - finish the assertions } + @Test + public void justMetrics() throws Exception { + Query query = Query.Builder() + .setMetrics(Arrays.asList(metric)) + .setName("q1") + .setTime(time) + .build(); + query.validate(); + assertEquals(metric.getMetric(), query.getMetrics().get(0).getMetric()); + } + + private Query.Builder getDefaultQueryBuilder() { return Query.Builder().setExpressions(Arrays.asList(expression)) .setFilters(Arrays.asList(filter)).setMetrics(Arrays.asList(metric)) From ab656051b4bb18d2935b8a4f7c77141503bfb597 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Tue, 27 Jul 2021 13:44:44 -0700 Subject: [PATCH 787/826] Fix #1885 by making the overall stats a concurrent map. Doh. --- src/stats/QueryStats.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/QueryStats.java b/src/stats/QueryStats.java index c470abbd03..b0edee5f88 100644 --- a/src/stats/QueryStats.java +++ b/src/stats/QueryStats.java @@ -249,7 +249,7 @@ public QueryStats(final String remote_address, final TSQuery query, executed = 1; query_start_ns = DateTime.nanoTime(); query_start_ms = DateTime.currentTimeMillis(); - overall_stats = new HashMap<QueryStat, Long>(); + overall_stats = new ConcurrentHashMap<QueryStat, Long>(); query_stats = new ConcurrentHashMap<Integer, Map<QueryStat, Long>>(1); scanner_stats = new ConcurrentHashMap<Integer, Map<Integer, Map<QueryStat, Long>>>(1); From 7b4994080ebf823042dda0de4d44da494c1d398c Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Thu, 29 Jul 2021 14:47:34 -0700 Subject: [PATCH 788/826] Fix #1959 by updating to the latest asyncbigtable client. --- .../asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 | 1 - .../asyncbigtable-0.4.3-jar-with-dependencies.jar.md5 | 1 + third_party/asyncbigtable/include.mk | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 create mode 100644 third_party/asyncbigtable/asyncbigtable-0.4.3-jar-with-dependencies.jar.md5 diff --git a/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 deleted file mode 100644 index ba351e7405..0000000000 --- a/third_party/asyncbigtable/asyncbigtable-0.3.0-jar-with-dependencies.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -4384ac07967ee99f54d4c29f9806d7e7 \ No newline at end of file diff --git a/third_party/asyncbigtable/asyncbigtable-0.4.3-jar-with-dependencies.jar.md5 b/third_party/asyncbigtable/asyncbigtable-0.4.3-jar-with-dependencies.jar.md5 new file mode 100644 index 0000000000..ae03a41a90 --- /dev/null +++ b/third_party/asyncbigtable/asyncbigtable-0.4.3-jar-with-dependencies.jar.md5 @@ -0,0 +1 @@ +0e8c09c99998241ce539f8a500692d50 \ No newline at end of file diff --git a/third_party/asyncbigtable/include.mk b/third_party/asyncbigtable/include.mk index 697e6cefe3..d05fe1cd06 100644 --- a/third_party/asyncbigtable/include.mk +++ b/third_party/asyncbigtable/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2015 The OpenTSDB Authors. +# Copyright (C) 2015-2021 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -13,9 +13,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -ASYNCBIGTABLE_VERSION := 0.3.1-20170903.031804-2 +ASYNCBIGTABLE_VERSION := 0.4.3 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://repo1.maven.org/maven2/com/pythian/opentsdb/asyncbigtable/$(ASYNCBIGTABLE_VERSION) $(ASYNCBIGTABLE): $(ASYNCBIGTABLE).md5 set dummy "$(ASYNCBIGTABLE_BASE_URL)" "$(ASYNCBIGTABLE)"; shift; $(FETCH_DEPENDENCY) From eb125a342040376450dd155fe600ff7d4b9c0eec Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Thu, 2 Sep 2021 15:27:11 -0700 Subject: [PATCH 789/826] Cut the 2.4.1 release. --- NEWS | 33 +++++++++++++++++++++++++++++++++ THANKS | 13 +++++++++++-- configure.ac | 4 ++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index be2f210687..969f8dbad3 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,37 @@ OpenTSDB - Changelog +* Version 2.4.1 (2021-09-02) + +Noteworthy Changes: + - Add a config flag to enable splitting queries across the rollup and + raw data tables. Another config determines when to split the queries. + - Fix for CVE-2020-35476 that now validates and limits the inputs for + Gnuplot query parameters to prevent remote code execution. + - Default log config will log CLI tools at INFO to standard out. + - New check_tsd_v2 script that evaluates individual metric groups given + a filter. + - Collect stats from meta cache plugins. + - Add a python script to list and pretty-print queries running on a TSD. + - Add a single, standalone TSD systemd startup script and default to that + instead of the multi-port TSD script. + +Bug Fixes: + - Fix the "--sync" flag for FSCK to wait for repairs to execute against + storage before proceeding. + - Fix expression queries to allow metric only and filterless queries. + - Fix an NPE in /api/query/last when appends are enabled. + - Fix races in the salt scanner and multigets on the storage maps. + - Fix rollup queries that need sum and count for downsampling instead of + group bys. + - Fix fuzzy row filters for later versions of HBase by using filter pairs. + And allow it to be combined with a regex filter. + - Fix stats from the individual salt scanners and overall query stats + concurrency issues. + - Rename the stat "maxScannerUidtoStringTime" to + "maxScannerUidToStringTime" + - Fix the min initial value for double values in the AggregationIterator + - Fix rollup queries producing unexpected results. + - Fix various UTs + - Support rollups in FSCK * Version 2.4.0 (2018-12-16) diff --git a/THANKS b/THANKS index 0d0a7fec73..dd4c950fdd 100644 --- a/THANKS +++ b/THANKS @@ -19,9 +19,11 @@ Anna Claiborne Aravind Gottipati <aravind.gottipati@gmail.com> Arvind Jayaprakash <work@anomalizer.net> Berk D. Demir <bdd@mindcast.org> +BHourlier Bikrant Neupane Bizhu Qiu -BHourlier +Björn Marschollek +Björn Zettergren Bryan Hernandez <bryan4887@gmail.com> Bryan Zubrod <bzubrod@adknowledge.com> Camden Narzt @@ -34,6 +36,7 @@ Christos Soulios Christophe Furmaniak Dave Barr <dave.barr@gmail.com> Davide D Amico +Designershao Dfsklar Eric Price Ethan Wang @@ -43,6 +46,7 @@ GreatSnoopy Guenther Schmuelling <schmuell@pepperdata.com> Haiyang Jiang Hari Krishna Dara +Hari Sekhon Hong Dai Thanh Hugo M Fernandes Hugo Trippaers <opensource@strocamp.net> @@ -99,8 +103,10 @@ Nikhil Benesch <me@designbynikhil.com> Nitin Aggarwal NoHarm Opsun +Øyvind Matheson Wergeland Qu Dong Fang Paula Keezer <paula.keezer@gmail.com> +pengmengqing Peter Edwards Peter Gotz <peter.s.goetz@googlemail.com> Peter Edwards @@ -108,8 +114,10 @@ Ping Yong Pradeep Chhetri <pradeep.chhetri89@gmail.com> Rajesh G Rohan Nog +Ronan Harmegnies Ryan Berdeen <ryan@ryanberdeen.com> Sean Miller +Selim Chergui Siddartha Guthikonda <siddartha.gu@gmail.com> Simon Matic Langford <simon@exemel.co.uk> Slawek Ligus <root@ooz.ie> @@ -119,8 +127,9 @@ Tay Ray Chuan <raychuan@iweb.nus.edu.sg> Thomas Krajca <t.l.krajca@gmail.com> Thomas Sanchez <thomas.sanchez@dotcloud.com> Tibor Vass -Tristan Colgate-McFarlane <tcolgate@gmail.com> +Tony Di Nucci <tony.dinucci@skyscanner.net> Tony Landells <tony.landells@gmail.com> +Tristan Colgate-McFarlane <tcolgate@gmail.com> Utkarsh Bhatnagar Vasiliy Kiryanov <vasiliy.kiryanov@gmail.com> Vitaliy Fuks diff --git a/configure.ac b/configure.ac index 20b3399356..f96c94451d 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2016 The OpenTSDB Authors. +# Copyright (C) 2011-2021 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.4.1-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.4.1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From 877cdfad2e456b566f366f8ccbb9cbcdbdaa2836 Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Fri, 8 Oct 2021 09:20:59 +0100 Subject: [PATCH 790/826] Fix race condition in UID lookup Ensure the scanner callback waits for all the UIDs to resolve before calling scan to retrieve the next set of results. Not waiting would cause the scanner to close prematurely and return incomplete results. --- src/core/SaltScanner.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index b5da4e2da7..3e571c025f 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -646,8 +646,7 @@ public Object call(final ArrayList<ArrayList<KeyValue>> rows) // TODO - more efficient resolution // TODO - byte set instead of a string for the uid may be faster if (filters != null && !filters.isEmpty()) { - lookups.clear(); - final String tsuid = + final String tsuid = UniqueId.uidToString(UniqueId.getTSUIDFromKey(key, TSDB.metrics_width(), Const.TIMESTAMP_BYTES)); if (skips.contains(tsuid)) { From 2f4bbfba2f9a32f9295123e8b90adba022c11ece Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@netflix.com> Date: Mon, 20 Dec 2021 13:22:08 -0800 Subject: [PATCH 791/826] Bump Logback version to patch CVE-2021-42550 --- third_party/logback/include.mk | 4 ++-- third_party/logback/logback-classic-1.0.9.jar.md5 | 1 - third_party/logback/logback-core-1.0.9.jar.md5 | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 third_party/logback/logback-classic-1.0.9.jar.md5 delete mode 100644 third_party/logback/logback-core-1.0.9.jar.md5 diff --git a/third_party/logback/include.mk b/third_party/logback/include.mk index 078f6eadfd..e9d2b34ebf 100644 --- a/third_party/logback/include.mk +++ b/third_party/logback/include.mk @@ -13,8 +13,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.0.13/logback-classic-1.0.13.jar -LOGBACK_VERSION := 1.0.13 +https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.9/logback-classic-1.2.9.jar +LOGBACK_VERSION := 1.2.9 LOGBACK_CLASSIC_VERSION := $(LOGBACK_VERSION) LOGBACK_CLASSIC := third_party/logback/logback-classic-$(LOGBACK_CLASSIC_VERSION).jar diff --git a/third_party/logback/logback-classic-1.0.9.jar.md5 b/third_party/logback/logback-classic-1.0.9.jar.md5 deleted file mode 100644 index 283adbfc00..0000000000 --- a/third_party/logback/logback-classic-1.0.9.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -ca99e6b10e9b2f46f264afc5cf1c13e1 diff --git a/third_party/logback/logback-core-1.0.9.jar.md5 b/third_party/logback/logback-core-1.0.9.jar.md5 deleted file mode 100644 index 6a41d62be9..0000000000 --- a/third_party/logback/logback-core-1.0.9.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -d1647b6efc66bd38237e4d8c484aa7b4 From 8c6a86ddbc367c7e4e2877973b70f77c105c6158 Mon Sep 17 00:00:00 2001 From: Simon Matic Langford <simon@exemel.co.uk> Date: Tue, 4 Jan 2022 13:58:49 +0000 Subject: [PATCH 792/826] Add logback 1.2.9 jar md5s --- third_party/logback/logback-classic-1.2.9.jar.md5 | 1 + third_party/logback/logback-core-1.2.9.jar.md5 | 1 + 2 files changed, 2 insertions(+) create mode 100644 third_party/logback/logback-classic-1.2.9.jar.md5 create mode 100644 third_party/logback/logback-core-1.2.9.jar.md5 diff --git a/third_party/logback/logback-classic-1.2.9.jar.md5 b/third_party/logback/logback-classic-1.2.9.jar.md5 new file mode 100644 index 0000000000..37d75e4e09 --- /dev/null +++ b/third_party/logback/logback-classic-1.2.9.jar.md5 @@ -0,0 +1 @@ +de212f6deebb4cf8b5c91853602db5ec \ No newline at end of file diff --git a/third_party/logback/logback-core-1.2.9.jar.md5 b/third_party/logback/logback-core-1.2.9.jar.md5 new file mode 100644 index 0000000000..6d94ceb6bb --- /dev/null +++ b/third_party/logback/logback-core-1.2.9.jar.md5 @@ -0,0 +1 @@ +e0e576b4001e1c99e551655b3fcbacbf \ No newline at end of file From a82a4f85f0fc1af554a104f28cc495451b26b1f6 Mon Sep 17 00:00:00 2001 From: Simon Matic Langford <simon@exemel.co.uk> Date: Wed, 5 Jan 2022 19:44:28 +0000 Subject: [PATCH 793/826] Enhance range tests and regex and add better default for output validator (#2217) * Enhance range tests and regex and add better default for output validator --- src/tsd/GraphHandler.java | 3 +- src/utils/Config.java | 1 + test/tsd/TestGraphHandler.java | 86 +++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 8e065bfc81..bbb265cb0d 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -70,8 +70,9 @@ final class GraphHandler implements HttpRpc { private static final boolean IS_WINDOWS = System.getProperty("os.name", "").contains("Windows"); + private static final String RANGE_COMPONENT = "\\\"?-?\\d*\\.?(\\d+)?([eE]-?\\d+)?\\\"?"; private static Pattern RANGE_VALIDATOR = Pattern.compile( - "\\[\\\"?-?\\d+\\.?(\\d+)?([eE]-?\\d+)?\\\"?:\\\"?-?(\\d+\\.?\\d+?)?([eE]-?\\d+)?\\\"?\\]"); + "\\["+RANGE_COMPONENT+":"+RANGE_COMPONENT+"]"); private static Pattern LABEL_VALIDATOR = Pattern.compile("[a-zA-z0-9 \\-_]"); private static Pattern KEY_VALIDATOR = Pattern.compile( "(out|left|top|center|right|horiz|box|bottom)?\\s?"); diff --git a/src/utils/Config.java b/src/utils/Config.java index 723df36c7a..c534900aca 100644 --- a/src/utils/Config.java +++ b/src/utils/Config.java @@ -556,6 +556,7 @@ protected void setDefaults() { default_map.put("tsd.core.storage_exception_handler.enable", "false"); default_map.put("tsd.core.uid.random_metrics", "false"); default_map.put("tsd.core.bulk.allow_out_of_order_timestamps", "false"); + default_map.put("tsd.gnuplot.options.allowlist", ";axis x1y2"); default_map.put("tsd.query.filter.expansion_limit", "4096"); default_map.put("tsd.query.skip_unresolved_tagvs", "false"); default_map.put("tsd.query.allow_simultaneous_duplicates", "true"); diff --git a/test/tsd/TestGraphHandler.java b/test/tsd/TestGraphHandler.java index 71311856fa..011ca631d6 100644 --- a/test/tsd/TestGraphHandler.java +++ b/test/tsd/TestGraphHandler.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; + import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -65,13 +66,94 @@ public final class TestGraphHandler { } @Test - public void setPlotParams() throws Exception { + public void setYRangeParams() throws Exception { Plot plot = mock(Plot.class); HttpQuery query = mock(HttpQuery.class); Map<String, List<String>> params = Maps.newHashMap(); when(query.getQueryString()).thenReturn(params); + + params.put("yrange", Lists.newArrayList("[0:1]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:0]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:42]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:-42]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:0.8]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:-0.8]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:42.4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:-42.4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:4e4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:-4e4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:4e-4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:-4e-4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:4.2e4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[:-4.2e4]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[0:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[-5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[0.5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[-0.5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[10.5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[-10.5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[10e5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[-10e5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[10e-5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[-10e-5:]")); + GraphHandler.setPlotParams(query, plot); + + params.put("yrange", Lists.newArrayList("[10.1e-5:]")); + GraphHandler.setPlotParams(query, plot); - params.put("yrange", Lists.newArrayList("[0:42]")); + params.put("yrange", Lists.newArrayList("[-10.1e-5:]")); GraphHandler.setPlotParams(query, plot); params.put("yrange", Lists.newArrayList("[33:system('touch /tmp/poc.txt')]")); From 9b62442ba5c006376f57ef250fb7debe1047c3bf Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@netflix.com> Date: Thu, 22 Dec 2022 19:59:51 -0800 Subject: [PATCH 794/826] Update some dependencies for #2262. WARNING: The minimum JDK is now version 8 due to Jackson. Fix the Json serializer to explicitly write nulls due to API changes in Jackson. Ignore a couple of histogram generating data files. --- src/tsd/HttpJsonSerializer.java | 4 ++-- test/core/LongHistogramDataPointForTest.java | 4 +++- test/core/LongHistogramDataPointForTestDecoder.java | 5 ++++- third_party/apache/commons-math3-3.6.1.jar.md5 | 1 + third_party/apache/include.mk | 4 ++-- third_party/gwt/gwt-dev-2.5.0.jar.md5 | 1 - third_party/gwt/gwt-user-2.5.0.jar.md5 | 1 - third_party/gwt/include.mk | 4 ++-- third_party/jackson/include.mk | 4 ++-- third_party/jackson/jackson-annotations-2.14.1.jar.md5 | 1 + third_party/jackson/jackson-annotations-2.4.3.jar.md5 | 1 - third_party/jackson/jackson-core-2.14.1.jar.md5 | 1 + third_party/jackson/jackson-core-2.4.3.jar.md5 | 1 - third_party/jackson/jackson-databind-2.14.1.jar.md5 | 1 + third_party/jackson/jackson-databind-2.4.3.jar.md5 | 1 - third_party/jexl/commons-logging-1.2.jar.md5 | 1 + third_party/jexl/include.mk | 4 ++-- third_party/logback/include.mk | 6 ++---- third_party/logback/logback-classic-1.0.13.jar.md5 | 1 - third_party/logback/logback-classic-1.3.4.jar.md5 | 1 + third_party/logback/logback-core-1.0.13.jar.md5 | 1 - third_party/logback/logback-core-1.3.4.jar.md5 | 1 + .../{mockito-1.9.0.jar.md5 => mockito-core-1.9.0.jar.md5} | 0 third_party/slf4j/include.mk | 2 +- third_party/slf4j/log4j-over-slf4j-1.7.2.jar.md5 | 1 - third_party/slf4j/log4j-over-slf4j-2.0.6.jar.md5 | 1 + third_party/slf4j/slf4j-api-1.7.2.jar.md5 | 1 - third_party/slf4j/slf4j-api-2.0.6.jar.md5 | 1 + 28 files changed, 29 insertions(+), 26 deletions(-) create mode 100644 third_party/apache/commons-math3-3.6.1.jar.md5 delete mode 100644 third_party/gwt/gwt-dev-2.5.0.jar.md5 delete mode 100644 third_party/gwt/gwt-user-2.5.0.jar.md5 create mode 100644 third_party/jackson/jackson-annotations-2.14.1.jar.md5 delete mode 100644 third_party/jackson/jackson-annotations-2.4.3.jar.md5 create mode 100644 third_party/jackson/jackson-core-2.14.1.jar.md5 delete mode 100644 third_party/jackson/jackson-core-2.4.3.jar.md5 create mode 100644 third_party/jackson/jackson-databind-2.14.1.jar.md5 delete mode 100644 third_party/jackson/jackson-databind-2.4.3.jar.md5 create mode 100644 third_party/jexl/commons-logging-1.2.jar.md5 delete mode 100644 third_party/logback/logback-classic-1.0.13.jar.md5 create mode 100644 third_party/logback/logback-classic-1.3.4.jar.md5 delete mode 100644 third_party/logback/logback-core-1.0.13.jar.md5 create mode 100644 third_party/logback/logback-core-1.3.4.jar.md5 rename third_party/mockito/{mockito-1.9.0.jar.md5 => mockito-core-1.9.0.jar.md5} (100%) delete mode 100644 third_party/slf4j/log4j-over-slf4j-1.7.2.jar.md5 create mode 100644 third_party/slf4j/log4j-over-slf4j-2.0.6.jar.md5 delete mode 100644 third_party/slf4j/slf4j-api-1.7.2.jar.md5 create mode 100644 third_party/slf4j/slf4j-api-2.0.6.jar.md5 diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index b3e7d6937b..18d8139dc9 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2013 The OpenTSDB Authors. +// Copyright (C) 2013-2023 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 @@ -859,7 +859,7 @@ public Object call(final ArrayList<Object> deferreds) throws Exception { final double value = dp.doubleValue(); if (Double.isNaN(value) && orig_query.fillPolicy() == FillPolicy.NULL) { - json.writeNumberField(Long.toString(timestamp), null); + json.writeNullField(Long.toString(timestamp)); } else { json.writeNumberField(Long.toString(timestamp), dp.doubleValue()); } diff --git a/test/core/LongHistogramDataPointForTest.java b/test/core/LongHistogramDataPointForTest.java index 65b84ec691..f716ad9f2f 100644 --- a/test/core/LongHistogramDataPointForTest.java +++ b/test/core/LongHistogramDataPointForTest.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2016-2017 The OpenTSDB Authors. +// Copyright (C) 2016-2023 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 @@ -18,7 +18,9 @@ import java.util.Map; import org.hbase.async.Bytes; +import org.junit.Ignore; +@Ignore public class LongHistogramDataPointForTest implements Histogram { private final int id; private long data; diff --git a/test/core/LongHistogramDataPointForTestDecoder.java b/test/core/LongHistogramDataPointForTestDecoder.java index f7381643e9..e7dd40dca9 100644 --- a/test/core/LongHistogramDataPointForTestDecoder.java +++ b/test/core/LongHistogramDataPointForTestDecoder.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2016-2017 The OpenTSDB Authors. +// Copyright (C) 2016-2023 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 @@ -12,6 +12,9 @@ // see <http://www.gnu.org/licenses/>. package net.opentsdb.core; +import org.junit.Ignore; + +@Ignore public class LongHistogramDataPointForTestDecoder extends HistogramDataPointCodec { @Override diff --git a/third_party/apache/commons-math3-3.6.1.jar.md5 b/third_party/apache/commons-math3-3.6.1.jar.md5 new file mode 100644 index 0000000000..2a5ecc945b --- /dev/null +++ b/third_party/apache/commons-math3-3.6.1.jar.md5 @@ -0,0 +1 @@ +5b730d97e4e6368069de1983937c508e diff --git a/third_party/apache/include.mk b/third_party/apache/include.mk index 5f4fb82d7b..991f95bb8c 100644 --- a/third_party/apache/include.mk +++ b/third_party/apache/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2013 The OpenTSDB Authors. +# Copyright (C) 2011-2022 The OpenTSDB Authors. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -22,7 +22,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -APACHE_MATH_VERSION := 3.4.1 +APACHE_MATH_VERSION := 3.6.1 APACHE_MATH := third_party/apache/commons-math3-$(APACHE_MATH_VERSION).jar APACHE_MATH_BASE_URL := https://repo1.maven.org/maven2/org/apache/commons/commons-math3/$(APACHE_MATH_VERSION) diff --git a/third_party/gwt/gwt-dev-2.5.0.jar.md5 b/third_party/gwt/gwt-dev-2.5.0.jar.md5 deleted file mode 100644 index 757d50e319..0000000000 --- a/third_party/gwt/gwt-dev-2.5.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -538df8db77bacc863f34b82c59d5632d diff --git a/third_party/gwt/gwt-user-2.5.0.jar.md5 b/third_party/gwt/gwt-user-2.5.0.jar.md5 deleted file mode 100644 index 7a290c8be2..0000000000 --- a/third_party/gwt/gwt-user-2.5.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -c58e29c1132ee6694e8f780e045f4d13 diff --git a/third_party/gwt/include.mk b/third_party/gwt/include.mk index 27f8699f3c..a3c911d71b 100644 --- a/third_party/gwt/include.mk +++ b/third_party/gwt/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2012 The OpenTSDB Authors. +# Copyright (C) 2011-2023 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -GWT_VERSION := 2.6.0 +GWT_VERSION := 2.6.1 GWT_DEV_VERSION := $(GWT_VERSION) GWT_DEV := third_party/gwt/gwt-dev-$(GWT_DEV_VERSION).jar diff --git a/third_party/jackson/include.mk b/third_party/jackson/include.mk index 7a0a904ce5..57002ac30b 100644 --- a/third_party/jackson/include.mk +++ b/third_party/jackson/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2014 The OpenTSDB Authors. +# Copyright (C) 2011-2022 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -JACKSON_VERSION := 2.9.5 +JACKSON_VERSION := 2.14.1 JACKSON_ANNOTATIONS_VERSION = $(JACKSON_VERSION) JACKSON_ANNOTATIONS := third_party/jackson/jackson-annotations-$(JACKSON_ANNOTATIONS_VERSION).jar diff --git a/third_party/jackson/jackson-annotations-2.14.1.jar.md5 b/third_party/jackson/jackson-annotations-2.14.1.jar.md5 new file mode 100644 index 0000000000..3e09d564c8 --- /dev/null +++ b/third_party/jackson/jackson-annotations-2.14.1.jar.md5 @@ -0,0 +1 @@ +da4742ba6aeb24bf1bd29382fd95647b diff --git a/third_party/jackson/jackson-annotations-2.4.3.jar.md5 b/third_party/jackson/jackson-annotations-2.4.3.jar.md5 deleted file mode 100644 index b921a9846f..0000000000 --- a/third_party/jackson/jackson-annotations-2.4.3.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -31ef4fa866f9d24960a6807c9c299e98 diff --git a/third_party/jackson/jackson-core-2.14.1.jar.md5 b/third_party/jackson/jackson-core-2.14.1.jar.md5 new file mode 100644 index 0000000000..72ffeeed96 --- /dev/null +++ b/third_party/jackson/jackson-core-2.14.1.jar.md5 @@ -0,0 +1 @@ +f9604a5f31129cdb7db4bdec90111850 \ No newline at end of file diff --git a/third_party/jackson/jackson-core-2.4.3.jar.md5 b/third_party/jackson/jackson-core-2.4.3.jar.md5 deleted file mode 100644 index d166db8725..0000000000 --- a/third_party/jackson/jackson-core-2.4.3.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -750ef3d86f04fe0d6d14d6ae904a6d2d diff --git a/third_party/jackson/jackson-databind-2.14.1.jar.md5 b/third_party/jackson/jackson-databind-2.14.1.jar.md5 new file mode 100644 index 0000000000..fe98f6bf13 --- /dev/null +++ b/third_party/jackson/jackson-databind-2.14.1.jar.md5 @@ -0,0 +1 @@ +3e3e7aab8799ccc169b10f244e6fb5b4 diff --git a/third_party/jackson/jackson-databind-2.4.3.jar.md5 b/third_party/jackson/jackson-databind-2.4.3.jar.md5 deleted file mode 100644 index 52f07b826c..0000000000 --- a/third_party/jackson/jackson-databind-2.4.3.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -4fcb9f74280eaa21de10191212c65b11 diff --git a/third_party/jexl/commons-logging-1.2.jar.md5 b/third_party/jexl/commons-logging-1.2.jar.md5 new file mode 100644 index 0000000000..4d1ffb1050 --- /dev/null +++ b/third_party/jexl/commons-logging-1.2.jar.md5 @@ -0,0 +1 @@ +040b4b4d8eac886f6b4a2a3bd2f31b00 \ No newline at end of file diff --git a/third_party/jexl/include.mk b/third_party/jexl/include.mk index b8ac41ff1a..9e8d3e5343 100644 --- a/third_party/jexl/include.mk +++ b/third_party/jexl/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2015 The OpenTSDB Authors. +# Copyright (C) 2015-2023 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -23,7 +23,7 @@ $(JEXL): $(JEXL).md5 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_VERSION := 1.2 COMMONS_LOGGING := third_party/jexl/commons-logging-$(COMMONS_LOGGING_VERSION).jar COMMONS_LOGGING_BASE_URL := https://repo1.maven.org/maven2/commons-logging/commons-logging/$(COMMONS_LOGGING_VERSION) diff --git a/third_party/logback/include.mk b/third_party/logback/include.mk index e9d2b34ebf..ba2f67e5c5 100644 --- a/third_party/logback/include.mk +++ b/third_party/logback/include.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2015 The OpenTSDB Authors. +# Copyright (C) 2015-2022 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -12,9 +12,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. - -https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.9/logback-classic-1.2.9.jar -LOGBACK_VERSION := 1.2.9 +LOGBACK_VERSION := 1.3.4 LOGBACK_CLASSIC_VERSION := $(LOGBACK_VERSION) LOGBACK_CLASSIC := third_party/logback/logback-classic-$(LOGBACK_CLASSIC_VERSION).jar diff --git a/third_party/logback/logback-classic-1.0.13.jar.md5 b/third_party/logback/logback-classic-1.0.13.jar.md5 deleted file mode 100644 index ae8b69cb80..0000000000 --- a/third_party/logback/logback-classic-1.0.13.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -18586a078b51918942002ec085338e19 diff --git a/third_party/logback/logback-classic-1.3.4.jar.md5 b/third_party/logback/logback-classic-1.3.4.jar.md5 new file mode 100644 index 0000000000..cc7fdb0a1f --- /dev/null +++ b/third_party/logback/logback-classic-1.3.4.jar.md5 @@ -0,0 +1 @@ +0b98f1f2ad1caa97556b979598038d3d \ No newline at end of file diff --git a/third_party/logback/logback-core-1.0.13.jar.md5 b/third_party/logback/logback-core-1.0.13.jar.md5 deleted file mode 100644 index d4093eb2a6..0000000000 --- a/third_party/logback/logback-core-1.0.13.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -945c6dc3c10d3ce784d456a8bbbd0262 diff --git a/third_party/logback/logback-core-1.3.4.jar.md5 b/third_party/logback/logback-core-1.3.4.jar.md5 new file mode 100644 index 0000000000..0e85214d09 --- /dev/null +++ b/third_party/logback/logback-core-1.3.4.jar.md5 @@ -0,0 +1 @@ +bbb33326f538cfd28186d5b4310b44fa \ No newline at end of file diff --git a/third_party/mockito/mockito-1.9.0.jar.md5 b/third_party/mockito/mockito-core-1.9.0.jar.md5 similarity index 100% rename from third_party/mockito/mockito-1.9.0.jar.md5 rename to third_party/mockito/mockito-core-1.9.0.jar.md5 diff --git a/third_party/slf4j/include.mk b/third_party/slf4j/include.mk index 49743b486a..7fa319e9d3 100644 --- a/third_party/slf4j/include.mk +++ b/third_party/slf4j/include.mk @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -SLF4J_VERSION = 1.7.7 +SLF4J_VERSION = 2.0.6 LOG4J_OVER_SLF4J_VERSION := $(SLF4J_VERSION) diff --git a/third_party/slf4j/log4j-over-slf4j-1.7.2.jar.md5 b/third_party/slf4j/log4j-over-slf4j-1.7.2.jar.md5 deleted file mode 100644 index 3b024cdfe3..0000000000 --- a/third_party/slf4j/log4j-over-slf4j-1.7.2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -cbd407d8ff67a6d54fd233fc0e0d8228 diff --git a/third_party/slf4j/log4j-over-slf4j-2.0.6.jar.md5 b/third_party/slf4j/log4j-over-slf4j-2.0.6.jar.md5 new file mode 100644 index 0000000000..0608e24c10 --- /dev/null +++ b/third_party/slf4j/log4j-over-slf4j-2.0.6.jar.md5 @@ -0,0 +1 @@ +a5ccd131feef393a926dcdfc80436d08 \ No newline at end of file diff --git a/third_party/slf4j/slf4j-api-1.7.2.jar.md5 b/third_party/slf4j/slf4j-api-1.7.2.jar.md5 deleted file mode 100644 index 4853af80dc..0000000000 --- a/third_party/slf4j/slf4j-api-1.7.2.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -ebf348e2831a3b610860fa134ad6f67f diff --git a/third_party/slf4j/slf4j-api-2.0.6.jar.md5 b/third_party/slf4j/slf4j-api-2.0.6.jar.md5 new file mode 100644 index 0000000000..31ad90d856 --- /dev/null +++ b/third_party/slf4j/slf4j-api-2.0.6.jar.md5 @@ -0,0 +1 @@ +0dd65c386e8c5f4e6e014de3f7a7ae60 \ No newline at end of file From 22b27ea30a859a6dbdcd65fcdf61190d46e1b677 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@netflix.com> Date: Mon, 10 Apr 2023 13:22:15 -0700 Subject: [PATCH 795/826] Tighten up the regexes for Gnuplot URI params per multiple security reports. The best way of avoiding RCEs is to disable Gnuplot, but this should help a little. --- src/tsd/GraphHandler.java | 14 +-- test/tsd/TestGraphHandler.java | 221 +++++++++++++++++++-------------- 2 files changed, 135 insertions(+), 100 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index bbb265cb0d..bf321e8564 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -72,15 +72,15 @@ final class GraphHandler implements HttpRpc { private static final String RANGE_COMPONENT = "\\\"?-?\\d*\\.?(\\d+)?([eE]-?\\d+)?\\\"?"; private static Pattern RANGE_VALIDATOR = Pattern.compile( - "\\["+RANGE_COMPONENT+":"+RANGE_COMPONENT+"]"); - private static Pattern LABEL_VALIDATOR = Pattern.compile("[a-zA-z0-9 \\-_]"); + "^\\["+RANGE_COMPONENT+":"+RANGE_COMPONENT+"]$"); + private static Pattern LABEL_VALIDATOR = Pattern.compile("^[a-zA-z0-9 \\-_]+$"); private static Pattern KEY_VALIDATOR = Pattern.compile( - "(out|left|top|center|right|horiz|box|bottom)?\\s?"); - private static Pattern STYLE_VALIDATOR = Pattern.compile("(linespoint|points|circles|dots)"); - private static Pattern COLOR_VALIDATOR = Pattern.compile("(x|X)[a-fA-F0-9]{6}"); - private static Pattern SMOOTH_VALIDATOR = Pattern.compile("unique|frequency|fnormal|cumulative|cnormal|bins|csplines|acsplines|mcsplines|bezier|sbezier|unwrap|zsort"); + "^out|left|top|center|right|horiz|box|bottom$"); + private static Pattern STYLE_VALIDATOR = Pattern.compile("^linespoint|points|circles|dots$"); + private static Pattern COLOR_VALIDATOR = Pattern.compile("^(x|X)[a-fA-F0-9]{6}$"); + private static Pattern SMOOTH_VALIDATOR = Pattern.compile("^unique|frequency|fnormal|cumulative|cnormal|bins|csplines|acsplines|mcsplines|bezier|sbezier|unwrap|zsort$"); // NOTE: This one should be tightened for only time based formatters. - private static Pattern FORMAT_VALIDATOR = Pattern.compile("(%[a-zA-Z])+[:\\/]?\\s?"); + private static Pattern FORMAT_VALIDATOR = Pattern.compile("^[%0-9.a-zA-Z \\-]+$"); private static Pattern WXH_VALIDATOR = Pattern.compile("^\\d+x\\d+$"); /** Number of times we had to do all the work up to running Gnuplot. */ private static final AtomicInteger graphs_generated diff --git a/test/tsd/TestGraphHandler.java b/test/tsd/TestGraphHandler.java index 011ca631d6..d2fbca43d4 100644 --- a/test/tsd/TestGraphHandler.java +++ b/test/tsd/TestGraphHandler.java @@ -67,102 +67,114 @@ public final class TestGraphHandler { @Test public void setYRangeParams() throws Exception { - Plot plot = mock(Plot.class); - HttpQuery query = mock(HttpQuery.class); - Map<String, List<String>> params = Maps.newHashMap(); - when(query.getQueryString()).thenReturn(params); + assertPlotParam("yrange","[0:1]"); + assertPlotParam("yrange", "[:]"); + assertPlotParam("yrange", "[:0]"); + assertPlotParam("yrange", "[:42]"); + assertPlotParam("yrange", "[:-42]"); + assertPlotParam("yrange", "[:0.8]"); + assertPlotParam("yrange", "[:-0.8]"); + assertPlotParam("yrange", "[:42.4]"); + assertPlotParam("yrange", "[:-42.4]"); + assertPlotParam("yrange", "[:4e4]"); + assertPlotParam("yrange", "[:-4e4]"); + assertPlotParam("yrange", "[:4e-4]"); + assertPlotParam("yrange", "[:-4e-4]"); + assertPlotParam("yrange", "[:4.2e4]"); + assertPlotParam("yrange", "[:-4.2e4]"); + assertPlotParam("yrange", "[0:]"); + assertPlotParam("yrange", "[5:]"); + assertPlotParam("yrange", "[-5:]"); + assertPlotParam("yrange", "[0.5:]"); + assertPlotParam("yrange", "[-0.5:]"); + assertPlotParam("yrange", "[10.5:]"); + assertPlotParam("yrange", "[-10.5:]"); + assertPlotParam("yrange", "[10e5:]"); + assertPlotParam("yrange", "[-10e5:]"); + assertPlotParam("yrange", "[10e-5:]"); + assertPlotParam("yrange", "[-10e-5:]"); + assertPlotParam("yrange", "[10.1e-5:]"); + assertPlotParam("yrange", "[-10.1e-5:]"); + assertPlotParam("yrange", "[-10.1e-5:-10.1e-6]"); + assertInvalidPlotParam("yrange", "[33:system('touch /tmp/poc.txt')]"); + } - params.put("yrange", Lists.newArrayList("[0:1]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:0]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:42]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:-42]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:0.8]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:-0.8]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:42.4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:-42.4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:4e4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:-4e4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:4e-4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:-4e-4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:4.2e4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[:-4.2e4]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[0:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[-5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[0.5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[-0.5:]")); - GraphHandler.setPlotParams(query, plot); + @Test + public void setKeyParams() throws Exception { + assertPlotParam("key", "out"); + assertPlotParam("key", "left"); + assertPlotParam("key", "top"); + assertPlotParam("key", "center"); + assertPlotParam("key", "right"); + assertPlotParam("key", "horiz"); + assertPlotParam("key", "box"); + assertPlotParam("key", "bottom"); + assertInvalidPlotParam("yrange", "out%20right%20top%0aset%20yrange%20[33:system(%20"); + } - params.put("yrange", Lists.newArrayList("[10.5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[-10.5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[10e5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[-10e5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[10e-5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[-10e-5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[10.1e-5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[-10.1e-5:]")); - GraphHandler.setPlotParams(query, plot); - - params.put("yrange", Lists.newArrayList("[33:system('touch /tmp/poc.txt')]")); - try { - GraphHandler.setPlotParams(query, plot); - fail("Expected BadRequestException"); - } catch (BadRequestException e) { } + @Test + public void setStyleParams() throws Exception { + assertPlotParam("style", "linespoint"); + assertPlotParam("style", "points"); + assertPlotParam("style", "circles"); + assertPlotParam("style", "dots"); + assertInvalidPlotParam("style", "dots%20[33:system(%20"); } - + + @Test + public void setLabelParams() throws Exception { + assertPlotParam("ylabel", "This is good"); + assertPlotParam("ylabel", " and so Is this - _ yay"); + assertInvalidPlotParam("ylabel", "[33:system(%20"); + assertInvalidPlotParam("title", "[33:system(%20"); + assertInvalidPlotParam("y2label", "[33:system(%20"); + } + + @Test + public void setColorParams() throws Exception { + assertPlotParam("bgcolor", "x000000"); + assertPlotParam("bgcolor", "XDEADBE"); + assertPlotParam("bgcolor", "%58DEADBE"); + assertInvalidPlotParam("bgcolor", "XDEADBEF"); + assertInvalidPlotParam("bgcolor", "%5BDEADBE"); + + assertPlotParam("fgcolor", "x000000"); + assertPlotParam("fgcolor", "XDEADBE"); + assertPlotParam("fgcolor", "%58DEADBE"); + assertInvalidPlotParam("fgcolor", "XDEADBEF"); + assertInvalidPlotParam("fgcolor", "%5BDEADBE"); + } + + @Test + public void setSmoothParams() throws Exception { + assertPlotParam("smooth", "unique"); + assertPlotParam("smooth", "frequency"); + assertPlotParam("smooth", "fnormal"); + assertPlotParam("smooth", "cumulative"); + assertPlotParam("smooth", "cnormal"); + assertPlotParam("smooth", "bins"); + assertPlotParam("smooth", "csplines"); + assertPlotParam("smooth", "acsplines"); + assertPlotParam("smooth", "mcsplines"); + assertPlotParam("smooth", "bezier"); + assertPlotParam("smooth", "sbezier"); + assertPlotParam("smooth", "unwrap"); + assertPlotParam("smooth", "zsort"); + assertInvalidPlotParam("smooth", "[33:system(%20"); + } + + @Test + public void setFormatParams() throws Exception { + assertPlotParam("yformat", "%25.2f"); + assertPlotParam("y2format", "%25.2f"); + assertPlotParam("xformat", "%25.2f"); + assertPlotParam("yformat", "%253.0em"); + assertPlotParam("yformat", "%253.0em%25%25"); + assertPlotParam("yformat", "%25.2f seconds"); + assertPlotParam("yformat", "%25.0f ms"); + assertInvalidPlotParam("yformat", "%252.[33:system"); + } + @Test // If the file doesn't exist, we don't use it, obviously. public void staleCacheFileDoesntExist() throws Exception { final File cachedfile = fakeFile("/cache/fake-file"); @@ -322,4 +334,27 @@ private static File fakeFile(final String path) { return file; } + private static void assertPlotParam(String param, String value) { + Plot plot = mock(Plot.class); + HttpQuery query = mock(HttpQuery.class); + Map<String, List<String>> params = Maps.newHashMap(); + when(query.getQueryString()).thenReturn(params); + + params.put(param, Lists.newArrayList(value)); + GraphHandler.setPlotParams(query, plot); + } + + private static void assertInvalidPlotParam(String param, String value) { + Plot plot = mock(Plot.class); + HttpQuery query = mock(HttpQuery.class); + Map<String, List<String>> params = Maps.newHashMap(); + when(query.getQueryString()).thenReturn(params); + + params.put(param, Lists.newArrayList(value)); + try { + GraphHandler.setPlotParams(query, plot); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + } + } From 07c4641471c6f5c2ab5aab615969e97211eb50d9 Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@netflix.com> Date: Mon, 10 Apr 2023 22:20:30 -0700 Subject: [PATCH 796/826] Improved fix for #2261. Regular expressions wouldn't catch the newlines or possibly other control characters. Now we'll use the TAG validation code to make sure the inputs are only plain ASCII printables first. Fixes CVE-2018-12972, CVE-2020-35476 --- src/tsd/GraphHandler.java | 44 +++++++++++++++++++++++++++++++--- test/tsd/TestGraphHandler.java | 44 ++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index bf321e8564..2af125edea 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -40,15 +40,17 @@ import com.google.common.base.Strings; import com.google.common.collect.Sets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import net.opentsdb.core.*; import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; import net.opentsdb.core.Query; import net.opentsdb.core.TSDB; import net.opentsdb.core.TSQuery; +import net.opentsdb.core.Tags; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.opentsdb.graph.Plot; import net.opentsdb.meta.Annotation; import net.opentsdb.stats.Histogram; @@ -667,6 +669,7 @@ static void setPlotDimensions(final HttpQuery query, final Plot plot) { String wxh = query.getQueryStringParam("wxh"); if (wxh != null && !wxh.isEmpty()) { wxh = URLDecoder.decode(wxh.trim()); + validateString("wxh", wxh); if (!WXH_VALIDATOR.matcher(wxh).find()) { throw new IllegalArgumentException("'wxh' was invalid. " + "Must satisfy the pattern " + WXH_VALIDATOR.toString()); @@ -744,6 +747,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { final Map<String, List<String>> querystring = query.getQueryString(); String value; if ((value = popParam(querystring, "yrange")) != null) { + validateString("yrange", value, "[:]"); if (!RANGE_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'yrange' was invalid. " + "Must be in the format [min:max]."); @@ -751,6 +755,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("yrange", value); } if ((value = popParam(querystring, "y2range")) != null) { + validateString("y2range", value, "[:]"); if (!RANGE_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'y2range' was invalid. " + "Must be in the format [min:max]."); @@ -758,6 +763,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("y2range", value); } if ((value = popParam(querystring, "ylabel")) != null) { + validateString("ylabel", value, " "); if (!LABEL_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'ylabel' was invalid. Must " + "satisfy the pattern " + LABEL_VALIDATOR.toString()); @@ -765,6 +771,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("ylabel", stringify(value)); } if ((value = popParam(querystring, "y2label")) != null) { + validateString("y2label", value, " "); if (!LABEL_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'y2label' was invalid. Must " + "satisfy the pattern " + LABEL_VALIDATOR.toString()); @@ -772,6 +779,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("y2label", stringify(value)); } if ((value = popParam(querystring, "yformat")) != null) { + validateString("yformat", value, "% "); if (!FORMAT_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'yformat' was invalid. Must " + "satisfy the pattern " + FORMAT_VALIDATOR.toString()); @@ -779,6 +787,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("format y", stringify(value)); } if ((value = popParam(querystring, "y2format")) != null) { + validateString("y2format", value, "% "); if (!FORMAT_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'y2format' was invalid. Must " + "satisfy the pattern " + FORMAT_VALIDATOR.toString()); @@ -786,6 +795,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("format y2", stringify(value)); } if ((value = popParam(querystring, "xformat")) != null) { + validateString("xformat", value, "% "); if (!FORMAT_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'xformat' was invalid. Must " + "satisfy the pattern " + FORMAT_VALIDATOR.toString()); @@ -799,6 +809,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("logscale y2", ""); } if ((value = popParam(querystring, "key")) != null) { + validateString("key", value); if (!KEY_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'key' was invalid. Must " + "satisfy the pattern " + KEY_VALIDATOR.toString()); @@ -806,6 +817,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("key", value); } if ((value = popParam(querystring, "title")) != null) { + validateString("title", value, " "); if (!LABEL_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'title' was invalid. Must " + "satisfy the pattern " + LABEL_VALIDATOR.toString()); @@ -813,6 +825,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("title", stringify(value)); } if ((value = popParam(querystring, "bgcolor")) != null) { + validateString("bgcolor", value); if (!COLOR_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'bgcolor' was invalid. Must " + "be a hex value e.g. 'xFFFFFF'"); @@ -820,6 +833,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("bgcolor", value); } if ((value = popParam(querystring, "fgcolor")) != null) { + validateString("fgcolor", value); if (!COLOR_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'fgcolor' was invalid. Must " + "be a hex value e.g. 'xFFFFFF'"); @@ -827,6 +841,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("fgcolor", value); } if ((value = popParam(querystring, "smooth")) != null) { + validateString("smooth", value); if (!SMOOTH_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'smooth' was invalid. Must " + "satisfy the pattern " + SMOOTH_VALIDATOR.toString()); @@ -834,6 +849,7 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { params.put("smooth", value); } if ((value = popParam(querystring, "style")) != null) { + validateString("style", value); if (!STYLE_VALIDATOR.matcher(value).find()) { throw new BadRequestException("'style' was invalid. Must " + "satisfy the pattern " + STYLE_VALIDATOR.toString()); @@ -1071,4 +1087,26 @@ static void logError(final HttpQuery query, final String msg, LOG.error(query.channel().toString() + ' ' + msg, e); } + static void validateString(final String what, final String s) { + validateString(what, s, ""); + } + + public static void validateString(final String what, final String s, String specials) { + if (s == null) { + throw new BadRequestException("Invalid " + what + ": null"); + } else if ("".equals(s)) { + throw new BadRequestException("Invalid " + what + ": empty string"); + } + final int n = s.length(); + for (int i = 0; i < n; i++) { + final char c = s.charAt(i); + if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') + || ('0' <= c && c <= '9') || c == '-' || c == '_' || c == '.' + || c == '/' || Character.isLetter(c) || specials.indexOf(c) != -1)) { + throw new BadRequestException("Invalid " + what + + " (\"" + s + "\"): illegal character: " + c); + } + } + } + } diff --git a/test/tsd/TestGraphHandler.java b/test/tsd/TestGraphHandler.java index d2fbca43d4..d2600f8df6 100644 --- a/test/tsd/TestGraphHandler.java +++ b/test/tsd/TestGraphHandler.java @@ -97,6 +97,7 @@ public void setYRangeParams() throws Exception { assertPlotParam("yrange", "[-10.1e-5:]"); assertPlotParam("yrange", "[-10.1e-5:-10.1e-6]"); assertInvalidPlotParam("yrange", "[33:system('touch /tmp/poc.txt')]"); + assertInvalidPlotParam("y2range", "[42:%0a[33:system('touch /tmp/poc.txt')]"); } @Test @@ -109,7 +110,8 @@ public void setKeyParams() throws Exception { assertPlotParam("key", "horiz"); assertPlotParam("key", "box"); assertPlotParam("key", "bottom"); - assertInvalidPlotParam("yrange", "out%20right%20top%0aset%20yrange%20[33:system(%20"); + assertInvalidPlotParam("key", "out%20right%20top%0aset%20yrange%20[33:system(%20"); + assertInvalidPlotParam("key", "%3Bsystem%20%22cat%20/home/ubuntuvm/secret.txt%20%3E/tmp/secret.txt%22%20%22"); } @Test @@ -118,16 +120,23 @@ public void setStyleParams() throws Exception { assertPlotParam("style", "points"); assertPlotParam("style", "circles"); assertPlotParam("style", "dots"); - assertInvalidPlotParam("style", "dots%20[33:system(%20"); + assertInvalidPlotParam("style", "dots%20%0a[33:system(%20"); + assertInvalidPlotParam("style", "%3Bsystem%20%22cat%20/home/ubuntuvm/secret.txt%20%3E/tmp/secret.txt%22%20%22\""); } @Test public void setLabelParams() throws Exception { assertPlotParam("ylabel", "This is good"); assertPlotParam("ylabel", " and so Is this - _ yay"); - assertInvalidPlotParam("ylabel", "[33:system(%20"); - assertInvalidPlotParam("title", "[33:system(%20"); - assertInvalidPlotParam("y2label", "[33:system(%20"); + assertInvalidPlotParam("ylabel", "system(%20no%0anewlines"); + assertInvalidPlotParam("title", "system(%20no%0anewlines"); + assertInvalidPlotParam("y2label", "system(%20no%0anewlines"); + } + + @Test + public void setWXH() throws Exception { + assertPlotDimension("wxh", "720x640"); + assertInvalidPlotDimension("wxh", "720%0ax640"); } @Test @@ -137,12 +146,14 @@ public void setColorParams() throws Exception { assertPlotParam("bgcolor", "%58DEADBE"); assertInvalidPlotParam("bgcolor", "XDEADBEF"); assertInvalidPlotParam("bgcolor", "%5BDEADBE"); + assertInvalidPlotParam("bgcolor", "xBDE%0AAD"); assertPlotParam("fgcolor", "x000000"); assertPlotParam("fgcolor", "XDEADBE"); assertPlotParam("fgcolor", "%58DEADBE"); assertInvalidPlotParam("fgcolor", "XDEADBEF"); assertInvalidPlotParam("fgcolor", "%5BDEADBE"); + assertInvalidPlotParam("fgcolor", "xBDE%0AAD"); } @Test @@ -160,7 +171,8 @@ public void setSmoothParams() throws Exception { assertPlotParam("smooth", "sbezier"); assertPlotParam("smooth", "unwrap"); assertPlotParam("smooth", "zsort"); - assertInvalidPlotParam("smooth", "[33:system(%20"); + assertInvalidPlotParam("smooth", "bezier%20system(%20"); + assertInvalidPlotParam("smooth", "fnormal%0asystem(%20"); } @Test @@ -172,7 +184,8 @@ public void setFormatParams() throws Exception { assertPlotParam("yformat", "%253.0em%25%25"); assertPlotParam("yformat", "%25.2f seconds"); assertPlotParam("yformat", "%25.0f ms"); - assertInvalidPlotParam("yformat", "%252.[33:system"); + assertInvalidPlotParam("yformat", "%252.system(%20"); + assertInvalidPlotParam("yformat", "%252.%0asystem(%20"); } @Test // If the file doesn't exist, we don't use it, obviously. @@ -344,6 +357,13 @@ private static void assertPlotParam(String param, String value) { GraphHandler.setPlotParams(query, plot); } + private static void assertPlotDimension(String param, String value) { + Plot plot = mock(Plot.class); + HttpQuery query = mock(HttpQuery.class); + when(query.getQueryStringParam(param)).thenReturn(value); + GraphHandler.setPlotParams(query, plot); + } + private static void assertInvalidPlotParam(String param, String value) { Plot plot = mock(Plot.class); HttpQuery query = mock(HttpQuery.class); @@ -357,4 +377,14 @@ private static void assertInvalidPlotParam(String param, String value) { } catch (BadRequestException e) { } } + private static void assertInvalidPlotDimension(String param, String value) { + Plot plot = mock(Plot.class); + HttpQuery query = mock(HttpQuery.class); + when(query.getQueryStringParam(param)).thenReturn(value); + try { + GraphHandler.setPlotDimensions(query, plot); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { } + } + } From fa88d3e4b5369f9fb73da384fab0b23e246309ba Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@netflix.com> Date: Mon, 10 Apr 2023 21:34:22 -0700 Subject: [PATCH 797/826] Fix for #2269 and #2267 XSS vulnerability. Escaping the user supplied input when outputing the HTML for the old BadRequest HTML handlers should help. Thanks to the reporters. Fixes CVE-2018-13003. --- src/tsd/HttpQuery.java | 13 +++++++++++-- test/tsd/TestHttpQuery.java | 23 +++++++++++++++++++++++ test/tsd/TestQueryRpc.java | 4 ++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index 2a7d81fa44..9cc93167dd 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -25,6 +25,7 @@ import java.util.HashSet; import java.util.List; +import com.google.common.html.HtmlEscapers; import net.opentsdb.core.Const; import net.opentsdb.core.TSDB; import net.opentsdb.graph.Plot; @@ -373,6 +374,10 @@ public void internalError(final Exception cause) { buf.append("\"}"); sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, buf); } else { + String response = ""; + if (pretty_exc != null) { + response = HtmlEscapers.htmlEscaper().escape(pretty_exc); + } sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, makePage("Internal Server Error", "Houston, we have a problem", "<blockquote>" @@ -380,7 +385,7 @@ public void internalError(final Exception cause) { + "Oops, sorry but your request failed due to a" + " server error.<br/><br/>" + "Please try again in 30 seconds.<pre>" - + pretty_exc + + response + "</pre></blockquote>")); } } @@ -420,6 +425,10 @@ public void badRequest(final BadRequestException exception) { buf.append("\"}"); sendReply(HttpResponseStatus.BAD_REQUEST, buf); } else { + String response = ""; + if (exception.getMessage() != null) { + response = HtmlEscapers.htmlEscaper().escape(exception.getMessage()); + } sendReply(HttpResponseStatus.BAD_REQUEST, makePage("Bad Request", "Looks like it's your fault this time", "<blockquote>" @@ -427,7 +436,7 @@ public void badRequest(final BadRequestException exception) { + "Sorry but your request was rejected as being" + " invalid.<br/><br/>" + "The reason provided was:<blockquote>" - + exception.getMessage() + + response + "</blockquote></blockquote>")); } } diff --git a/test/tsd/TestHttpQuery.java b/test/tsd/TestHttpQuery.java index 5660ce0461..1efa626145 100644 --- a/test/tsd/TestHttpQuery.java +++ b/test/tsd/TestHttpQuery.java @@ -795,6 +795,18 @@ public void internalErrorDeprecated() { query.response().getContent().toString(Charset.forName("UTF-8")) .substring(0, 15)); } + + @Test + public void internalErrorDeprecatedHTMLEscaped() { + HttpQuery query = NettyMocks.getQuery(tsdb, ""); + query.internalError(new Exception("<script>alert(document.cookie)</script>")); + + assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR, + query.response().getStatus()); + assertTrue(query.response().getContent().toString(Charset.forName("UTF-8")).contains( + "<script>alert(document.cookie)</script>" + )); + } @Test public void internalErrorDeprecatedJSON() { @@ -849,6 +861,17 @@ public void badRequestDeprecated() { query.response().getContent().toString(Charset.forName("UTF-8")) .substring(0, 15)); } + + @Test + public void badRequestDeprecatedHTMLEscaped() { + HttpQuery query = NettyMocks.getQuery(tsdb, "/"); + query.badRequest(new BadRequestException("<script>alert(document.cookie)</script>")); + + assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); + assertTrue(query.response().getContent().toString(Charset.forName("UTF-8")).contains( + "The reason provided was:<blockquote><script>alert(document.cookie)</script>" + )); + } @Test public void badRequestDeprecatedJSON() { diff --git a/test/tsd/TestQueryRpc.java b/test/tsd/TestQueryRpc.java index f9f855a32a..c38b3bc0d6 100644 --- a/test/tsd/TestQueryRpc.java +++ b/test/tsd/TestQueryRpc.java @@ -518,7 +518,7 @@ public void postQueryNoMetricBadRequest() throws Exception { assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String json = query.response().getContent().toString(Charset.forName("UTF-8")); - assertTrue(json.contains("No such name for 'foo': 'metrics'")); + assertTrue(json.contains("No such name for 'foo': 'metrics'")); } @Test @@ -579,7 +579,7 @@ public void executeNSU() throws Exception { assertEquals(HttpResponseStatus.BAD_REQUEST, query.response().getStatus()); final String json = query.response().getContent().toString(Charset.forName("UTF-8")); - assertTrue(json.contains("No such name for 'foo': 'metrics'")); + assertTrue(json.contains("No such name for 'foo': 'metrics'")); } @Test From db2863725f301d3f3acaeb7b6be2b3f15646de85 Mon Sep 17 00:00:00 2001 From: Uri Okrent <uokrent@gmail.com> Date: Tue, 11 Apr 2023 14:36:53 -0400 Subject: [PATCH 798/826] Trigger callback chain when reached limit. (#2204) fix OpenTSDB#839 Co-authored-by: Bhaa Shakur <bhaa@vastdata.com> --- src/tsd/HttpJsonSerializer.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 18d8139dc9..53913180f8 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -927,7 +927,9 @@ public Deferred<Object> call(final Object obj) throws Exception { // We want the serializer to execute serially so we need to create a callback // chain so that when one DPsResolver is finished, it triggers the next to // start serializing. - final Deferred<Object> cb_chain = new Deferred<Object>(); + final int LIMIT = 1 << 13; + int counter = 0; + Deferred<Object> cb_chain = new Deferred<Object>(); for (DataPoints[] separate_dps : results) { for (DataPoints dps : separate_dps) { @@ -936,6 +938,17 @@ public Deferred<Object> call(final Object obj) throws Exception { } catch (Exception e) { throw new RuntimeException("Unexpected error durring resolution", e); } + if (++counter >= LIMIT) { + counter = 0; + // trigger the callback chain chunk here + cb_chain.callback(null); + try { + cb_chain.joinUninterruptibly(); + } catch (Exception e1) { + // chain already joined + } + cb_chain = new Deferred<Object>(); + } } } @@ -970,7 +983,7 @@ public ChannelBuffer call(final Object obj) } } - // trigger the callback chain here + // trigger the callback chain here - will be joined from outside cb_chain.callback(null); return cb_chain.addCallback(new FinalCB()); } From fd78f0e66269d766594837518035e374fe7964e6 Mon Sep 17 00:00:00 2001 From: Bhaa Shakur <bhaa@vastdata.com> Date: Sun, 9 Jul 2023 16:32:00 +0300 Subject: [PATCH 799/826] Use ConcurrentHashMap when serializing tags. fix OpenTSDB#1055 --- src/tsd/HttpJsonSerializer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tsd/HttpJsonSerializer.java b/src/tsd/HttpJsonSerializer.java index 53913180f8..f0371cd44f 100644 --- a/src/tsd/HttpJsonSerializer.java +++ b/src/tsd/HttpJsonSerializer.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.LinkedHashSet; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferOutputStream; @@ -673,7 +674,7 @@ class DPsResolver implements Callback<Deferred<Object>, Object> { /** Has to be final to be shared with the nested classes */ final StringBuilder metric = new StringBuilder(256); /** Resolved tags */ - final Map<String, String> tags = new HashMap<String, String>(); + final Map<String, String> tags = new ConcurrentHashMap<String, String>(); /** Resolved aggregated tags */ final List<String> agg_tags = new ArrayList<String>(); /** A list storing the metric and tag resolve calls */ From 9de0f9f31e2524ccb5843b631ded9c4ba09f5a17 Mon Sep 17 00:00:00 2001 From: theultimatequestion <joao.fontes.gg@gmail.com> Date: Wed, 5 Jul 2023 15:35:53 +0000 Subject: [PATCH 800/826] Test for null, avoid NPE fix OpenTSDB#2280 --- src/tsd/GraphHandler.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 2af125edea..9d8ae2c32a 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -204,10 +204,12 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) } options = query.getQueryStringParams("o"); - for (int i = 0; i < options.size(); i++) { - if (!allow_list.contains(options.get(i))) { - throw new BadRequestException("Query option at index " + i - + " was not in the allow list."); + if (!(options == null)) { + for (int i = 0; i < options.size(); i++) { + if (!allow_list.contains(options.get(i))) { + throw new BadRequestException("Query option at index " + i + + " was not in the allow list."); + } } } } From 441deba150aab9c7d03453a557c591cb855304bc Mon Sep 17 00:00:00 2001 From: ddoshy <2142904+ddoshy@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:20:17 -0400 Subject: [PATCH 801/826] Update opentsdb_restart.py (#1515) Use 'systemctl' rather than 'service' --- tools/opentsdb_restart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index 9c63679c34..3c67f6f39e 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Restart opentsdb. Called using -XX:OnOutOfMemoryError=<this script> -Because it's calling the 'service opentsdb' command, should be run as root. +Because it's calling the 'systemctl *action* opentsdb' command, should be run as root. This is known to work with python2.6 and above. """ @@ -12,7 +12,7 @@ if 'NAME' in os.environ: service_name = os.environ['NAME'] -subprocess.call(["service", service_name, "stop"]) +subprocess.call(["systemctl", "stop", service_name]) # Close any file handles we inherited from our parent JVM. We need # to do this before restarting so that the socket isn't held open. openfiles = [int(f) for f in os.listdir("/proc/self/fd")] @@ -20,4 +20,4 @@ # that there is less chance of errors with those standard streams. # Other files start at fd 3. os.closerange(3, max(openfiles)) -subprocess.call(["service", service_name, "start"]) +subprocess.call(["systemctl", "start", service_name]) From 1aae2b7ff9f5e81449a5d8e6c6e88cfb73938b07 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <johann8384@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:26:56 -0400 Subject: [PATCH 802/826] Revert "Update opentsdb_restart.py (#1515)" (#2298) This reverts commit 441deba150aab9c7d03453a557c591cb855304bc. --- tools/opentsdb_restart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index 3c67f6f39e..9c63679c34 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Restart opentsdb. Called using -XX:OnOutOfMemoryError=<this script> -Because it's calling the 'systemctl *action* opentsdb' command, should be run as root. +Because it's calling the 'service opentsdb' command, should be run as root. This is known to work with python2.6 and above. """ @@ -12,7 +12,7 @@ if 'NAME' in os.environ: service_name = os.environ['NAME'] -subprocess.call(["systemctl", "stop", service_name]) +subprocess.call(["service", service_name, "stop"]) # Close any file handles we inherited from our parent JVM. We need # to do this before restarting so that the socket isn't held open. openfiles = [int(f) for f in os.listdir("/proc/self/fd")] @@ -20,4 +20,4 @@ # that there is less chance of errors with those standard streams. # Other files start at fd 3. os.closerange(3, max(openfiles)) -subprocess.call(["systemctl", "start", service_name]) +subprocess.call(["service", service_name, "start"]) From f73715819d4ce287fca4b60ce81a3af36dbae4b7 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <johann8384@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:21:17 -0400 Subject: [PATCH 803/826] Improve RPC Method Consistency (#2043) * Made HTTP Request method checking consistent, fixes a few cases where behavior is unexpected. Simplified loading of internal RPC Handlers Stop Sending BAD_REQUEST response as a PNG, allowed random code execution! Fixes #793 Fixes #781 Fixes #831 Fixes #830 * Fixes for #831, #830, #781, #793 --- src/tsd/AnnotationRpc.java | 15 +++----- src/tsd/GraphHandler.java | 15 ++++++-- src/tsd/HttpQuery.java | 4 ++ src/tsd/HttpRpc.java | 2 +- src/tsd/LogsRpc.java | 8 +++- src/tsd/PutDataPointRpc.java | 6 +-- src/tsd/QueryRpc.java | 14 +++---- src/tsd/RpcHandler.java | 8 ++++ src/tsd/RpcManager.java | 17 ++++----- src/tsd/SearchRpc.java | 7 +--- src/tsd/StaticFileRpc.java | 9 ++++- src/tsd/StatsRpc.java | 15 +++----- src/tsd/SuggestRpc.java | 10 ++--- src/tsd/TreeRpc.java | 11 +++--- src/tsd/UniqueIdRpc.java | 71 ++++++++++++++++-------------------- 15 files changed, 107 insertions(+), 105 deletions(-) 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<Annotation> 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<Annotation> 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 9d8ae2c32a..0ae7fd8d97 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -40,7 +40,10 @@ import com.google.common.base.Strings; import com.google.common.collect.Sets; -import net.opentsdb.core.*; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.opentsdb.core.Const; import net.opentsdb.core.DataPoint; import net.opentsdb.core.DataPoints; @@ -125,6 +128,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")) { @@ -202,18 +209,18 @@ private void doGraph(final TSDB tsdb, final HttpQuery query) allow_list.add(allow); } } - + options = query.getQueryStringParams("o"); if (!(options == null)) { for (int i = 0; i < options.size(); i++) { if (!allow_list.contains(options.get(i))) { - throw new BadRequestException("Query option at index " + i + throw new BadRequestException("Query option at index " + i + " was not in the allow list."); } } } } - + if (options == null) { options = new ArrayList<String>(tsdbqueries.length); for (int i = 0; i < tsdbqueries.length; i++) { diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index 9cc93167dd..10df901840 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -424,6 +424,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 { String response = ""; if (exception.getMessage() != null) { 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 <http://www.gnu.org/licenses/>. 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<String> logs = new ArrayList<String>(); for (String log : logmsgs) { diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index a8dd330ea5..8edc37957f 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<IncomingDataPoint> dps; //noinspection TryWithIdenticalCatches 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 a841e49c2a..1424bc4217 100644 --- a/src/tsd/RpcHandler.java +++ b/src/tsd/RpcHandler.java @@ -247,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..38740fd06c 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -133,7 +133,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<RpcPlugin> rpcBuilder = ImmutableList.builder(); @@ -242,10 +242,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, @@ -350,8 +350,6 @@ private void initializeBuiltinRpcs(final OperationMode mode, http.put("api/uid", new UniqueIdRpc()); } } - - if (enableDieDieDie) { final DieDieDie diediedie = new DieDieDie(); @@ -377,6 +375,7 @@ private void initializeBuiltinRpcs(final OperationMode mode, protected void initializeHttpRpcPlugins(final OperationMode mode, final String[] pluginClassNames, final ImmutableMap.Builder<String, HttpRpcPlugin> http) { + for (final String plugin : pluginClassNames) { final HttpRpcPlugin rpc = createAndInitialize(plugin, HttpRpcPlugin.class); validateHttpRpcPluginPath(rpc.getPath()); @@ -627,8 +626,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( @@ -653,8 +652,8 @@ public Deferred<Object> 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<String, String> version = new HashMap<String, String>(); 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/<type> or /api/search/<type> 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 <code>" + uri + "</code>"); } @@ -41,6 +47,7 @@ public void execute(final TSDB tsdb, final HttpQuery query) if (uri.indexOf("..", 3) > 0) { throw new BadRequestException("Malformed URI <code>" + uri + "</code>"); } + 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 <http://www.gnu.org/licenses/>. 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<Object> 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..1e864d8c91 100644 --- a/src/tsd/UniqueIdRpc.java +++ b/src/tsd/UniqueIdRpc.java @@ -87,13 +87,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<String, List<String>> source; if (query.method() == HttpMethod.POST) { source = query.serializer().parseUidAssignV1(); @@ -160,6 +161,8 @@ 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) { @@ -168,15 +171,15 @@ private void handleUIDMeta(final TSDB tsdb, final HttpQuery query) { 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) { final UIDMeta meta; @@ -185,30 +188,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<Deferred<UIDMeta>, Boolean> { - + @Override public Deferred<UIDMeta> 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<UIDMeta> process_meta = meta.syncToStorage(tsdb, - method == HttpMethod.PUT).addCallbackDeferring(new SyncCB()); + final Deferred<UIDMeta> 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,12 +220,12 @@ public Deferred<UIDMeta> 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) { final UIDMeta meta; @@ -231,23 +234,18 @@ public Deferred<UIDMeta> 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,6 +257,8 @@ public Deferred<UIDMeta> 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) { @@ -445,10 +445,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"); } } @@ -492,13 +488,10 @@ private UIDMeta parseUIDMetaQS(final HttpQuery query) { */ 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"); - } + RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName()); final HashMap<String, String> source; + if (query.method() == HttpMethod.POST) { source = query.serializer().parseUidRenameV1(); } else { From 55f4c854f8b8f5084eec55b8eaf7ab72765a7d02 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <johann8384@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:28:42 -0400 Subject: [PATCH 804/826] Update opentsdb_restart.py (#1515) (#2299) Use 'systemctl' rather than 'service' Co-authored-by: ddoshy <2142904+ddoshy@users.noreply.github.com> --- tools/opentsdb_restart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/opentsdb_restart.py b/tools/opentsdb_restart.py index 9c63679c34..3c67f6f39e 100644 --- a/tools/opentsdb_restart.py +++ b/tools/opentsdb_restart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Restart opentsdb. Called using -XX:OnOutOfMemoryError=<this script> -Because it's calling the 'service opentsdb' command, should be run as root. +Because it's calling the 'systemctl *action* opentsdb' command, should be run as root. This is known to work with python2.6 and above. """ @@ -12,7 +12,7 @@ if 'NAME' in os.environ: service_name = os.environ['NAME'] -subprocess.call(["service", service_name, "stop"]) +subprocess.call(["systemctl", "stop", service_name]) # Close any file handles we inherited from our parent JVM. We need # to do this before restarting so that the socket isn't held open. openfiles = [int(f) for f in os.listdir("/proc/self/fd")] @@ -20,4 +20,4 @@ # that there is less chance of errors with those standard streams. # Other files start at fd 3. os.closerange(3, max(openfiles)) -subprocess.call(["service", service_name, "start"]) +subprocess.call(["systemctl", "start", service_name]) From 0b0b1485647e9ee6282d55c446eb8b7e2e47fb76 Mon Sep 17 00:00:00 2001 From: Simon Matic Langford <simon@exemel.co.uk> Date: Tue, 2 Jul 2019 06:49:31 +0100 Subject: [PATCH 805/826] Ensure we always add the {} for group_by filters, otherwise the non group_by filters act as group_by filters --- src/tsd/client/MetricForm.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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<Filter> 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()) { From db1782af2fee15031a41c1a4f7158a4dd742b2bb Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring <itamar@itamarst.org> Date: Wed, 24 Feb 2021 16:43:50 -0500 Subject: [PATCH 806/826] Status API (#1742) * Start of new HTTP API. * Start of new "is this table available across all/some regions" API. * Finish plausibly OK implementation of checking table availability. * Finish plausibly ok Telnet/HTTP status endpoint. * It compiles. * Minimal live test works (for success case). * Set a timeout on RPC queries. * Unit test for the HTTP status RPC query. * Start tests for checkNecessaryTablesAvailability(). * Checkpoint. Not working, probably because of old Mockito. * Fix API usage. * Got the test to pass. * More unit tests. --- Makefile.am | 2 + src/core/TSDB.java | 154 +++++++++++++++++++- src/tsd/RpcManager.java | 85 ++++++++++- test/core/TestTSDBTableAvailability.java | 171 +++++++++++++++++++++++ test/tsd/TestStatusRpc.java | 97 +++++++++++++ 5 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 test/core/TestTSDBTableAvailability.java create mode 100644 test/tsd/TestStatusRpc.java diff --git a/Makefile.am b/Makefile.am index da55b8b30b..33a374d6a4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -332,6 +332,7 @@ test_SRC := \ 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 \ @@ -425,6 +426,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/src/core/TSDB.java b/src/core/TSDB.java index b3e5633cad..4fb3acfab7 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; @@ -105,7 +107,18 @@ public enum OperationMode { READONLY, WRITEONLY } - + + /** 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; @@ -736,6 +749,145 @@ public Deferred<ArrayList<Object>> 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<ArrayList<Boolean>> getTableRegionAvailability(String table) { + final String table_id = config.getString(table); + + /** Convert result to true. */ + final class SuccessToBoolCallback implements Callback<Boolean, ArrayList<KeyValue>> { + @Override + public Boolean call(final ArrayList<KeyValue> o) { + LOG.info("Check HBase availability, got success."); + return true; + } + } + + /** Convert error result to false. */ + final class FailureToBoolCallback implements Callback<Boolean, Exception> { + @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<Deferred<ArrayList<Boolean>>,List<RegionLocation>> { + @Override + public Deferred<ArrayList<Boolean>> call(final List<RegionLocation> regions) { + LOG.info("Availability check got this many regions: " + regions.size()); + ArrayList<Deferred<Boolean>> available = new ArrayList<Deferred<Boolean>>(); + 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<TableAvailability> 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<TableAvailability,ArrayList<Boolean>> { + @Override + public TableAvailability call(final ArrayList<Boolean> 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<TableAvailability,Exception> { + @Override + public TableAvailability call(final Exception e) { + LOG.error("Failed to get regions during table availability check", e); + return TableAvailability.NONE; + } + } + + ArrayList<Deferred<TableAvailability>> tables = new ArrayList<Deferred<TableAvailability>>(); + 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<TableAvailability,ArrayList<TableAvailability>> { + @Override + public TableAvailability call(final ArrayList<TableAvailability> 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() diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 38740fd06c..8989d673b5 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<String, HttpRpcPlugin> http_plugin_commands; /** List of activated RPC plugins */ private ImmutableList<RpcPlugin> 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; @@ -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()); @@ -479,6 +486,8 @@ protected <T> T createAndInitialize(final String pluginClassName, final Class<T> * (think of it as {@code Deferred<Void>}). */ public Deferred<ArrayList<Object>> shutdown() { + status.shutdown(); + // Clear shared instance. INSTANCE.set(null); @@ -638,6 +647,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<Object> updateStatus(final TSDB tsdb) { + // Once we're in shutdown mode the status never changes. + if (status == "shutting-down") { + return Deferred.fromResult(null); + } + + Deferred<TableAvailability> availability = tsdb.checkNecessaryTablesAvailability(); + + final class AvailabilityToStatusCB implements Callback<Object,TableAvailability> { + @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<Object> execute(final TSDB tsdb, final Channel chan, + final String[] cmd) { + final class WriteStatusCB implements Callback<Object,Object> { + @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<Object,Object> { + @Override + public Object call(final Object o) { + final HashMap<String, String> result = new HashMap<String, String>(); + 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<Object> execute(final TSDB tsdb, final Channel chan, 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 <http://www.gnu.org/licenses/>. +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<List<RegionLocation>> d = new Deferred<List<RegionLocation>>(); + d.callback(new Exception()); + Deferred<List<RegionLocation>> d2 = new Deferred<List<RegionLocation>>(); + 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<List<RegionLocation>> d = new Deferred<List<RegionLocation>>(); + d.callback(new ArrayList<RegionLocation>()); + when(tsdb.getClient().locateRegions(uid_table)).thenReturn(d); + + Deferred<List<RegionLocation>> d2 = new Deferred<List<RegionLocation>>(); + d2.callback(new ArrayList<RegionLocation>()); + when(tsdb.getClient().locateRegions(data_table)).thenReturn(d2); + assertEquals(tsdb.checkNecessaryTablesAvailability().join(), + TSDB.TableAvailability.NONE); + } + + private TSDB createTSDB(ArrayList<Boolean> uid_regions, + ArrayList<Boolean> data_regions) { + TSDB original = new TSDB(mock(HBaseClient.class), config); + TSDB tsdb = PowerMockito.spy(original); + + Deferred<ArrayList<Boolean>> get_results = new Deferred<ArrayList<Boolean>>(); + get_results.callback(uid_regions); + Deferred<ArrayList<Boolean>> get_results2 = new Deferred<ArrayList<Boolean>>(); + 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<Boolean> region_availability = new ArrayList<Boolean>(); + 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<Boolean> region_availability = new ArrayList<Boolean>(); + 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<Boolean> full_availability = new ArrayList<Boolean>(); + full_availability.add(true); + full_availability.add(true); + ArrayList<Boolean> partial_availability = new ArrayList<Boolean>(); + partial_availability.add(false); + partial_availability.add(true); + ArrayList<Boolean> no_availability = new ArrayList<Boolean>(); + 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/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 <http://www.gnu.org/licenses/>. +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\"}"); + } +} From 08ea306f5859acced13580b5b0ae1644ddead85c Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Wed, 24 Feb 2021 14:56:34 -0500 Subject: [PATCH 807/826] WIP: Pr 1762 (#2036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow end_time = start_time to be able to query and delete a single datapoint. * Updating tests to support start and end time being the same Co-authored-by: Øyvind Matheson Wergeland <oyvind@wergeland.org> --- src/core/TSQuery.java | 4 ++-- src/core/TsdbQuery.java | 8 ++++---- test/core/TestTsdbQuery.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/TSQuery.java b/src/core/TSQuery.java index 1d525b6c9f..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 +"]"); } diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index de2a3779a2..08c03a8fd3 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -269,9 +269,9 @@ public void setStartTime(final long timestamp) { 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; } @@ -300,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; } diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 445685d732..0e40fd1b82 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -84,7 +84,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); From 93867ca30a16377b7012312bff1c71c940c67b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= <bjorn.marschollek@skyscanner.net> Date: Wed, 24 Feb 2021 19:58:34 +0100 Subject: [PATCH 808/826] Add support for splitting rollup queries (#1853) * Add an SLA config flag for rollup intervals Adds a configuration option for rollup intervals to specify their maximum acceptable delay. Queries that cover a time between now and that maximum delay will need to query other tables for that time interval. * Add global config flag to enable splitting queries Adds a global config flag to enable splitting queries that would hit the rollup table, but the rollup table has a delay SLA configured. In that case, this feature allows splitting a query into to; one that gets the data from the rollups table until the time where it's guaranteed to be available, and the rest from the raw table. * Add a new SplitRollupQuery Adds a SplitRollupQuery class that suports splitting a rollup query into two separate queries. This is useful for when a rollup table is filled by e.g. a batch job that processes the data from the previous day on a daily basis. Rollup data for yesterday will then only be available some time today. This delay SLA can be configured on a per-table basis. The delay would specify by how much time the table can be behind real time. If a query comes in that would query data from that blackout period where data is only available in the raw table, but not yet guaranteed to be in the rollup table, the incoming query can be split into two using the SplitRollupQuery class. It wraps a query that queries the rollup table until the last guaranteed to be available timestamp based on the SLA; and one that gets the remaining data from the raw table. * Extract an AbstractQuery Extracts an AbstractQuery from the TsdbQuery implementation since we'd like to reuse some parts of it in other Query classes (in this case SplitRollupQuery) * Extract an AbstractSpanGroup * Avoid NullPointerException when setting start time Avoids a NullPointerException that happened when we were trying to set the start time on a query that would be eligible to split, but due to the SLA config only hit the raw table anyway. * Scale timestamps to milliseconds for split queries Scales all timestamps for split queries to milliseconds. It's important to maintain consistent units between all the partial queries that make up the bigger one. * Fix starting time error for split queries Fixes a bug that would happen when the start time of a query aligns perfectly with the time configured in the SLA for the delay of a rollup table. For a defined SLA, e.g. 1 day, if the start time of the query was exactly 1 day ago, the end time of the rollups part of the query would be updated and then be equal to its start time. That isn't allowed and causes a query exception. --- src/core/SplitRollupQuery.java | 2 +- src/core/SplitRollupSpanGroup.java | 3 +- src/core/TsdbQuery.java | 100 ++++++++++++++-------------- test/core/TestSplitRollupQuery.java | 2 +- 4 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/core/SplitRollupQuery.java b/src/core/SplitRollupQuery.java index b4c513a9bc..f422985377 100644 --- a/src/core/SplitRollupQuery.java +++ b/src/core/SplitRollupQuery.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2021 The OpenTSDB Authors. +// Copyright (C) 2010-2021 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 diff --git a/src/core/SplitRollupSpanGroup.java b/src/core/SplitRollupSpanGroup.java index bcbcd0684a..1333c11e34 100644 --- a/src/core/SplitRollupSpanGroup.java +++ b/src/core/SplitRollupSpanGroup.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2021 The OpenTSDB Authors. +// Copyright (C) 2012-2021 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 @@ -119,7 +119,6 @@ public Map<String, String> call(ArrayList<Map<String, String>> resolvedTags) thr */ @Override public Bytes.ByteMap<byte[]> getTagUids() { - Bytes.ByteMap<byte[]> tagUids = new Bytes.ByteMap<byte[]>(); for (SpanGroup group : spanGroups) { tagUids.putAll(group.getTagUids()); diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 08c03a8fd3..31f0637e9b 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -98,22 +98,22 @@ final class TsdbQuery extends AbstractQuery { /** 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. @@ -132,7 +132,7 @@ final class TsdbQuery extends AbstractQuery { /** Specifies the various options for rate calculations */ private RateOptions rate_options; - + /** Aggregator function to use. */ private Aggregator aggregator; @@ -141,55 +141,55 @@ final class TsdbQuery extends AbstractQuery { /** 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<RollupInterval> 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<String> 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<TagVFilter> 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<Float> 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 @@ -199,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 @@ -207,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()); @@ -217,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 @@ -249,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 @@ -266,7 +266,7 @@ 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()) { @@ -1464,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); @@ -1498,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()) }))); @@ -1510,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()) }))); @@ -1527,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<ScanFilter> combined = new ArrayList<ScanFilter>(2); combined.add(existing); @@ -1545,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(); @@ -1567,10 +1567,10 @@ else if (pre_aggregate) { else { tableName = tsdb.dataTable(); } - + return tableName; } - + /** Returns the UNIX timestamp from which we must start scanning. */ long getScanStartTimeSeconds() { // Begin with the raw query start time. @@ -1580,15 +1580,15 @@ 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; @@ -1627,11 +1627,11 @@ long getScanEndTimeSeconds() { 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()); } diff --git a/test/core/TestSplitRollupQuery.java b/test/core/TestSplitRollupQuery.java index 9ce3082b3d..559aaa1717 100644 --- a/test/core/TestSplitRollupQuery.java +++ b/test/core/TestSplitRollupQuery.java @@ -1,5 +1,5 @@ // This file is part of OpenTSDB. -// Copyright (C) 2021 The OpenTSDB Authors. +// Copyright (C) 2012-2021 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 From 8a34eb4af1c0e1ab29d695025dc7bfac751255a0 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Mon, 26 Oct 2020 19:20:03 -0400 Subject: [PATCH 809/826] Added tracking of metrics which are null due to auto_metric being disabled Fixes #786 (#2042) --- src/core/IncomingDataPoints.java | 71 +++++++------------------------- src/core/TSDB.java | 7 ++++ 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/src/core/IncomingDataPoints.java b/src/core/IncomingDataPoints.java index 0456e47be2..108e641a6c 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<byte[]> rowKeyTemplateAsync(final TSDB tsdb, - final String metric, final Map<String, String> 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<byte[]> 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<byte[], byte[]> { - 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<Deferred<byte[]>, ArrayList<byte[]>> { - public Deferred<byte[]> call(final ArrayList<byte[]> 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<String, String> tags) { checkMetricAndTags(metric, tags); try { diff --git a/src/core/TSDB.java b/src/core/TSDB.java index 4fb3acfab7..c901735817 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -968,6 +968,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"); From 9e0df25308cf02c7783b3acab2bed618097b60a6 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Mon, 26 Oct 2020 18:00:23 -0400 Subject: [PATCH 810/826] Fixed function description Fixes #841 (#2040) --- src/core/Query.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Query.java b/src/core/Query.java index 455f641a25..5cec6c7c80 100644 --- a/src/core/Query.java +++ b/src/core/Query.java @@ -197,7 +197,7 @@ public Deferred<Object> 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. */ From d078c1f359012ea77eaf4e25308a016f72d042ec Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@verizonmedia.com> Date: Mon, 12 Oct 2020 10:18:07 -0700 Subject: [PATCH 811/826] Fix the Screw Driver config. --- screwdriver.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/screwdriver.yaml b/screwdriver.yaml index 1357d02594..0f5a502801 100644 --- a/screwdriver.yaml +++ b/screwdriver.yaml @@ -1,11 +1,10 @@ shared: - image: maven:3-adoptopenjdk-8 + image: maven 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 python -y && ./build.sh pom.xml && mvn clean test --quiet From 0c3c3525958fecb760583bd2a3c1d3febcc6064c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Marschollek?= <bjorn.marschollek@skyscanner.net> Date: Thu, 22 Aug 2019 16:09:54 +0100 Subject: [PATCH 812/826] Fix concurrent result reporting from scanners Fixes a concurrency bug where scanners report their results into a map and would overwrite each other's results Resolves: #1753 --- src/core/SaltScanner.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index 3e571c025f..d7ef541171 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -942,6 +942,7 @@ private void validateAndTriggerCallback( histMap.put(scannersRunning, histograms); } + int scannersRunning = countdown.decrementAndGet(); if (scannersRunning <= 0) { try { mergeAndReturnResults(); From a425dd57790c854b55a97dfe82fe4b1fe1388d71 Mon Sep 17 00:00:00 2001 From: Ronan Harmegnies <ronan.harmegnies@3ds.com> Date: Thu, 26 Dec 2019 16:04:38 +0100 Subject: [PATCH 813/826] ExplicitTags filtering with FuzzyFilters --- src/query/QueryUtil.java | 129 ++++++++++++++++++---------- test/core/TestTsdbQueryQueries.java | 1 - 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index bf19f2e46b..fc7f5e2e89 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -191,9 +191,9 @@ public static String getRowKeyUIDRegex( * @return A regular expression string to pass to the storage layer. */ private static String getRowKeyUIDRegex( - final ByteMap<byte[][]> row_key_literals, + final ByteMap<byte[][]> row_key_literals, final boolean explicit_tags) { - final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + + 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(); @@ -213,7 +213,7 @@ private static String getRowKeyUIDRegex( .append(prefix_width) .append("}"); - final Iterator<Entry<byte[], byte[][]>> it = row_key_literals == null ? + final Iterator<Entry<byte[], byte[][]>> it = row_key_literals == null ? new ByteMap<byte[][]>().iterator() : row_key_literals.iterator(); while(it.hasNext()) { @@ -221,9 +221,9 @@ private static String getRowKeyUIDRegex( // 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 = + final boolean not_key = entry.getValue() != null && entry.getValue().length == 0; - + // Skip any number of tags. if (!explicit_tags) { buf.append("(?:.{").append(tagsize).append("})*"); @@ -235,7 +235,7 @@ private static String getRowKeyUIDRegex( 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|..)/ @@ -253,13 +253,13 @@ private static String getRowKeyUIDRegex( } 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("})*"); @@ -267,9 +267,9 @@ private static String getRowKeyUIDRegex( buf.append("$"); return buf.toString(); } - + /** - * Crafts a list of FuzzyFilters for scanning over data table rows and + * 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. @@ -280,7 +280,7 @@ private static String getRowKeyUIDRegex( private static List<FuzzyFilterPair> buildFuzzyFilters( final ByteMap<byte[][]> row_key_literals, final byte[] fuzzy_key) { - final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + + 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(); @@ -296,36 +296,36 @@ private static List<FuzzyFilterPair> buildFuzzyFilters( } final List<FuzzyFilterPair> fuzzy_filter_pairs = new ArrayList<FuzzyFilterPair>(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_fuzzy_mask[fuzzy_offset++] = + (row_key_literals != null) ? (byte)1 : (byte)0; } - + // first pass to build the key and mask Iterator<Entry<byte[], byte[][]>> it = row_key_literals.iterator(); while(it.hasNext()) { Entry<byte[], byte[][]> entry = it.next(); - final boolean not_key = + 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, + System.arraycopy(tag_key, 0, first_fuzzy_key, fuzzy_offset, name_width); for (int i=0; i<name_width; i++) { - first_fuzzy_mask[fuzzy_offset++] = 0; + first_fuzzy_mask[fuzzy_offset++] = 0; } final byte[] tag_value; @@ -334,10 +334,10 @@ private static List<FuzzyFilterPair> buildFuzzyFilters( } else { tag_value = null; } - + if (tag_value!=null) { - System.arraycopy(tag_value, 0, - first_fuzzy_key, fuzzy_offset, value_width); + System.arraycopy(tag_value, 0, + first_fuzzy_key, fuzzy_offset, value_width); for (int i=0; i<value_width; i++) { first_fuzzy_mask[fuzzy_offset++] = 0; } @@ -365,9 +365,9 @@ private static List<FuzzyFilterPair> buildFuzzyFilters( if (entry.getValue()!=null && entry.getValue().length > 1) { for (int i=1; i<entry.getValue().length; i++) { final byte[] tag_value = entry.getValue()[i]; - byte[] local_fuzzy_key = + byte[] local_fuzzy_key = Arrays.copyOf(first_fuzzy_key, row_key_size); - System.arraycopy(tag_value, 0, + System.arraycopy(tag_value, 0, local_fuzzy_key, fuzzy_offset, value_width); fuzzy_filter_pairs.add( @@ -376,12 +376,12 @@ private static List<FuzzyFilterPair> buildFuzzyFilters( } fuzzy_offset += value_width; } - + // Sort filters list over rowkey Collections.sort(fuzzy_filter_pairs, FUZZY_FILTER_CMP); return fuzzy_filter_pairs; } - + /** * Comparator that sorts the fuzzy filter list ascending based on the row * key. @@ -393,7 +393,7 @@ public int compare(FuzzyFilterPair pair1, FuzzyFilterPair pair2) { } } 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. @@ -428,24 +428,24 @@ public static void setDataTableScanFilter( if (group_bys != null) { Collections.sort(group_bys, Bytes.MEMCMP); } - - final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + + + final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES; - + final FuzzyRowFilter fuzzy_filter; - if (explicit_tags && - enable_fuzzy_filter && - row_key_literals != null && + 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() * + + 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, + System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, scanner.getCurrentKey().length); - - final List<FuzzyFilterPair> fuzzy_filter_pairs = + + final List<FuzzyFilterPair> 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( @@ -468,22 +468,22 @@ public static void setDataTableScanFilter( } else { fuzzy_filter = null; } - + 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 + ": " + + LOG.debug("Regex for scanner: " + scanner + ": " + byteRegexToString(regex)); } - regex_filter = new KeyRegexpFilter(regex.toString(), + regex_filter = new KeyRegexpFilter(regex.toString(), Const.ASCII_CHARSET); } else { regex_filter = null; } - + if (fuzzy_filter != null && !Strings.isNullOrEmpty(regex)) { - final FilterList filter = new FilterList(Lists.newArrayList(fuzzy_filter, + final FilterList filter = new FilterList(Lists.newArrayList(fuzzy_filter, regex_filter),Operator.MUST_PASS_ALL); scanner.setFilter(filter); } else if (fuzzy_filter != null) { @@ -491,6 +491,47 @@ public static void setDataTableScanFilter( } else if (!Strings.isNullOrEmpty(regex)) { scanner.setFilter(regex_filter); } + + if (explicit_tags && enable_fuzzy_filter) { + final List<FuzzyFilterPair> fuzzy_filter_pairs = + buildFuzzyFilters(row_key_literals); + + // The Fuzzy Filter list is sorted: the first and last filters row key + // can be used to build a start and stop keys for the scanner + final byte[] start_key = Arrays.copyOf( + fuzzy_filter_pairs.get(0).getRowKey(), + fuzzy_filter_pairs.get(0).getRowKey().length); + System.arraycopy(scanner.getCurrentKey(), 0, start_key, 0, prefix_width); + + final byte[] stop_key = Arrays.copyOf( + fuzzy_filter_pairs.get(fuzzy_filter_pairs.size()-1).getRowKey(), + start_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(start_key); + scanner.setStopKey(stop_key); + scanner.setFilter(new FuzzyRowFilter(fuzzy_filter_pairs)); + } else { + final String regex = getRowKeyUIDRegex(row_key_literals, explicit_tags); + final KeyRegexpFilter regex_filter = new KeyRegexpFilter( + regex.toString(), Const.ASCII_CHARSET); + if (LOG.isDebugEnabled()) { + LOG.debug("Regex for scanner: " + scanner + ": " + + byteRegexToString(regex)); + } + + scanner.setFilter(regex_filter); + } } /** diff --git a/test/core/TestTsdbQueryQueries.java b/test/core/TestTsdbQueryQueries.java index b6c0359aeb..977e72e150 100644 --- a/test/core/TestTsdbQueryQueries.java +++ b/test/core/TestTsdbQueryQueries.java @@ -1742,7 +1742,6 @@ public void filterExplicitTagsGroupByOK() throws Exception { assertEquals(300, dps[0].aggregatedSize()); // 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); From 58f34ce7fe1ff4bcc13d6b8e7ff3d88af836a05a Mon Sep 17 00:00:00 2001 From: Neil Fordyce <neil.fordyce@skyscanner.net> Date: Wed, 29 May 2019 14:46:47 +0100 Subject: [PATCH 814/826] Fix SaltScanner race condition on spans maps (#1651) * Fix SaltScanner race condition on spans maps * Fix 1.6 compatibility --- test/core/TestSaltScannerHistogram.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/core/TestSaltScannerHistogram.java b/test/core/TestSaltScannerHistogram.java index 76d779b0fd..5925df891d 100644 --- a/test/core/TestSaltScannerHistogram.java +++ b/test/core/TestSaltScannerHistogram.java @@ -301,6 +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(Maps.difference(spans, scanner.scan().joinUninterruptibly()).areEqual()); assertTrue(Maps.difference(spans, scanner.scanHistogram().joinUninterruptibly()).areEqual()); assertEquals(0, spans.size()); } From d3919cf68ce3df34bf31d52672618477879e4385 Mon Sep 17 00:00:00 2001 From: John Seekins <johnseekins@users.noreply.github.com> Date: Wed, 29 May 2019 07:43:19 -0600 Subject: [PATCH 815/826] Add "check_tsd_v2" script (#1567) Enhanced check_tsd script evaluates each individual metric group separately when given a filter --- tools/check_tsd_v2 | 49 +++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/tools/check_tsd_v2 b/tools/check_tsd_v2 index 8d92b48e90..f93c93fe5e 100755 --- a/tools/check_tsd_v2 +++ b/tools/check_tsd_v2 @@ -3,6 +3,7 @@ from urllib import request import json import operator +import time import logging from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter @@ -34,7 +35,6 @@ def _get_metrics(query, timeout): :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: @@ -57,6 +57,8 @@ def build_query(args): else: query = "http" query += "://{}:{}/api/query?".format(args.host, args.port) + query += "start={}s-ago&noAnnotations=true&m={}:".format(args.duration, + args.aggregator) query += "start={}s-ago&".format(args.duration) if args.ignore_recent > 0: query +="end={}s-ago&".format(args.ignore_recent) @@ -126,7 +128,7 @@ def build_comparisons(expressions): return sorted_comp -def _process_metric(m, args, comparisons): +def _process_metric(m, args, comparisons, now): """ Evaluate a single metric from OpenTSDB. In this case, a metric is a object containing a list @@ -136,6 +138,7 @@ def _process_metric(m, args, comparisons): :param dict m: the actual metric data :param dict args: all arguments needed to perform evaluations :param list comparisons: all comparison tuples + :param float now: the current time (generated by time.time()) :returns: object describing the metric evaluated and its state :rtype: dict """ @@ -151,13 +154,18 @@ def _process_metric(m, args, comparisons): 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 + # handle out-of-time metrics + if args.ignore_recent: + try: + ts = float(ts) + except ValueError: + log.error("Bad timestamp for {}: {}".format(",".join(mresult["tags"]), ts)) + mresult["crit_alarm"] = True + break + delta = now - ts + if delta >= args.ignore_recent: + log.debug("Timestamp outside evaluation range: {}".format(ts)) + continue avglist.append(d) for comparison in comparisons: comparator, value, alarm = comparison @@ -165,23 +173,17 @@ def _process_metric(m, args, comparisons): 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 + if args.percent_over > 0 and mresult["crit_percent"] > args.percent_over: + mresult["crit_alarm"] = True 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 + if args.percent_over > 0 and mresult["warn_percent"] > args.percent_over: + mresult["warn_alarm"] = True 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 @@ -199,8 +201,9 @@ def process_metrics(query, args, comparisons): :returns: yields each evaluated metric object as it compeletes :rtype: dict (generator) """ + now = time.time() for m in _get_metrics(query, args.timeout): - yield _process_metric(m, args, comparisons) + yield _process_metric(m, args, comparisons, now) def cli_opts(): @@ -234,7 +237,7 @@ def cli_opts(): 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.") + help="Ignore data points that are that >= seconds ago.") 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.") @@ -275,6 +278,8 @@ def main(): total = [] for r in process_metrics(query, args, comparisons): total.append(r["tags"]) + if not r["crit_alarm"] and not r["warn_alarm"]: + continue if args.alarm_empty and r["empty"]: log.info("{} => no data returned in range.".format(",".join(r["tags"]))) crits.append(r["tags"]) @@ -288,7 +293,7 @@ def main(): 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"]), + log.info("{} => alarmed {} times in range. ({}% alarms). Avg. Value: {}".format(",".join(r["tags"]), alerts, perc, r["metric_avg"])) log.info("{} total metrics processed".format(len(total))) crit_count = len(crits) From ea85a758f4b9095f8662b1e3bd7c7ba6d8bf2ed3 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Mon, 26 Oct 2020 17:38:21 -0400 Subject: [PATCH 816/826] Updating maven central urls and versions to match what is available now (#2039) Fixes #1899 Fixes #1941 --- third_party/apache/commons-math3-3.4.1.jar.md5 | 2 +- .../jackson/jackson-annotations-2.9.10.jar.md5 | 1 + third_party/jackson/jackson-core-2.9.10.jar.md5 | 1 + .../jackson/jackson-databind-2.9.10.jar.md5 | 1 + third_party/kryo/asm-4.0.jar.md5.1 | 1 + third_party/kryo/include.mk | 14 ++++++++------ third_party/kryo/kryo-3.0.0.jar.md5 | 1 + third_party/kryo/kryo-4.0.0.jar.md5 | 1 + third_party/kryo/minlog-1.3.jar.md5 | 1 + third_party/kryo/reflectasm-1.10.0-shaded.jar.md5 | 1 + 10 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 third_party/jackson/jackson-annotations-2.9.10.jar.md5 create mode 100644 third_party/jackson/jackson-core-2.9.10.jar.md5 create mode 100644 third_party/jackson/jackson-databind-2.9.10.jar.md5 create mode 100644 third_party/kryo/asm-4.0.jar.md5.1 create mode 100644 third_party/kryo/kryo-3.0.0.jar.md5 create mode 100644 third_party/kryo/kryo-4.0.0.jar.md5 create mode 100644 third_party/kryo/minlog-1.3.jar.md5 create mode 100644 third_party/kryo/reflectasm-1.10.0-shaded.jar.md5 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/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/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 9b5a1ca9fb..f8d8d5f15e 100644 --- a/third_party/kryo/include.mk +++ b/third_party/kryo/include.mk @@ -13,16 +13,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. -KRYO_VERSION := 2.21.1 +KRYO_VERSION := 3.0.0 KRYO := third_party/kryo/kryo-$(KRYO_VERSION).jar -KRYO_BASE_URL := https://repo1.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 := https://repo1.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) @@ -34,11 +34,13 @@ 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 := https://repo1.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) + +https://repo1.maven.org/maven2/com/esotericsoftware/reflectasm/1.10.0/reflectasm-1.10.0-shaded.jar 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 From 96a5585e5ff607c1383dc805bdf7966f1b74efbf Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Mon, 26 Oct 2020 16:42:29 -0400 Subject: [PATCH 817/826] Re-introduce query timeouts. (#2035) Co-authored-by: Itamar Turner-Trauring <itamar@itamarst.org> --- src/core/SaltScanner.java | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index d7ef541171..a17524b85c 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -491,7 +491,8 @@ final class ScannerCB implements Callback<Object, private long rows_pre_filter = 0; private long dps_post_filter = 0; private long rows_post_filter = 0; - + private long query_timeout = tsdb.getConfig().getLong("tsd.query.timeout"); + public ScannerCB(final Scanner scanner, final int index) { this.scanner = scanner; this.index = index; @@ -554,7 +555,24 @@ public Object call(final ArrayList<ArrayList<KeyValue>> rows) final List<Deferred<Object>> lookups = filters != null && !filters.isEmpty() ? new ArrayList<Deferred<Object>>(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 From d83b65548c5020ee23ed1ea7c6fce1bcbd025271 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Mon, 26 Oct 2020 16:38:44 -0400 Subject: [PATCH 818/826] Pr 1663 (#1966) * Make UniqueIdRpc aware of the mode * Update javadoc on new method and rename test methods to be more descriptive Co-authored-by: Simon Matic Langford <simon@exemel.co.uk> --- src/core/TSDB.java | 24 +++++++-- src/tsd/RpcManager.java | 5 +- src/tsd/UniqueIdRpc.java | 88 +++++++++++++++++++++++---------- test/tsd/TestUniqueIdRpc.java | 91 ++++++++++++++++++++++++++++++++++- 4 files changed, 177 insertions(+), 31 deletions(-) diff --git a/src/core/TSDB.java b/src/core/TSDB.java index c901735817..4fbeaaaa42 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -103,9 +103,27 @@ 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. diff --git a/src/tsd/RpcManager.java b/src/tsd/RpcManager.java index 8989d673b5..32ccfff40b 100644 --- a/src/tsd/RpcManager.java +++ b/src/tsd/RpcManager.java @@ -312,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: @@ -328,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; @@ -354,7 +355,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)); } } diff --git a/src/tsd/UniqueIdRpc.java b/src/tsd/UniqueIdRpc.java index 1e864d8c91..34ffbc3379 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 { @@ -165,53 +171,61 @@ private void handleUIDMeta(final TSDB tsdb, final HttpQuery query) { // 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()) { meta = query.serializer().parseUidMetaV1(); } 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<Deferred<UIDMeta>, Boolean> { - + @Override public Deferred<UIDMeta> 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<UIDMeta> process_meta = meta.syncToStorage(tsdb, - method == HttpMethod.PUT).addCallbackDeferring(new SyncCB()); + final Deferred<UIDMeta> 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)); @@ -220,32 +234,41 @@ public Deferred<UIDMeta> 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()) { meta = query.serializer().parseUidMetaV1(); } 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"); } } @@ -261,7 +284,11 @@ private void handleTSMeta(final TSDB tsdb, final HttpQuery query) { // 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")) { tsuid = query.getQueryStringParam("tsuid"); @@ -313,6 +340,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,7 +462,11 @@ 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()) { meta = query.serializer().parseTSMetaV1(); @@ -487,11 +522,14 @@ private UIDMeta parseUIDMetaQS(final HttpQuery query) { * @param query The query for this request */ private void handleRename(final TSDB tsdb, final HttpQuery query) { + 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<String, String> source; - if (query.method() == HttpMethod.POST) { source = query.serializer().parseUidRenameV1(); } else { 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 { From b68cc8c5063005b95bd91e7a558c78a511de0831 Mon Sep 17 00:00:00 2001 From: Zephyr Guo <gzh1992n@gmail.com> Date: Thu, 16 May 2019 00:45:34 +0800 Subject: [PATCH 819/826] fix #1581 by correcting an edge case in TsdbQuery.getScanEndTimeSeconds() (#1582) --- src/core/TsdbQuery.java | 3 ++- test/core/TestTsdbQuery.java | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/core/TsdbQuery.java b/src/core/TsdbQuery.java index 31f0637e9b..17947c72d2 100644 --- a/src/core/TsdbQuery.java +++ b/src/core/TsdbQuery.java @@ -1614,7 +1614,8 @@ long getScanStartTimeSeconds() { } /** Returns the UNIX timestamp at which we must stop scanning. */ - long getScanEndTimeSeconds() { + @VisibleForTesting + protected long getScanEndTimeSeconds() { // Begin with the raw query end time. long end = getEndTime(); diff --git a/test/core/TestTsdbQuery.java b/test/core/TestTsdbQuery.java index 0e40fd1b82..d22bdfed0a 100644 --- a/test/core/TestTsdbQuery.java +++ b/test/core/TestTsdbQuery.java @@ -27,6 +27,8 @@ 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; @@ -102,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(); From e3f33f8d1a0e774cd8ded8da72c10e4f4bf7b57d Mon Sep 17 00:00:00 2001 From: Zephyr Guo <gzh1992n@gmail.com> Date: Mon, 28 Jan 2019 04:31:09 +0800 Subject: [PATCH 820/826] CORE: (#1472) - Add RpcResponder for handling callbacks asynchronously UTILS: - Add two convenient methods in Config Signed-off-by: Chris Larsen <clarsen@verizonmedia.com> --- Makefile.am | 2 + src/core/RpcResponder.java | 110 +++++++++++++++++++++++++++++++ src/core/TSDB.java | 51 +++++++++++--- src/tsd/PutDataPointRpc.java | 107 ++++++++++++++++-------------- src/utils/Config.java | 35 ++++++++++ test/core/TestRpcResponsder.java | 65 ++++++++++++++++++ 6 files changed, 311 insertions(+), 59 deletions(-) create mode 100644 src/core/RpcResponder.java create mode 100644 test/core/TestRpcResponsder.java diff --git a/Makefile.am b/Makefile.am index 33a374d6a4..329d366dd4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -82,6 +82,7 @@ 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 \ @@ -323,6 +324,7 @@ 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 \ 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 <http://www.gnu.org/licenses/>. +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/TSDB.java b/src/core/TSDB.java index 4fbeaaaa42..53e6fb15fc 100644 --- a/src/core/TSDB.java +++ b/src/core/TSDB.java @@ -165,6 +165,9 @@ public enum TableAvailability { /** 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 @@ -382,7 +385,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()); } @@ -1850,20 +1856,43 @@ public String toString() { } } + final class RpcResponsderShutdown implements Callback<Object, Object> { + @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<Deferred<Object>, ArrayList<Object>> { - public Deferred<Object> call(final ArrayList<Object> args) { + public Deferred<Object> call(final ArrayList<Object> args) { + Callback<Object, Object> 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<Object, Exception> { - 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; @@ -1877,13 +1906,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<Object, ArrayList<Object>> { - public Object call(ArrayList<Object> compactions) throws Exception { + public Object call(ArrayList<Object> compactions) throws Exception { return null; } } @@ -2393,4 +2423,9 @@ final Deferred<Object> 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); + } + } diff --git a/src/tsd/PutDataPointRpc.java b/src/tsd/PutDataPointRpc.java index 8edc37957f..039d9f4e30 100644 --- a/src/tsd/PutDataPointRpc.java +++ b/src/tsd/PutDataPointRpc.java @@ -612,60 +612,65 @@ class GroupCB implements Callback<Object, ArrayList<Boolean>> { public GroupCB(final int queued) { this.queued = queued; } - + @Override public Object call(final ArrayList<Boolean> 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<String, Object> summary = new HashMap<String, Object>(); - 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<String, Object> summary = new HashMap<String, Object>(); + 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/utils/Config.java b/src/utils/Config.java index c534900aca..5c9e78b83f 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; @@ -340,6 +341,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 + * <code>Int</code>, then <code>default_val</code> 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 +438,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 + * <code>boolean</code>, then <code>default_val</code> 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 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 <http://www.gnu.org/licenses/>. +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(); + } + } + + +} From 766f0f6594a2243838777d876eda632cb44886bd Mon Sep 17 00:00:00 2001 From: Zephyr Guo <gzh1992n@gmail.com> Date: Thu, 10 Jan 2019 13:33:01 +0800 Subject: [PATCH 821/826] Fix a compilation error about missing FirstDifference (#1471) Signed-off-by: Chris Larsen <clarsen@yahoo-inc.com> --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index 329d366dd4..9c673c902e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -128,6 +128,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 \ From 94cd3a1019dc323e0118122da03ef0b89f7e5a3d Mon Sep 17 00:00:00 2001 From: Chris Larsen <clarsen@yahoo-inc.com> Date: Wed, 9 Jan 2019 21:32:34 -0800 Subject: [PATCH 822/826] Bump version to 2.5.0-SNAPSHOT. --- configure.ac | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index f96c94451d..09130214e2 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2021 The OpenTSDB Authors. +# Copyright (C) 2011-2024 The OpenTSDB Authors. # # This library is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.4.1], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.5.0-SNAPSHOT], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) From f6ea437e6d5a50238f7bcff0560831bb866b4795 Mon Sep 17 00:00:00 2001 From: Dai Feng <Eduardo95@users.noreply.github.com> Date: Tue, 18 Dec 2018 23:54:32 +0800 Subject: [PATCH 823/826] =?UTF-8?q?For=20branch=20next,=20add=20an=20expre?= =?UTF-8?q?ssion=20function=20named=20FirstDifference,=20wh=E2=80=A6=20(#1?= =?UTF-8?q?458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * For branch next, add an expression function named FirstDifference, which calculates the first difference of a time series. I noticed there is MovingAverage calculation, so I thought maybe I can enrich the mathematics functions into that. * add some unit tests for FirstDifference --- src/query/expression/ExpressionFactory.java | 1 + src/query/expression/FirstDifference.java | 101 +++++ .../query/expression/TestFirstDifference.java | 348 ++++++++++++++++++ 3 files changed, 450 insertions(+) create mode 100644 src/query/expression/FirstDifference.java create mode 100644 test/query/expression/TestFirstDifference.java 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 <http://www.gnu.org/licenses/>. +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<DataPoints[]> query_results, final List<String> 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<DataPoint> dps = new ArrayList<DataPoint>(); + final SeekableView view = points.iterator(); + List<Double> nums = new ArrayList<Double>(); + List<Long> times = new ArrayList<Long>(); + while (view.hasNext()) { + DataPoint pt = view.next(); + nums.add(pt.toDouble()); + times.add(pt.timestamp()); + } + List<Double> diff = new ArrayList<Double>(); + diff.add(0.0); + for (int j =0;j<nums.size()-1;j++){ + diff.add(nums.get(j+1) - nums.get(j)); + } + for (int j =0;j<nums.size();j++){ + dps.add(MutableDataPoint.ofDoubleValue(times.get(j), diff.get(j))); + } + final DataPoint[] results = new DataPoint[dps.size()]; + dps.toArray(results); + return new net.opentsdb.query.expression.PostAggregatedDataPoints(points, results); + } + + + + @Override + public String writeStringField(final List<String> query_params, + final String inner_expression) { + return "firstDiff(" + inner_expression + ")"; + } + +} \ No newline at end of file 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 <http://www.gnu.org/licenses/>. +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<DataPoints[]> query_results; + private List<String> 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<DataPoints[]>(1); + query_results.add(group_bys); + + params = new ArrayList<String>(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.<DataPoints[]>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")); + } +} From a81e38696bbf3ad533bb8864c9c0a17b04e63624 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Thu, 12 Dec 2024 10:58:21 -0500 Subject: [PATCH 824/826] Added NormalizeTagPlugin support merges #1525 --- Makefile.am | 1 + src/normalize/NormalizePlugin.java | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/normalize/NormalizePlugin.java diff --git a/Makefile.am b/Makefile.am index 9c673c902e..f046853a18 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,6 +114,7 @@ tsdb_SRC := \ src/meta/TSMeta.java \ src/meta/TSUIDQuery.java \ src/meta/UIDMeta.java \ + src/normalize/NormalizePlugin.java \ src/query/QueryUtil.java \ src/query/QueryLimitOverride.java \ src/query/expression/Absolute.java \ diff --git a/src/normalize/NormalizePlugin.java b/src/normalize/NormalizePlugin.java new file mode 100644 index 0000000000..6eae2bd9b5 --- /dev/null +++ b/src/normalize/NormalizePlugin.java @@ -0,0 +1,21 @@ +package net.opentsdb.normalize; + +import com.stumbleupon.async.Deferred; +import net.opentsdb.core.TSDB; +import net.opentsdb.stats.StatsCollector; + +import java.util.Map; + +public abstract class NormalizePlugin { + + public abstract void initialize(final TSDB tsdb); + + public abstract Deferred<Object> shutdown(); + + public abstract String version(); + + public abstract void collectStats(final StatsCollector collector); + + public abstract Map<String, String> normalizeTags(Map<String, String> tags); + +} \ No newline at end of file From 38cb8af247da9ae5edf68974478c730fd15d57f0 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Thu, 12 Dec 2024 13:20:44 -0500 Subject: [PATCH 825/826] Setting version to 2.5.0-RC1 --- .gitignore | 1 + configure.ac | 2 +- pom.xml.in | 6 +-- src/core/SaltScanner.java | 1 - src/core/SplitRollupSpanGroup.java | 1 + src/query/QueryUtil.java | 75 +++++++----------------------- 6 files changed, 23 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 133071f206..70966b8fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ tools/docker/opentsdb.conf fat-jar-pom.xml src-resources/ test-resources/ +third_party/*/*.jar diff --git a/configure.ac b/configure.ac index 09130214e2..37e8e7c768 100644 --- a/configure.ac +++ b/configure.ac @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. # Semantic Versioning (see http://semver.org/). -AC_INIT([opentsdb], [2.5.0-SNAPSHOT], [opentsdb@googlegroups.com]) +AC_INIT([opentsdb], [2.5.0-RC1], [opentsdb@googlegroups.com]) AC_CONFIG_AUX_DIR([build-aux]) AM_INIT_AUTOMAKE([foreign]) diff --git a/pom.xml.in b/pom.xml.in index 18f46f539a..7c1eadee68 100644 --- a/pom.xml.in +++ b/pom.xml.in @@ -70,8 +70,8 @@ <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> - <source>1.6</source> - <target>1.6</target> + <source>1.8</source> + <target>1.8</target> <compilerArgument>-Xlint</compilerArgument> <excludes> <exclude>**/client/*.java</exclude> @@ -326,7 +326,7 @@ <goal>javacc</goal> </goals> <configuration> - <jdkVersion>1.6</jdkVersion> + <jdkVersion>1.8</jdkVersion> <javadocFriendlyComments>true</javadocFriendlyComments> <packageName>net.opentsdb.query.expression.parser</packageName> <sourceDirectory>${basedir}/src/</sourceDirectory> diff --git a/src/core/SaltScanner.java b/src/core/SaltScanner.java index a17524b85c..76ec83bb01 100644 --- a/src/core/SaltScanner.java +++ b/src/core/SaltScanner.java @@ -960,7 +960,6 @@ private void validateAndTriggerCallback( histMap.put(scannersRunning, histograms); } - int scannersRunning = countdown.decrementAndGet(); if (scannersRunning <= 0) { try { mergeAndReturnResults(); diff --git a/src/core/SplitRollupSpanGroup.java b/src/core/SplitRollupSpanGroup.java index 1333c11e34..188fdd4b44 100644 --- a/src/core/SplitRollupSpanGroup.java +++ b/src/core/SplitRollupSpanGroup.java @@ -119,6 +119,7 @@ public Map<String, String> call(ArrayList<Map<String, String>> resolvedTags) thr */ @Override public Bytes.ByteMap<byte[]> getTagUids() { + Bytes.ByteMap<byte[]> tagUids = new Bytes.ByteMap<byte[]>(); for (SpanGroup group : spanGroups) { tagUids.putAll(group.getTagUids()); diff --git a/src/query/QueryUtil.java b/src/query/QueryUtil.java index fc7f5e2e89..f04e3fb0b1 100644 --- a/src/query/QueryUtil.java +++ b/src/query/QueryUtil.java @@ -418,39 +418,39 @@ public static void setDataTableScanFilter( final boolean explicit_tags, final boolean enable_fuzzy_filter, final int end_time) { - + // no-op - if ((group_bys == null || group_bys.isEmpty()) - && (row_key_literals == null || row_key_literals.isEmpty())) { + if ((group_bys == null || group_bys.isEmpty()) + && (row_key_literals == null || row_key_literals.isEmpty())) { return; } - + if (group_bys != null) { Collections.sort(group_bys, Bytes.MEMCMP); } final int prefix_width = Const.SALT_WIDTH() + TSDB.metrics_width() + - Const.TIMESTAMP_BYTES; + Const.TIMESTAMP_BYTES; final FuzzyRowFilter fuzzy_filter; if (explicit_tags && - enable_fuzzy_filter && - row_key_literals != null && - !row_key_literals.isEmpty()) { + 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()))]; + (TSDB.tagk_width() + TSDB.tagv_width()))]; System.arraycopy(scanner.getCurrentKey(), 0, fuzzy_key, 0, - scanner.getCurrentKey().length); + scanner.getCurrentKey().length); final List<FuzzyFilterPair> fuzzy_filter_pairs = - buildFuzzyFilters(row_key_literals, fuzzy_key); + 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); + 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(); @@ -474,66 +474,25 @@ public static void setDataTableScanFilter( if (!Strings.isNullOrEmpty(regex)) { if (LOG.isDebugEnabled()) { LOG.debug("Regex for scanner: " + scanner + ": " + - byteRegexToString(regex)); + byteRegexToString(regex)); } regex_filter = new KeyRegexpFilter(regex.toString(), - Const.ASCII_CHARSET); + Const.ASCII_CHARSET); } else { regex_filter = null; } if (fuzzy_filter != null && !Strings.isNullOrEmpty(regex)) { final FilterList filter = new FilterList(Lists.newArrayList(fuzzy_filter, - regex_filter),Operator.MUST_PASS_ALL); + 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); } - - if (explicit_tags && enable_fuzzy_filter) { - final List<FuzzyFilterPair> fuzzy_filter_pairs = - buildFuzzyFilters(row_key_literals); - - // The Fuzzy Filter list is sorted: the first and last filters row key - // can be used to build a start and stop keys for the scanner - final byte[] start_key = Arrays.copyOf( - fuzzy_filter_pairs.get(0).getRowKey(), - fuzzy_filter_pairs.get(0).getRowKey().length); - System.arraycopy(scanner.getCurrentKey(), 0, start_key, 0, prefix_width); - - final byte[] stop_key = Arrays.copyOf( - fuzzy_filter_pairs.get(fuzzy_filter_pairs.size()-1).getRowKey(), - start_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(start_key); - scanner.setStopKey(stop_key); - scanner.setFilter(new FuzzyRowFilter(fuzzy_filter_pairs)); - } else { - final String regex = getRowKeyUIDRegex(row_key_literals, explicit_tags); - final KeyRegexpFilter regex_filter = new KeyRegexpFilter( - regex.toString(), Const.ASCII_CHARSET); - if (LOG.isDebugEnabled()) { - LOG.debug("Regex for scanner: " + scanner + ": " + - byteRegexToString(regex)); - } - - scanner.setFilter(regex_filter); - } } - + /** * Creates a regular expression with a list of or'd TUIDs to compare * against the rows in storage. From 0f681b7545d9999506900da5f1c4dbe433dbfb43 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy <jonathan.creasy@gmail.com> Date: Thu, 12 Dec 2024 15:01:04 -0500 Subject: [PATCH 826/826] Cut the 2.5.0-RC1 Release. --- NEWS | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/NEWS b/NEWS index 969f8dbad3..c75ffd3ebf 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,36 @@ OpenTSDB - Changelog +* Version 2.5.0RC1 (2024-12-12) + +Noteworthy Changes: + - Bump Jackson to 2.14.1 WARNING: The minimum JDK is now version 8 due to Jackson. (#2263) + - Enhance range tests and regex and add better default for output validator (#2217) + - Converting to using SystemD systemctl (#1515) + - Status API, Start of a new HTTP API (#1742) + - Allow end_time = start_time to be able to query and delete a single datapoint. (#2036) + - Add support for splitting rollup queries (#1853) + - ExplicitTags filtering with FuzzyFilters (#1896) + - Add "check_tsd_v2" script (#1567) + - Do PutDataPointRpc.GroupCB asynchronously (#1472) + - Tag Normalization Plugin Support (#1525) + +Bug Fixes: + - Fix race condition in UID lookup (#2176) + - Tighten up the regexes for Gnuplot URI params per multiple security reports. (#2272) + - Trigger callback chain when reached limit. (#2204, #839) + - Use ConcurrentHashMap when serializing tags. (#2282, #1055) + - Test for null, avoid NPE (#2281, #2280) + - Ensure we always add the {} for group_by filters, otherwise the non group_by filters act as group_by filters (#1697) + - Added tracking of metrics which are null due to auto_metric being disabled Fixes (#786,#2042) + - Fixed function description Fixes (#841,#2040) + - Fix concurrent result reporting from scanners (#1753) + - Fix SaltScanner race condition on spans maps (#1651) + - Fix an edge case in TsdbQuery.getScanEndTimeSeconds() (#1581, #1582) + - Fix a compilation error about missing FirstDifference (#1471) + +Security Fixes: + - Bump Logback version to patch CVE-2021-42550 (#2208, #2218) + - Make sure the inputs are only plain ASCII printables first. Fixes CVE-2018-12972, CVE-2020-35476 (#2275) + * Version 2.4.1 (2021-09-02) Noteworthy Changes: